Index: trunk/extensions/CategoryBrowser/CategoryBrowserView.php |
— | — | @@ -27,7 +27,7 @@ |
28 | 28 | * * Add this line at the end of your LocalSettings.php file : |
29 | 29 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
30 | 30 | * |
31 | | - * @version 0.2.1 |
| 31 | + * @version 0.3.1 |
32 | 32 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
33 | 33 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
34 | 34 | * @addtogroup Extensions |
— | — | @@ -60,9 +60,10 @@ |
61 | 61 | # {{{ ajax link template |
62 | 62 | $this->ajax_onclick = ''; |
63 | 63 | $this->ajax_link_text = ''; |
| 64 | + $this->ajax_title_attr = ''; |
64 | 65 | if ( !isset( $this->ajax_link_tpl ) ) { |
65 | 66 | $this->ajax_link_tpl = |
66 | | - array( '__tag' => 'a', 'class' => 'cb_sublink', 'href' => '', 'onclick' => &$this->ajax_onclick, 0 => &$this->ajax_link_text ); |
| 67 | + array( '__tag' => 'a', 'class' => 'cb_sublink', 'href' => '', 'onclick' => &$this->ajax_onclick, 'title' => &$this->ajax_title_attr, 0 => &$this->ajax_link_text ); |
67 | 68 | } |
68 | 69 | # }}} |
69 | 70 | } |
— | — | @@ -152,7 +153,6 @@ |
153 | 154 | } |
154 | 155 | } /* end of CB_AbstractPagesView class */ |
155 | 156 | |
156 | | - |
157 | 157 | class CB_CategoriesView extends CB_AbstractPagesView { |
158 | 158 | |
159 | 159 | function generateList() { |
— | — | @@ -190,16 +190,15 @@ |
191 | 191 | } else { |
192 | 192 | $cat_title_obj = Title::makeTitle( NS_CATEGORY, $cat_title_str ); |
193 | 193 | } |
| 194 | + $js_cat_string = "'" . Xml::escapeJsString( $cat_title_str ) . "'"; |
194 | 195 | |
195 | | - # calculate exact number of pages alone |
196 | | - $cat->pages_only = intval( $cat->cat_pages ) - intval( $cat->cat_subcats ) - intval( $cat->cat_files ); |
197 | 196 | # generate tree "expand" sign |
198 | 197 | $this->initAjaxLinkTpl(); |
199 | 198 | if ( $cat->cat_subcats === NULL ) { |
200 | 199 | $cat_expand_sign = 'x'; |
201 | 200 | $subcat_count_hint = ''; |
202 | 201 | } elseif ( $cat->cat_subcats > 0 ) { |
203 | | - $this->ajax_onclick = 'return CategoryBrowser.subCatsPlus(this,' . $cat->cat_id . ')'; |
| 202 | + $this->ajax_onclick = 'return CategoryBrowser.subCatsPlus(this,' . $js_cat_string . ')'; |
204 | 203 | $this->ajax_link_text = '+'; |
205 | 204 | $cat_expand_sign = CB_XML::toText( $this->ajax_link_tpl ); |
206 | 205 | $subcat_count_hint = wfMsgExt( 'cb_has_subcategories', array( 'parsemag' ), $cat->cat_subcats ); |
— | — | @@ -211,20 +210,30 @@ |
212 | 211 | # create AJAX links for viewing categories, pages, files, belonging to this category |
213 | 212 | $ajax_links = ''; |
214 | 213 | $this->initAjaxLinkTpl(); |
215 | | - if ( !empty( $cat->cat_id ) ) { |
216 | | - $this->ajax_onclick = 'return CategoryBrowser.subCatsLink(this,' . $cat->cat_id . ')'; |
217 | | - $this->ajax_link_text = wfMsgExt( 'cb_has_subcategories', array( 'parsemag' ), $cat->cat_subcats ); |
218 | | - $cat_subcats = ( ( $cat->cat_subcats > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
219 | 214 | |
220 | | - $this->ajax_onclick = 'return CategoryBrowser.pagesLink(this,' . $cat->cat_id . ')'; |
221 | | - $this->ajax_link_text = wfMsgExt( 'cb_has_pages', array( 'parsemag' ), $cat->pages_only ); |
222 | | - $cat_pages = ( ( $cat->pages_only > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
| 215 | + $this->ajax_onclick = 'return CategoryBrowser.subCatsLink(this,' . $js_cat_string . ')'; |
| 216 | + $this->ajax_link_text = wfMsgExt( 'cb_has_subcategories', array( 'parsemag' ), $cat->cat_subcats ); |
| 217 | + $cat_subcats = ( ( $cat->cat_subcats > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
223 | 218 | |
224 | | - $this->ajax_onclick = 'return CategoryBrowser.filesLink(this,' . $cat->cat_id . ')'; |
225 | | - $this->ajax_link_text = wfMsgExt( 'cb_has_files', array( 'parsemag' ), $cat->cat_files ); |
226 | | - $cat_files = ( ( $cat->cat_files > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
227 | | - $ajax_links .= $cat_subcats . $cat_pages . $cat_files; |
| 219 | + $this->ajax_onclick = 'return CategoryBrowser.pagesLink(this,' . $js_cat_string . ')'; |
| 220 | + $this->ajax_link_text = wfMsgExt( 'cb_has_pages', array( 'parsemag' ), $cat->cat_pages_only ); |
| 221 | + $cat_pages = ( ( $cat->cat_pages_only > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
| 222 | + |
| 223 | + $this->ajax_onclick = 'return CategoryBrowser.filesLink(this,' . $js_cat_string . ')'; |
| 224 | + $this->ajax_link_text = wfMsgExt( 'cb_has_files', array( 'parsemag' ), $cat->cat_files ); |
| 225 | + $cat_files = ( ( $cat->cat_files > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
| 226 | + |
| 227 | + if ( CB_Setup::$allowNestedParents ) { |
| 228 | + $this->ajax_onclick = 'return CategoryBrowser.parentCatsLink(this,' . $js_cat_string . ')'; |
| 229 | + $this->ajax_link_text = '↑'; |
| 230 | + $this->ajax_title_attr = wfMsg( 'cb_has_parentcategories' ); |
| 231 | + $cat_parentcats = ' | ' . CB_XML::toText( $this->ajax_link_tpl ); |
| 232 | + } else { |
| 233 | + $cat_parentcats = ''; |
228 | 234 | } |
| 235 | + |
| 236 | + $ajax_links .= $cat_subcats . $cat_pages . $cat_files . $cat_parentcats; |
| 237 | + |
229 | 238 | $cat_link = CB_Setup::$skin->link( $cat_title_obj, $cat_title_obj->getText() ); |
230 | 239 | # show sortkey, when it does not match title name |
231 | 240 | $cat_link .= $this->addSortkey( $cat_title_obj, $cat ); |
— | — | @@ -252,7 +261,8 @@ |
253 | 262 | $page_link = ''; |
254 | 263 | $page_tpl = |
255 | 264 | array( '__tag' => 'div', 'class' => 'cb_cat_container', '__end' => "\n", |
256 | | - array( '__tag' => 'div', 'class' => 'cb_cat_item', 0 => &$page_link ) |
| 265 | + array( '__tag' => 'span', 'class' => 'cb_cat_expand', 0 => '' ), // empty cb_cat_expand makes line height matching to CB_CategoriesView |
| 266 | + array( '__tag' => 'span', 'class' => 'cb_cat_item', 0 => &$page_link ) |
257 | 267 | ); |
258 | 268 | # }}} |
259 | 269 | # create list of pages |
— | — | @@ -271,12 +281,8 @@ |
272 | 282 | |
273 | 283 | } /* end of CB_PagesView class */ |
274 | 284 | |
275 | | -class CB_FilesView extends CB_AbstractPagesView { |
| 285 | +class CB_FilesView extends CB_PagesView { |
276 | 286 | |
277 | | - function __construct( CB_SubPager $pager ) { |
278 | | - parent::__construct( $pager ); |
279 | | - } |
280 | | - |
281 | 287 | function generateList() { |
282 | 288 | if ( $this->pager->offset == -1 ) { |
283 | 289 | return ''; // list has no entries |
Index: trunk/extensions/CategoryBrowser/CategoryBrowserPage.php |
— | — | @@ -27,7 +27,7 @@ |
28 | 28 | * * Add this line at the end of your LocalSettings.php file : |
29 | 29 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
30 | 30 | * |
31 | | - * @version 0.2.1 |
| 31 | + * @version 0.3.1 |
32 | 32 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
33 | 33 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
34 | 34 | * @addtogroup Extensions |
— | — | @@ -53,24 +53,23 @@ |
54 | 54 | |
55 | 55 | var $source_ranges = |
56 | 56 | array( |
57 | | -// start of test entries |
58 | | -// NOTE: do not forget that only '>=', '<=', '=' comparsions are allowed, otherwise the bug check will be "triggered" |
59 | | -// array( '(', 'cat_pages >= 1000', 'OR', 'cat_subcats >= 10', ')', 'AND', 'cat_files >= 100' ), |
60 | | -// array( 'cat_pages >= 1000', 'OR', 'cat_subcats >= 10', 'AND', 'cat_files >= 100' ), |
61 | | -// array( 'cat_pages >= 100', 'OR', 'cat_subcats >= 10' ), |
62 | | -// array( '(', 'cat_pages <= 100', 'AND', 'cat_pages >= 10', ')', 'OR', '(', 'cat_subcats >= 0', 'AND', 'cat_subcats <= 10', ')' ), |
63 | | -// end of test entries |
64 | | - array( '' ), // default value "all", |
65 | | - array( 'cat_pages >= 100', 'OR', 'cat_subcats >= 1', 'OR', 'cat_files >= 10' ), |
66 | | - array( 'cat_pages >= 1000', 'OR', 'cat_subcats >= 10', 'OR', 'cat_files >= 100' ), |
67 | | - array( 'cat_pages >= 10000', 'OR', 'cat_subcats >= 100', 'OR', 'cat_files >= 1000' ), |
68 | | - array( 'cat_subcats >= 1' ), |
69 | | - array( 'cat_pages >= 1' ), |
70 | | - array( 'cat_files >= 1' ), |
71 | | - array( 'cat_subcats = 0' ), |
72 | | - array( 'cat_pages = 0' ), |
73 | | - array( 'cat_files = 0' ), |
74 | | - ); |
| 57 | + // start of test entries |
| 58 | +/* '(_gep1000_or_ges10_)_and_gef100', |
| 59 | + 'gep1000_or_ges10_and_gef100', |
| 60 | + 'gep100_or_ges10', |
| 61 | + '(_lep100_and_gep10_)_or_(_ges0_and_les10_)', */ |
| 62 | + // end of test entries |
| 63 | + '', // default value "all", |
| 64 | + 'gep10000_or_ges100_or_gef1000', |
| 65 | + 'gep1000_or_ges10_or_gef100', |
| 66 | + 'gep100_or_ges1_or_gef10', |
| 67 | + 'ges1', |
| 68 | + 'gep1', |
| 69 | + 'gef1', |
| 70 | + 'eqs0', |
| 71 | + 'eqp0', |
| 72 | + 'eqf0' |
| 73 | + ); |
75 | 74 | var $ranges; |
76 | 75 | |
77 | 76 | function execute( $param ) { |
— | — | @@ -105,15 +104,20 @@ |
106 | 105 | # {{{ top template |
107 | 106 | $condSelector = ''; |
108 | 107 | $catlist = array(); |
109 | | - $js_setNameFilter = 'CategoryBrowser.setNameFilter( this )'; |
110 | | - $nameFilterFields = array( |
111 | | - array( '__tag' => 'input', 'type' => 'text', 'onkeyup' => $js_setNameFilter, 'onchange' => $js_setNameFilter, 'id' => 'cb_cat_name_filter' ) |
112 | | - ); |
| 108 | + $js_setFilter = 'CategoryBrowser.setFilter()'; |
| 109 | + $filterFields = array(); |
113 | 110 | if ( CB_Setup::$cat_title_CI != '' ) { |
114 | 111 | // case insensitive search is possible |
115 | | - $nameFilterFields[] = wfMsg( 'cb_cat_name_filter_ci' ); |
116 | | - $nameFilterFields[] = array( '__tag' => 'input', 'type' => 'checkbox', 'onchange' => $js_setNameFilter, 'id' => 'cb_cat_name_filter_ci', 'checked' => null ); |
| 112 | + $filterFields[] = array( '__tag' => 'span', 0 => wfMsg( 'cb_cat_name_filter_ci' ) ); |
| 113 | + $filterFields[] = array( '__tag' => 'input', 'class' => 'cb_filter_field', 'type' => 'checkbox', 'onchange' => $js_setFilter, 'id' => 'cb_cat_name_filter_ci', 'title' => wfMsg( 'cb_cat_name_filter_ci' ), 'checked' => null ); |
117 | 114 | } |
| 115 | + if ( CB_Setup::$allowNoParentsOnly ) { |
| 116 | + $filterFields[] = |
| 117 | + array( |
| 118 | + array( '__tag' => 'span', 'class' => 'cb_filter_field', 0 => wfMsg( 'cb_show_no_parents_only' ) ), |
| 119 | + array( '__tag' => 'input', 'class' => 'cb_filter_field', 'type' => 'checkbox', 'onchange' => $js_setFilter, 'onclick' => $js_setFilter, 'title' => wfMsg( 'cb_show_no_parents_only' ), 'id' =>'cb_cat_no_parents_only' ) |
| 120 | + ); |
| 121 | + } |
118 | 122 | $top_tpl = |
119 | 123 | array( '__tag' => 'table', 'class' => 'cb_top_container', '__end' => "\n", |
120 | 124 | array( '__tag' => 'tr', '__end' => "\n", |
— | — | @@ -121,8 +125,14 @@ |
122 | 126 | ), |
123 | 127 | array( '__tag' => 'tr', '__end' => "\n", |
124 | 128 | array( '__tag' => 'td', 'class' => 'cb_toolbox_bottom', '__end' => "\n", |
125 | | - array( wfMsg( 'cb_cat_name_filter' ) ), |
126 | | - &$nameFilterFields, |
| 129 | + array( '__tag' => 'div', 'class' => 'cb_filter_container', |
| 130 | + wfMsg( 'cb_cat_name_filter' ), |
| 131 | + array( '__tag' => 'input', 'type' => 'text', 'onkeyup' => $js_setFilter, 'onchange' => $js_setFilter, 'id' => 'cb_cat_name_filter' ), |
| 132 | + array( '__tag' => 'input', 'type' => 'button', 'class' => 'cb_filter_field', 'onclick' => 'CategoryBrowser.clearNameFilter(this)', 'title' => wfMsg( 'cb_cat_name_filter_clear' ), 'value' =>'※' ) |
| 133 | + ), |
| 134 | + array( '__tag' => 'div', 'class' => 'cb_filter_container', |
| 135 | + &$filterFields |
| 136 | + ) |
127 | 137 | ) |
128 | 138 | ), |
129 | 139 | array( '__tag' => 'tr', '__end' => "\n", |
Index: trunk/extensions/CategoryBrowser/CategoryBrowser_i18n.php |
— | — | @@ -27,7 +27,7 @@ |
28 | 28 | * * Add this line at the end of your LocalSettings.php file : |
29 | 29 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
30 | 30 | * |
31 | | - * @version 0.2.1 |
| 31 | + * @version 0.3.1 |
32 | 32 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
33 | 33 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
34 | 34 | * @addtogroup Extensions |
— | — | @@ -49,12 +49,15 @@ |
50 | 50 | 'cb_ie6_warning' => 'The condition editor does not work in Internet Explorer 6.0 or earlier versions. |
51 | 51 | However, browsing of pre-defined conditions should work normally. |
52 | 52 | Please change or upgrade your browser, if possible.', |
| 53 | + 'cb_show_no_parents_only' => 'Show only categories which has no parents', |
53 | 54 | 'cb_cat_name_filter' => 'Search for category by name:', |
54 | | - 'cb_cat_name_filter_ci' => '(case insensitive)', |
| 55 | + 'cb_cat_name_filter_clear' => 'Press to clear category name filter', |
| 56 | + 'cb_cat_name_filter_ci' => 'Case insensitive', |
55 | 57 | 'cb_copy_line_hint' => 'Use the [+] and [>+] buttons to copy and paste operators into the selected expression', |
56 | 58 | 'cb_has_subcategories' => '$1 {{PLURAL:$1|subcategory|subcategories}}', |
57 | 59 | 'cb_has_pages' => '$1 {{PLURAL:$1|page|pages}}', |
58 | 60 | 'cb_has_files' => '$1 {{PLURAL:$1|file|files}}', |
| 61 | + 'cb_has_parentcategories' => 'parent categories (if any)', |
59 | 62 | 'cb_previous_items_link' => 'Previous', |
60 | 63 | 'cb_previous_items_stats' => ' ($1 - $2)', |
61 | 64 | 'cb_previous_items_line' => '$1 $2', |
— | — | @@ -112,12 +115,15 @@ |
113 | 116 | 'cb_ie6_warning' => 'Редактор выражений не поддерживается в Internet Explorer версии 6.0 или более ранних. |
114 | 117 | Возможен лишь просмотр предопределенных выражений. |
115 | 118 | Пожалуйста поменяйте или обновите ваш браузер.', |
| 119 | + 'cb_show_no_parents_only' => 'Показывать только категории без родителей', |
116 | 120 | 'cb_cat_name_filter' => 'Поиск категории по имени:', |
117 | | - 'cb_cat_name_filter_ci' => '(без учёта регистра)', |
| 121 | + 'cb_cat_name_filter_clear' => 'Нажмите здесь для очистки поля поиска категории по имени', |
| 122 | + 'cb_cat_name_filter_ci' => 'Без учёта регистра', |
118 | 123 | 'cb_copy_line_hint' => 'Используйте кнопки [+] и [>+] для копирования оператора в выбранное выражение', |
119 | 124 | 'cb_has_subcategories' => '$1 {{PLURAL:$1|подкатегория|подкатегории|подкатегорий}}', |
120 | 125 | 'cb_has_pages' => '$1 {{PLURAL:$1|страница|страницы|страниц}}', |
121 | 126 | 'cb_has_files' => '$1 {{PLURAL:$1|файл|файла|файлов}}', |
| 127 | + 'cb_has_parentcategories' => 'родительские категории (если есть)', |
122 | 128 | 'cb_previous_items_link' => 'Предыдущие', |
123 | 129 | 'cb_next_items_link' => 'Следующие', |
124 | 130 | 'cb_next_items_stats' => ' (начиная с $1)', |
Index: trunk/extensions/CategoryBrowser/INSTALL |
— | — | @@ -1,4 +1,4 @@ |
2 | | -MediaWiki extension CategoryBrowser, version 0.2.1 |
| 2 | +MediaWiki extension CategoryBrowser, version 0.3.1 |
3 | 3 | |
4 | 4 | * download the latest available version and extract it to your wiki extension directory. |
5 | 5 | * add the following line to LocalSettings.php |
Index: trunk/extensions/CategoryBrowser/CategoryBrowser.php |
— | — | @@ -27,7 +27,7 @@ |
28 | 28 | * * Add this line at the end of your LocalSettings.php file : |
29 | 29 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
30 | 30 | * |
31 | | - * @version 0.2.1 |
| 31 | + * @version 0.3.1 |
32 | 32 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
33 | 33 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
34 | 34 | * @addtogroup Extensions |
— | — | @@ -48,7 +48,7 @@ |
49 | 49 | |
50 | 50 | class CB_Setup { |
51 | 51 | |
52 | | - static $version = '0.2.1'; |
| 52 | + static $version = '0.3.1'; |
53 | 53 | static $ExtDir; // filesys path with windows path fix |
54 | 54 | static $ScriptPath; // apache virtual path |
55 | 55 | static $cat_pages_ranges; // ??? |
— | — | @@ -58,9 +58,12 @@ |
59 | 59 | static $response; |
60 | 60 | static $cookie_prefix; |
61 | 61 | |
62 | | - // case insensitive collation of category table 'cat_title' field |
63 | | - static $cat_title_CI = ''; |
| 62 | + // by default, enable browsing of category parents (reverse browsing) |
| 63 | + static $allowNestedParents = true; |
64 | 64 | |
| 65 | + // by default, allow optional selecting of categories which has no parents only |
| 66 | + static $allowNoParentsOnly = true; |
| 67 | + |
65 | 68 | // number of files to show in gallery row |
66 | 69 | static $imageGalleryPerRow = 4; |
67 | 70 | |
— | — | @@ -69,7 +72,11 @@ |
70 | 73 | static $pagesLimit = CB_PAGING_ROWS; |
71 | 74 | static $filesLimit = null; |
72 | 75 | static $filesMaxRows = CB_FILES_MAX_ROWS; |
| 76 | + static $parentsLimit = CB_PAGING_ROWS; |
73 | 77 | |
| 78 | + // case insensitive collation of category table 'cat_title' field |
| 79 | + static $cat_title_CI = ''; |
| 80 | + |
74 | 81 | /** |
75 | 82 | * Add this extension to the mediawiki's extensions list. |
76 | 83 | */ |
— | — | @@ -91,9 +98,12 @@ |
92 | 99 | $wgAutoloadClasses['CB_XML'] = |
93 | 100 | $wgAutoloadClasses['CB_SqlCond'] = self::$ExtDir . '/CategoryBrowserBasic.php'; |
94 | 101 | |
| 102 | + $wgAutoloadClasses['CB_AbstractPager'] = |
95 | 103 | $wgAutoloadClasses['CB_RootPager'] = |
96 | | - $wgAutoloadClasses['CB_SubPager'] = self::$ExtDir . '/CategoryBrowserModel.php'; |
| 104 | + $wgAutoloadClasses['CB_SubPager'] = |
| 105 | + $wgAutoloadClasses['CB_ParentPager'] = self::$ExtDir . '/CategoryBrowserModel.php'; |
97 | 106 | |
| 107 | + $wgAutoloadClasses['CB_AbstractPagesView'] = |
98 | 108 | $wgAutoloadClasses['CB_CategoriesView'] = |
99 | 109 | $wgAutoloadClasses['CB_PagesView'] = |
100 | 110 | $wgAutoloadClasses['CB_FilesView'] = self::$ExtDir . '/CategoryBrowserView.php'; |
Index: trunk/extensions/CategoryBrowser/category_browser.css |
— | — | @@ -35,10 +35,18 @@ |
36 | 36 | } |
37 | 37 | |
38 | 38 | input#cb_cat_name_filter { |
39 | | - margin: 0 1em 0 1em; |
40 | | - width: 20em; |
| 39 | + margin: 0 0 0 0.5em; |
| 40 | + width: 16em; |
41 | 41 | } |
42 | 42 | |
| 43 | +div.cb_filter_container { |
| 44 | + margin: 0 0 0.5em 0; |
| 45 | +} |
| 46 | + |
| 47 | +.cb_filter_field { |
| 48 | + margin: 0 0 0 0.5em; |
| 49 | +} |
| 50 | + |
43 | 51 | div#cb_root_container { |
44 | 52 | width: 100%; |
45 | 53 | height: 100%; |
Index: trunk/extensions/CategoryBrowser/CategoryBrowserModel.php |
— | — | @@ -27,7 +27,7 @@ |
28 | 28 | * * Add this line at the end of your LocalSettings.php file : |
29 | 29 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
30 | 30 | * |
31 | | - * @version 0.2.1 |
| 31 | + * @version 0.3.1 |
32 | 32 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
33 | 33 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
34 | 34 | * @addtogroup Extensions |
— | — | @@ -40,6 +40,11 @@ |
41 | 41 | abstract class CB_AbstractPager { |
42 | 42 | |
43 | 43 | var $db; |
| 44 | + var $page_table; |
| 45 | + var $category_table; |
| 46 | + var $categorylinks_table; |
| 47 | + // select pages number without number of categories or files |
| 48 | + static $cat_pages_only = '(cat_pages - cat_subcats - cat_files) as cat_pages_only'; |
44 | 49 | |
45 | 50 | /* pager position (actual offset) |
46 | 51 | * 0 means pager has no previous elements |
— | — | @@ -50,12 +55,15 @@ |
51 | 56 | var $query_offset; |
52 | 57 | /* indicates, whether the pager has further elements */ |
53 | 58 | var $hasMoreEntries = false; |
54 | | - /* maximal number of entries per page (actual number of entries on page) */ |
| 59 | + /* |
| 60 | + * maximal number of entries per page (actual number of entries on page) |
| 61 | + * warning: is not initialized until $this->getCurrentRows() call |
| 62 | + */ |
55 | 63 | var $limit = 0; |
56 | 64 | /* provided "source" limit */ |
57 | 65 | var $query_limit; |
58 | 66 | /* array of current entries */ |
59 | | - var $entries; |
| 67 | + var $entries = array(); |
60 | 68 | |
61 | 69 | /* |
62 | 70 | * abstract query (doesn't instantinate) |
— | — | @@ -64,6 +72,9 @@ |
65 | 73 | */ |
66 | 74 | function __construct( $offset, $limit ) { |
67 | 75 | $this->db = & wfGetDB( DB_SLAVE ); |
| 76 | + $this->page_table = $this->db->tableName( 'page' ); |
| 77 | + $this->category_table = $this->db->tableName( 'category' ); |
| 78 | + $this->categorylinks_table = $this->db->tableName( 'categorylinks' ); |
68 | 79 | $this->query_limit = intval( $limit ); |
69 | 80 | $this->query_offset = intval( $offset ); |
70 | 81 | } |
— | — | @@ -101,39 +112,79 @@ |
102 | 113 | return ( ( $this->hasMoreEntries ) ? $this->offset + $this->limit : 0 ); |
103 | 114 | } |
104 | 115 | |
| 116 | +} /* end of CB_AbstractPager class */ |
| 117 | + |
| 118 | +/* |
| 119 | + * parent categories pager |
| 120 | + */ |
| 121 | +class CB_ParentPager extends CB_AbstractPager { |
| 122 | + |
| 123 | + // database cat_title of children category (with underscores, dbkey-like) |
| 124 | + var $childCatName; |
| 125 | + |
| 126 | + function __construct( $childCatName, $offset, $limit ) { |
| 127 | + parent::__construct( $offset, $limit ); |
| 128 | + $this->childCatName = $childCatName; |
| 129 | + } |
| 130 | + |
105 | 131 | /* |
106 | | - * suggests, what kind of view should be used for instance of the model |
107 | | - * @return name of view method |
108 | | - * otherwise throws an error |
109 | | - * warning: will fail, when called before calling $this->getCurrentRows() ! |
110 | | - * warning: $this->limit is not set properly before calling $this->getCurrentRows() ! |
111 | | - * todo: unused anymore; keep because might be useful in some cases |
| 132 | + * set offset, limit, hasMoreEntries and entries |
| 133 | + * @param $offset SQL offset |
| 134 | + * @param $limit SQL limit |
112 | 135 | */ |
113 | | - function getListType() { |
114 | | - // it is not enough to check $this->entries[0], |
115 | | - // because some broken tables might have just some (not all) cat_title = NULL or page_title = NULL |
116 | | - foreach ( $this->entries as &$entry ) { |
117 | | - if ( isset( $entry->page_namespace ) && $entry->page_namespace == NS_FILE ) { return 'generateFilesList'; } |
118 | | - if ( isset( $entry->page_title ) ) { return 'generatePagesList'; } |
119 | | - if ( isset( $entry->cat_title ) ) { return 'generateCatList'; } |
120 | | - if ( isset( $entry->cl_sortkey ) ) { return 'generateCatList'; } |
| 136 | + function getCurrentRows() { |
| 137 | + if ( !CB_Setup::$allowNestedParents ) { |
| 138 | + return; |
121 | 139 | } |
122 | | - throw new MWException( 'Entries not initialized in ' . __METHOD__ ); |
| 140 | + /* |
| 141 | + SELECT null cl_sortkey,cat_id,cat_title,cat_subcats,cat_pages,cat_files FROM `wiki_page` INNER JOIN `wiki_categorylinks` ON (cl_from = page_id AND page_namespace=14 AND page_title='Жизненные_ценности' ) INNER JOIN `wiki_category` ON (cat_title = cl_to) ORDER BY cat_title |
| 142 | + */ |
| 143 | + $query_string = |
| 144 | + "SELECT null cl_sortkey,cat_id,cat_title,cat_subcats," . self::$cat_pages_only . ",cat_files " . |
| 145 | + "FROM {$this->page_table} " . |
| 146 | + "INNER JOIN {$this->categorylinks_table} ON (cl_from = page_id AND page_namespace=14 AND page_title=" . $this->db->addQuotes( $this->childCatName ) . ") " . |
| 147 | + "INNER JOIN {$this->category_table} ON (cat_title = cl_to) " . |
| 148 | + "ORDER BY cat_title "; |
| 149 | + $res = $this->db->query( $query_string . "LIMIT {$this->query_offset}," . ( $this->query_limit + 1 ), __METHOD__ ); |
| 150 | + $this->setEntries( $res ); |
123 | 151 | } |
124 | 152 | |
125 | | -} /* end of CB_AbstractPager class */ |
| 153 | + /* |
| 154 | + * returns JS function call used to navigate to the previous page of this pager |
| 155 | + * when placeholders = true, empty html placeholder should be generated in pager view instead of link |
| 156 | + * when placeholders = false, nothing ('') will be generated in pager view instead of link |
| 157 | + */ |
| 158 | + function getPrevAjaxLink() { |
| 159 | + $result = (object) array( |
| 160 | + "call" => "return CategoryBrowser.parentCatsNav(this,'" . Xml::escapeJsString( $this->childCatName ) . "'," . $this->getPrevOffset() . ( ( $this->limit == CB_PAGING_ROWS ) ? '' : ',' . $this->limit ) . ')', |
| 161 | + "placeholders" => false |
| 162 | + ); |
| 163 | + return $result; |
| 164 | + } |
126 | 165 | |
| 166 | + /* |
| 167 | + * returns JS function call used to navigate to the next page of this pager |
| 168 | + * when placeholders = true, empty html placeholder should be generated in pager view instead of link |
| 169 | + * when placeholders = false, nothing ('') will be generated in pager view instead of link |
| 170 | + */ |
| 171 | + function getNextAjaxLink() { |
| 172 | + $result = (object) array( |
| 173 | + "call" => "return CategoryBrowser.parentCatsNav(this,'" . Xml::escapeJsString( $this->childCatName ) . "'," . $this->getNextOffset() . ( ( $this->limit == CB_PAGING_ROWS ) ? '' : ',' . $this->limit ) . ')', |
| 174 | + "placeholders" => false |
| 175 | + ); |
| 176 | + return $result; |
| 177 | + } |
| 178 | + |
| 179 | +} /* end of CB_ParentPager class */ |
| 180 | + |
127 | 181 | /* |
128 | 182 | * subentries (subcategories, pages, files) pager |
129 | 183 | * TODO: gracefully set offset = 0 when too large offset was given |
130 | 184 | */ |
131 | 185 | class CB_SubPager extends CB_AbstractPager { |
132 | 186 | |
133 | | - var $page_table; |
134 | | - var $category_table; |
135 | | - var $categorylinks_table; |
136 | | - // database ID of parent category |
137 | | - var $parentCatId; |
| 187 | + // database cat_title of parent category (with underscores, dbkey-like) |
| 188 | + var $parentCatName; |
138 | 189 | // database fields to query |
139 | 190 | var $select_fields; |
140 | 191 | // namespace SQL condition (WHERE part) |
— | — | @@ -144,18 +195,15 @@ |
145 | 196 | /* |
146 | 197 | * creates subcategory list pager |
147 | 198 | * |
148 | | - * @param $parentCatId id of parent category |
| 199 | + * @param $parentCatName cat_title of parent category |
149 | 200 | * @param $offset SQL offset |
150 | 201 | * @param $limit SQL limit |
151 | 202 | * |
152 | | - * TODO: query count of parentCatId subcategories/pages/files in category table for progress / percentage display |
| 203 | + * TODO: query count of parent subcategories/pages/files in category table for progress / percentage display |
153 | 204 | */ |
154 | | - function __construct( $parentCatId, $offset, $limit, $js_nav_func, $select_fields = '*', $ns_cond = '' ) { |
| 205 | + function __construct( $parentCatName, $offset, $limit, $js_nav_func, $select_fields = '*', $ns_cond = '' ) { |
155 | 206 | parent::__construct( $offset, $limit ); |
156 | | - $this->page_table = $this->db->tableName( 'page' ); |
157 | | - $this->category_table = $this->db->tableName( 'category' ); |
158 | | - $this->categorylinks_table = $this->db->tableName( 'categorylinks' ); |
159 | | - $this->parentCatId = $parentCatId; |
| 207 | + $this->parentCatName = $parentCatName; |
160 | 208 | $this->select_fields = $select_fields; |
161 | 209 | $this->ns_cond = $ns_cond; |
162 | 210 | $this->js_nav_func = $js_nav_func; |
— | — | @@ -167,19 +215,16 @@ |
168 | 216 | * @param $limit SQL limit |
169 | 217 | */ |
170 | 218 | function getCurrentRows() { |
171 | | - /* TODO: change the query to more optimal one (no subselects) |
172 | | - * SELECT cl_sortkey,cat_id,cat_title,cat_subcats,cat_pages,cat_files FROM `wiki_page` INNER JOIN `wiki_categorylinks` FORCE INDEX (cl_sortkey) ON (cl_from = page_id) LEFT JOIN `wiki_category` ON (cat_title = page_title AND page_namespace = 14) WHERE cl_to IN (SELECT cat_title FROM wiki_category WHERE cat_id = 44) AND page_namespace = 14 ORDER BY cl_sortkey LIMIT 0,11 |
| 219 | + /* |
| 220 | + * SELECT cl_sortkey,cat_id,cat_title,cat_subcats,cat_pages,cat_files FROM `wiki_page` INNER JOIN `wiki_categorylinks` FORCE INDEX (cl_sortkey) ON (cl_from = page_id) LEFT JOIN `wiki_category` ON (cat_title = page_title AND page_namespace = 14) WHERE cl_to = 'parent category name' AND page_namespace = 14 ORDER BY cl_sortkey LIMIT 0,11 |
173 | 221 | */ |
174 | 222 | $query_string = |
175 | 223 | "SELECT {$this->select_fields} " . |
176 | 224 | "FROM {$this->page_table} " . |
177 | | - "INNER JOIN {$this->categorylinks_table} FORCE INDEX (cl_sortkey) ON cl_from = page_id " . |
178 | | - "LEFT JOIN {$this->category_table} ON cat_title = page_title AND page_namespace = " . NS_CATEGORY . " " . |
179 | | - "WHERE cl_to IN (" . |
180 | | - "SELECT cat_title " . |
181 | | - "FROM {$this->category_table} " . |
182 | | - "WHERE cat_id = " . $this->db->addQuotes( $this->parentCatId ) . |
183 | | - ") " . ( ( $this->ns_cond == '' ) ? '' : "AND {$this->ns_cond} " ) . |
| 225 | + "INNER JOIN {$this->categorylinks_table} FORCE INDEX (cl_sortkey) ON (cl_from = page_id) " . |
| 226 | + "LEFT JOIN {$this->category_table} ON (cat_title = page_title AND page_namespace = " . NS_CATEGORY . ") " . |
| 227 | + "WHERE cl_to = " . $this->db->addQuotes( $this->parentCatName ) . |
| 228 | + ( ( $this->ns_cond == '' ) ? ' ' : "AND {$this->ns_cond} " ) . |
184 | 229 | "ORDER BY cl_sortkey "; |
185 | 230 | $res = $this->db->query( $query_string . "LIMIT {$this->query_offset}," . ( $this->query_limit + 1 ), __METHOD__ ); |
186 | 231 | $this->setEntries( $res ); |
— | — | @@ -192,7 +237,7 @@ |
193 | 238 | */ |
194 | 239 | function getPrevAjaxLink() { |
195 | 240 | $result = (object) array( |
196 | | - "call" => "return CategoryBrowser.{$this->js_nav_func}(this," . $this->parentCatId . "," . $this->getPrevOffset() . ( ( $this->limit == CB_PAGING_ROWS ) ? '' : ',' . $this->limit ) . ')', |
| 241 | + "call" => "return CategoryBrowser.{$this->js_nav_func}(this,'" . Xml::escapeJsString( $this->parentCatName ) . "'," . $this->getPrevOffset() . ( ( $this->limit == CB_PAGING_ROWS ) ? '' : ',' . $this->limit ) . ')', |
197 | 242 | "placeholders" => false |
198 | 243 | ); |
199 | 244 | return $result; |
— | — | @@ -205,7 +250,7 @@ |
206 | 251 | */ |
207 | 252 | function getNextAjaxLink() { |
208 | 253 | $result = (object) array( |
209 | | - "call" => "return CategoryBrowser.{$this->js_nav_func}(this," . $this->parentCatId . ',' . $this->getNextOffset() . ( ( $this->limit == CB_PAGING_ROWS ) ? '' : ',' . $this->limit ) . ')', |
| 254 | + "call" => "return CategoryBrowser.{$this->js_nav_func}(this,'" . Xml::escapeJsString( $this->parentCatName ) . "'," . $this->getNextOffset() . ( ( $this->limit == CB_PAGING_ROWS ) ? '' : ',' . $this->limit ) . ')', |
210 | 255 | "placeholders" => false |
211 | 256 | ); |
212 | 257 | return $result; |
— | — | @@ -228,6 +273,9 @@ |
229 | 274 | */ |
230 | 275 | var $sqlCond = null; |
231 | 276 | |
| 277 | + // by default, query for all categories, not just these which have no parents |
| 278 | + var $noParentsOnly = false; |
| 279 | + |
232 | 280 | // category name filter (LIKE) |
233 | 281 | var $nameFilter = ''; |
234 | 282 | // category name filter case-insensetive flag (when true, tries to use insensetive LIKE COLLATE) |
— | — | @@ -278,7 +326,7 @@ |
279 | 327 | public static function newFromCategoryRange( $ranges ) { |
280 | 328 | $rp = null; |
281 | 329 | foreach ( $ranges as &$range ) { |
282 | | - $rp = CB_RootPager::newFromInfixTokens( $range->infix_decoded ); |
| 330 | + $rp = CB_RootPager::newFromInfixTokens( $range->infix_tokens ); |
283 | 331 | if ( is_object( $rp ) && $rp->offset != -1 ) { |
284 | 332 | break; |
285 | 333 | } |
— | — | @@ -287,12 +335,21 @@ |
288 | 336 | } |
289 | 337 | |
290 | 338 | /* |
| 339 | + * setup query only for categories, which does not have the parents |
| 340 | + * @param: $noParentsOnly - boolean flag |
| 341 | + * true - query only for categories, which does not have the parents |
| 342 | + */ |
| 343 | + function setNoParentsOnly( $noParentsOnly ) { |
| 344 | + $this->noParentsOnly = (boolean) $noParentsOnly; |
| 345 | + } |
| 346 | + |
| 347 | + /* |
291 | 348 | * filter catetories by names |
292 | 349 | * @param $cat_name_filter - string category name begins from |
293 | 350 | * @param $cat_name_filter_ci - boolean, true attempts to use case-insensetive search, when available |
294 | 351 | */ |
295 | 352 | function setNameFilter( $cat_name_filter, $cat_name_filter_ci ) { |
296 | | - $this->nameFilter = ltrim( $cat_name_filter ); |
| 353 | + $this->nameFilter = str_replace( ' ', '_', ltrim( $cat_name_filter ) ); |
297 | 354 | $this->nameFilterCI = $cat_name_filter_ci; |
298 | 355 | } |
299 | 356 | |
— | — | @@ -312,13 +369,26 @@ |
313 | 370 | $conds .= ' COLLATE ' . $this->db->addQuotes( CB_Setup::$cat_title_CI ); |
314 | 371 | } |
315 | 372 | } |
316 | | - $options = array( 'OFFSET' => $this->query_offset, 'ORDER BY' => 'cat_title', 'LIMIT' => $this->query_limit + 1 ); |
317 | | - $res = $this->db->select( 'category', |
318 | | - array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ), |
319 | | - $conds, |
320 | | - __METHOD__, |
321 | | - $options |
322 | | - ); |
| 373 | + if ( CB_Setup::$allowNoParentsOnly && $this->noParentsOnly ) { |
| 374 | + /* |
| 375 | + SELECT * FROM `wiki_category` INNER JOIN `wiki_categorylinks` AS childs ON (cat_title = cl_to) LEFT JOIN `wiki_page` ON ( childs.cl_to = page_title AND page_namespace = 14 ) LEFT JOIN `wiki_categorylinks` AS parents ON ( page_id = parents.cl_from ) WHERE parents.cl_from IS NULL GROUP BY childs.cl_to ORDER BY childs.cl_to |
| 376 | + */ |
| 377 | + $conds = "parents.cl_from IS NULL" . ( ($conds == '') ? '' : " AND ${conds}" ); |
| 378 | + $query_string = |
| 379 | + "SELECT cat_id, cat_title, " . self::$cat_pages_only . ", cat_subcats, cat_files FROM {$this->category_table} " . |
| 380 | + "INNER JOIN {$this->categorylinks_table} AS childs ON (cat_title = cl_to) " . |
| 381 | + "LEFT JOIN {$this->page_table} ON (childs.cl_to = page_title AND page_namespace = 14) " . |
| 382 | + "LEFT JOIN {$this->categorylinks_table} AS parents ON (page_id = parents.cl_from) " . |
| 383 | + "WHERE ${conds} GROUP BY childs.cl_to ORDER BY childs.cl_to "; |
| 384 | + $res = $this->db->query( $query_string . "LIMIT {$this->query_offset}," . ( $this->query_limit + 1 ), __METHOD__ ); |
| 385 | + } else { |
| 386 | + $res = $this->db->select( 'category', |
| 387 | + array( 'cat_id', 'cat_title', self::$cat_pages_only, 'cat_subcats', 'cat_files' ), |
| 388 | + $conds, |
| 389 | + __METHOD__, |
| 390 | + array( 'OFFSET' => $this->query_offset, 'ORDER BY' => 'cat_title', 'LIMIT' => $this->query_limit + 1 ) |
| 391 | + ); |
| 392 | + } |
323 | 393 | /* set actual offset, limit, hasMoreEntries and entries */ |
324 | 394 | $this->setEntries( $res ); |
325 | 395 | } |
Index: trunk/extensions/CategoryBrowser/category_browser_rtl.css |
— | — | @@ -36,10 +36,18 @@ |
37 | 37 | } |
38 | 38 | |
39 | 39 | input#cb_cat_name_filter { |
40 | | - margin: 0 1em 0 1em; |
41 | | - width: 20em; |
| 40 | + margin: 0 0.5em 0 0; |
| 41 | + width: 16em; |
42 | 42 | } |
43 | 43 | |
| 44 | +div.cb_filter_container { |
| 45 | + margin: 0 0 0.5em 0; |
| 46 | +} |
| 47 | + |
| 48 | +.cb_filter_field { |
| 49 | + margin: 0 0.5em 0 0; |
| 50 | +} |
| 51 | + |
44 | 52 | div#cb_root_container { |
45 | 53 | width: 100%; |
46 | 54 | height: 100%; |
Index: trunk/extensions/CategoryBrowser/CategoryBrowserBasic.php |
— | — | @@ -27,7 +27,7 @@ |
28 | 28 | * * Add this line at the end of your LocalSettings.php file : |
29 | 29 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
30 | 30 | * |
31 | | - * @version 0.2.1 |
| 31 | + * @version 0.3.1 |
32 | 32 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
33 | 33 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
34 | 34 | * @addtogroup Extensions |
— | — | @@ -37,7 +37,6 @@ |
38 | 38 | die( "This file is a part of MediaWiki extension.\n" ); |
39 | 39 | } |
40 | 40 | |
41 | | -define( 'CB_COND_TOKEN_MATCH', '`^\s*(cat_subcats|cat_pages|cat_files)\s*(>=|<=|=)\s*(\d+)\s*$`' ); |
42 | 41 | define( 'CB_ENCODED_TOKEN_MATCH', '`^(ge|le|eq)(p|s|f)(\d+)$`' ); |
43 | 42 | |
44 | 43 | /* render output data */ |
— | — | @@ -223,45 +222,48 @@ |
224 | 223 | var $local_tokens; |
225 | 224 | |
226 | 225 | /* |
227 | | - * @param $tokens - list of SQL condition tokens (infix or polish) |
228 | | - * comparsions like "a > 1" are treated like single-ops |
| 226 | + * @param $tokens - list of encoded SQL condition tokens |
| 227 | + * (infix or RPN - doesn't matter for localization) |
| 228 | + * comparsions are single-ops |
229 | 229 | */ |
230 | | - function __construct( $tokens ) { |
231 | | - if ( is_array( $tokens ) ) { |
232 | | - $this->src_tokens = $tokens; |
233 | | - } else { |
234 | | - $this->src_tokens = array( '' ); // default "all" |
235 | | - } |
236 | | - if ( count( $this->src_tokens ) == 1 && $this->src_tokens[0] == '' ) { |
237 | | - $this->local_tokens = array( wfMsg( 'cb_all_op' ) ); // localized "all" |
| 230 | + function __construct( array $tokens ) { |
| 231 | + $this->src_tokens = $tokens; |
| 232 | + if ( count( $this->src_tokens ) < 1 ) { |
| 233 | + $this->local_tokens = array( wfMsg( 'cb_all_op' ) ); // localized default "all" |
238 | 234 | return; |
239 | 235 | } |
240 | 236 | foreach ( $tokens as &$token ) { |
241 | | - $field = $num = ''; |
242 | | - switch ( strtoupper( $token ) ) { |
243 | | - case '(' : $op = 'lbracket'; break; |
244 | | - case ')' : $op = 'rbracket'; break; |
245 | | - case 'OR' : $op = 'or'; break; |
246 | | - case 'AND' : $op = 'and'; break; |
247 | | - default : // comparsion subexpression |
248 | | - preg_match_all( CB_COND_TOKEN_MATCH, $token, $matches, PREG_SET_ORDER ); |
| 237 | + $op = $field = $num = ''; |
| 238 | + switch ( $token->type ) { |
| 239 | + case 'bracket' : |
| 240 | + if ( $token->value == '(' ) { |
| 241 | + $op = 'lbracket'; |
| 242 | + } else { // $token->value == ')' |
| 243 | + $op = 'rbracket'; |
| 244 | + } |
| 245 | + break; |
| 246 | + case 'logical' : |
| 247 | + if ( $token->value == 'or' ) { |
| 248 | + $op = 'or'; |
| 249 | + } else { // $token->value == 'and' |
| 250 | + $op = 'and'; |
| 251 | + } |
| 252 | + break; |
| 253 | + case 'comparsion' : // comparsion subexpression |
| 254 | + $matches = array(); |
| 255 | + preg_match_all( CB_ENCODED_TOKEN_MATCH, $token->value, $matches, PREG_SET_ORDER ); |
249 | 256 | if ( count( $matches ) == 1 && isset( $matches[0] ) && count( $matches[0] ) == 4 ) { |
250 | | - list( $expr, $field, $cmp, $num ) = $matches[0]; |
251 | | - switch ( $cmp ) { |
252 | | - case '>=' : $op = 'ge'; break; |
253 | | - case '<=' : $op = 'le'; break; |
254 | | - case '=' : $op = 'eq'; break; |
255 | | - default: |
256 | | - $this->src_tokens = array( '' ); // default "all" |
257 | | - $this->local_tokens = array( wfMsg( 'cb_all_op' ) ); // localized default "all" |
258 | | - throw new MWException( 'Invalid operator ' . CB_Setup::entities( $token ) . ' in ' . __METHOD__ ); |
259 | | - } |
260 | | - } else { |
261 | | - $this->src_tokens = array( '' ); // default "all" |
262 | | - $this->local_tokens = array( wfMsg( 'cb_all_op' ) ); // localized default "all" |
263 | | - throw new MWException( 'Invalid operation ' . CB_Setup::entities( $token ) . ' in ' . __METHOD__ ); |
| 257 | + // localize comparsion op |
| 258 | + list( $expr, $cmp, $field, $num ) = $matches[0]; |
| 259 | + $op = $cmp; |
| 260 | + $field = CB_SqlCond::$decoded_fields[ $field ]; |
264 | 261 | } |
| 262 | + break; |
265 | 263 | } |
| 264 | + if ( $op == '' ) { |
| 265 | + $this->src_tokens = array(); // default "all" |
| 266 | + $this->local_tokens = array( wfMsg( 'cb_all_op' ) ); // localized default "all" |
| 267 | + throw new MWException( 'Invalid operation ' . CB_Setup::entities( $token ) . ' in ' . __METHOD__ ); } |
266 | 268 | if ( $field == '' ) { |
267 | 269 | $this->local_tokens[] = wfMsg( "cb_${op}_op" ); |
268 | 270 | } elseif ( $num == '' ) { |
— | — | @@ -286,190 +288,182 @@ |
287 | 289 | */ |
288 | 290 | class CB_SqlCond { |
289 | 291 | |
| 292 | + # general terminology : queue is a string of underscore separated operations |
| 293 | + # token arrays (RPN or infix) are an arrays of token objects |
| 294 | + # each is corresponding to queue's operations and vice versa |
| 295 | + |
| 296 | + # decoded_fields and sql_fields differ because in current scheme ( MW <= 1.16 ) |
| 297 | + # cat_pages include cat_subcats and cat_fields count as well |
| 298 | + # while we need to query against actual number of pages only |
| 299 | + |
| 300 | + # opcodes mapping for i18n |
290 | 301 | static $decoded_fields = array( 'p' => 'cat_pages', 's' => 'cat_subcats', 'f' => 'cat_files' ); |
291 | | - static $decoded_cmps = array( 'ge' => '>=', 'le' => '<=', 'eq' => '=' ); |
292 | | - |
| 302 | + # opcodes mapping for SQL |
| 303 | + static $sql_fields = array( 'p' => '(cat_pages - cat_subcats - cat_files)', 's' => 'cat_subcats', 'f' => 'cat_files' ); |
| 304 | + static $sql_cmps = array( 'ge' => '>=', 'le' => '<=', 'eq' => '=' ); |
| 305 | + # opcodes mapping for encoded queues (JS & cookie) |
293 | 306 | static $encoded_fields = array( 'cat_subcats' => 's', 'cat_pages' => 'p', 'cat_files' => 'f' ); |
294 | 307 | static $encoded_cmps = array( '>=' => 'ge', '<=' => 'le', '=' => 'eq' ); |
295 | 308 | |
296 | | - # reverse polish operations queue (decoded form, every op is an element of array) |
| 309 | + # RPN array (encoded form, every token is an element of array) |
297 | 310 | # comparsions like "a > 1" are treated like single-ops |
298 | | - # initialized in constructor (public static function) |
299 | | - var $queue; |
300 | | - private $queue_pos; // current position in output queue; used to generate triples |
| 311 | + # initialized in constructor (public static functions) |
| 312 | + var $rpn_tokens; |
| 313 | + private $rpn_tokens_pos; // current position in rpn_tokens; used to generate triples |
301 | 314 | |
302 | | - # infix operations queue |
| 315 | + # infix operations array (encoded form, every token is an element of array) |
303 | 316 | # comparsions like "a > 1" are treated like single-ops |
304 | | - var $infix_queue; |
| 317 | + # initialized in constructor (public static functions) |
| 318 | + var $infix_tokens; |
305 | 319 | |
306 | | - // used privately by decodeToken() |
| 320 | + // used privately by parseEncodedToken(), getSqlToken() |
307 | 321 | private static $valid_logical_ops; |
308 | 322 | private static $valid_bracket_ops; |
309 | 323 | |
310 | 324 | /* |
311 | | - * constructor (creates an instance, initializes $this->queue, returns an instance) |
| 325 | + * constructor (creates an instance, initializes $this->rpn_tokens, returns an instance) |
312 | 326 | * |
313 | | - * converts encoded reverse polish operations queue (string) to |
314 | | - * decoded reverse polish operations queue (array $this->queue) (1:1) |
315 | | - * @param $enc - string encoded reverse polish operations queue |
| 327 | + * converts encoded RPN queue (string) to RPN tokens array ($this->rpn_tokens) (1:1) |
| 328 | + * @param $enc - string encoded RPN queue |
316 | 329 | * (underscore-separated encoded polish tokens) |
317 | 330 | */ |
318 | 331 | public static function newFromEncodedPolishQueue( $enc ) { |
| 332 | + if ( !is_string( $enc ) ) { |
| 333 | + throw new MWException( 'First argument should be a string in ' . __METHOD__ ); |
| 334 | + } |
319 | 335 | $sc = new CB_SqlCond(); |
| 336 | + # parseEncodedToken(), getSqlToken() in RPN mode |
320 | 337 | self::$valid_logical_ops = array( 'and', 'or' ); |
321 | 338 | self::$valid_bracket_ops = array(); |
322 | | - $sc->queue = array(); |
| 339 | + $sc->rpn_tokens = array(); |
323 | 340 | $q = explode( '_', $enc ); |
324 | 341 | # {{{ validation of expression |
325 | 342 | $cmp_count = $logical_count = 0; |
326 | 343 | # }}} |
327 | 344 | foreach ( $q as &$token ) { |
328 | | - $result = self::decodeToken( $token ); |
329 | | - $sc->queue[] = $result->token; |
| 345 | + $result = self::parseEncodedToken( $token ); |
| 346 | + $sc->rpn_tokens[] = $result; |
330 | 347 | if ( $result->type == 'comparsion' ) { |
331 | 348 | $cmp_count++; |
332 | 349 | } elseif ( $result->type == 'logical' ) { |
333 | 350 | $logical_count++; |
334 | 351 | } else { |
335 | 352 | # tampered or bugged $enc, return default "all" instead |
336 | | - $sc->queue = array(); |
| 353 | + $sc->rpn_tokens = array(); |
337 | 354 | return $sc; |
338 | 355 | } |
339 | 356 | } |
340 | 357 | if ( $cmp_count < 1 || $cmp_count != $logical_count + 1 ) { |
341 | 358 | # tampered or bugged $enc, return default "all" instead |
342 | | - $sc->queue = array(); |
| 359 | + $sc->rpn_tokens = array(); |
343 | 360 | return $sc; |
344 | 361 | } |
345 | 362 | if ( $logical_count > CB_MAX_LOGICAL_OP ) { |
346 | 363 | # too complex $enc (fabricated or non-realistic), return default "all" instead |
347 | | - $sc->queue = array(); |
| 364 | + $sc->rpn_tokens = array(); |
348 | 365 | return $sc; |
349 | 366 | } |
350 | 367 | return $sc; |
351 | 368 | } |
352 | 369 | |
353 | 370 | /* |
354 | | - * constructor (creates an instance, initializes $this->infix_queue, returns an instance) |
| 371 | + * constructor (creates an instance, initializes $this->infix_tokens, returns an instance) |
355 | 372 | * |
356 | 373 | * converts encoded infix operations queue (string) to |
357 | | - * decoded infix operations queue (array $this->infix_queue) (1:1) |
358 | | - * then fills reverse polish operations queue $this->queue |
| 374 | + * decoded infix operations array $this->infix_tokens (1:1) |
| 375 | + * then fills RPN tokens array $this->rpn_tokens |
359 | 376 | * @param $enc - string encoded infix operations queue |
360 | 377 | * (underscore-separated encoded infix tokens) |
361 | 378 | */ |
362 | 379 | public static function newFromEncodedInfixQueue( $enc ) { |
| 380 | + if ( !is_string( $enc ) ) { |
| 381 | + throw new MWException( 'First argument should be a string in ' . __METHOD__ ); |
| 382 | + } |
| 383 | + # parseEncodedToken(), getSqlToken() in infix mode |
363 | 384 | self::$valid_logical_ops = array( 'and', 'or' ); |
364 | 385 | self::$valid_bracket_ops = array( '(', ')' ); |
365 | | - $infix_queue = array(); |
| 386 | + $infix_tokens = array(); |
366 | 387 | $q = explode( '_', $enc ); |
367 | 388 | # {{{ validation of expression |
368 | 389 | $brackets_level = 0; $prev_type = ''; |
369 | 390 | # }}} |
370 | 391 | foreach ( $q as &$token ) { |
371 | | - $result = self::decodeToken( $token ); |
372 | | - $infix_queue[] = $result->token; |
| 392 | + $result = self::parseEncodedToken( $token ); |
| 393 | + $infix_tokens[] = $result; |
373 | 394 | if ( $result->type == 'bracket' ) { |
374 | | - if ( $result->token == '(' ) { |
| 395 | + if ( $result->value == '(' ) { |
375 | 396 | $brackets_level++; |
376 | 397 | } else { |
377 | 398 | $brackets_level--; |
378 | 399 | } |
379 | 400 | if ( $brackets_level < 0 ) { |
380 | 401 | # tampered or bugged $enc, use default "all" instead |
381 | | - $infix_queue = array(); |
| 402 | + $infix_tokens = array(); |
382 | 403 | break; |
383 | 404 | } |
384 | 405 | } elseif ( $result->type == 'logical' ) { |
385 | 406 | if ( $prev_type == '' || $prev_type == 'logical' ) { |
386 | 407 | # tampered or bugged $enc, use default "all" instead |
387 | | - $infix_queue = array(); |
| 408 | + $infix_tokens = array(); |
388 | 409 | break; |
389 | 410 | } |
390 | 411 | } elseif ( $result->type == 'comparsion' ) { |
391 | 412 | if ( $prev_type == 'comparsion' ) { |
392 | 413 | # tampered or bugged $enc, use default "all" instead |
393 | | - $infix_queue = array(); |
| 414 | + $infix_tokens = array(); |
394 | 415 | break; |
395 | 416 | } |
396 | 417 | } else { |
397 | 418 | # tampered or bugged $enc, use default "all" instead |
398 | | - $infix_queue = array(); |
| 419 | + $infix_tokens = array(); |
399 | 420 | break; |
400 | 421 | } |
401 | 422 | $prev_type = $result->type; |
402 | 423 | } |
403 | 424 | if ( $brackets_level != 0 ) { |
404 | 425 | # tampered or bugged $enc, use default "all" instead |
405 | | - $infix_queue = array(); |
| 426 | + $infix_tokens = array(); |
406 | 427 | } |
407 | | - return self::newFromInfixTokens( $infix_queue ); |
| 428 | + return self::newFromInfixTokens( $infix_tokens ); |
408 | 429 | } |
409 | 430 | |
410 | | - private static function decodeToken( $token ) { |
411 | | - $result = (object) array( 'type' => 'unknown', 'token' => '' ); |
412 | | - $matches = array(); |
413 | | - preg_match_all( CB_ENCODED_TOKEN_MATCH, $token, $matches, PREG_SET_ORDER ); |
414 | | - if ( count( $matches ) == 1 && isset( $matches[0] ) && count( $matches[0] ) == 4 ) { |
415 | | - // decode comparsion op |
416 | | - $result->token = self::$decoded_fields[ $matches[0][2] ] . ' ' . self::$decoded_cmps[ $matches[0][1] ] . ' ' . (int) $matches[0][3]; |
417 | | - $result->type = 'comparsion'; |
418 | | - return $result; |
419 | | - } |
420 | | - $lo_token = strtolower( $token ); |
421 | | - if ( in_array( $lo_token, self::$valid_logical_ops ) ) { |
422 | | - // decode logical op |
423 | | - // we store logical ops uppercase for the "prettiness" |
424 | | - $result->token = strtoupper( $lo_token ); |
425 | | - $result->type = 'logical'; |
426 | | - return $result; |
427 | | - } |
428 | | - if ( in_array( $lo_token, self::$valid_bracket_ops ) ) { |
429 | | - // decode bracket op |
430 | | - $result->token = $lo_token; |
431 | | - $result->type = 'bracket'; |
432 | | - return $result; |
433 | | - } |
434 | | - return $result; |
435 | | - } |
436 | | - |
437 | 431 | /* |
438 | | - * constructor (creates an instance, initializes $this->queue, returns an instance) |
| 432 | + * constructor (creates an instance, initializes $this->rpn_tokens, returns an instance) |
439 | 433 | * |
440 | | - * fills up polish notation array $this->queue from infix $tokens provided |
441 | | - * @param $tokens - array of infix tokens |
| 434 | + * fills up RPN array $this->rpn_tokens from infix $tokens array provided |
| 435 | + * @param $tokens - array of encoded infix tokens |
442 | 436 | */ |
443 | | - # converts list of given infix $tokens into $this->queue of reverse polish notation |
444 | | - # TODO: more throughout checks for invalid tokens given |
445 | 437 | public static function newFromInfixTokens( array $tokens ) { |
446 | 438 | $sc = new CB_SqlCond(); |
447 | | - $stack = array(); // every element is stdobject with token and prio fields |
448 | | - $sc->queue = array(); |
| 439 | + $stack = array(); // every element is stdClass with stdClass(token) or stdClass(stdClass(token),prio) fields |
| 440 | + $sc->rpn_tokens = array(); |
449 | 441 | foreach ( $tokens as &$token ) { |
450 | | - switch ( strtoupper( $token ) ) { |
451 | | - case '(' : |
452 | | - $prio = 0; |
453 | | - array_push( $stack, (object) array( 'token' => $token, 'prio' => $prio ) ); |
454 | | - break; |
455 | | - case ')' : |
456 | | - $prio = 1; |
457 | | - while ( $last = array_pop( $stack ) ) { |
458 | | - if ( is_object( $last ) ) { |
459 | | - if ( $last->token == '(' ) { |
460 | | - break; |
| 442 | + switch ( $token->type ) { |
| 443 | + case 'bracket' : |
| 444 | + if ( $token->value == '(' ) { |
| 445 | + $prio = 0; |
| 446 | + array_push( $stack, (object) array( 'token' => $token, 'prio' => $prio ) ); |
| 447 | + } else { // $token->value == ')' |
| 448 | + $prio = 1; |
| 449 | + while ( $last = array_pop( $stack ) ) { |
| 450 | + if ( isset( $last->prio ) ) { |
| 451 | + # last element in stack is an container with 'prio' property (different array_push'es) |
| 452 | + if ( $last->token->value == '(' ) { |
| 453 | + break; |
| 454 | + } |
| 455 | + array_push( $sc->rpn_tokens, $last->token ); |
| 456 | + } else { |
| 457 | + # last element in stack is just a token object |
| 458 | + throw new MWException( 'Open / closing brackets mismatch in ' . __METHOD__ ); |
461 | 459 | } |
462 | | - array_push( $sc->queue, $last->token ); |
463 | | - } else { |
464 | | - throw new MWException( 'Open / closing brackets mismatch in ' . __METHOD__ ); |
465 | 460 | } |
466 | 461 | } |
467 | 462 | break; |
468 | | - case 'OR' : |
469 | | - case 'AND' : |
470 | | - $prio = strtoupper( $token ) == 'OR' ? 2 : 3; |
| 463 | + case 'logical' : |
| 464 | + $prio = $token->value == 'or' ? 2 : 3; |
471 | 465 | while ( $last = array_pop( $stack ) ) { |
472 | | - if ( is_object( $last ) && $last->prio >= $prio ) { |
473 | | - array_push( $sc->queue, $last->token ); |
| 466 | + if ( isset( $last->prio ) && $last->prio >= $prio ) { |
| 467 | + array_push( $sc->rpn_tokens, $last->token ); |
474 | 468 | } else { |
475 | 469 | array_push( $stack, $last ); |
476 | 470 | break; |
— | — | @@ -478,18 +472,81 @@ |
479 | 473 | array_push( $stack, (object) array( 'token' => $token, 'prio' => $prio ) ); |
480 | 474 | break; |
481 | 475 | default : // comparsion subexpression |
482 | | - array_push( $sc->queue, $token ); |
| 476 | + array_push( $sc->rpn_tokens, $token ); |
483 | 477 | } |
484 | 478 | } |
485 | 479 | while ( $last = array_pop( $stack ) ) { |
486 | | - if ( !is_object( $last ) ) { |
| 480 | + if ( !isset( $last->prio ) ) { |
487 | 481 | break; |
488 | 482 | } |
489 | | - array_push( $sc->queue, $last->token ); |
| 483 | + array_push( $sc->rpn_tokens, $last->token ); |
490 | 484 | } |
491 | 485 | return $sc; |
492 | 486 | } |
493 | 487 | |
| 488 | + /* |
| 489 | + * @param $value - encoded token value |
| 490 | + * @return encoded token object |
| 491 | + */ |
| 492 | + private static function parseEncodedToken( $value ) { |
| 493 | + if ( !is_string( $value ) ) { |
| 494 | + throw new MWException( 'First argument should be a string in ' . __METHOD__ ); |
| 495 | + } |
| 496 | + $result = (object) array( 'type' => 'unknown', 'value' => '' ); |
| 497 | + $matches = array(); |
| 498 | + preg_match_all( CB_ENCODED_TOKEN_MATCH, $value, $matches, PREG_SET_ORDER ); |
| 499 | + if ( count( $matches ) == 1 && isset( $matches[0] ) && count( $matches[0] ) == 4 ) { |
| 500 | + // decode comparsion op |
| 501 | + $result->value = $matches[0][1] . $matches[0][2] . intval( $matches[0][3] ); |
| 502 | + $result->type = 'comparsion'; |
| 503 | + return $result; |
| 504 | + } |
| 505 | + $lo_value = strtolower( $value ); |
| 506 | + if ( in_array( $lo_value, self::$valid_logical_ops ) ) { |
| 507 | + // decode logical op |
| 508 | + $result->value = $lo_value; |
| 509 | + $result->type = 'logical'; |
| 510 | + return $result; |
| 511 | + } |
| 512 | + if ( ($opcode = array_search( $value, self::$valid_bracket_ops )) !== false ) { |
| 513 | + // decode bracket op |
| 514 | + $result->value = $opcode; |
| 515 | + $result->type = 'bracket'; |
| 516 | + return $result; |
| 517 | + } |
| 518 | + return $result; |
| 519 | + } |
| 520 | + |
| 521 | + /* |
| 522 | + * @param $token - encoded token object |
| 523 | + * @result - SQL token |
| 524 | + */ |
| 525 | + private static function getSqlToken( stdClass $token ) { |
| 526 | + switch ( $token->type ) { |
| 527 | + case 'comparsion' : |
| 528 | + $matches = array(); |
| 529 | + preg_match_all( CB_ENCODED_TOKEN_MATCH, $token->value, $matches, PREG_SET_ORDER ); |
| 530 | + if ( count( $matches ) == 1 && isset( $matches[0] ) && count( $matches[0] ) == 4 ) { |
| 531 | + // decode comparsion op |
| 532 | + return self::$sql_fields[ $matches[0][2] ] . ' ' . self::$sql_cmps[ $matches[0][1] ] . ' ' . (int) $matches[0][3]; |
| 533 | + } |
| 534 | + break; |
| 535 | + case 'logical' : |
| 536 | + if ( in_array( $token->value, self::$valid_logical_ops ) ) { |
| 537 | + // decode logical op |
| 538 | + // we store logical ops uppercase for the "prettiness" |
| 539 | + return strtoupper( $token->value ); |
| 540 | + } |
| 541 | + break; |
| 542 | + case 'bracket' : |
| 543 | + if ( ($opcode = array_search( $token->value, self::$valid_bracket_ops )) !== false ) { |
| 544 | + // decode bracket op |
| 545 | + return self::$valid_bracket_ops[ $opcode ]; |
| 546 | + } |
| 547 | + } |
| 548 | + throw new MWException( 'Invalid operation type=' . CB_Setup::specialchars( $token->type ) . ' value=' . CB_Setup::specialchars( $token->value ) . ' in ' . __METHOD__ ); |
| 549 | + } |
| 550 | + |
494 | 551 | /* |
495 | 552 | src:'(', 'cat_pages > 1000', 'OR', 'cat_subcats > 10', ')', 'AND', 'cat_files > 100' |
496 | 553 | dst:cat_pages > 1000;cat_subcats > 10;OR;cat_files > 100;AND |
— | — | @@ -509,26 +566,26 @@ |
510 | 567 | ('','OR',('cat_subcats > 10','AND','cat_files > 100')) 'cat_subcats > 10' becomes left param of current triple, because right param is already occupied (level 1) |
511 | 568 | ('cat_pages > 1000','OR',('cat_subcats > 10','AND','cat_files > 100')) going level up because current triple was occupied; 'cat_pages > 1000' becomes left param of current triple |
512 | 569 | |
513 | | -1. global counter of queue position, getting elements consequtively from right to left |
| 570 | +1. global counter of rpn_tokens position, getting elements consequtively from right to left |
514 | 571 | 2. operators are going to current entry, in case currept op is free, otherwise going recursively from right to left (which position is inoccupied) |
515 | 572 | 3. operands are going to current entry, right to left |
516 | 573 | 4. generating the string recursively going from left to right |
517 | 574 | |
518 | 575 | in actual code (null,null,null) isset() is used instead of ('','','') |
519 | 576 | */ |
520 | | - # generate triples (see example above) from $this->queue |
| 577 | + # generate triples (see example above) from $this->rpn_tokens |
521 | 578 | # |
522 | 579 | # param &$currTriple - recursively adds new triples to $currTriple |
523 | | - private function buildTriples( &$currTriple ) { |
| 580 | + private function buildTriples( array &$currTriple ) { |
524 | 581 | # pre-initialize current triple |
525 | | - # recursively feed triples with queue tokens, right to left |
526 | | - while ( $this->queue_pos >= 0 ) { |
527 | | - $token = $this->queue[ $this->queue_pos ]; |
528 | | - if ( preg_match( '`^AND|OR$`i', $token ) ) { |
529 | | - // operators |
| 582 | + # recursively feed triples with rpn_tokens tokens, right to left |
| 583 | + while ( $this->rpn_tokens_pos >= 0 ) { |
| 584 | + $token = $this->rpn_tokens[ $this->rpn_tokens_pos ]; |
| 585 | + if ( $token->type == 'logical' ) { |
| 586 | + // our subset of polish notation has only logical operators |
530 | 587 | if ( !isset( $currTriple[1] ) ) { |
531 | 588 | $currTriple[1] = $token; |
532 | | - $this->queue_pos--; |
| 589 | + $this->rpn_tokens_pos--; |
533 | 590 | } elseif ( !isset( $currTriple[2] ) ) { |
534 | 591 | $currTriple[2] = array(); |
535 | 592 | $this->buildTriples( $currTriple[2] ); |
— | — | @@ -539,13 +596,13 @@ |
540 | 597 | return; |
541 | 598 | } |
542 | 599 | } else { |
543 | | - // operands |
| 600 | + // comparsions are treated as operands |
544 | 601 | if ( !isset( $currTriple[2] ) ) { |
545 | 602 | $currTriple[2] = $token; |
546 | | - $this->queue_pos--; |
| 603 | + $this->rpn_tokens_pos--; |
547 | 604 | } elseif ( !isset( $currTriple[0] ) ) { |
548 | 605 | $currTriple[0] = $token; |
549 | | - $this->queue_pos--; |
| 606 | + $this->rpn_tokens_pos--; |
550 | 607 | } else { |
551 | 608 | return; |
552 | 609 | } |
— | — | @@ -555,38 +612,41 @@ |
556 | 613 | |
557 | 614 | /* |
558 | 615 | * build properly bracketed infix expression string |
559 | | - * also builds $this->infix_queue array |
| 616 | + * also builds $this->infix_tokens array |
560 | 617 | * from triples tree previousely built by CategoryFilter::buildTriples (left to right) |
561 | 618 | */ |
562 | 619 | private $infixLevel; // used to do not include brackets at level 0 |
563 | | - private function getInfixExpr( &$out, $currTriple ) { |
| 620 | + private function getInfixExpr( &$out, array $currTriple ) { |
| 621 | + # parseEncodedToken(), getSqlToken() in infix mode |
| 622 | + self::$valid_logical_ops = array( 'and', 'or' ); |
| 623 | + self::$valid_bracket_ops = array( '(', ')' ); |
564 | 624 | $this->infixLevel++; |
565 | 625 | if ( $this->infixLevel != 0 ) { |
566 | | - $this->infix_queue[] = '('; |
| 626 | + $this->infix_tokens[] = (object) array( 'type' => 'bracket', 'value' => '(' ); |
567 | 627 | $out .= '('; |
568 | 628 | } |
569 | 629 | if ( isset( $currTriple[0] ) ) { |
570 | 630 | if ( is_array( $currTriple[0] ) ) { |
571 | 631 | $this->getInfixExpr( $out, $currTriple[0] ); |
572 | 632 | } else { |
573 | | - $this->infix_queue[] = $currTriple[0]; |
574 | | - $out .= $currTriple[0]; |
| 633 | + $this->infix_tokens[] = $currTriple[0]; |
| 634 | + $out .= $this->getSqlToken( $currTriple[0] ); |
575 | 635 | } |
576 | 636 | } |
577 | 637 | if ( isset( $currTriple[1] ) ) { |
578 | | - $this->infix_queue[] = $currTriple[1]; |
579 | | - $out .= ' ' . $currTriple[1] . ' '; |
| 638 | + $this->infix_tokens[] = $currTriple[1]; |
| 639 | + $out .= ' ' . $this->getSqlToken( $currTriple[1] ) . ' '; |
580 | 640 | } |
581 | 641 | if ( isset( $currTriple[2] ) ) { |
582 | 642 | if ( is_array( $currTriple[2] ) ) { |
583 | 643 | $this->getInfixExpr( $out, $currTriple[2] ); |
584 | 644 | } else { |
585 | | - $this->infix_queue[] = $currTriple[2]; |
586 | | - $out .= $currTriple[2]; |
| 645 | + $this->infix_tokens[] = $currTriple[2]; |
| 646 | + $out .= $this->getSqlToken( $currTriple[2] ); |
587 | 647 | } |
588 | 648 | } |
589 | 649 | if ( $this->infixLevel != 0 ) { |
590 | | - $this->infix_queue[] = ')'; |
| 650 | + $this->infix_tokens[] = (object) array( 'type' => 'bracket', 'value' => ')' ); |
591 | 651 | $out .= ')'; |
592 | 652 | } |
593 | 653 | $this->infixLevel--; |
— | — | @@ -594,79 +654,49 @@ |
595 | 655 | |
596 | 656 | /* |
597 | 657 | * get SQL condition expression with full brackets (to indicate operators priority) |
598 | | - * *** !!also builds $this->infix_queue array!! *** |
| 658 | + * *** !!also builds $this->infix_tokens array!! *** |
599 | 659 | */ |
600 | 660 | function getCond() { |
601 | 661 | $rootTriple = array(); |
602 | | - $this->queue_pos = count( $this->queue ) - 1; |
| 662 | + $this->rpn_tokens_pos = count( $this->rpn_tokens ) - 1; |
603 | 663 | $this->buildTriples( $rootTriple ); |
604 | 664 | $out = ''; |
605 | 665 | $this->infixLevel = -1; |
606 | | - $this->infix_queue = array(); |
607 | | - # also builds $this->infix_queue array |
| 666 | + $this->infix_tokens = array(); |
| 667 | + # also builds $this->infix_tokens array |
608 | 668 | $this->getInfixExpr( $out, $rootTriple ); |
609 | | - if ( count( $this->infix_queue ) == 0 ) { |
610 | | - $this->infix_queue = array( '' ); // default "all" |
| 669 | + if ( count( $this->infix_tokens ) == 0 ) { |
| 670 | + $this->infix_tokens = array(); // default "all" |
611 | 671 | } |
612 | 672 | return $out; |
613 | 673 | } |
614 | 674 | |
615 | 675 | /* |
616 | | - * get encoded queue to be stored in a cookie or passed from PHP AJAX handler to js callback |
617 | | - * @param $infix set true when infix queue is decoded, otherwise brackets cause to reset to default "all" |
| 676 | + * get encoded queue string to be stored in a cookie or passed from PHP AJAX handler to js callback |
| 677 | + * @param $infix set true when infix queue is decoded, otherwise brackets will cause to reset to default "all" |
618 | 678 | * |
619 | 679 | */ |
620 | 680 | function getEncodedQueue( $is_infix = false ) { |
621 | 681 | $result = ''; |
622 | 682 | if ( $is_infix ) { |
623 | | - $valid_single_ops = array( '(', ')', 'or', 'and' ); |
624 | | - if ( !is_array( $this->infix_queue ) ) { |
| 683 | + if ( !is_array( $this->infix_tokens ) ) { |
625 | 684 | $this->getCond(); |
626 | 685 | } |
627 | | - $queue = &$this->infix_queue; |
| 686 | + $tokens = &$this->infix_tokens; |
628 | 687 | } else { |
629 | | - $valid_single_ops = array( 'or', 'and' ); |
630 | | - $queue = &$this->queue; |
| 688 | + $tokens = &$this->rpn_tokens; |
631 | 689 | } |
632 | | - if ( count( $queue ) == 1 && $queue[0] == '' ) { |
| 690 | + if ( count( $tokens ) < 1 ) { |
633 | 691 | return 'all'; // default "show all" |
634 | 692 | } |
635 | 693 | $firstElem = true; |
636 | | - foreach ( $queue as &$token ) { |
| 694 | + foreach ( $tokens as &$token ) { |
637 | 695 | if ( $firstElem ) { |
638 | 696 | $firstElem = false; |
639 | 697 | } else { |
640 | 698 | $result .= '_'; |
641 | 699 | } |
642 | | - $field = $num = ''; |
643 | | - if ( in_array( $lo_token = strtolower( $token ), $valid_single_ops ) ) { |
644 | | - $op = $lo_token; |
645 | | - } else { |
646 | | - // comparsion subexpression ? |
647 | | - preg_match_all( CB_COND_TOKEN_MATCH, $token, $matches, PREG_SET_ORDER ); |
648 | | - if ( count( $matches ) == 1 && isset( $matches[0] ) && count( $matches[0] ) == 4 ) { |
649 | | - list( $expr, $field, $cmp, $num ) = $matches[0]; |
650 | | - if ( isset( self::$encoded_fields[ $field ] ) ) { |
651 | | - $field = self::$encoded_fields[ $field ]; |
652 | | - } else { |
653 | | - return 'all'; // default "show all" |
654 | | - } |
655 | | - if ( isset( self::$encoded_cmps[ $cmp ] ) ) { |
656 | | - $op = self::$encoded_cmps[ $cmp ]; |
657 | | - } else { |
658 | | - return 'all'; // default "show all" |
659 | | - } |
660 | | - } else { |
661 | | - return 'all'; // default "show all" |
662 | | - } |
663 | | - } |
664 | | - if ( $field == '' ) { |
665 | | - $result .= $op; |
666 | | - } elseif ( $num == '' ) { |
667 | | - $result .= $op . $field; |
668 | | - } else { |
669 | | - $result .= $op . $field . $num; |
670 | | - } |
| 700 | + $result .= $token->value; |
671 | 701 | } |
672 | 702 | return $result; |
673 | 703 | } |
Index: trunk/extensions/CategoryBrowser/category_browser.js |
— | — | @@ -26,7 +26,7 @@ |
27 | 27 | * * Add this line at the end of your LocalSettings.php file : |
28 | 28 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
29 | 29 | * |
30 | | - * @version 0.2.1 |
| 30 | + * @version 0.3.1 |
31 | 31 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
32 | 32 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
33 | 33 | * @addtogroup Extensions |
— | — | @@ -122,6 +122,15 @@ |
123 | 123 | return null; |
124 | 124 | }, |
125 | 125 | |
| 126 | + findParentByClassName : function( obj, className ) { |
| 127 | + for ( var parentObj = obj.parentNode; parentObj != null; parentObj = parentObj.parentNode ) { |
| 128 | + if ( parentObj.nodeType == 1 && parentObj.className == className ) { |
| 129 | + return parentObj; |
| 130 | + } |
| 131 | + } |
| 132 | + return parentObj; |
| 133 | + }, |
| 134 | + |
126 | 135 | /* |
127 | 136 | * TODO: unused, remove |
128 | 137 | * usage example: CB_lib.setCookie( 'rootcond', eventObj.value, 24 * 60 * 60, '/' ); |
— | — | @@ -233,7 +242,7 @@ |
234 | 243 | 'error': { 'color': 'red', 'bgr': 'aquamarine' }, |
235 | 244 | 'copy': { 'color': 'white', 'bgr': 'blue' } |
236 | 245 | }, |
237 | | - // delay in seconds before performing ajax call to reduce server load |
| 246 | + // delay in milliseconds before performing ajax call to reduce server load |
238 | 247 | 'ajaxPollTimeout' : 2000 |
239 | 248 | } /* end of CB_Setup */ |
240 | 249 | |
— | — | @@ -243,12 +252,17 @@ |
244 | 253 | * AJAX category tree section |
245 | 254 | */ |
246 | 255 | |
247 | | - // currently selected encoded reverse polish queue |
| 256 | + // {{{ root query condition parameters |
| 257 | + // currently selected encoded reverse polish queue (constructed in CB_ConditionEditor) |
248 | 258 | rootCond : null, |
| 259 | + // query only categories which has no parents |
| 260 | + noParentsOnly : false, |
249 | 261 | // category name filter (LIKE) |
250 | 262 | nameFilter : '', |
251 | 263 | // category name filter case-insensetive flag (when true, queries use LIKE COLLATE) |
252 | 264 | nameFilterCI : false, |
| 265 | + // }}} |
| 266 | + |
253 | 267 | // limit of pager query (also passed further via AJAX call) |
254 | 268 | pagerLimit : null, |
255 | 269 | // nested container object |
— | — | @@ -262,7 +276,7 @@ |
263 | 277 | */ |
264 | 278 | rootCats : function( rootCond, offset, limit ) { |
265 | 279 | this.rootCond = rootCond; |
266 | | - var param = [this.rootCond, this.nameFilter, this.nameFilterCI, offset]; |
| 280 | + var param = [this.rootCond, this.nameFilter, this.nameFilterCI, this.noParentsOnly, offset]; |
267 | 281 | if ( limit ) { |
268 | 282 | param.push( limit ); |
269 | 283 | } else { |
— | — | @@ -291,6 +305,7 @@ |
292 | 306 | /* |
293 | 307 | * create / get nested DOM container of the specified category DOM node |
294 | 308 | * @param node - category DOM node |
| 309 | + * @return DOM node of current nested container |
295 | 310 | */ |
296 | 311 | getNestedContainer : function( node ) { |
297 | 312 | var container = node.lastChild; |
— | — | @@ -302,64 +317,88 @@ |
303 | 318 | container.className = 'cb_nested_container'; |
304 | 319 | node.appendChild( container ); |
305 | 320 | } |
306 | | - this.container = container; |
| 321 | + return container; |
307 | 322 | }, |
308 | 323 | |
309 | 324 | /* |
310 | 325 | * switch visibility of container |
311 | | - * @param: id - set id attribute of nested container (used in setContainerVisibility() method) |
| 326 | + * @param: id - set id attribute of nested container (indicates the type of content placed into container) |
312 | 327 | */ |
313 | | - setContainerVisibility : function( id ) { |
314 | | - if ( this.container.style.display == 'none' || |
315 | | - this.container.getAttribute( 'id' ) != id ) { |
| 328 | + setContainerVisibility : function( id, isVisible ) { |
| 329 | + if ( isVisible ) { |
316 | 330 | this.container.style.display = 'block'; |
317 | 331 | } else { |
| 332 | + this.container.innerHTML = ''; |
318 | 333 | this.container.style.display = 'none'; |
319 | 334 | } |
320 | 335 | this.container.setAttribute( 'id', id ); |
321 | 336 | }, |
322 | 337 | |
323 | | - subCatsNav : function( eventObj, catId, offset, limit ) { |
324 | | - var param = ['subcats', catId]; |
325 | | - if ( offset ) { |
326 | | - param.push( offset ); |
327 | | - if ( limit ) { |
328 | | - param.push( limit ); |
329 | | - } |
| 338 | + subLink : function( eventObj, catName, pagerType, plusMode ) { |
| 339 | + eventObj.blur(); |
| 340 | + // find 'cat_expand_sign' container |
| 341 | + var expandSign = CB_lib.findParentByClassName( eventObj, 'cb_cat_controls' ); |
| 342 | + if ( expandSign === null || |
| 343 | + (expandSign = expandSign.firstChild) === null || |
| 344 | + (expandSign = expandSign.firstChild) === null ) { |
| 345 | + alert( 'Cannot find expand sign container in CategoryBrowser.subCatsLink() ' ); |
330 | 346 | } |
331 | | - this.subOffset( eventObj, param ); |
| 347 | + var treeNode = this.findNode( eventObj ); |
| 348 | + if ( treeNode == null ) { |
| 349 | + alert( 'Cannot find DOM node object of event click object in CategoryBrowser.subCatsLink()' ); |
| 350 | + return; |
| 351 | + } |
| 352 | + this.container = this.getNestedContainer( treeNode ); |
| 353 | + // when clicking an expanded (opened) node "sign", force collapsing (close node) |
| 354 | + var forceCollapsing = typeof plusMode !== 'undefined' && expandSign.innerHTML == '-'; |
| 355 | + if ( forceCollapsing || |
| 356 | + ( this.container.style.display != 'none' && |
| 357 | + this.container.getAttribute( 'id' ) == 'cb_nested_'+pagerType ) ) { |
| 358 | + // collapsing |
| 359 | + expandSign.innerHTML = '+'; |
| 360 | + this.setContainerVisibility( '', false ); |
| 361 | + } else { |
| 362 | + // expanding |
| 363 | + expandSign.innerHTML = '-'; |
| 364 | + var param = [pagerType, catName]; |
| 365 | + sajax_do_call( "CategoryBrowser::getSubOffsetHtml", param, this.container ); |
| 366 | + this.setContainerVisibility( 'cb_nested_'+pagerType, true ); |
| 367 | + } |
332 | 368 | return false; |
333 | 369 | }, |
334 | 370 | |
335 | | - subCatsPlus : function( eventObj, catId ) { |
336 | | - eventObj.blur(); |
337 | | - eventObj.innerHTML = ( eventObj.innerHTML == '+' ) ? '-' : '+'; |
338 | | - var param = ['subcats', catId]; |
339 | | - this.subOffset( eventObj, param ); |
340 | | - this.setContainerVisibility( 'cb_nested_subcats' ); |
341 | | - return false; |
| 371 | + subCatsPlus : function( eventObj, catName ) { |
| 372 | + return this.subLink( eventObj, catName, 'subcats', true ); |
342 | 373 | }, |
343 | 374 | |
344 | | - subCatsLink : function( eventObj, catId ) { |
345 | | - eventObj.blur(); |
346 | | - var param = ['subcats', catId]; |
347 | | - this.subOffset( eventObj, param ); |
348 | | - this.setContainerVisibility( 'cb_nested_subcats' ); |
349 | | - return false; |
| 375 | + subCatsLink : function( eventObj, catName ) { |
| 376 | + return this.subLink( eventObj, catName, 'subcats' ); |
350 | 377 | }, |
351 | 378 | |
352 | | - subOffset : function( eventObj, param ) { |
| 379 | + pagesLink : function( eventObj, catName ) { |
| 380 | + return this.subLink( eventObj, catName, 'pages' ); |
| 381 | + }, |
| 382 | + |
| 383 | + filesLink : function( eventObj, catName ) { |
| 384 | + return this.subLink( eventObj, catName, 'files' ); |
| 385 | + }, |
| 386 | + |
| 387 | + parentCatsLink : function( eventObj, catName ) { |
| 388 | + return this.subLink( eventObj, catName, 'parents' ); |
| 389 | + }, |
| 390 | + |
| 391 | + subNav : function( eventObj, param ) { |
353 | 392 | var treeNode = this.findNode( eventObj ); |
354 | 393 | if ( treeNode == null ) { |
355 | | - alert( 'Cannot find DOM node object of event click object in CategoryBrowser.subOffset()' ); |
| 394 | + alert( 'Cannot find DOM node object of event click object in CategoryBrowser.subNav()' ); |
356 | 395 | return; |
357 | 396 | } |
358 | | - this.getNestedContainer( treeNode ); |
| 397 | + this.container = this.getNestedContainer( treeNode ); |
359 | 398 | sajax_do_call( "CategoryBrowser::getSubOffsetHtml", param, this.container ); |
360 | 399 | }, |
361 | 400 | |
362 | | - pagesNav : function( eventObj, catId, offset, limit ) { |
363 | | - var param = ['pages', catId]; |
| 401 | + subCatsNav : function( eventObj, catName, offset, limit ) { |
| 402 | + var param = ['subcats', catName]; |
364 | 403 | if ( offset ) { |
365 | 404 | param.push( offset ); |
366 | 405 | if ( limit ) { |
— | — | @@ -367,20 +406,25 @@ |
368 | 407 | } |
369 | 408 | } |
370 | 409 | // used .parentNode to skip self container |
371 | | - this.subOffset( eventObj.parentNode, param ); |
| 410 | + this.subNav( eventObj.parentNode, param ); |
372 | 411 | return false; |
373 | 412 | }, |
374 | 413 | |
375 | | - pagesLink : function( eventObj, catId ) { |
376 | | - eventObj.blur(); |
377 | | - var param = ['pages', catId]; |
378 | | - this.subOffset( eventObj, param ); |
379 | | - this.setContainerVisibility( 'cb_nested_pages' ); |
| 414 | + pagesNav : function( eventObj, catName, offset, limit ) { |
| 415 | + var param = ['pages', catName]; |
| 416 | + if ( offset ) { |
| 417 | + param.push( offset ); |
| 418 | + if ( limit ) { |
| 419 | + param.push( limit ); |
| 420 | + } |
| 421 | + } |
| 422 | + // used .parentNode to skip self container |
| 423 | + this.subNav( eventObj.parentNode, param ); |
380 | 424 | return false; |
381 | 425 | }, |
382 | 426 | |
383 | | - filesNav : function( eventObj, catId, offset, limit ) { |
384 | | - var param = ['files', catId]; |
| 427 | + filesNav : function( eventObj, catName, offset, limit ) { |
| 428 | + var param = ['files', catName]; |
385 | 429 | if ( offset ) { |
386 | 430 | param.push( offset ); |
387 | 431 | if ( limit ) { |
— | — | @@ -388,63 +432,71 @@ |
389 | 433 | } |
390 | 434 | } |
391 | 435 | // used .parentNode to skip self container |
392 | | - this.subOffset( eventObj.parentNode, param ); |
| 436 | + this.subNav( eventObj.parentNode, param ); |
393 | 437 | return false; |
394 | 438 | }, |
395 | 439 | |
396 | | - filesLink : function( eventObj, catId ) { |
| 440 | + parentCatsNav : function( eventObj, catName, offset, limit ) { |
| 441 | + var param = ['parents', catName]; |
| 442 | + if ( offset ) { |
| 443 | + param.push( offset ); |
| 444 | + if ( limit ) { |
| 445 | + param.push( limit ); |
| 446 | + } |
| 447 | + } |
| 448 | + // used .parentNode to skip self container |
| 449 | + this.subNav( eventObj.parentNode, param ); |
| 450 | + return false; |
| 451 | + }, |
| 452 | + |
| 453 | + searchForRoot : function( eventObj, catName ) { |
397 | 454 | eventObj.blur(); |
398 | | - var param = ['files', catId]; |
399 | | - this.subOffset( eventObj, param ); |
400 | | - this.setContainerVisibility( 'cb_nested_files' ); |
| 455 | + document.getElementById( 'cb_cat_name_filter' ).value = catName; |
| 456 | + this.setFilter(); |
401 | 457 | return false; |
402 | 458 | }, |
403 | 459 | |
404 | | - setNameFilter : function( eventObj ) { |
405 | | - var id = eventObj.getAttribute( 'id' ); |
| 460 | + setFilter : function() { |
406 | 461 | if ( this.rootCond === null ) { |
407 | 462 | this.rootCond = document.getElementById( 'cb_expr_select' ).value; |
408 | 463 | if ( this.rootCond === null ) { |
409 | | - alert( 'Cannot find selected rootCond option in CategoryBrowser.setNameFilter' ); |
| 464 | + alert( 'Cannot find selected rootCond option in CategoryBrowser.setFilter' ); |
410 | 465 | } |
411 | 466 | } |
412 | | - switch ( id ) { |
413 | | - case 'cb_cat_name_filter' : |
414 | | - /* more reliable to check in nameFilterCall |
415 | | - this.nameFilter = eventObj.value; |
416 | | - CB_lib.log( 'setNameFilter nameFilter='+this.nameFilter ); */ |
417 | | - break; |
418 | | - case 'cb_cat_name_filter_ci' : |
419 | | - /* more reliable to check in nameFilterCall |
420 | | - this.nameFilterCI = eventObj.checked; */ |
421 | | - break; |
422 | | - default : alert( 'CategoryBrowser.setNameFilter was called with unknown event object id='+id ); |
423 | | - } |
424 | | - window.setTimeout( function() { CategoryBrowser.nameFilterCall(); }, CB_Setup.ajaxPollTimeout ); |
| 467 | + window.setTimeout( function() { CategoryBrowser.filterPoll(); }, CB_Setup.ajaxPollTimeout ); |
425 | 468 | return true; |
426 | 469 | }, |
427 | 470 | |
428 | | - nameFilterCall : function() { |
| 471 | + clearNameFilter : function( eventObj ) { |
| 472 | + this.searchForRoot( eventObj, '' ); |
| 473 | + document.getElementById( 'cb_cat_name_filter' ).focus(); |
| 474 | + return false; |
| 475 | + }, |
| 476 | + |
| 477 | + filterPoll : function() { |
429 | 478 | if ( this.rootCond === null ) { |
430 | 479 | this.rootCond = document.getElementById( 'cb_expr_select' ).value; |
431 | 480 | } |
432 | 481 | var nameFilter = document.getElementById( 'cb_cat_name_filter' ).value; |
433 | 482 | var CIcheckbox = document.getElementById( 'cb_cat_name_filter_ci' ); |
434 | | - if ( CIcheckbox !== null ) { |
435 | | - var nameFilterCI = CIcheckbox.checked; |
436 | | - } |
| 483 | + var nameFilterCI = CIcheckbox !== null ? CIcheckbox.checked : false; |
| 484 | + var noParentsCheckbox = document.getElementById( 'cb_cat_no_parents_only' ); |
| 485 | + var noParentsOnly = noParentsCheckbox !== null ? noParentsCheckbox.checked : false; |
437 | 486 | if ( this.rootCond !== null && |
438 | | - ( this.nameFilter != nameFilter || this.nameFilterCI != nameFilterCI ) ) { |
| 487 | + ( this.nameFilter != nameFilter || |
| 488 | + this.nameFilterCI != nameFilterCI || |
| 489 | + this.noParentsOnly != noParentsOnly ) ) { |
439 | 490 | // in case nameFilter field was changed, update the root pager |
440 | 491 | this.nameFilter = nameFilter; |
441 | 492 | this.nameFilterCI = nameFilterCI; |
442 | | - var param = [this.rootCond, this.nameFilter, this.nameFilterCI, 0]; |
| 493 | + this.noParentsOnly = noParentsOnly; |
| 494 | + var param = [this.rootCond, this.nameFilter, this.nameFilterCI, this.noParentsOnly, 0]; |
443 | 495 | if ( this.pagerLimit !== null ) { |
444 | 496 | param.push( this.pagerLimit ); |
445 | 497 | } |
446 | 498 | sajax_do_call( "CategoryBrowser::getRootOffsetHtml", param, document.getElementById( "cb_root_container" ) ); |
447 | 499 | } |
448 | | - window.setTimeout( function() { CategoryBrowser.nameFilterCall(); }, CB_Setup.ajaxPollTimeout ); |
| 500 | + window.setTimeout( function() { CategoryBrowser.filterPoll(); }, CB_Setup.ajaxPollTimeout ); |
449 | 501 | }, |
450 | 502 | |
451 | 503 | /* |
— | — | @@ -461,7 +513,7 @@ |
462 | 514 | CB_lib.log('setExpr selectedEncInfixQueue='+selectedEncInfixQueue); |
463 | 515 | this.pagerLimit = pagerLimit; |
464 | 516 | CB_lib.log( 'setExpr refreshing with value='+eventObj.value ); |
465 | | - sajax_do_call( "CategoryBrowser::getRootOffsetHtml", [this.rootCond, this.nameFilter, this.nameFilterCI, 0, this.pagerLimit], document.getElementById( "cb_root_container" ) ); |
| 517 | + sajax_do_call( "CategoryBrowser::getRootOffsetHtml", [this.rootCond, this.nameFilter, this.nameFilterCI, this.noParentsOnly, 0, this.pagerLimit], document.getElementById( "cb_root_container" ) ); |
466 | 518 | CB_ConditionEditor.createExpr( selectedEncInfixQueue ); |
467 | 519 | return true; |
468 | 520 | } |
— | — | @@ -1621,7 +1673,7 @@ |
1622 | 1674 | var encInfixQueue = this.conditionLine.getEncodedExpr(); |
1623 | 1675 | var appliedOption = CB_lib.getSelectOption( document.getElementById( 'cb_expr_select' ), encInfixQueue, 'infixexpr' ); |
1624 | 1676 | var setCookie = appliedOption == null ? 1 : 0; |
1625 | | - var param = [ encInfixQueue, CategoryBrowser.nameFilter, CategoryBrowser.nameFilterCI, setCookie ]; |
| 1677 | + var param = [ encInfixQueue, CategoryBrowser.nameFilter, CategoryBrowser.nameFilterCI, CategoryBrowser.noParentsOnly, setCookie ]; |
1626 | 1678 | if ( CategoryBrowser.pagerLimit !== null ) { |
1627 | 1679 | param.push( CategoryBrowser.pagerLimit ); |
1628 | 1680 | } |
Index: trunk/extensions/CategoryBrowser/CategoryBrowserCtrl.php |
— | — | @@ -27,7 +27,7 @@ |
28 | 28 | * * Add this line at the end of your LocalSettings.php file : |
29 | 29 | * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
30 | 30 | * |
31 | | - * @version 0.2.1 |
| 31 | + * @version 0.3.1 |
32 | 32 | * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
33 | 33 | * @author Dmitriy Sintsov <questpc@rambler.ru> |
34 | 34 | * @addtogroup Extensions |
— | — | @@ -131,8 +131,9 @@ |
132 | 132 | static function generateRanges( array &$source_ranges ) { |
133 | 133 | $ranges = array(); |
134 | 134 | foreach ( $source_ranges as $infix_queue ) { |
135 | | - $sqlCond = CB_SqlCond::newFromInfixTokens( $infix_queue ); |
136 | | - $ranges[] = (object) array( 'infix_decoded' => $infix_queue, 'polish_encoded' => $sqlCond->getEncodedQueue( false ) ); |
| 135 | + $sqlCond = CB_SqlCond::newFromEncodedInfixQueue( $infix_queue ); |
| 136 | + $sqlCond->getCond(); // build $sqlCond->infix_tokens |
| 137 | + $ranges[] = (object) array( 'infix_tokens' => $sqlCond->infix_tokens, 'rpn_queue' => $sqlCond->getEncodedQueue( false ) ); |
137 | 138 | } |
138 | 139 | return $ranges; |
139 | 140 | } |
— | — | @@ -147,14 +148,14 @@ |
148 | 149 | $encPolishQueue = $sqlCond->getEncodedQueue( false ); |
149 | 150 | $queueExists = false; |
150 | 151 | foreach ( $ranges as &$range ) { |
151 | | - if ( $range->polish_encoded == $encPolishQueue ) { |
| 152 | + if ( $range->rpn_queue == $encPolishQueue ) { |
152 | 153 | $queueExists = true; |
153 | 154 | break; |
154 | 155 | } |
155 | 156 | } |
156 | 157 | if ( !$queueExists ) { |
157 | | - $sqlCond->getCond(); // build infix queue array |
158 | | - $ranges[] = (object) array( 'infix_decoded' => $sqlCond->infix_queue, 'polish_encoded' => $encPolishQueue ); |
| 158 | + $sqlCond->getCond(); // build $sqlCond->infix_tokens array |
| 159 | + $ranges[] = (object) array( 'infix_tokens' => $sqlCond->infix_tokens, 'rpn_queue' => $encPolishQueue ); |
159 | 160 | } |
160 | 161 | } |
161 | 162 | |
— | — | @@ -184,7 +185,7 @@ |
185 | 186 | $selectedEncPolishQueue = $rootPager->sqlCond->getEncodedQueue( false ); |
186 | 187 | foreach ( $ranges as &$range ) { |
187 | 188 | $condOptList[] = self::generateOption( $range, $selectedEncPolishQueue ); |
188 | | - if ( $range->polish_encoded == $selectedEncPolishQueue ) { |
| 189 | + if ( $range->rpn_queue == $selectedEncPolishQueue ) { |
189 | 190 | $queueFound = true; |
190 | 191 | } |
191 | 192 | } |
— | — | @@ -202,11 +203,11 @@ |
203 | 204 | $condOptTpl = |
204 | 205 | array( '__tag' => $nodeName, 'value' => &$condOptVal, 'infixexpr' => &$condOptInfix, 0 => &$condOptName, '__end' => "\n" ); |
205 | 206 | # }}} |
206 | | - $le = new CB_LocalExpr( $range->infix_decoded ); |
207 | | - $condOptVal = CB_Setup::specialchars( $range->polish_encoded ); |
208 | | - $sqlCond = CB_SqlCond::newFromEncodedPolishQueue( $range->polish_encoded ); |
| 207 | + $le = new CB_LocalExpr( $range->infix_tokens ); |
| 208 | + $condOptVal = CB_Setup::specialchars( $range->rpn_queue ); |
| 209 | + $sqlCond = CB_SqlCond::newFromEncodedPolishQueue( $range->rpn_queue ); |
209 | 210 | $condOptInfix = CB_Setup::specialchars( $sqlCond->getEncodedQueue( true ) ); |
210 | | - if ( $range->polish_encoded == $selectedValue ) { |
| 211 | + if ( $range->rpn_queue == $selectedValue ) { |
211 | 212 | $condOptTpl['selected'] = null; |
212 | 213 | } |
213 | 214 | $condOptName = CB_Setup::entities( $le->toString() ); |
— | — | @@ -219,20 +220,23 @@ |
220 | 221 | * @param $args[0] : encoded reverse polish queue |
221 | 222 | * @param $args[1] : category name filter string |
222 | 223 | * @param $args[2] : category name filter case insensitive flag |
223 | | - * @param $args[3] : offset (optional) |
224 | | - * @param $args[4] : limit (optional) |
| 224 | + * @param $args[3] : browse only categories which has no parents flag (by default, false) |
| 225 | + * @param $args[4] : offset (optional) |
| 226 | + * @param $args[5] : limit (optional) |
225 | 227 | */ |
226 | 228 | static function getRootOffsetHtml() { |
227 | 229 | wfLoadExtensionMessages( 'CategoryBrowser' ); |
228 | 230 | $args = func_get_args(); |
229 | | - $limit = ( count( $args ) > 4 ) ? abs( intval( $args[4] ) ) : CB_PAGING_ROWS; |
230 | | - $offset = ( count( $args ) > 3 ) ? abs( intval( $args[3] ) ) : 0; |
| 231 | + $limit = ( count( $args ) > 5 ) ? abs( intval( $args[5] ) ) : CB_PAGING_ROWS; |
| 232 | + $offset = ( count( $args ) > 4 ) ? abs( intval( $args[4] ) ) : 0; |
| 233 | + $noParentsOnly = ( count( $args ) > 3 ) ? $args[3] == 'true' : false; |
231 | 234 | $nameFilterCI = ( count( $args ) > 2 ) ? $args[2] == 'true' : false; |
232 | 235 | $nameFilter = ( count( $args ) > 1 ) ? $args[1] : ''; |
233 | 236 | $encPolishQueue = ( count( $args ) > 0 ) ? $args[0] : 'all'; |
234 | 237 | $cb = new CategoryBrowser(); |
235 | 238 | $sqlCond = CB_SqlCond::newFromEncodedPolishQueue( $encPolishQueue ); |
236 | 239 | $rootPager = CB_RootPager::newFromSqlCond( $sqlCond, $offset, $limit ); |
| 240 | + $rootPager->setNoParentsOnly( $noParentsOnly ); |
237 | 241 | $rootPager->setNameFilter( $nameFilter, $nameFilterCI ); |
238 | 242 | $rootPager->getCurrentRows(); |
239 | 243 | $catView = new CB_CategoriesView( $rootPager ); |
— | — | @@ -241,9 +245,35 @@ |
242 | 246 | } |
243 | 247 | |
244 | 248 | /* |
| 249 | + * called via AJAX to setup custom edited expression cookie then display category root offset |
| 250 | + * @param $args[0] : encoded infix expression |
| 251 | + * @param $args[1] : category name filter string |
| 252 | + * @param $args[2] : category name filter case insensitive flag |
| 253 | + * @param $args[3] : browse only categories which has no parents flag (by default, false) |
| 254 | + * @param $args[4] : 1 - cookie has to be set, 0 - cookie should not be set (expression is pre-defined or already was stored) |
| 255 | + * @param $args[5] : pager limit (optional) |
| 256 | + */ |
| 257 | + static function applyEncodedQueue() { |
| 258 | + CB_Setup::initUser(); |
| 259 | + $args = func_get_args(); |
| 260 | + $limit = ( ( count( $args ) > 5 ) ? intval( $args[5] ) : CB_PAGING_ROWS ); |
| 261 | + $setCookie = ( ( count( $args ) > 4 ) ? $args[4] != 0 : false ); |
| 262 | + $noParentsOnly = ( count( $args ) > 3 ) ? $args[3] == 'true' : false; |
| 263 | + $nameFilterCI = ( count( $args ) > 2 ) ? $args[2] == 'true' : false; |
| 264 | + $nameFilter = ( count( $args ) > 1 ) ? $args[1] : ''; |
| 265 | + $encInfixQueue = ( ( count( $args ) > 0 ) ? $args[0] : 'all' ); |
| 266 | + $sqlCond = CB_SqlCond::newFromEncodedInfixQueue( $encInfixQueue ); |
| 267 | + $encPolishQueue = $sqlCond->getEncodedQueue( false ); |
| 268 | + if ( $setCookie ) { |
| 269 | + CB_Setup::setCookie( 'rootcond', $encPolishQueue, time() + 60 * 60 * 24 * 7 ); |
| 270 | + } |
| 271 | + return self::getRootOffsetHtml( $encPolishQueue, $nameFilter, $nameFilterCI, $noParentsOnly, 0, $limit ); |
| 272 | + } |
| 273 | + |
| 274 | + /* |
245 | 275 | * called via AJAX to get list of (subcategories,pages,files) for specitied parent category id, offset, limit |
246 | 276 | * @param $args[0] : type of pager ('subcats','pages','files') |
247 | | - * @param $args[1] : parent category id |
| 277 | + * @param $args[1] : parent category name |
248 | 278 | * @param $args[2] : offset (optional) |
249 | 279 | * @param $args[3] : limit (optional) |
250 | 280 | */ |
— | — | @@ -251,7 +281,7 @@ |
252 | 282 | $pager_types = array( |
253 | 283 | 'subcats' => array( |
254 | 284 | 'js_nav_func' => "subCatsNav", |
255 | | - 'select_fields' => "cl_sortkey, cat_id, cat_title, cat_subcats, cat_pages, cat_files", |
| 285 | + 'select_fields' => "cl_sortkey, cat_id, cat_title, cat_subcats, " . CB_AbstractPager::$cat_pages_only . ", cat_files", |
256 | 286 | 'ns_cond' => "page_namespace = " . NS_CATEGORY, |
257 | 287 | 'default_limit' => CB_Setup::$categoriesLimit |
258 | 288 | ), |
— | — | @@ -266,6 +296,9 @@ |
267 | 297 | 'select_fields' => "page_title, page_namespace, page_len, page_is_redirect", |
268 | 298 | 'ns_cond' => "page_namespace = " . NS_FILE, |
269 | 299 | 'default_limit' => CB_Setup::$filesLimit |
| 300 | + ), |
| 301 | + 'parents' => array( |
| 302 | + 'default_limit' => CB_Setup::$parentsLimit |
270 | 303 | ) |
271 | 304 | ); |
272 | 305 | wfLoadExtensionMessages( 'CategoryBrowser' ); |
— | — | @@ -277,18 +310,23 @@ |
278 | 311 | if ( !isset( $pager_types[ $pager_type ] ) ) { |
279 | 312 | return 'Unknown pager type=' . CB_Setup::specialchars( $pager_type ) . ' in ' . __METHOD__; |
280 | 313 | } |
281 | | - $pager_setup = & $pager_types[ $args[0] ]; |
| 314 | + $pager_setup = & $pager_types[ $pager_type ]; |
282 | 315 | $limit = ( count( $args ) > 3 ) ? abs( intval( $args[3] ) ) : $pager_setup[ 'default_limit' ]; |
283 | 316 | $offset = ( count( $args ) > 2 ) ? abs( intval( $args[2] ) ) : 0; |
284 | | - $parentCatId = abs( intval( $args[1] ) ); |
| 317 | + $parentCatName = $args[1]; |
285 | 318 | $cb = new CategoryBrowser(); |
286 | | - $pager = new CB_SubPager( $parentCatId, $offset, $limit, |
287 | | - $pager_setup[ 'js_nav_func' ], |
288 | | - $pager_setup[ 'select_fields' ], |
289 | | - $pager_setup[ 'ns_cond' ] ); |
| 319 | + if ( $pager_type == 'parents' ) { |
| 320 | + $pager = new CB_ParentPager( $parentCatName, $offset, $limit ); |
| 321 | + } else { |
| 322 | + $pager = new CB_SubPager( $parentCatName, $offset, $limit, |
| 323 | + $pager_setup[ 'js_nav_func' ], |
| 324 | + $pager_setup[ 'select_fields' ], |
| 325 | + $pager_setup[ 'ns_cond' ] ); |
| 326 | + } |
290 | 327 | $pager->getCurrentRows(); |
291 | 328 | switch ( $pager_type ) { |
292 | 329 | case 'subcats' : |
| 330 | + case 'parents' : |
293 | 331 | $view = new CB_CategoriesView( $pager ); |
294 | 332 | break; |
295 | 333 | case 'pages' : |
— | — | @@ -314,30 +352,6 @@ |
315 | 353 | } |
316 | 354 | |
317 | 355 | /* |
318 | | - * called via AJAX to setup custom edited expression cookie then display category root offset |
319 | | - * @param $args[0] : encoded infix expression |
320 | | - * @param $args[1] : category name filter string |
321 | | - * @param $args[2] : category name filter case insensitive flag |
322 | | - * @param $args[3] : 1 - cookie has to be set, 0 - cookie should not be set (expression is pre-defined or already was stored) |
323 | | - * @param $args[4] : pager limit (optional) |
324 | | - */ |
325 | | - static function applyEncodedQueue() { |
326 | | - CB_Setup::initUser(); |
327 | | - $args = func_get_args(); |
328 | | - $limit = ( ( count( $args ) > 4 ) ? intval( $args[4] ) : CB_PAGING_ROWS ); |
329 | | - $setCookie = ( ( count( $args ) > 3 ) ? $args[3] != 0 : false ); |
330 | | - $nameFilterCI = ( count( $args ) > 2 ) ? $args[2] == 'true' : false; |
331 | | - $nameFilter = ( count( $args ) > 1 ) ? $args[1] : ''; |
332 | | - $encInfixQueue = ( ( count( $args ) > 0 ) ? $args[0] : 'all' ); |
333 | | - $sqlCond = CB_SqlCond::newFromEncodedInfixQueue( $encInfixQueue ); |
334 | | - $encPolishQueue = $sqlCond->getEncodedQueue( false ); |
335 | | - if ( $setCookie ) { |
336 | | - CB_Setup::setCookie( 'rootcond', $encPolishQueue, time() + 60 * 60 * 24 * 7 ); |
337 | | - } |
338 | | - return self::getRootOffsetHtml( $encPolishQueue, $nameFilter, $nameFilterCI, 0, $limit ); |
339 | | - } |
340 | | - |
341 | | - /* |
342 | 356 | * called via AJAX to generate new selected option when the selected rootcond is new (the rootcond cookie was set) |
343 | 357 | * @param $args[0] currently selected expression in encoded infix format |
344 | 358 | */ |
Index: trunk/extensions/CategoryBrowser/README |
— | — | @@ -1,4 +1,4 @@ |
2 | | -MediaWiki extension CategoryBrowser, version 0.2.1 |
| 2 | +MediaWiki extension CategoryBrowser, version 0.3.1 |
3 | 3 | |
4 | 4 | CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
5 | 5 | Categories can be filtered with pre-defined and also with used-defined conditions. |