Index: trunk/extensions/CategoryBrowser/CategoryBrowserPage.php |
— | — | @@ -0,0 +1,152 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of CategoryBrowser. |
| 6 | + * |
| 7 | + * CategoryBrowser is free software; you can redistribute it and/or modify |
| 8 | + * it under the terms of the GNU General Public License as published by |
| 9 | + * the Free Software Foundation; either version 2 of the License, or |
| 10 | + * (at your option) any later version. |
| 11 | + * |
| 12 | + * CategoryBrowser is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | + * GNU General Public License for more details. |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU General Public License |
| 18 | + * along with CategoryBrowser; if not, write to the Free Software |
| 19 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 20 | + * |
| 21 | + * ***** END LICENSE BLOCK ***** |
| 22 | + * |
| 23 | + * CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
| 24 | + * |
| 25 | + * To activate this extension : |
| 26 | + * * Create a new directory named CategoryBrowser into the directory "extensions" of MediaWiki. |
| 27 | + * * Place the files from the extension archive there. |
| 28 | + * * Add this line at the end of your LocalSettings.php file : |
| 29 | + * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
| 30 | + * |
| 31 | + * @version 0.2.0 |
| 32 | + * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
| 33 | + * @author Dmitriy Sintsov <questpc@rambler.ru> |
| 34 | + * @addtogroup Extensions |
| 35 | + */ |
| 36 | + |
| 37 | +if( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is a part of MediaWiki extension.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +class CategoryBrowserPage extends SpecialPage { |
| 42 | + |
| 43 | + function __construct() { |
| 44 | + parent::__construct( 'CategoryBrowser' ); |
| 45 | + wfLoadExtensionMessages( 'CategoryBrowser' ); |
| 46 | + CB_Setup::initUser(); |
| 47 | + } |
| 48 | + |
| 49 | + function setHeaders() { |
| 50 | + global $wgOut, $wgContLang; |
| 51 | + parent::setHeaders(); |
| 52 | + CategoryBrowser::headScripts( $wgOut, $wgContLang->isRTL() ); |
| 53 | + } |
| 54 | + |
| 55 | + var $source_ranges = |
| 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 | + ); |
| 75 | + var $ranges; |
| 76 | + |
| 77 | + function execute( $param ) { |
| 78 | + global $wgOut; |
| 79 | + $this->setHeaders(); |
| 80 | + $this->ranges = CategoryBrowser::generateRanges( $this->source_ranges ); |
| 81 | + $cb = new CategoryBrowser(); |
| 82 | + # try to create rootPager from rootcond cookie value |
| 83 | + if ( is_string( $encPolishQueue = CB_Setup::getCookie( 'rootcond' ) ) ) { |
| 84 | + $sqlCond = CB_SqlCond::newFromEncodedPolishQueue( $encPolishQueue ); |
| 85 | + $rootPager = CB_RootPager::newFromSqlCond( $sqlCond ); |
| 86 | + # add selected condition to range, if not duplicate |
| 87 | + CategoryBrowser::addRange( $this->ranges, $rootPager->sqlCond ); |
| 88 | + } else { |
| 89 | + # otherwise, try to create rootPager from the list of predefined infix queues (ranges) |
| 90 | + if ( !is_object( $rootPager = CB_RootPager::newFromCategoryRange( $this->ranges ) ) ) { |
| 91 | + return; |
| 92 | + } |
| 93 | + } |
| 94 | + $rootPager->getCurrentRows(); |
| 95 | + /* reverse polish queue encode / decode validations */ |
| 96 | + $testCond = CB_SqlCond::newFromEncodedPolishQueue( $rootPager->sqlCond->getEncodedQueue( false ) ); |
| 97 | + if ( $rootPager->sqlCond->getCond() != $testCond->getCond() ) { |
| 98 | + throw new MWException( 'Infix queue was not re-built correctly from encoded polish queue in ' . __METHOD__ ); |
| 99 | + } |
| 100 | + /* infix queue encode / decode validations */ |
| 101 | + $testCond = CB_SqlCond::newFromEncodedInfixQueue( $rootPager->sqlCond->getEncodedQueue( true ) ); |
| 102 | + if ( $rootPager->sqlCond->getCond() != $testCond->getCond() ) { |
| 103 | + throw new MWException( 'Infix queue was not re-built correctly from encoded infix queue in ' . __METHOD__ ); |
| 104 | + } |
| 105 | + /* end of validations */ |
| 106 | + # {{{ top template |
| 107 | + $condSelector = ''; |
| 108 | + $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 | + ); |
| 113 | + if ( CB_Setup::$cat_title_CI != '' ) { |
| 114 | + // 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 ); |
| 117 | + } |
| 118 | + $top_tpl = |
| 119 | + array( '__tag'=>'table', 'class'=>'cb_top_container', '__end'=>"\n", |
| 120 | + array( '__tag'=>'tr', '__end'=>"\n", |
| 121 | + array( '__tag'=>'td', 'class'=>'cb_toolbox_top', '__end'=>"\n", 0=>&$condSelector ) |
| 122 | + ), |
| 123 | + array( '__tag'=>'tr', '__end'=>"\n", |
| 124 | + array( '__tag'=>'td', 'class'=>'cb_toolbox_bottom', '__end'=>"\n", |
| 125 | + array( wfMsg( 'cb_cat_name_filter' ) ), |
| 126 | + $nameFilterFields, |
| 127 | + ) |
| 128 | + ), |
| 129 | + array( '__tag'=>'tr', '__end'=>"\n", |
| 130 | + array( '__tag'=>'td', 'class'=>'cb_toolbox', 'style'=>'display:none; ', '__end'=>"\n", |
| 131 | + array( '__tag'=>'div', 'id'=>'cb_editor_container', 0=>'' ), |
| 132 | + array( '__tag'=>'div', 'class'=>'cb_separate_container', 0=>'' /* holder of apply button */ ) |
| 133 | + ) |
| 134 | + ), |
| 135 | + array( '__tag'=>'tr', '__end'=>"\n", |
| 136 | + array( '__tag'=>'td', 'class'=>'cb_toolbox', 'style'=>'display:none; ', '__end'=>"\n", |
| 137 | + array( '__tag'=>'div', 'class'=>'cb_copy_line_hint', 0=>wfMsg( 'cb_copy_line_hint' ) ), |
| 138 | + array( '__tag'=>'div', 'id'=>'cb_editor_controls', 0=>'' ) |
| 139 | + ) |
| 140 | + ), |
| 141 | + array( '__tag'=>'tr', '__end'=>"\n", |
| 142 | + array( '__tag'=>'td', '__end'=>"\n", |
| 143 | + array( '__tag'=>'div', 'id'=>'cb_root_container', 0=>&$catlist ) |
| 144 | + ) |
| 145 | + ) |
| 146 | + ); |
| 147 | + # }}} |
| 148 | + $condSelector = CategoryBrowser::generateSelector( $this->ranges, $rootPager ); |
| 149 | + $catlist = $cb->generateCatList( $rootPager ); |
| 150 | + $wgOut->addHTML( CB_XML::toText( $top_tpl ) ); |
| 151 | + } |
| 152 | + |
| 153 | +} /* end of CategoryBrowserPage class */ |
Property changes on: trunk/extensions/CategoryBrowser/CategoryBrowserPage.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 154 | + native |
Index: trunk/extensions/CategoryBrowser/CategoryBrowser_i18n.php |
— | — | @@ -0,0 +1,117 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of CategoryBrowser. |
| 6 | + * |
| 7 | + * CategoryBrowser is free software; you can redistribute it and/or modify |
| 8 | + * it under the terms of the GNU General Public License as published by |
| 9 | + * the Free Software Foundation; either version 2 of the License, or |
| 10 | + * (at your option) any later version. |
| 11 | + * |
| 12 | + * CategoryBrowser is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | + * GNU General Public License for more details. |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU General Public License |
| 18 | + * along with CategoryBrowser; if not, write to the Free Software |
| 19 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 20 | + * |
| 21 | + * ***** END LICENSE BLOCK ***** |
| 22 | + * |
| 23 | + * CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
| 24 | + * |
| 25 | + * To activate this extension : |
| 26 | + * * Create a new directory named CategoryBrowser into the directory "extensions" of MediaWiki. |
| 27 | + * * Place the files from the extension archive there. |
| 28 | + * * Add this line at the end of your LocalSettings.php file : |
| 29 | + * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
| 30 | + * |
| 31 | + * @version 0.2.0 |
| 32 | + * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
| 33 | + * @author Dmitriy Sintsov <questpc@rambler.ru> |
| 34 | + * @addtogroup Extensions |
| 35 | + */ |
| 36 | + |
| 37 | +/** |
| 38 | + * Messages list. |
| 39 | + */ |
| 40 | + |
| 41 | +$messages = array(); |
| 42 | + |
| 43 | +/** English (English) |
| 44 | + * @author QuestPC |
| 45 | + */ |
| 46 | +$messages['en'] = array( |
| 47 | + 'categorybrowser' => 'Category Browser', |
| 48 | + 'categorybrowser-desc' => 'Provides [[Special:CategoryBrowser]] page to filter out most populated categories and to navigate them using AJAX-interface', |
| 49 | + 'cb_requires_javascript' => 'Category Browser extension requires Javascript to be enabled in browser', |
| 50 | + 'cb_ie6_warning' => 'Condition editor does not work in Internet Explorer 6.0 or earlier versions. However, browsing of pre-defined conditions should work normally. Please change / upgrade your browser, if possible.', |
| 51 | + 'cb_cat_name_filter' => 'Search for category by name:', |
| 52 | + 'cb_cat_name_filter_ci' => '(case insensitive)', |
| 53 | + 'cb_copy_line_hint' => 'Use [+] and [>+] buttons to copy / paste operators into selected expression', |
| 54 | + 'cb_has_subcategories' => '$1 {{PLURAL:$1|subcategory|subcategories}}', |
| 55 | + 'cb_has_pages' => '$1 {{PLURAL:$1|page|pages}}', |
| 56 | + 'cb_has_files' => '$1 {{PLURAL:$1|file|files}}', |
| 57 | + 'cb_previous_items_link' => 'Previous', |
| 58 | + 'cb_previous_items_stats' => ' ($1 - $2)', |
| 59 | + 'cb_next_items_link' => 'Next', |
| 60 | + 'cb_next_items_stats' => ' (from $1)', |
| 61 | + 'cb_cat_subcats' => 'subcategories', |
| 62 | + 'cb_cat_pages' => 'pages', |
| 63 | + 'cb_cat_files' => 'files', |
| 64 | + 'cb_apply_button' => 'Apply', |
| 65 | + 'cb_op1_template' => '$1[$2]', |
| 66 | + 'cb_op2_template' => '$1 $2 $3', |
| 67 | + 'cb_all_op' => 'All', |
| 68 | + 'cb_lbracket_op' => '(', |
| 69 | + 'cb_rbracket_op' => ')', |
| 70 | + 'cb_or_op' => 'or', |
| 71 | + 'cb_and_op' => 'and', |
| 72 | + 'cb_ge_op' => '>=', |
| 73 | + 'cb_le_op' => '<=', |
| 74 | + 'cb_eq_op' => '=', |
| 75 | + 'cb_edit_left_hint' => 'Move left, if possible', |
| 76 | + 'cb_edit_right_hint' => 'Move right, if possible', |
| 77 | + 'cb_edit_remove_hint' => 'Delete, if possible', |
| 78 | + 'cb_edit_copy_hint' => 'Copy operator to clipboard', |
| 79 | + 'cb_edit_append_hint' => 'Insert operator to last position', |
| 80 | + 'cb_edit_clear_hint' => 'Clear current expression (select all)', |
| 81 | + 'cb_edit_paste_hint' => 'Paste operator into current position, if possible', |
| 82 | + 'cb_edit_paste_right_hint' => 'Paste operator into next position, if possible', |
| 83 | +); |
| 84 | + |
| 85 | +/** Russian (Русский) |
| 86 | + * @author QuestPC |
| 87 | + */ |
| 88 | +$messages['ru'] = array( |
| 89 | + 'categorybrowser' => 'Просмотр категорий', |
| 90 | + 'categorybrowser-desc' => 'Предоставляет специальную страницу [[Служебная:CategoryBrowser]] для выбора наиболее ёмких категорий вики сайта с целью последующей навигации с использованием AJAX-интерфейса', |
| 91 | + 'cb_requires_javascript' => 'Расширение для просмотра категорий требует включения поддержки Javascript в браузере', |
| 92 | + 'cb_ie6_warning' => 'Редактор выражений не поддерживается в версии Internet Explorer 6.0 или более ранних. Возможен лишь просмотр предопределенных выражений. Пожалуйста поменяйте или обновите ваш браузер.', |
| 93 | + 'cb_cat_name_filter' => 'Поиск категории по имени:', |
| 94 | + 'cb_cat_name_filter_ci' => '(без учёта регистра)', |
| 95 | + 'cb_copy_line_hint' => 'Используйте кнопки [+] и [>+] для копирования оператора в выбранное выражение', |
| 96 | + 'cb_has_subcategories' => '$1 {{PLURAL:$1|подкатегория|подкатегории|подкатегорий}}', |
| 97 | + 'cb_has_pages' => '$1 {{PLURAL:$1|страница|страницы|страниц}}', |
| 98 | + 'cb_has_files' => '$1 {{PLURAL:$1|файл|файла|файлов}}', |
| 99 | + 'cb_previous_items_link' => 'Предыдущие', |
| 100 | + 'cb_previous_items_stats' => ' ($1 - $2)', |
| 101 | + 'cb_next_items_link' => 'Следующие', |
| 102 | + 'cb_next_items_stats' => ' (начиная с $1)', |
| 103 | + 'cb_cat_subcats' => 'подкатегорий', |
| 104 | + 'cb_cat_pages' => 'страниц', |
| 105 | + 'cb_cat_files' => 'файлов', |
| 106 | + 'cb_apply_button' => 'Применить', |
| 107 | + 'cb_all_op' => 'Все', |
| 108 | + 'cb_or_op' => 'или', |
| 109 | + 'cb_and_op' => 'и', |
| 110 | + 'cb_edit_left_hint' => 'Переместить влево, если возможно', |
| 111 | + 'cb_edit_right_hint' => 'Переместить вправо, если возможно', |
| 112 | + 'cb_edit_remove_hint' => 'Удалить, если возможно', |
| 113 | + 'cb_edit_copy_hint' => 'Скопировать оператор в буфер обмена', |
| 114 | + 'cb_edit_append_hint' => 'Вставить оператор в последнюю позицию', |
| 115 | + 'cb_edit_clear_hint' => 'Очистить текущее выражение (выбрать всё)', |
| 116 | + 'cb_edit_paste_hint' => 'Вставить оператор в текущую позицию, если возможно', |
| 117 | + 'cb_edit_paste_right_hint' => 'Вставить оператор в следующую позицию, если возможно', |
| 118 | +); |
Property changes on: trunk/extensions/CategoryBrowser/CategoryBrowser_i18n.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 119 | + native |
Index: trunk/extensions/CategoryBrowser/INSTALL |
— | — | @@ -0,0 +1,8 @@ |
| 2 | +MediaWiki extension CategoryBrowser, version 0.2.0 |
| 3 | + |
| 4 | +* download the latest available version and extract it to your wiki extension directory. |
| 5 | +* add the following line to LocalSettings.php |
| 6 | +require_once( "$IP/extensions/CategoryBrowser/CategoryBrowser.php" ); |
| 7 | +* check out Special:Version page to verify the installation |
| 8 | + |
| 9 | +See http://www.mediawiki.org/wiki/Extension:CategoryBrowser for further details. |
\ No newline at end of file |
Index: trunk/extensions/CategoryBrowser/CategoryBrowser.php |
— | — | @@ -0,0 +1,181 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of CategoryBrowser. |
| 6 | + * |
| 7 | + * CategoryBrowser is free software; you can redistribute it and/or modify |
| 8 | + * it under the terms of the GNU General Public License as published by |
| 9 | + * the Free Software Foundation; either version 2 of the License, or |
| 10 | + * (at your option) any later version. |
| 11 | + * |
| 12 | + * CategoryBrowser is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | + * GNU General Public License for more details. |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU General Public License |
| 18 | + * along with CategoryBrowser; if not, write to the Free Software |
| 19 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 20 | + * |
| 21 | + * ***** END LICENSE BLOCK ***** |
| 22 | + * |
| 23 | + * CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
| 24 | + * |
| 25 | + * To activate this extension : |
| 26 | + * * Create a new directory named CategoryBrowser into the directory "extensions" of MediaWiki. |
| 27 | + * * Place the files from the extension archive there. |
| 28 | + * * Add this line at the end of your LocalSettings.php file : |
| 29 | + * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
| 30 | + * |
| 31 | + * @version 0.2.0 |
| 32 | + * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
| 33 | + * @author Dmitriy Sintsov <questpc@rambler.ru> |
| 34 | + * @addtogroup Extensions |
| 35 | + */ |
| 36 | + |
| 37 | +if( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is a part of MediaWiki extension.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +/* default minimal count of DB rows to start paging */ |
| 42 | +define( 'CB_PAGING_ROWS', 20 ); |
| 43 | +/* maximal number of logical operations in SQL filter (condition) */ |
| 44 | +define( 'CB_MAX_LOGICAL_OP', 5 ); |
| 45 | + |
| 46 | +CB_Setup::init(); |
| 47 | + |
| 48 | +class CB_Setup { |
| 49 | + |
| 50 | + static $version = '0.2.0'; |
| 51 | + static $ExtDir; // filesys path with windows path fix |
| 52 | + static $ScriptPath; // apache virtual path |
| 53 | + static $cat_pages_ranges; // ??? |
| 54 | + |
| 55 | + static $skin = null; |
| 56 | + static $user; |
| 57 | + static $response; |
| 58 | + static $cookie_prefix; |
| 59 | + |
| 60 | + // case insensitive collation of category table 'cat_title' field |
| 61 | + static $cat_title_CI = ''; |
| 62 | + |
| 63 | + // number of files to show in gallery row |
| 64 | + static $imageGalleryPerRow = 4; |
| 65 | + |
| 66 | + /** |
| 67 | + * Add this extension to the mediawiki's extensions list. |
| 68 | + */ |
| 69 | + static function init() { |
| 70 | + global $wgScriptPath; |
| 71 | + global $wgExtensionMessagesFiles; |
| 72 | + global $wgAutoloadClasses; |
| 73 | + global $wgExtensionCredits; |
| 74 | + global $wgSpecialPages; |
| 75 | + global $wgSpecialPageGroups; |
| 76 | + global $wgAjaxExportList; |
| 77 | + |
| 78 | + self::$ExtDir = str_replace( "\\", "/", dirname(__FILE__) ); |
| 79 | + $top_dir = array_pop( explode( '/', self::$ExtDir ) ); |
| 80 | + self::$ScriptPath = $wgScriptPath . '/extensions' . ( ( $top_dir == 'extensions' ) ? '' : '/' . $top_dir ); |
| 81 | + $wgExtensionMessagesFiles['CategoryBrowser'] = self::$ExtDir . '/CategoryBrowser_i18n.php'; |
| 82 | + // do not forget to autoload all the required classes (for AJAX to work correctly) |
| 83 | + $wgAutoloadClasses['CB_XML'] = |
| 84 | + $wgAutoloadClasses['CB_SqlCond'] = self::$ExtDir . '/CategoryBrowserBasic.php'; |
| 85 | + $wgAutoloadClasses['CB_RootPager'] = |
| 86 | + $wgAutoloadClasses['CB_SubPager'] = |
| 87 | + $wgAutoloadClasses['CategoryBrowser'] = self::$ExtDir . '/CategoryBrowserMain.php'; |
| 88 | + $wgAutoloadClasses['CategoryBrowserPage'] = self::$ExtDir . '/CategoryBrowserPage.php'; |
| 89 | + |
| 90 | + $wgExtensionCredits['specialpage'][] = array( |
| 91 | + 'name' => 'CategoryBrowser', |
| 92 | + 'author' => 'QuestPC', |
| 93 | + 'url' => 'http://www.mediawiki.org/wiki/Extension:CategoryBrowser', |
| 94 | + 'descriptionmsg' => 'categorybrowser-desc', |
| 95 | + ); |
| 96 | + $wgSpecialPages['CategoryBrowser'] = array( 'CategoryBrowserPage' ); |
| 97 | + $wgSpecialPageGroups['CategoryBrowser'] = 'pages'; |
| 98 | + $wgAjaxExportList[] = 'CategoryBrowser::getRootOffsetHtml'; |
| 99 | + $wgAjaxExportList[] = 'CategoryBrowser::getSubOffsetHtml'; |
| 100 | + $wgAjaxExportList[] = 'CategoryBrowser::applyEncodedQueue'; |
| 101 | + $wgAjaxExportList[] = 'CategoryBrowser::generateSelectedOption'; |
| 102 | + } |
| 103 | + |
| 104 | + /* |
| 105 | + * should not be called from LocalSettings.php |
| 106 | + * should be called only when the wiki is fully initialized |
| 107 | + */ |
| 108 | + static function initUser() { |
| 109 | + global $wgUser, $wgRequest, $wgSkin; |
| 110 | + // TODO: add more encoding mappings |
| 111 | + $collation_CS_CI = array( 'utf8_bin'=>'utf8_general_ci' ); |
| 112 | + self::$user = is_object( $wgUser ) ? $wgUser : new User(); |
| 113 | + self::$skin = is_object( $wgUser ) ? self::$user->getSkin() : $wgSkin; |
| 114 | + self::$response = $wgRequest->response(); |
| 115 | + self::$cookie_prefix = 'CategoryBrowser_' . self::$user->getId() . '_'; |
| 116 | + // find out current collation of category table 'cat_title' field |
| 117 | + // this is required to switch between CI and CS search |
| 118 | + $db = & wfGetDB( DB_SLAVE ); |
| 119 | + $category_table = $db->tableName( 'category' ); |
| 120 | + $db_result = $db->query( "SHOW FULL COLUMNS FROM ${category_table}" ); |
| 121 | + self::$cat_title_CI = ''; |
| 122 | + $cat_title_CS = ''; |
| 123 | + while ( $row = $db->fetchObject( $db_result ) ) { |
| 124 | + if ( $row->Field == 'cat_title' ) { |
| 125 | + $cat_title_CS = $row->Collation; |
| 126 | + if ( isset( $collation_CS_CI[ $cat_title_CS ] ) ) { |
| 127 | + self::$cat_title_CI = $collation_CS_CI[ $cat_title_CS ]; |
| 128 | + } |
| 129 | + break; |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + static function entities( &$s ) { |
| 135 | + return htmlentities( $s, ENT_COMPAT, 'UTF-8' ); |
| 136 | + } |
| 137 | + |
| 138 | + static function specialchars( &$s ) { |
| 139 | + return htmlspecialchars( $s, ENT_COMPAT, 'UTF-8' ); |
| 140 | + } |
| 141 | + |
| 142 | + static function getFullCookieName( $cookievar ) { |
| 143 | + global $wgCookiePrefix; |
| 144 | + if ( !is_string( self::$cookie_prefix ) ) { |
| 145 | + throw new MWException( 'You have to call CB_Setup::initUser before to use ' . __METHOD__ ); |
| 146 | + } |
| 147 | + return $wgCookiePrefix . self::$cookie_prefix . $cookievar; |
| 148 | + } |
| 149 | + |
| 150 | + static function getJsCookiePrefix() { |
| 151 | + global $wgCookiePrefix; |
| 152 | + if ( !is_string( self::$cookie_prefix ) ) { |
| 153 | + throw new MWException( 'You have to call CB_Setup::initUser before to use ' . __METHOD__ ); |
| 154 | + } |
| 155 | + return Xml::escapeJsString( $wgCookiePrefix . self::$cookie_prefix ); |
| 156 | + } |
| 157 | + |
| 158 | + /* # urlencode cookie illegal chars (not needed anymore, was used with self::$user->getName()) |
| 159 | + * $cookie_illegal_chars = array( "=", ",", ";", " ", "\t", "\r", "\n", "\013", "\014" ); |
| 160 | + * $cookie_replacement_chars = array( "%23", "%2C", "%3B", "%20", "%09", "%0D", "%0A", "%0B", "%0C" ); |
| 161 | + * self::$cookie_prefix = str_replace( $cookie_illegal_chars, $cookie_replacement_chars, self::$cookie_prefix ); |
| 162 | + */ |
| 163 | + static function setCookie( $cookievar, $val, $time ) { |
| 164 | + global $wgCookieHttpOnly; |
| 165 | + // User::setCookies() is not suitable for our needs because it's called only for non-anonymous users |
| 166 | + // our cookie has to be accessible in javascript |
| 167 | + // todo: cookie is not set / read in JS anymore, don't modify $wgCookieHttpOnly |
| 168 | + $wgCookieHttpOnly_save = $wgCookieHttpOnly; |
| 169 | + $wgCookieHttpOnly = false; |
| 170 | + if ( !is_string( self::$cookie_prefix) || !is_object( self::$response ) ) { |
| 171 | + throw new MWException( 'You have to call CB_Setup::initUser before to use ' . __METHOD__ ); |
| 172 | + } |
| 173 | + self::$response->setcookie( self::$cookie_prefix . $cookievar, $val, $time ); |
| 174 | + $wgCookieHttpOnly = $wgCookieHttpOnly_save; |
| 175 | + } |
| 176 | + |
| 177 | + static function getCookie( $cookievar ) { |
| 178 | + $idx = self::getFullCookieName( $cookievar ); |
| 179 | + return isset( $_COOKIE[ $idx ] ) ? $_COOKIE[ $idx ] : null; |
| 180 | + } |
| 181 | + |
| 182 | +} |
Property changes on: trunk/extensions/CategoryBrowser/CategoryBrowser.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 183 | + native |
Index: trunk/extensions/CategoryBrowser/category_browser.css |
— | — | @@ -0,0 +1,192 @@ |
| 2 | +noscript.cb_noscript { |
| 3 | + font-size:120%; |
| 4 | + font-weight: bold; |
| 5 | + color: red; |
| 6 | +} |
| 7 | + |
| 8 | +table.cb_top_container { |
| 9 | + width: 100%; |
| 10 | +} |
| 11 | + |
| 12 | +table.cb_top_container td { |
| 13 | + border-collapse: collapse; |
| 14 | + vertical-align: middle; |
| 15 | + text-align: left; |
| 16 | +} |
| 17 | + |
| 18 | +td.cb_toolbox_top { |
| 19 | + border-top: 1px dashed lightgray !important; |
| 20 | + border-left: 1px dashed lightgray !important; |
| 21 | + border-right: 1px dashed lightgray !important; |
| 22 | +} |
| 23 | + |
| 24 | +td.cb_toolbox_bottom { |
| 25 | + border-bottom: 1px dashed lightgray !important; |
| 26 | + border-left: 1px dashed lightgray !important; |
| 27 | + border-right: 1px dashed lightgray !important; |
| 28 | +} |
| 29 | + |
| 30 | +td.cb_toolbox { |
| 31 | + border: 1px dashed lightgray !important; |
| 32 | +} |
| 33 | + |
| 34 | +select#cb_expr_select { |
| 35 | + width: 100%; |
| 36 | +} |
| 37 | + |
| 38 | +input#cb_cat_name_filter { |
| 39 | + margin: 0 1em 0 1em; |
| 40 | + width: 20em; |
| 41 | +} |
| 42 | + |
| 43 | +div#cb_root_container { |
| 44 | + width: 100%; |
| 45 | + height: 100%; |
| 46 | + overflow-x: auto; |
| 47 | +} |
| 48 | + |
| 49 | +div.cb_cat_container { |
| 50 | + float: left; |
| 51 | + clear: both; |
| 52 | + white-space: nowrap; |
| 53 | + width: 100%; |
| 54 | + min-width: 20px; |
| 55 | + min-height: 1px; |
| 56 | +} |
| 57 | + |
| 58 | +div.cb_nested_container { |
| 59 | + float:left; |
| 60 | + clear:both; |
| 61 | + padding-left:2em; |
| 62 | + white-space: nowrap; |
| 63 | + border-top: 2px dashed lightgray; |
| 64 | + border-bottom: 2px dashed lightgray; |
| 65 | + margin-top: 1px; |
| 66 | + margin-bottom: 1px; |
| 67 | + width:auto; |
| 68 | + min-width:20px; |
| 69 | +} |
| 70 | + |
| 71 | +div.cb_cat_controls { |
| 72 | + white-space: nowrap; |
| 73 | + width: 100%; |
| 74 | + clear: both; |
| 75 | +} |
| 76 | + |
| 77 | +/* was: |
| 78 | +div.cb_cat_expand { |
| 79 | + float: left; |
| 80 | + width: 1em; |
| 81 | + min-width: 1em; |
| 82 | +} |
| 83 | +*/ |
| 84 | +span.cb_cat_expand { |
| 85 | + white-space: nowrap; |
| 86 | + font-family: Fixed, monospace; |
| 87 | + font-style : normal; |
| 88 | + padding-right: 0.5em; |
| 89 | +} |
| 90 | + |
| 91 | +/* was: |
| 92 | +div.cb_cat_item { |
| 93 | + float: left; |
| 94 | + min-width: 20px; |
| 95 | + width: auto; |
| 96 | +} |
| 97 | +*/ |
| 98 | +span.cb_cat_item { |
| 99 | +} |
| 100 | + |
| 101 | +div.cb_token_container { |
| 102 | + text-align: center; |
| 103 | + border: 1px solid lightgray !important; |
| 104 | + float:left; |
| 105 | + width:auto; |
| 106 | + height:1.5em; |
| 107 | + margin:5px 5px 1.8em 5px; |
| 108 | + padding:7px; |
| 109 | + color: black; |
| 110 | + background-color: white; |
| 111 | +} |
| 112 | + |
| 113 | +div.cb_token_inputs { |
| 114 | + position: relative; |
| 115 | + left: 0em; |
| 116 | + top: -1.5em; |
| 117 | +} |
| 118 | + |
| 119 | +div.cb_popup_controls { |
| 120 | + position: relative; |
| 121 | + width: auto; |
| 122 | + height: 1.5em; |
| 123 | + display: block; |
| 124 | + visibility: hidden; |
| 125 | + left: 0em; |
| 126 | + top: 2.5em; |
| 127 | + z-index: 10; |
| 128 | +} |
| 129 | + |
| 130 | +div.cb_control_button { |
| 131 | + cursor: pointer; |
| 132 | + font-weight: bold; |
| 133 | + color: white; |
| 134 | + background-color: gray; |
| 135 | + position: static; |
| 136 | + border: 1px solid red; |
| 137 | + display: inline; |
| 138 | + margin: 1px; |
| 139 | + padding: 0px 1px 0px 1px; |
| 140 | + z-index: 10; |
| 141 | +} |
| 142 | + |
| 143 | +span.cb_virtual_select { |
| 144 | + border: 1px dashed lightgray; |
| 145 | + margin-left: 3px; |
| 146 | + margin-right: 3px; |
| 147 | + padding: 3px; |
| 148 | +} |
| 149 | + |
| 150 | +span.cb_comment { |
| 151 | + color: gray; |
| 152 | +} |
| 153 | + |
| 154 | +a.cb_sublink { |
| 155 | + font-weight:normal; |
| 156 | + font-style:italic; |
| 157 | + color: black !important; |
| 158 | + text-decoration: none !important; |
| 159 | + white-space: nowrap; |
| 160 | +} |
| 161 | + |
| 162 | +a.cb_sublink:hover { |
| 163 | + font-weight:bold; |
| 164 | + white-space: nowrap; |
| 165 | +} |
| 166 | + |
| 167 | +#cb_editor_container select, #cb_editor_container input, .cb_separate_container input { |
| 168 | + border: 1px solid gray; |
| 169 | + border-collapse:collapse; |
| 170 | + margin:0px; |
| 171 | + padding:0px; |
| 172 | + letter-spacing:-1px; |
| 173 | +} |
| 174 | + |
| 175 | +input#cb_apply_button { |
| 176 | + letter-spacing:1px; |
| 177 | +} |
| 178 | + |
| 179 | +div.cb_separate_container { |
| 180 | + width: 100%; |
| 181 | + clear: both; |
| 182 | +} |
| 183 | + |
| 184 | +div.cb_files_container { |
| 185 | + float: left; |
| 186 | + clear: both; |
| 187 | +} |
| 188 | + |
| 189 | +div.cb_copy_line_hint { |
| 190 | + width: 100%; |
| 191 | + font-size: 90%; |
| 192 | + color: gray; |
| 193 | +} |
Property changes on: trunk/extensions/CategoryBrowser/category_browser.css |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 194 | + native |
Index: trunk/extensions/CategoryBrowser/COPYING |
— | — | @@ -0,0 +1,309 @@ |
| 2 | +The CategoryBrowser extension may be copied and redistributed under either the |
| 3 | +DWTFYWWI license or the GNU General Public License, at the option of the |
| 4 | +licensee. The text of both licenses is given below. |
| 5 | + |
| 6 | +The majority of this extension is written by (and copyright) Tim Starling. Minor |
| 7 | +modifications have been made by various members of the MediaWiki development |
| 8 | +team. |
| 9 | + |
| 10 | +------------------------------------------------------------------------------- |
| 11 | + |
| 12 | + DWTFYWWI LICENSE |
| 13 | + Version 1, January 2006 |
| 14 | + |
| 15 | + Copyright (C) 2010 Dmitriy Sintsov (QuestPC) |
| 16 | + |
| 17 | + Preamble |
| 18 | + |
| 19 | + The licenses for most software are designed to take away your |
| 20 | +freedom to share and change it. By contrast, the DWTFYWWI or Do |
| 21 | +Whatever The Fuck You Want With It license is intended to guarantee |
| 22 | +your freedom to share and change the software--to make sure the |
| 23 | +software is free for all its users. |
| 24 | + |
| 25 | + DWTFYWWI LICENSE |
| 26 | + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION |
| 27 | +0. The author grants everyone permission to do whatever the fuck they |
| 28 | +want with the software, whatever the fuck that may be. |
| 29 | + |
| 30 | +------------------------------------------------------------------------------- |
| 31 | + |
| 32 | + GNU GENERAL PUBLIC LICENSE |
| 33 | + Version 2, June 1991 |
| 34 | + |
| 35 | + Copyright (C) 1989, 1991 Free Software Foundation, Inc., |
| 36 | + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| 37 | + Everyone is permitted to copy and distribute verbatim copies |
| 38 | + of this license document, but changing it is not allowed. |
| 39 | + |
| 40 | + Preamble |
| 41 | + |
| 42 | + The licenses for most software are designed to take away your |
| 43 | +freedom to share and change it. By contrast, the GNU General Public |
| 44 | +License is intended to guarantee your freedom to share and change free |
| 45 | +software--to make sure the software is free for all its users. This |
| 46 | +General Public License applies to most of the Free Software |
| 47 | +Foundation's software and to any other program whose authors commit to |
| 48 | +using it. (Some other Free Software Foundation software is covered by |
| 49 | +the GNU Lesser General Public License instead.) You can apply it to |
| 50 | +your programs, too. |
| 51 | + |
| 52 | + When we speak of free software, we are referring to freedom, not |
| 53 | +price. Our General Public Licenses are designed to make sure that you |
| 54 | +have the freedom to distribute copies of free software (and charge for |
| 55 | +this service if you wish), that you receive source code or can get it |
| 56 | +if you want it, that you can change the software or use pieces of it |
| 57 | +in new free programs; and that you know you can do these things. |
| 58 | + |
| 59 | + To protect your rights, we need to make restrictions that forbid |
| 60 | +anyone to deny you these rights or to ask you to surrender the rights. |
| 61 | +These restrictions translate to certain responsibilities for you if you |
| 62 | +distribute copies of the software, or if you modify it. |
| 63 | + |
| 64 | + For example, if you distribute copies of such a program, whether |
| 65 | +gratis or for a fee, you must give the recipients all the rights that |
| 66 | +you have. You must make sure that they, too, receive or can get the |
| 67 | +source code. And you must show them these terms so they know their |
| 68 | +rights. |
| 69 | + |
| 70 | + We protect your rights with two steps: (1) copyright the software, and |
| 71 | +(2) offer you this license which gives you legal permission to copy, |
| 72 | +distribute and/or modify the software. |
| 73 | + |
| 74 | + Also, for each author's protection and ours, we want to make certain |
| 75 | +that everyone understands that there is no warranty for this free |
| 76 | +software. If the software is modified by someone else and passed on, we |
| 77 | +want its recipients to know that what they have is not the original, so |
| 78 | +that any problems introduced by others will not reflect on the original |
| 79 | +authors' reputations. |
| 80 | + |
| 81 | + Finally, any free program is threatened constantly by software |
| 82 | +patents. We wish to avoid the danger that redistributors of a free |
| 83 | +program will individually obtain patent licenses, in effect making the |
| 84 | +program proprietary. To prevent this, we have made it clear that any |
| 85 | +patent must be licensed for everyone's free use or not licensed at all. |
| 86 | + |
| 87 | + The precise terms and conditions for copying, distribution and |
| 88 | +modification follow. |
| 89 | + |
| 90 | + GNU GENERAL PUBLIC LICENSE |
| 91 | + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION |
| 92 | + |
| 93 | + 0. This License applies to any program or other work which contains |
| 94 | +a notice placed by the copyright holder saying it may be distributed |
| 95 | +under the terms of this General Public License. The "Program", below, |
| 96 | +refers to any such program or work, and a "work based on the Program" |
| 97 | +means either the Program or any derivative work under copyright law: |
| 98 | +that is to say, a work containing the Program or a portion of it, |
| 99 | +either verbatim or with modifications and/or translated into another |
| 100 | +language. (Hereinafter, translation is included without limitation in |
| 101 | +the term "modification".) Each licensee is addressed as "you". |
| 102 | + |
| 103 | +Activities other than copying, distribution and modification are not |
| 104 | +covered by this License; they are outside its scope. The act of |
| 105 | +running the Program is not restricted, and the output from the Program |
| 106 | +is covered only if its contents constitute a work based on the |
| 107 | +Program (independent of having been made by running the Program). |
| 108 | +Whether that is true depends on what the Program does. |
| 109 | + |
| 110 | + 1. You may copy and distribute verbatim copies of the Program's |
| 111 | +source code as you receive it, in any medium, provided that you |
| 112 | +conspicuously and appropriately publish on each copy an appropriate |
| 113 | +copyright notice and disclaimer of warranty; keep intact all the |
| 114 | +notices that refer to this License and to the absence of any warranty; |
| 115 | +and give any other recipients of the Program a copy of this License |
| 116 | +along with the Program. |
| 117 | + |
| 118 | +You may charge a fee for the physical act of transferring a copy, and |
| 119 | +you may at your option offer warranty protection in exchange for a fee. |
| 120 | + |
| 121 | + 2. You may modify your copy or copies of the Program or any portion |
| 122 | +of it, thus forming a work based on the Program, and copy and |
| 123 | +distribute such modifications or work under the terms of Section 1 |
| 124 | +above, provided that you also meet all of these conditions: |
| 125 | + |
| 126 | + a) You must cause the modified files to carry prominent notices |
| 127 | + stating that you changed the files and the date of any change. |
| 128 | + |
| 129 | + b) You must cause any work that you distribute or publish, that in |
| 130 | + whole or in part contains or is derived from the Program or any |
| 131 | + part thereof, to be licensed as a whole at no charge to all third |
| 132 | + parties under the terms of this License. |
| 133 | + |
| 134 | + c) If the modified program normally reads commands interactively |
| 135 | + when run, you must cause it, when started running for such |
| 136 | + interactive use in the most ordinary way, to print or display an |
| 137 | + announcement including an appropriate copyright notice and a |
| 138 | + notice that there is no warranty (or else, saying that you provide |
| 139 | + a warranty) and that users may redistribute the program under |
| 140 | + these conditions, and telling the user how to view a copy of this |
| 141 | + License. (Exception: if the Program itself is interactive but |
| 142 | + does not normally print such an announcement, your work based on |
| 143 | + the Program is not required to print an announcement.) |
| 144 | + |
| 145 | +These requirements apply to the modified work as a whole. If |
| 146 | +identifiable sections of that work are not derived from the Program, |
| 147 | +and can be reasonably considered independent and separate works in |
| 148 | +themselves, then this License, and its terms, do not apply to those |
| 149 | +sections when you distribute them as separate works. But when you |
| 150 | +distribute the same sections as part of a whole which is a work based |
| 151 | +on the Program, the distribution of the whole must be on the terms of |
| 152 | +this License, whose permissions for other licensees extend to the |
| 153 | +entire whole, and thus to each and every part regardless of who wrote it. |
| 154 | + |
| 155 | +Thus, it is not the intent of this section to claim rights or contest |
| 156 | +your rights to work written entirely by you; rather, the intent is to |
| 157 | +exercise the right to control the distribution of derivative or |
| 158 | +collective works based on the Program. |
| 159 | + |
| 160 | +In addition, mere aggregation of another work not based on the Program |
| 161 | +with the Program (or with a work based on the Program) on a volume of |
| 162 | +a storage or distribution medium does not bring the other work under |
| 163 | +the scope of this License. |
| 164 | + |
| 165 | + 3. You may copy and distribute the Program (or a work based on it, |
| 166 | +under Section 2) in object code or executable form under the terms of |
| 167 | +Sections 1 and 2 above provided that you also do one of the following: |
| 168 | + |
| 169 | + a) Accompany it with the complete corresponding machine-readable |
| 170 | + source code, which must be distributed under the terms of Sections |
| 171 | + 1 and 2 above on a medium customarily used for software interchange; or, |
| 172 | + |
| 173 | + b) Accompany it with a written offer, valid for at least three |
| 174 | + years, to give any third party, for a charge no more than your |
| 175 | + cost of physically performing source distribution, a complete |
| 176 | + machine-readable copy of the corresponding source code, to be |
| 177 | + distributed under the terms of Sections 1 and 2 above on a medium |
| 178 | + customarily used for software interchange; or, |
| 179 | + |
| 180 | + c) Accompany it with the information you received as to the offer |
| 181 | + to distribute corresponding source code. (This alternative is |
| 182 | + allowed only for noncommercial distribution and only if you |
| 183 | + received the program in object code or executable form with such |
| 184 | + an offer, in accord with Subsection b above.) |
| 185 | + |
| 186 | +The source code for a work means the preferred form of the work for |
| 187 | +making modifications to it. For an executable work, complete source |
| 188 | +code means all the source code for all modules it contains, plus any |
| 189 | +associated interface definition files, plus the scripts used to |
| 190 | +control compilation and installation of the executable. However, as a |
| 191 | +special exception, the source code distributed need not include |
| 192 | +anything that is normally distributed (in either source or binary |
| 193 | +form) with the major components (compiler, kernel, and so on) of the |
| 194 | +operating system on which the executable runs, unless that component |
| 195 | +itself accompanies the executable. |
| 196 | + |
| 197 | +If distribution of executable or object code is made by offering |
| 198 | +access to copy from a designated place, then offering equivalent |
| 199 | +access to copy the source code from the same place counts as |
| 200 | +distribution of the source code, even though third parties are not |
| 201 | +compelled to copy the source along with the object code. |
| 202 | + |
| 203 | + 4. You may not copy, modify, sublicense, or distribute the Program |
| 204 | +except as expressly provided under this License. Any attempt |
| 205 | +otherwise to copy, modify, sublicense or distribute the Program is |
| 206 | +void, and will automatically terminate your rights under this License. |
| 207 | +However, parties who have received copies, or rights, from you under |
| 208 | +this License will not have their licenses terminated so long as such |
| 209 | +parties remain in full compliance. |
| 210 | + |
| 211 | + 5. You are not required to accept this License, since you have not |
| 212 | +signed it. However, nothing else grants you permission to modify or |
| 213 | +distribute the Program or its derivative works. These actions are |
| 214 | +prohibited by law if you do not accept this License. Therefore, by |
| 215 | +modifying or distributing the Program (or any work based on the |
| 216 | +Program), you indicate your acceptance of this License to do so, and |
| 217 | +all its terms and conditions for copying, distributing or modifying |
| 218 | +the Program or works based on it. |
| 219 | + |
| 220 | + 6. Each time you redistribute the Program (or any work based on the |
| 221 | +Program), the recipient automatically receives a license from the |
| 222 | +original licensor to copy, distribute or modify the Program subject to |
| 223 | +these terms and conditions. You may not impose any further |
| 224 | +restrictions on the recipients' exercise of the rights granted herein. |
| 225 | +You are not responsible for enforcing compliance by third parties to |
| 226 | +this License. |
| 227 | + |
| 228 | + 7. If, as a consequence of a court judgment or allegation of patent |
| 229 | +infringement or for any other reason (not limited to patent issues), |
| 230 | +conditions are imposed on you (whether by court order, agreement or |
| 231 | +otherwise) that contradict the conditions of this License, they do not |
| 232 | +excuse you from the conditions of this License. If you cannot |
| 233 | +distribute so as to satisfy simultaneously your obligations under this |
| 234 | +License and any other pertinent obligations, then as a consequence you |
| 235 | +may not distribute the Program at all. For example, if a patent |
| 236 | +license would not permit royalty-free redistribution of the Program by |
| 237 | +all those who receive copies directly or indirectly through you, then |
| 238 | +the only way you could satisfy both it and this License would be to |
| 239 | +refrain entirely from distribution of the Program. |
| 240 | + |
| 241 | +If any portion of this section is held invalid or unenforceable under |
| 242 | +any particular circumstance, the balance of the section is intended to |
| 243 | +apply and the section as a whole is intended to apply in other |
| 244 | +circumstances. |
| 245 | + |
| 246 | +It is not the purpose of this section to induce you to infringe any |
| 247 | +patents or other property right claims or to contest validity of any |
| 248 | +such claims; this section has the sole purpose of protecting the |
| 249 | +integrity of the free software distribution system, which is |
| 250 | +implemented by public license practices. Many people have made |
| 251 | +generous contributions to the wide range of software distributed |
| 252 | +through that system in reliance on consistent application of that |
| 253 | +system; it is up to the author/donor to decide if he or she is willing |
| 254 | +to distribute software through any other system and a licensee cannot |
| 255 | +impose that choice. |
| 256 | + |
| 257 | +This section is intended to make thoroughly clear what is believed to |
| 258 | +be a consequence of the rest of this License. |
| 259 | + |
| 260 | + 8. If the distribution and/or use of the Program is restricted in |
| 261 | +certain countries either by patents or by copyrighted interfaces, the |
| 262 | +original copyright holder who places the Program under this License |
| 263 | +may add an explicit geographical distribution limitation excluding |
| 264 | +those countries, so that distribution is permitted only in or among |
| 265 | +countries not thus excluded. In such case, this License incorporates |
| 266 | +the limitation as if written in the body of this License. |
| 267 | + |
| 268 | + 9. The Free Software Foundation may publish revised and/or new versions |
| 269 | +of the General Public License from time to time. Such new versions will |
| 270 | +be similar in spirit to the present version, but may differ in detail to |
| 271 | +address new problems or concerns. |
| 272 | + |
| 273 | +Each version is given a distinguishing version number. If the Program |
| 274 | +specifies a version number of this License which applies to it and "any |
| 275 | +later version", you have the option of following the terms and conditions |
| 276 | +either of that version or of any later version published by the Free |
| 277 | +Software Foundation. If the Program does not specify a version number of |
| 278 | +this License, you may choose any version ever published by the Free Software |
| 279 | +Foundation. |
| 280 | + |
| 281 | + 10. If you wish to incorporate parts of the Program into other free |
| 282 | +programs whose distribution conditions are different, write to the author |
| 283 | +to ask for permission. For software which is copyrighted by the Free |
| 284 | +Software Foundation, write to the Free Software Foundation; we sometimes |
| 285 | +make exceptions for this. Our decision will be guided by the two goals |
| 286 | +of preserving the free status of all derivatives of our free software and |
| 287 | +of promoting the sharing and reuse of software generally. |
| 288 | + |
| 289 | + NO WARRANTY |
| 290 | + |
| 291 | + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY |
| 292 | +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN |
| 293 | +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES |
| 294 | +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED |
| 295 | +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| 296 | +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS |
| 297 | +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE |
| 298 | +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, |
| 299 | +REPAIR OR CORRECTION. |
| 300 | + |
| 301 | + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
| 302 | +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR |
| 303 | +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, |
| 304 | +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING |
| 305 | +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED |
| 306 | +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY |
| 307 | +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER |
| 308 | +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE |
| 309 | +POSSIBILITY OF SUCH DAMAGES. |
| 310 | + |
Index: trunk/extensions/CategoryBrowser/category_browser_rtl.css |
— | — | @@ -0,0 +1,193 @@ |
| 2 | +/* generated automatically with CSSJanus http://cssjanus.commoner.com/ */ |
| 3 | +noscript.cb_noscript { |
| 4 | + font-size:120%; |
| 5 | + font-weight: bold; |
| 6 | + color: red; |
| 7 | +} |
| 8 | + |
| 9 | +table.cb_top_container { |
| 10 | + width: 100%; |
| 11 | +} |
| 12 | + |
| 13 | +table.cb_top_container td { |
| 14 | + border-collapse: collapse; |
| 15 | + vertical-align: middle; |
| 16 | + text-align: right; |
| 17 | +} |
| 18 | + |
| 19 | +td.cb_toolbox_top { |
| 20 | + border-top: 1px dashed lightgray !important; |
| 21 | + border-right: 1px dashed lightgray !important; |
| 22 | + border-left: 1px dashed lightgray !important; |
| 23 | +} |
| 24 | + |
| 25 | +td.cb_toolbox_bottom { |
| 26 | + border-bottom: 1px dashed lightgray !important; |
| 27 | + border-right: 1px dashed lightgray !important; |
| 28 | + border-left: 1px dashed lightgray !important; |
| 29 | +} |
| 30 | + |
| 31 | +td.cb_toolbox { |
| 32 | + border: 1px dashed lightgray !important; |
| 33 | +} |
| 34 | + |
| 35 | +select#cb_expr_select { |
| 36 | + width: 100%; |
| 37 | +} |
| 38 | + |
| 39 | +input#cb_cat_name_filter { |
| 40 | + margin: 0 1em 0 1em; |
| 41 | + width: 20em; |
| 42 | +} |
| 43 | + |
| 44 | +div#cb_root_container { |
| 45 | + width: 100%; |
| 46 | + height: 100%; |
| 47 | + overflow-x: auto; |
| 48 | +} |
| 49 | + |
| 50 | +div.cb_cat_container { |
| 51 | + float: right; |
| 52 | + clear: both; |
| 53 | + white-space: nowrap; |
| 54 | + width: 100%; |
| 55 | + min-width: 20px; |
| 56 | + min-height: 1px; |
| 57 | +} |
| 58 | + |
| 59 | +div.cb_nested_container { |
| 60 | + float:right; |
| 61 | + clear:both; |
| 62 | + padding-right:2em; |
| 63 | + white-space: nowrap; |
| 64 | + border-top: 2px dashed lightgray; |
| 65 | + border-bottom: 2px dashed lightgray; |
| 66 | + margin-top: 1px; |
| 67 | + margin-bottom: 1px; |
| 68 | + width:auto; |
| 69 | + min-width:20px; |
| 70 | +} |
| 71 | + |
| 72 | +div.cb_cat_controls { |
| 73 | + white-space: nowrap; |
| 74 | + width: 100%; |
| 75 | + clear: both; |
| 76 | +} |
| 77 | + |
| 78 | +/* was: |
| 79 | +div.cb_cat_expand { |
| 80 | + float: left; |
| 81 | + width: 1em; |
| 82 | + min-width: 1em; |
| 83 | +} |
| 84 | +*/ |
| 85 | +span.cb_cat_expand { |
| 86 | + white-space: nowrap; |
| 87 | + font-family: Fixed, monospace; |
| 88 | + font-style : normal; |
| 89 | + padding-left: 0.5em; |
| 90 | +} |
| 91 | + |
| 92 | +/* was: |
| 93 | +div.cb_cat_item { |
| 94 | + float: left; |
| 95 | + min-width: 20px; |
| 96 | + width: auto; |
| 97 | +} |
| 98 | +*/ |
| 99 | +span.cb_cat_item { |
| 100 | +} |
| 101 | + |
| 102 | +div.cb_token_container { |
| 103 | + text-align: center; |
| 104 | + border: 1px solid lightgray !important; |
| 105 | + float:right; |
| 106 | + width:auto; |
| 107 | + height:1.5em; |
| 108 | + margin:5px 5px 1.8em 5px; |
| 109 | + padding:7px; |
| 110 | + color: black; |
| 111 | + background-color: white; |
| 112 | +} |
| 113 | + |
| 114 | +div.cb_token_inputs { |
| 115 | + position: relative; |
| 116 | + right: 0em; |
| 117 | + top: -1.5em; |
| 118 | +} |
| 119 | + |
| 120 | +div.cb_popup_controls { |
| 121 | + position: relative; |
| 122 | + width: auto; |
| 123 | + height: 1.5em; |
| 124 | + display: block; |
| 125 | + visibility: hidden; |
| 126 | + right: 0em; |
| 127 | + top: 2.5em; |
| 128 | + z-index: 10; |
| 129 | +} |
| 130 | + |
| 131 | +div.cb_control_button { |
| 132 | + cursor: pointer; |
| 133 | + font-weight: bold; |
| 134 | + color: white; |
| 135 | + background-color: gray; |
| 136 | + position: static; |
| 137 | + border: 1px solid red; |
| 138 | + display: inline; |
| 139 | + margin: 1px; |
| 140 | + padding: 0px 1px 0px 1px; |
| 141 | + z-index: 10; |
| 142 | +} |
| 143 | + |
| 144 | +span.cb_virtual_select { |
| 145 | + border: 1px dashed lightgray; |
| 146 | + margin-right: 3px; |
| 147 | + margin-left: 3px; |
| 148 | + padding: 3px; |
| 149 | +} |
| 150 | + |
| 151 | +span.cb_comment { |
| 152 | + color: gray; |
| 153 | +} |
| 154 | + |
| 155 | +a.cb_sublink { |
| 156 | + font-weight:normal; |
| 157 | + font-style:italic; |
| 158 | + color: black !important; |
| 159 | + text-decoration: none !important; |
| 160 | + white-space: nowrap; |
| 161 | +} |
| 162 | + |
| 163 | +a.cb_sublink:hover { |
| 164 | + font-weight:bold; |
| 165 | + white-space: nowrap; |
| 166 | +} |
| 167 | + |
| 168 | +#cb_editor_container select, #cb_editor_container input, .cb_separate_container input { |
| 169 | + border: 1px solid gray; |
| 170 | + border-collapse:collapse; |
| 171 | + margin:0px; |
| 172 | + padding:0px; |
| 173 | + letter-spacing:-1px; |
| 174 | +} |
| 175 | + |
| 176 | +input#cb_apply_button { |
| 177 | + letter-spacing:1px; |
| 178 | +} |
| 179 | + |
| 180 | +div.cb_separate_container { |
| 181 | + width: 100%; |
| 182 | + clear: both; |
| 183 | +} |
| 184 | + |
| 185 | +div.cb_files_container { |
| 186 | + float: right; |
| 187 | + clear: both; |
| 188 | +} |
| 189 | + |
| 190 | +div.cb_copy_line_hint { |
| 191 | + width: 100%; |
| 192 | + font-size: 90%; |
| 193 | + color: gray; |
| 194 | +} |
\ No newline at end of file |
Property changes on: trunk/extensions/CategoryBrowser/category_browser_rtl.css |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 195 | + native |
Index: trunk/extensions/CategoryBrowser/CategoryBrowserBasic.php |
— | — | @@ -0,0 +1,674 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of CategoryBrowser. |
| 6 | + * |
| 7 | + * CategoryBrowser is free software; you can redistribute it and/or modify |
| 8 | + * it under the terms of the GNU General Public License as published by |
| 9 | + * the Free Software Foundation; either version 2 of the License, or |
| 10 | + * (at your option) any later version. |
| 11 | + * |
| 12 | + * CategoryBrowser is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | + * GNU General Public License for more details. |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU General Public License |
| 18 | + * along with CategoryBrowser; if not, write to the Free Software |
| 19 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 20 | + * |
| 21 | + * ***** END LICENSE BLOCK ***** |
| 22 | + * |
| 23 | + * CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
| 24 | + * |
| 25 | + * To activate this extension : |
| 26 | + * * Create a new directory named CategoryBrowser into the directory "extensions" of MediaWiki. |
| 27 | + * * Place the files from the extension archive there. |
| 28 | + * * Add this line at the end of your LocalSettings.php file : |
| 29 | + * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
| 30 | + * |
| 31 | + * @version 0.2.0 |
| 32 | + * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
| 33 | + * @author Dmitriy Sintsov <questpc@rambler.ru> |
| 34 | + * @addtogroup Extensions |
| 35 | + */ |
| 36 | + |
| 37 | +if( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is a part of MediaWiki extension.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +define( 'CB_COND_TOKEN_MATCH', '`^\s*(cat_subcats|cat_pages|cat_files)\s*(>=|<=|=)\s*(\d+)\s*$`' ); |
| 42 | +define( 'CB_ENCODED_TOKEN_MATCH', '`^(ge|le|eq)(p|s|f)(\d+)$`' ); |
| 43 | + |
| 44 | +/* render output data */ |
| 45 | +class CB_XML { |
| 46 | + // the stucture of $tag is like this: |
| 47 | + // array( "__tag"=>"td", "class"=>"myclass", 0=>"text before li", 1=>array( "__tag"=>"li", 0=>"text inside li" ), 2=>"text after li" ) |
| 48 | + // both tagged and tagless lists are supported |
| 49 | + static function toText( &$tag ) { |
| 50 | + $tag_open = ""; |
| 51 | + $tag_close = ""; |
| 52 | + $tag_val = null; |
| 53 | + if ( is_array( $tag ) ) { |
| 54 | + ksort( $tag ); |
| 55 | + if ( array_key_exists( '__tag', $tag ) ) { |
| 56 | + # list inside of tag |
| 57 | + $tag_open .= "<" . $tag[ '__tag' ]; |
| 58 | + foreach( $tag as $attr_key=>&$attr_val ) { |
| 59 | + if ( is_int( $attr_key ) ) { |
| 60 | + if ( $tag_val === null ) |
| 61 | + $tag_val = ""; |
| 62 | + if ( is_array( $attr_val ) ) { |
| 63 | + # recursive tags |
| 64 | + $tag_val .= self::toText( $attr_val ); |
| 65 | + } else { |
| 66 | + # text |
| 67 | + $tag_val .= $attr_val; |
| 68 | + } |
| 69 | + } else { |
| 70 | + # string keys are for tag attributes |
| 71 | + if ( substr( $attr_key, 0, 2 ) != "__" ) { |
| 72 | + # include only non-reserved attributes |
| 73 | + if ( $attr_val !== null ) { |
| 74 | + $tag_open .= " $attr_key=\"" . $attr_val . "\""; |
| 75 | + } else { |
| 76 | + # null value of attribute is a special value for option selected |
| 77 | + $tag_open .= " $attr_key"; |
| 78 | + } |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | + if ( $tag_val !== null ) { |
| 83 | + $tag_open .= ">"; |
| 84 | + $tag_close .= "</" . $tag[ '__tag' ] . ">"; |
| 85 | + } else { |
| 86 | + $tag_open .= " />"; |
| 87 | + } |
| 88 | + if ( array_key_exists( '__end', $tag ) ) { |
| 89 | + $tag_close .= $tag[ '__end' ]; |
| 90 | + } |
| 91 | + } else { |
| 92 | + # tagless list |
| 93 | + $tag_val = ""; |
| 94 | + foreach( $tag as $attr_key=>&$attr_val ) { |
| 95 | + if ( is_int( $attr_key ) ) { |
| 96 | + if ( is_array( $attr_val ) ) { |
| 97 | + # recursive tags |
| 98 | + $tag_val .= self::toText( $attr_val ); |
| 99 | + } else { |
| 100 | + # text |
| 101 | + $tag_val .= $attr_val; |
| 102 | + } |
| 103 | + } else { |
| 104 | + ob_start(); |
| 105 | + var_dump($tag); |
| 106 | + $tagdump=ob_get_contents(); |
| 107 | + ob_end_clean(); |
| 108 | + $tag_val = "invalid argument: tagless list cannot have tag attribute values in key=$attr_key, $tagdump"; |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + } else { |
| 113 | + # just a text |
| 114 | + $tag_val = $tag; |
| 115 | + } |
| 116 | + return $tag_open . $tag_val . $tag_close; |
| 117 | + } |
| 118 | + |
| 119 | + # creates one "htmlobject" row of the table |
| 120 | + # elements of $row can be either a string/number value of cell or an array( "count"=>colspannum, "attribute"=>value, 0=>html_inside_tag ) |
| 121 | + # attribute maps can be like this: ("name"=>0, "count"=>colspan" ) |
| 122 | + static function newRow( $row, $rowattrs = "", $celltag = "td", $attribute_maps = null ) { |
| 123 | + $result = ""; |
| 124 | + if ( count( $row ) > 0 ) { |
| 125 | + foreach ( $row as &$cell ) { |
| 126 | + if ( !is_array( $cell ) ) { |
| 127 | + $cell = array( 0=>$cell ); |
| 128 | + } |
| 129 | + $cell[ '__tag' ] = $celltag; |
| 130 | + $cell[ '__end' ] = "\n"; |
| 131 | + if ( is_array( $attribute_maps ) ) { |
| 132 | + # converts ("count"=>3) to ("colspan"=>3) in table headers - don't use frequently |
| 133 | + foreach ( $attribute_maps as $key=>$val ) { |
| 134 | + if ( array_key_exists( $key, $cell ) ) { |
| 135 | + $cell[ $val ] = $cell[ $key ]; |
| 136 | + unset( $cell[ $key ] ); |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + $result = array( '__tag'=>'tr', 0=>$row, '__end'=>"\n" ); |
| 142 | + if ( is_array( $rowattrs ) ) { |
| 143 | + $result = array_merge( $rowattrs, $result ); |
| 144 | + } elseif ( $rowattrs !== "" ) { |
| 145 | + $result[0][] = __METHOD__ . ':invalid rowattrs supplied'; |
| 146 | + } |
| 147 | + } |
| 148 | + return $result; |
| 149 | + } |
| 150 | + |
| 151 | + # add row to the table |
| 152 | + static function addRow( &$table, $row, $rowattrs = "", $celltag = "td", $attribute_maps = null ) { |
| 153 | + $table[] = self::newRow( $row, $rowattrs, $celltag, $attribute_maps ); |
| 154 | + } |
| 155 | + |
| 156 | + # add column to the table |
| 157 | + static function addColumn( &$table, $column, $rowattrs = "", $celltag = "td", $attribute_maps = null ) { |
| 158 | + if ( count( $column ) > 0 ) { |
| 159 | + $row = 0; |
| 160 | + foreach ( $column as &$cell ) { |
| 161 | + if ( !is_array( $cell ) ) { |
| 162 | + $cell = array( 0=>$cell ); |
| 163 | + } |
| 164 | + $cell[ '__tag' ] = $celltag; |
| 165 | + $cell[ '__end' ] = "\n"; |
| 166 | + if ( is_array( $attribute_maps ) ) { |
| 167 | + # converts ("count"=>3) to ("rowspan"=>3) in table headers - don't use frequently |
| 168 | + foreach ( $attribute_maps as $key=>$val ) { |
| 169 | + if ( array_key_exists( $key, $cell ) ) { |
| 170 | + $cell[ $val ] = $cell[ $key ]; |
| 171 | + unset( $cell[ $key ] ); |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + if ( is_array( $rowattrs ) ) { |
| 176 | + $cell = array_merge( $rowattrs, $cell ); |
| 177 | + } elseif ( $rowattrs !== "" ) { |
| 178 | + $cell[ 0 ] = __METHOD__ . ':invalid rowattrs supplied'; |
| 179 | + } |
| 180 | + if ( !array_key_exists( $row, $table ) ) { |
| 181 | + $table[ $row ] = array( '__tag'=>'tr', '__end'=>"\n" ); |
| 182 | + } |
| 183 | + $table[ $row ][] = $cell; |
| 184 | + if ( array_key_exists( 'rowspan', $cell ) ) { |
| 185 | + $row += intval( $cell[ 'rowspan' ] ); |
| 186 | + } else { |
| 187 | + $row++; |
| 188 | + } |
| 189 | + } |
| 190 | + $result = array( '__tag'=>'tr', 0=>$column, '__end'=>"\n" ); |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + static function displayRow( $row, $rowattrs = "", $celltag = "td", $attribute_maps = null ) { |
| 195 | + return self::toText( self::newRow( $row, $rowattrs, $celltag, $attribute_maps ) ); |
| 196 | + } |
| 197 | + |
| 198 | + // use newRow() or addColumn() to add resulting row/column to the table |
| 199 | + // if you want to use the resulting row with toText(), don't forget to apply attrs=array('__tag'=>'td') |
| 200 | + static function applyAttrsToRow( &$row, $attrs ) { |
| 201 | + if ( is_array( $attrs ) && count( $attrs > 0 ) ) { |
| 202 | + foreach( $row as &$cell ) { |
| 203 | + if ( !is_array( $cell ) ) { |
| 204 | + $cell = array_merge( $attrs, array( $cell ) ); |
| 205 | + } else { |
| 206 | + foreach( $attrs as $attr_key=>$attr_val ) { |
| 207 | + if ( !array_key_exists( $attr_key, $cell ) ) { |
| 208 | + $cell[ $attr_key ] = $attr_val; |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | +} /* end of CB_XML class */ |
| 216 | + |
| 217 | +/* |
| 218 | + * Localization of SQL tokens list |
| 219 | + * comparsions like "a > 1" are treated like single-ops |
| 220 | + */ |
| 221 | +class CB_LocalExpr { |
| 222 | + |
| 223 | + var $src_tokens; |
| 224 | + var $local_tokens; |
| 225 | + |
| 226 | + /* |
| 227 | + * @param $tokens - list of SQL condition tokens (infix or polish) |
| 228 | + * comparsions like "a > 1" are treated like single-ops |
| 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" |
| 238 | + return; |
| 239 | + } |
| 240 | + 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 ); |
| 249 | + 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__ ); |
| 264 | + } |
| 265 | + } |
| 266 | + if ( $field == '' ) { |
| 267 | + $this->local_tokens[] = wfMsg( "cb_${op}_op" ); |
| 268 | + } elseif ( $num == '' ) { |
| 269 | + $this->local_tokens[] = wfMsg( 'cb_op1_template', wfMsg( "cb_${op}_op" ), wfMsg( "cb_${field}" ) ); |
| 270 | + } else { |
| 271 | + $this->local_tokens[] = wfMsg( 'cb_op2_template', wfMsg( "cb_${field}" ), wfMsg( "cb_${op}_op" ), $num ); |
| 272 | + } |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + function toString() { |
| 277 | + return implode( ' ', $this->local_tokens ); |
| 278 | + } |
| 279 | + |
| 280 | +} /* end of CB_LocalExpr class */ |
| 281 | + |
| 282 | +/* builds a bracketed sql condition either from the list of infix array $tokens or |
| 283 | + * from encoded reverse polish operations string $enc |
| 284 | + * |
| 285 | + * properly bracketed sql condition uses brackets to display the actual priority of expression |
| 286 | + * |
| 287 | + */ |
| 288 | +class CB_SqlCond { |
| 289 | + |
| 290 | + static $decoded_fields = array( 'p'=>'cat_pages', 's'=>'cat_subcats', 'f'=>'cat_files' ); |
| 291 | + static $decoded_cmps = array( 'ge'=>'>=', 'le'=>'<=', 'eq'=>'=' ); |
| 292 | + |
| 293 | + static $encoded_fields = array( 'cat_subcats'=>'s', 'cat_pages'=>'p', 'cat_files'=>'f' ); |
| 294 | + static $encoded_cmps = array( '>='=>'ge', '<='=>'le', '='=>'eq' ); |
| 295 | + |
| 296 | + # reverse polish operations queue (decoded form, every op is an element of array) |
| 297 | + # 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 |
| 301 | + |
| 302 | + # infix operations queue |
| 303 | + # comparsions like "a > 1" are treated like single-ops |
| 304 | + var $infix_queue; |
| 305 | + |
| 306 | + // used privately by decodeToken() |
| 307 | + private static $valid_logical_ops; |
| 308 | + private static $valid_bracket_ops; |
| 309 | + |
| 310 | + /* |
| 311 | + * constructor (creates an instance, initializes $this->queue, returns an instance) |
| 312 | + * |
| 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 |
| 316 | + * (underscore-separated encoded polish tokens) |
| 317 | + */ |
| 318 | + public static function newFromEncodedPolishQueue( $enc ) { |
| 319 | + $sc = new CB_SqlCond(); |
| 320 | + self::$valid_logical_ops = array( 'and', 'or' ); |
| 321 | + self::$valid_bracket_ops = array(); |
| 322 | + $sc->queue = array(); |
| 323 | + $q = explode( '_', $enc ); |
| 324 | + # {{{ validation of expression |
| 325 | + $cmp_count = $logical_count = 0; |
| 326 | + # }}} |
| 327 | + foreach( $q as &$token ) { |
| 328 | + $result = self::decodeToken( $token ); |
| 329 | + $sc->queue[] = $result->token; |
| 330 | + if ( $result->type == 'comparsion' ) { |
| 331 | + $cmp_count++; |
| 332 | + } elseif ( $result->type == 'logical' ) { |
| 333 | + $logical_count++; |
| 334 | + } else { |
| 335 | + # tampered or bugged $enc, return default "all" instead |
| 336 | + $sc->queue = array(); |
| 337 | + return $sc; |
| 338 | + } |
| 339 | + } |
| 340 | + if ( $cmp_count < 1 || $cmp_count != $logical_count + 1 ) { |
| 341 | + # tampered or bugged $enc, return default "all" instead |
| 342 | + $sc->queue = array(); |
| 343 | + return $sc; |
| 344 | + } |
| 345 | + if ( $logical_count > CB_MAX_LOGICAL_OP ) { |
| 346 | + # too complex $enc (fabricated or non-realistic), return default "all" instead |
| 347 | + $sc->queue = array(); |
| 348 | + return $sc; |
| 349 | + } |
| 350 | + return $sc; |
| 351 | + } |
| 352 | + |
| 353 | + /* |
| 354 | + * constructor (creates an instance, initializes $this->infix_queue, returns an instance) |
| 355 | + * |
| 356 | + * 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 |
| 359 | + * @param $enc - string encoded infix operations queue |
| 360 | + * (underscore-separated encoded infix tokens) |
| 361 | + */ |
| 362 | + public static function newFromEncodedInfixQueue( $enc ) { |
| 363 | + self::$valid_logical_ops = array( 'and', 'or' ); |
| 364 | + self::$valid_bracket_ops = array( '(', ')' ); |
| 365 | + $infix_queue = array(); |
| 366 | + $q = explode( '_', $enc ); |
| 367 | + # {{{ validation of expression |
| 368 | + $brackets_level = 0; $prev_type = ''; |
| 369 | + # }}} |
| 370 | + foreach( $q as &$token ) { |
| 371 | + $result = self::decodeToken( $token ); |
| 372 | + $infix_queue[] = $result->token; |
| 373 | + if ( $result->type == 'bracket' ) { |
| 374 | + if ( $result->token == '(' ) { |
| 375 | + $brackets_level++; |
| 376 | + } else { |
| 377 | + $brackets_level--; |
| 378 | + } |
| 379 | + if ( $brackets_level < 0 ) { |
| 380 | + # tampered or bugged $enc, use default "all" instead |
| 381 | + $infix_queue = array(); |
| 382 | + break; |
| 383 | + } |
| 384 | + } elseif ( $result->type == 'logical' ) { |
| 385 | + if ( $prev_type == '' || $prev_type == 'logical' ) { |
| 386 | + # tampered or bugged $enc, use default "all" instead |
| 387 | + $infix_queue = array(); |
| 388 | + break; |
| 389 | + } |
| 390 | + } elseif ( $result->type == 'comparsion' ) { |
| 391 | + if ( $prev_type == 'comparsion' ) { |
| 392 | + # tampered or bugged $enc, use default "all" instead |
| 393 | + $infix_queue = array(); |
| 394 | + break; |
| 395 | + } |
| 396 | + } else { |
| 397 | + # tampered or bugged $enc, use default "all" instead |
| 398 | + $infix_queue = array(); |
| 399 | + break; |
| 400 | + } |
| 401 | + $prev_type = $result->type; |
| 402 | + } |
| 403 | + if ( $brackets_level != 0 ) { |
| 404 | + # tampered or bugged $enc, use default "all" instead |
| 405 | + $infix_queue = array(); |
| 406 | + } |
| 407 | + return self::newFromInfixTokens( $infix_queue ); |
| 408 | + } |
| 409 | + |
| 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 | + /* |
| 438 | + * constructor (creates an instance, initializes $this->queue, returns an instance) |
| 439 | + * |
| 440 | + * fills up polish notation array $this->queue from infix $tokens provided |
| 441 | + * @param $tokens - array of infix tokens |
| 442 | + */ |
| 443 | + # converts list of given infix $tokens into $this->queue of reverse polish notation |
| 444 | + # TODO: more throughout checks for invalid tokens given |
| 445 | + public static function newFromInfixTokens( array $tokens ) { |
| 446 | + $sc = new CB_SqlCond(); |
| 447 | + $stack = array(); // every element is stdobject with token and prio fields |
| 448 | + $sc->queue = array(); |
| 449 | + 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; |
| 461 | + } |
| 462 | + array_push( $sc->queue, $last->token ); |
| 463 | + } else { |
| 464 | + throw new MWException( 'Open / closing brackets mismatch in ' . __METHOD__ ); |
| 465 | + } |
| 466 | + } |
| 467 | + break; |
| 468 | + case 'OR' : |
| 469 | + case 'AND' : |
| 470 | + $prio = strtoupper( $token ) == 'OR' ? 2 : 3; |
| 471 | + while ( $last = array_pop( $stack ) ) { |
| 472 | + if ( is_object( $last) && $last->prio >= $prio ) { |
| 473 | + array_push( $sc->queue, $last->token ); |
| 474 | + } else { |
| 475 | + array_push( $stack, $last ); |
| 476 | + break; |
| 477 | + } |
| 478 | + } |
| 479 | + array_push( $stack, (object) array( 'token'=>$token, 'prio'=>$prio ) ); |
| 480 | + break; |
| 481 | + default : // comparsion subexpression |
| 482 | + array_push( $sc->queue, $token ); |
| 483 | + } |
| 484 | + } |
| 485 | + while ( $last = array_pop( $stack ) ) { |
| 486 | + if ( !is_object( $last ) ) { |
| 487 | + break; |
| 488 | + } |
| 489 | + array_push( $sc->queue, $last->token ); |
| 490 | + } |
| 491 | + return $sc; |
| 492 | + } |
| 493 | + |
| 494 | +/* |
| 495 | +src:'(', 'cat_pages > 1000', 'OR', 'cat_subcats > 10', ')', 'AND', 'cat_files > 100' |
| 496 | +dst:cat_pages > 1000;cat_subcats > 10;OR;cat_files > 100;AND |
| 497 | + |
| 498 | +('','AND','') 'AND' comes to initial empty triple (level 0) |
| 499 | +('','AND','cat_files > 100') 'cat_files > 100' becomes right param of current triple (level 0) |
| 500 | +(('','OR',''),'AND','cat_files > 100') 'OR' becomes left recursive param of current triple (level 1), because right param is already occupied |
| 501 | +(('','OR','cat_subcats > 10'),'AND','cat_files > 100') 'cat_subcats > 10' becomes right param of current triple (level 1) |
| 502 | +(('cat_pages > 1000','OR','cat_subcats > 10'),'AND','cat_files > 100') 'cat_pages > 1000' becomes right param of current triple (level 1) |
| 503 | + |
| 504 | +src:'cat_pages > 1000', 'OR', 'cat_subcats > 10', 'AND', 'cat_files > 100' |
| 505 | +dst:cat_pages > 1000;cat_subcats > 10;cat_files > 100;AND;OR |
| 506 | + |
| 507 | +('','OR','') 'OR' comes to initial empty triple (level 0) |
| 508 | +('','OR',('','AND','')) 'AND' becomes right recursive entry (level 1) |
| 509 | +('','OR',('','AND','cat_files > 100')) 'cat_files > 100' becomes right param of current triple (level 1) |
| 510 | +('','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 | +('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 | + |
| 513 | +1. global counter of queue position, getting elements consequtively from right to left |
| 514 | +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 | +3. operands are going to current entry, right to left |
| 516 | +4. generating the string recursively going from left to right |
| 517 | + |
| 518 | +in actual code (null,null,null) isset() is used instead of ('','','') |
| 519 | +*/ |
| 520 | + # generate triples (see example above) from $this->queue |
| 521 | + # |
| 522 | + # param &$currTriple - recursively adds new triples to $currTriple |
| 523 | + private function buildTriples( &$currTriple ) { |
| 524 | + # 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 |
| 530 | + if ( !isset( $currTriple[1] ) ) { |
| 531 | + $currTriple[1] = $token; |
| 532 | + $this->queue_pos--; |
| 533 | + } elseif ( !isset( $currTriple[2] ) ) { |
| 534 | + $currTriple[2] = array(); |
| 535 | + $this->buildTriples( $currTriple[2] ); |
| 536 | + } elseif ( !isset( $currTriple[0] ) ) { |
| 537 | + $currTriple[0] = array(); |
| 538 | + $this->buildTriples( $currTriple[0] ); |
| 539 | + } else { |
| 540 | + return; |
| 541 | + } |
| 542 | + } else { |
| 543 | + // operands |
| 544 | + if ( !isset( $currTriple[2] ) ) { |
| 545 | + $currTriple[2] = $token; |
| 546 | + $this->queue_pos--; |
| 547 | + } elseif ( !isset( $currTriple[0] ) ) { |
| 548 | + $currTriple[0] = $token; |
| 549 | + $this->queue_pos--; |
| 550 | + } else { |
| 551 | + return; |
| 552 | + } |
| 553 | + } |
| 554 | + } |
| 555 | + } |
| 556 | + |
| 557 | + /* |
| 558 | + * build properly bracketed infix expression string |
| 559 | + * also builds $this->infix_queue array |
| 560 | + * from triples tree previousely built by CategoryFilter::buildTriples (left to right) |
| 561 | + */ |
| 562 | + private $infixLevel; // used to do not include brackets at level 0 |
| 563 | + private function getInfixExpr( &$out, $currTriple ) { |
| 564 | + $this->infixLevel++; |
| 565 | + if ( $this->infixLevel != 0 ) { |
| 566 | + $this->infix_queue[] = '('; |
| 567 | + $out .= '('; |
| 568 | + } |
| 569 | + if ( isset( $currTriple[0] ) ) { |
| 570 | + if ( is_array( $currTriple[0] ) ) { |
| 571 | + $this->getInfixExpr( $out, $currTriple[0] ); |
| 572 | + } else { |
| 573 | + $this->infix_queue[] = $currTriple[0]; |
| 574 | + $out .= $currTriple[0]; |
| 575 | + } |
| 576 | + } |
| 577 | + if ( isset( $currTriple[1] ) ) { |
| 578 | + $this->infix_queue[] = $currTriple[1]; |
| 579 | + $out .= ' ' . $currTriple[1] . ' '; |
| 580 | + } |
| 581 | + if ( isset( $currTriple[2] ) ) { |
| 582 | + if ( is_array( $currTriple[2] ) ) { |
| 583 | + $this->getInfixExpr( $out, $currTriple[2] ); |
| 584 | + } else { |
| 585 | + $this->infix_queue[] = $currTriple[2]; |
| 586 | + $out .= $currTriple[2]; |
| 587 | + } |
| 588 | + } |
| 589 | + if ( $this->infixLevel != 0 ) { |
| 590 | + $this->infix_queue[] = ')'; |
| 591 | + $out .= ')'; |
| 592 | + } |
| 593 | + $this->infixLevel--; |
| 594 | + } |
| 595 | + |
| 596 | + /* |
| 597 | + * get SQL condition expression with full brackets (to indicate operators priority) |
| 598 | + * *** !!also builds $this->infix_queue array!! *** |
| 599 | + */ |
| 600 | + function getCond() { |
| 601 | + $rootTriple = array(); |
| 602 | + $this->queue_pos = count( $this->queue ) - 1; |
| 603 | + $this->buildTriples( $rootTriple ); |
| 604 | + $out = ''; |
| 605 | + $this->infixLevel = -1; |
| 606 | + $this->infix_queue = array(); |
| 607 | + # also builds $this->infix_queue array |
| 608 | + $this->getInfixExpr( $out, $rootTriple ); |
| 609 | + if ( count( $this->infix_queue ) == 0 ) { |
| 610 | + $this->infix_queue = array( '' ); // default "all" |
| 611 | + } |
| 612 | + return $out; |
| 613 | + } |
| 614 | + |
| 615 | + /* |
| 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" |
| 618 | + * |
| 619 | + */ |
| 620 | + function getEncodedQueue( $is_infix = false ) { |
| 621 | + $result = ''; |
| 622 | + if ( $is_infix ) { |
| 623 | + $valid_single_ops = array( '(', ')', 'or', 'and' ); |
| 624 | + if ( !is_array( $this->infix_queue ) ) { |
| 625 | + $this->getCond(); |
| 626 | + } |
| 627 | + $queue = &$this->infix_queue; |
| 628 | + } else { |
| 629 | + $valid_single_ops = array( 'or', 'and' ); |
| 630 | + $queue = &$this->queue; |
| 631 | + } |
| 632 | + if ( count( $queue ) == 1 && $queue[0] == '' ) { |
| 633 | + return 'all'; // default "show all" |
| 634 | + } |
| 635 | + $firstElem = true; |
| 636 | + foreach ( $queue as &$token ) { |
| 637 | + if ( $firstElem ) { |
| 638 | + $firstElem = false; |
| 639 | + } else { |
| 640 | + $result .= '_'; |
| 641 | + } |
| 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 | + } |
| 671 | + } |
| 672 | + return $result; |
| 673 | + } |
| 674 | + |
| 675 | +} /* end of CB_SqlCond class */ |
Property changes on: trunk/extensions/CategoryBrowser/CategoryBrowserBasic.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 676 | + native |
Index: trunk/extensions/CategoryBrowser/category_browser.js |
— | — | @@ -0,0 +1,1665 @@ |
| 2 | +/** |
| 3 | + * ***** BEGIN LICENSE BLOCK ***** |
| 4 | + * This file is part of CategoryBrowser. |
| 5 | + * |
| 6 | + * CategoryBrowser is free software; you can redistribute it and/or modify |
| 7 | + * it under the terms of the GNU General Public License as published by |
| 8 | + * the Free Software Foundation; either version 2 of the License, or |
| 9 | + * (at your option) any later version. |
| 10 | + * |
| 11 | + * CategoryBrowser is distributed in the hope that it will be useful, |
| 12 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | + * GNU General Public License for more details. |
| 15 | + * |
| 16 | + * You should have received a copy of the GNU General Public License |
| 17 | + * along with CategoryBrowser; if not, write to the Free Software |
| 18 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 19 | + * |
| 20 | + * ***** END LICENSE BLOCK ***** |
| 21 | + * |
| 22 | + * CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
| 23 | + * |
| 24 | + * To activate this extension : |
| 25 | + * * Create a new directory named CategoryBrowser into the directory "extensions" of MediaWiki. |
| 26 | + * * Place the files from the extension archive there. |
| 27 | + * * Add this line at the end of your LocalSettings.php file : |
| 28 | + * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
| 29 | + * |
| 30 | + * @version 0.2.0 |
| 31 | + * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
| 32 | + * @author Dmitriy Sintsov <questpc@rambler.ru> |
| 33 | + * @addtogroup Extensions |
| 34 | + */ |
| 35 | + |
| 36 | +/* |
| 37 | + * basic functions |
| 38 | + */ |
| 39 | +var CB_lib = { |
| 40 | + log : function( s ) { |
| 41 | + if ( typeof console != "undefined" ) { |
| 42 | + console.log( s ); |
| 43 | + } |
| 44 | + }, |
| 45 | + |
| 46 | + /* |
| 47 | + * get Internet Explorer version |
| 48 | + * @return version of Internet Explorer or 1000 (indicating the use of another browser) |
| 49 | + */ |
| 50 | + getIEver : function() { |
| 51 | + var rv = 1000; |
| 52 | + if (navigator.appName == 'Microsoft Internet Explorer') { |
| 53 | + var ua = navigator.userAgent; |
| 54 | + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); |
| 55 | + if (re.exec(ua) != null) { |
| 56 | + rv = parseFloat( RegExp.$1 ); |
| 57 | + } |
| 58 | + } |
| 59 | + return rv; |
| 60 | + }, |
| 61 | + |
| 62 | + /*** general event handling ***/ |
| 63 | + addEvent : function ( domObj, type, fn ) { |
| 64 | + if ( domObj.addEventListener ) { |
| 65 | + domObj.addEventListener( type, fn, false ); |
| 66 | + } else if ( domObj.attachEvent ) { |
| 67 | + domObj["e"+type+fn] = fn; |
| 68 | + domObj[type+fn] = function() { domObj["e"+type+fn]( window.event ); } |
| 69 | + domObj.attachEvent( "on"+type, domObj[type+fn] ); |
| 70 | + } else { |
| 71 | + domObj["on"+type] = domObj["e"+type+fn]; |
| 72 | + alert( 'Your browser does not support proper event attaching' ); |
| 73 | + } |
| 74 | + }, |
| 75 | + |
| 76 | + getEventObj : function ( event, stopPropagation ) { |
| 77 | + var obj; |
| 78 | + if ( typeof event.target !== 'undefined' ) { |
| 79 | + obj = event.target; |
| 80 | + if ( stopPropagation ) { |
| 81 | + event.stopPropagation(); |
| 82 | + } |
| 83 | + } else { |
| 84 | + obj = event.srcElement; |
| 85 | + if ( stopPropagation ) { |
| 86 | + event.cancelBubble = true; |
| 87 | + } |
| 88 | + } |
| 89 | + return obj; |
| 90 | + }, |
| 91 | + |
| 92 | + // basename prefix of user's cookies |
| 93 | + cookiePrefix : null, |
| 94 | + |
| 95 | + /* |
| 96 | + * TODO: unused, remove |
| 97 | + */ |
| 98 | + setCookiePrefix : function( name ) { |
| 99 | + this.cookiePrefix = name; |
| 100 | + }, |
| 101 | + |
| 102 | + /* |
| 103 | + * TODO: unused, remove |
| 104 | + * @return empty string in case cookie value is empty, null when cookie is not set |
| 105 | + */ |
| 106 | + getCookie : function ( cookieName ) { |
| 107 | + var ca, cn, keyval, key, val; |
| 108 | + ca = document.cookie.split( ';' ); |
| 109 | + for ( var i=0; i < ca.length; i++ ) { |
| 110 | + keyval = ca[i].split( '=' ); |
| 111 | + // trim whitespace |
| 112 | + key = keyval[0].replace(/^\s+|\s+$/g, ''); |
| 113 | + if ( key == (this.cookiePrefix + cookieName) ) { |
| 114 | + if ( keyval.length > 1 ) { |
| 115 | + return unescape( keyval[1].replace(/^\s+|\s+$/g, '') ); |
| 116 | + } else { |
| 117 | + // cookie exists but has no value |
| 118 | + return ""; |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + // cookie not found |
| 123 | + return null; |
| 124 | + }, |
| 125 | + |
| 126 | + /* |
| 127 | + * TODO: unused, remove |
| 128 | + * usage example: CB_lib.setCookie( 'rootcond', eventObj.value, 24 * 60 * 60, '/' ); |
| 129 | + */ |
| 130 | + setCookie : function( cookieName, value, expires, path, domain, secure ) { |
| 131 | + // set time, it's in milliseconds |
| 132 | + var today = new Date(); |
| 133 | + today.setTime( today.getTime() ); |
| 134 | + |
| 135 | + /* |
| 136 | + if the expires variable is set, make the correct |
| 137 | + expires time, the current script below will set |
| 138 | + it for x number of days, to make it for hours, |
| 139 | + delete * 24, for minutes, delete * 60 * 24 |
| 140 | + */ |
| 141 | + if ( expires ) { |
| 142 | + expires = expires * 1000 // * 60 * 60 * 24; |
| 143 | + } |
| 144 | + var expires_date = new Date( today.getTime() + expires ); |
| 145 | + |
| 146 | + document.cookie = this.cookiePrefix + cookieName + "=" +escape( value ) + |
| 147 | + ( ( expires ) ? ";expires=" + expires_date.toGMTString() : "" ) + |
| 148 | + ( ( path ) ? ";path=" + path : "" ) + |
| 149 | + ( ( domain ) ? ";domain=" + domain : "" ) + |
| 150 | + ( ( secure ) ? ";secure" : "" ); |
| 151 | + }, |
| 152 | + |
| 153 | + /*** simple form elements generators ***/ |
| 154 | + /* |
| 155 | + * generate select/option from the list given |
| 156 | + * @param optionsList - object key/value pairs |
| 157 | + * @param selectedOption - selected key in optionsList |
| 158 | + */ |
| 159 | + htmlSelector : function( optionsList, selectedOption ) { |
| 160 | + var option; |
| 161 | + var select = document.createElement( 'select' ); |
| 162 | + for ( var key in optionsList ) { |
| 163 | + option = document.createElement( 'option' ); |
| 164 | + option.setAttribute( 'value', key ); |
| 165 | + option.appendChild( document.createTextNode( optionsList[ key ] ) ); |
| 166 | + if ( key == selectedOption ) { |
| 167 | + option.setAttribute( 'selected', '' ); |
| 168 | + } |
| 169 | + select.appendChild( option ); |
| 170 | + } |
| 171 | + return select; |
| 172 | + }, |
| 173 | + |
| 174 | + /* |
| 175 | + * generate input box from value given |
| 176 | + * @param value to put into input box |
| 177 | + */ |
| 178 | + textInput : function( value ) { |
| 179 | + var vs = value.toString(); |
| 180 | + var input = document.createElement( 'input' ); |
| 181 | + input.setAttribute( 'type', 'text' ); |
| 182 | + input.style.width = (vs.length) * 0.75 + "em"; |
| 183 | + input.setAttribute( 'value', vs ); |
| 184 | + return input; |
| 185 | + }, |
| 186 | + |
| 187 | + /* |
| 188 | + * find option node of select which has the specified value of value attribute |
| 189 | + * @param select select node |
| 190 | + * @param value of option attribute to find (if not specified, uses select.value) |
| 191 | + * @param attribute name of option attribute which value has to be searched for (if not specified, used 'value' attribute) |
| 192 | + */ |
| 193 | + getSelectOption : function( select, value, attribute ) { |
| 194 | + var node; |
| 195 | + var attribute = typeof attribute == 'undefined' ? 'value' : attribute; |
| 196 | + var valueToFind = typeof value == 'undefined' ? select.value : value; |
| 197 | + if ( typeof valueToFind == 'undefined' ) { |
| 198 | + alert( 'No value to find or no selected option in CB_lib.getSelectOption' ); |
| 199 | + return null; |
| 200 | + } |
| 201 | + for ( var i=0; i < select.childNodes.length; i++ ) { |
| 202 | + node = select.childNodes[i]; |
| 203 | + if ( node.nodeType == 1 ) { |
| 204 | + if ( node.getAttribute( attribute ) == valueToFind ) { |
| 205 | + return select.childNodes[i]; |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | + return null; |
| 210 | + }, |
| 211 | + |
| 212 | + divButton : function( buttonOperation, buttonText, buttonTitle ) { |
| 213 | + var element = document.createElement( 'div' ); |
| 214 | + element.className = 'cb_control_button'; |
| 215 | + element.appendChild( document.createTextNode( buttonText ) ); |
| 216 | + element.setAttribute( 'buttonoperation', buttonOperation ); |
| 217 | + if ( typeof buttonTitle !== 'undefined' && buttonTitle != '' ) { |
| 218 | + element.setAttribute( 'title', buttonTitle ); |
| 219 | + } |
| 220 | + return element; |
| 221 | + } |
| 222 | + |
| 223 | +} /* end of CB_lib */ |
| 224 | + |
| 225 | +/* |
| 226 | + * UI settings |
| 227 | + */ |
| 228 | +var CB_Setup = { |
| 229 | + isIE: CB_lib.getIEver(), |
| 230 | + |
| 231 | + // various colors used in CB_ConditionEditor / CB_EditLine / CB_Token |
| 232 | + colors : { |
| 233 | + 'samples': { 'color': 'white', 'bgr': 'gray' }, |
| 234 | + 'error': { 'color': 'red', 'bgr': 'aquamarine' }, |
| 235 | + 'copy': { 'color': 'white', 'bgr': 'blue' } |
| 236 | + }, |
| 237 | + // delay in seconds before performing ajax call to reduce server load |
| 238 | + 'ajaxPollTimeout' : 2000 |
| 239 | +} /* end of CB_Setup */ |
| 240 | + |
| 241 | +var CategoryBrowser = { |
| 242 | + |
| 243 | +/* |
| 244 | + * AJAX category tree section |
| 245 | + */ |
| 246 | + |
| 247 | + // currently selected encoded reverse polish queue |
| 248 | + rootCond : null, |
| 249 | + // category name filter (LIKE) |
| 250 | + nameFilter : '', |
| 251 | + // category name filter case-insensetive flag (when true, queries use LIKE COLLATE) |
| 252 | + nameFilterCI : false, |
| 253 | + // limit of pager query (also passed further via AJAX call) |
| 254 | + pagerLimit : null, |
| 255 | + // nested container object |
| 256 | + container : null, |
| 257 | + |
| 258 | + /* |
| 259 | + * @param encPolishQueue encoded queue in reverse polish format (SQL condition) |
| 260 | + * @param rootCond condition string in encoded polish format |
| 261 | + * @param offset queue offset |
| 262 | + * @param limit (optional) |
| 263 | + */ |
| 264 | + rootCats : function( rootCond, offset, limit ) { |
| 265 | + this.rootCond = rootCond; |
| 266 | + var param = [this.rootCond, this.nameFilter, this.nameFilterCI, offset]; |
| 267 | + if ( limit ) { |
| 268 | + param.push( limit ); |
| 269 | + } else { |
| 270 | + if ( this.pagerLimit !== null ) { |
| 271 | + param.push( this.pagerLimit ); |
| 272 | + } |
| 273 | + } |
| 274 | + sajax_do_call( "CategoryBrowser::getRootOffsetHtml", param, document.getElementById( "cb_root_container" ) ); |
| 275 | + return false; |
| 276 | + }, |
| 277 | + |
| 278 | + /* |
| 279 | + * find DOM node of currently clicked control |
| 280 | + * @param eventObj - click event object |
| 281 | + * @return DOM node (which lastChild may contain the nested list of entries) if found, null otherwise |
| 282 | + */ |
| 283 | + findNode : function( eventObj ) { |
| 284 | + for ( var node = eventObj.parentNode; node != null; node = node.parentNode ) { |
| 285 | + if ( node.className == 'cb_cat_container' ) { |
| 286 | + break; |
| 287 | + } |
| 288 | + } |
| 289 | + return node; |
| 290 | + }, |
| 291 | + |
| 292 | + /* |
| 293 | + * create / get nested DOM container of the specified category DOM node |
| 294 | + * @param node - category DOM node |
| 295 | + */ |
| 296 | + getNestedContainer : function( node ) { |
| 297 | + var container = node.lastChild; |
| 298 | + if ( !( container && |
| 299 | + container.nodeType == 1 && |
| 300 | + container.className == 'cb_nested_container' ) ) { |
| 301 | + container = document.createElement( 'DIV' ); |
| 302 | + container.style.display = 'none'; |
| 303 | + container.className = 'cb_nested_container'; |
| 304 | + node.appendChild( container ); |
| 305 | + } |
| 306 | + this.container = container; |
| 307 | + }, |
| 308 | + |
| 309 | + /* |
| 310 | + * switch visibility of container |
| 311 | + * @param: id - set id attribute of nested container (used in setContainerVisibility() method) |
| 312 | + */ |
| 313 | + setContainerVisibility : function( id ) { |
| 314 | + if ( this.container.style.display == 'none' || |
| 315 | + this.container.getAttribute( 'id' ) != id ) { |
| 316 | + this.container.style.display = 'block'; |
| 317 | + } else { |
| 318 | + this.container.style.display = 'none'; |
| 319 | + } |
| 320 | + this.container.setAttribute( 'id', id ); |
| 321 | + }, |
| 322 | + |
| 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 | + } |
| 330 | + } |
| 331 | + this.subOffset( eventObj, param ); |
| 332 | + return false; |
| 333 | + }, |
| 334 | + |
| 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; |
| 342 | + }, |
| 343 | + |
| 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; |
| 350 | + }, |
| 351 | + |
| 352 | + subOffset : function( eventObj, param ) { |
| 353 | + var treeNode = this.findNode( eventObj ); |
| 354 | + if ( treeNode == null ) { |
| 355 | + alert( 'Cannot find DOM node object of event click object in CategoryBrowser.subOffset()' ); |
| 356 | + return; |
| 357 | + } |
| 358 | + this.getNestedContainer( treeNode ); |
| 359 | + sajax_do_call( "CategoryBrowser::getSubOffsetHtml", param, this.container ); |
| 360 | + }, |
| 361 | + |
| 362 | + pagesNav : function( eventObj, catId, offset, limit ) { |
| 363 | + var param = ['pages', catId]; |
| 364 | + if ( offset ) { |
| 365 | + param.push( offset ); |
| 366 | + if ( limit ) { |
| 367 | + param.push( limit ); |
| 368 | + } |
| 369 | + } |
| 370 | + // used .parentNode to skip self container |
| 371 | + this.subOffset( eventObj.parentNode, param ); |
| 372 | + return false; |
| 373 | + }, |
| 374 | + |
| 375 | + pagesLink : function( eventObj, catId ) { |
| 376 | + eventObj.blur(); |
| 377 | + var param = ['pages', catId]; |
| 378 | + this.subOffset( eventObj, param ); |
| 379 | + this.setContainerVisibility( 'cb_nested_pages' ); |
| 380 | + return false; |
| 381 | + }, |
| 382 | + |
| 383 | + filesNav : function( eventObj, catId, offset, limit ) { |
| 384 | + var param = ['files', catId]; |
| 385 | + if ( offset ) { |
| 386 | + param.push( offset ); |
| 387 | + if ( limit ) { |
| 388 | + param.push( limit ); |
| 389 | + } |
| 390 | + } |
| 391 | + // used .parentNode to skip self container |
| 392 | + this.subOffset( eventObj.parentNode, param ); |
| 393 | + return false; |
| 394 | + }, |
| 395 | + |
| 396 | + filesLink : function( eventObj, catId ) { |
| 397 | + eventObj.blur(); |
| 398 | + var param = ['files', catId]; |
| 399 | + this.subOffset( eventObj, param ); |
| 400 | + this.setContainerVisibility( 'cb_nested_files' ); |
| 401 | + return false; |
| 402 | + }, |
| 403 | + |
| 404 | + setNameFilter : function( eventObj ) { |
| 405 | + var id = eventObj.getAttribute( 'id' ); |
| 406 | + if ( this.rootCond === null ) { |
| 407 | + this.rootCond = document.getElementById( 'cb_expr_select' ).value; |
| 408 | + if ( this.rootCond === null ) { |
| 409 | + alert( 'Cannot find selected rootCond option in CategoryBrowser.setNameFilter' ); |
| 410 | + } |
| 411 | + } |
| 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 ); |
| 425 | + return true; |
| 426 | + }, |
| 427 | + |
| 428 | + nameFilterCall : function() { |
| 429 | + if ( this.rootCond === null ) { |
| 430 | + this.rootCond = document.getElementById( 'cb_expr_select' ).value; |
| 431 | + } |
| 432 | + var nameFilter = document.getElementById( 'cb_cat_name_filter' ).value; |
| 433 | + var CIcheckbox = document.getElementById( 'cb_cat_name_filter_ci' ); |
| 434 | + if ( CIcheckbox !== null ) { |
| 435 | + var nameFilterCI = CIcheckbox.checked; |
| 436 | + } |
| 437 | + if ( this.rootCond !== null && |
| 438 | + ( this.nameFilter != nameFilter || this.nameFilterCI != nameFilterCI ) ) { |
| 439 | + // in case nameFilter field was changed, update the root pager |
| 440 | + this.nameFilter = nameFilter; |
| 441 | + this.nameFilterCI = nameFilterCI; |
| 442 | + var param = [this.rootCond, this.nameFilter, this.nameFilterCI, 0]; |
| 443 | + if ( this.pagerLimit !== null ) { |
| 444 | + param.push( this.pagerLimit ); |
| 445 | + } |
| 446 | + sajax_do_call( "CategoryBrowser::getRootOffsetHtml", param, document.getElementById( "cb_root_container" ) ); |
| 447 | + } |
| 448 | + window.setTimeout( function() { CategoryBrowser.nameFilterCall(); }, CB_Setup.ajaxPollTimeout ); |
| 449 | + }, |
| 450 | + |
| 451 | + /* |
| 452 | + * condition selector (with cookie manager) |
| 453 | + * warning! use CB_lib.log(); placing debug alert() in js code may screw up event handling |
| 454 | + */ |
| 455 | + setExpr : function( eventObj, pagerLimit ) { |
| 456 | + this.rootCond = eventObj.value; |
| 457 | + var selectedOption = CB_lib.getSelectOption( eventObj ); |
| 458 | + if ( selectedOption === null ) { |
| 459 | + alert( 'Cannot find selected option in CategoryBrowser.setExpr' ); |
| 460 | + } |
| 461 | + var selectedEncInfixQueue = selectedOption.getAttribute( 'infixexpr' ); |
| 462 | + CB_lib.log('setExpr selectedEncInfixQueue='+selectedEncInfixQueue); |
| 463 | + this.pagerLimit = pagerLimit; |
| 464 | + 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" ) ); |
| 466 | + CB_ConditionEditor.createExpr( selectedEncInfixQueue ); |
| 467 | + return true; |
| 468 | + } |
| 469 | + |
| 470 | +} /* end of CategoryBrowser */ |
| 471 | + |
| 472 | +/* |
| 473 | + * editor part |
| 474 | + */ |
| 475 | + |
| 476 | + /* |
| 477 | + * token mvc all in one |
| 478 | + * mvc is not separated to avoid unnecessarily code monstrousity |
| 479 | + * @token - encoded infix token string (single) |
| 480 | + * @lineInstanceName - string property name of CB_ConditionEditor, where the token is stored |
| 481 | + * ( CB_ConditionEditor[lineInstanceName] is an instance of CB_EditLine ) |
| 482 | + * @index - numeric index of CB_Token instance in CB_ConditionEditor[lineInstanceName] |
| 483 | + * ( CB_ConditionEditor[lineInstanceName][index] is an instance of CB_Token ) |
| 484 | + * (also should match DOM container attribute in CB_Token.prototype.findLineInstanceName method) |
| 485 | + * @colors - optional object {'color':color, 'bgr':backgroundColor} colors of node (node is a DOM container) |
| 486 | + */ |
| 487 | +function CB_Token( token, lineInstanceName, index, colors ) { |
| 488 | + this.type = 'undef'; |
| 489 | + CB_lib.log( 'token='+token ); |
| 490 | + switch ( token.toLowerCase() ) { |
| 491 | + case 'all': this.type = 'select'; this.op = 'all'; break; |
| 492 | + case '(' : this.type = 'bracket'; this.op = 'lbracket'; break; |
| 493 | + case ')' : this.type = 'bracket'; this.op = 'rbracket'; break; |
| 494 | + case 'and' : this.type = 'logic'; this.op = 'and'; break; |
| 495 | + case 'or' : this.type = 'logic'; this.op = 'or'; break; |
| 496 | + default : |
| 497 | + var cmp = token.split( /^(ge|le|eq)(p|s|f)(\d+)$/g ); |
| 498 | + if ( cmp.length == 5 ) { |
| 499 | + // comparsion operation |
| 500 | + this.type = 'comparsion'; |
| 501 | + this.op = cmp[1].toLowerCase(); |
| 502 | + this.field = cmp[2]; |
| 503 | + this.number = parseInt( cmp[3] ); |
| 504 | + } else { |
| 505 | + // IE regexp fix |
| 506 | + if ( token.length > 3 ) { |
| 507 | + var op = token.substring( 0, 2 ); |
| 508 | + var field = token.substring( 2, 3 ); |
| 509 | + var number = token.substring( 3 ); |
| 510 | + if ( op.match( /^ge|le|eq$/ ) !== null && |
| 511 | + field.match( /^p|s|f$/ ) !== null && |
| 512 | + number.match( /^\d+$/ ) !== null ) { |
| 513 | + // comparsion operation |
| 514 | + this.type = 'comparsion'; |
| 515 | + this.op = op; |
| 516 | + this.field = field; |
| 517 | + this.number = number; |
| 518 | + } |
| 519 | + } |
| 520 | + } |
| 521 | + } |
| 522 | + this.lineInstanceName = lineInstanceName; |
| 523 | + this.node = document.createElement( 'div' ); |
| 524 | + this.node.className = 'cb_token_container'; |
| 525 | + this.colors = { 'color': '', 'bgr': '' }; |
| 526 | + // controlButtonsList - object key / value pair of controlButtons |
| 527 | + this.controlButtonsList = {}; |
| 528 | + if ( typeof colors !== 'undefined' ) { |
| 529 | + this.setColors( colors ); |
| 530 | + } |
| 531 | + this.setIndex( index ); |
| 532 | + this.clearNode(); |
| 533 | + CB_lib.addEvent( this.node, 'mouseover', CB_Token.prototype.nodeMouseOver ); |
| 534 | + if ( this.type == 'undef' ) { |
| 535 | + alert( 'Invalid token type='+this.type+' in CB_Token constructor' ); |
| 536 | + } else if ( this.type == 'bracket' ) { |
| 537 | + this.buildNodePoly = CB_Token.prototype.buildBracketNode; |
| 538 | + this.toString = CB_Token.prototype.BracketToString; |
| 539 | + } else if ( this.type == 'select' ) { |
| 540 | + this.buildNodePoly = CB_Token.prototype.buildSelectNode; |
| 541 | + this.toString = CB_Token.prototype.OpToString; |
| 542 | + } else if ( this.type == 'logic' ) { |
| 543 | + this.buildNodePoly = CB_Token.prototype.buildLogicNode; |
| 544 | + this.toString = CB_Token.prototype.OpToString; |
| 545 | + } else if ( this.type == 'comparsion' ) { |
| 546 | + this.buildNodePoly = CB_Token.prototype.buildComparsionNode; |
| 547 | + this.toString = CB_Token.prototype.Op2ToString; |
| 548 | + } else { |
| 549 | + alert( 'Unimplemented node type='+this.type+' in CB_Token constructor' ); |
| 550 | + } |
| 551 | +} |
| 552 | +/*** |
| 553 | + * token's CB_EditLine instance name find, CB_EditLine.tokens index set / find |
| 554 | +***/ |
| 555 | +CB_Token.prototype.findLineInstanceName = function( domObj ) { |
| 556 | + var instanceName; |
| 557 | + for ( var obj = domObj; obj !== null; obj = obj.parentNode ) { |
| 558 | + if ( ( instanceName = obj.getAttribute( 'lineinstancename' ) ) !== null ) { |
| 559 | + return instanceName; |
| 560 | + } |
| 561 | + } |
| 562 | + alert( 'Cannot find token\'s CB_EditLine instance name in CB_Token.findLineInstanceName' ); |
| 563 | +} |
| 564 | +CB_Token.prototype.setIndex = function( index ) { |
| 565 | + this.index = index; |
| 566 | + this.node.setAttribute( 'tokenindex', index ); |
| 567 | +} |
| 568 | +CB_Token.prototype.findIndex = function( domObj ) { |
| 569 | + var index, result = null; |
| 570 | + for ( var obj = domObj; obj !== null; obj = obj.parentNode ) { |
| 571 | + if ( ( index = obj.getAttribute( 'tokenindex' ) ) !== null ) { |
| 572 | + result = parseInt( index ); |
| 573 | + if ( isNaN( result ) || result < 0 ) { |
| 574 | + alert( 'Invalid (non-numeric or negative) value of token index='+result+' in CB_Token.findIndex' ); |
| 575 | + } |
| 576 | + return result; |
| 577 | + } |
| 578 | + } |
| 579 | + alert( 'Cannot find token index in CB_Token.findIndex' ); |
| 580 | +} |
| 581 | +/*** token DOM node colors, currently used in token copy / paste / validation code ***/ |
| 582 | +CB_Token.prototype.setColors = function( colors ) { |
| 583 | + // we need a copy of properties, not a source object reference! |
| 584 | + if ( typeof colors !== 'undefined' ) { |
| 585 | + if ( typeof colors.color !== 'undefined' ) { |
| 586 | + this.colors.color = colors.color; |
| 587 | + this.node.style.color = colors.color; |
| 588 | + } |
| 589 | + if ( typeof colors.bgr !== 'undefined' ) { |
| 590 | + this.colors.bgr = colors.bgr; |
| 591 | + this.node.style.backgroundColor = colors.bgr; |
| 592 | + } |
| 593 | + } |
| 594 | +} |
| 595 | +/* |
| 596 | + * adds / removes buttons in token controlButtonsList |
| 597 | + * @param appendControlButtons object key / value pairs (use value=null to "remove" the button) |
| 598 | + */ |
| 599 | +CB_Token.prototype.setControlButtons = function( appendControlButtons ) { |
| 600 | + for ( var key in appendControlButtons ) { |
| 601 | + this.controlButtonsList[ key ] = appendControlButtons[ key ]; |
| 602 | + } |
| 603 | +} |
| 604 | +/*** |
| 605 | + * token editor controls (move / delete existing tokens) |
| 606 | +***/ |
| 607 | +/* |
| 608 | + * initialize the node by removing the dynamic controls and appending editor's buttons |
| 609 | + * called by buildNodePoly() implementations |
| 610 | + */ |
| 611 | +CB_Token.prototype.clearNode = function() { |
| 612 | + this.node.innerHTML = ''; |
| 613 | + this.node.appendChild( this.createPopupControls() ); |
| 614 | +} |
| 615 | +/* |
| 616 | + * creates token control (container) |
| 617 | + * 1. div class='cb_popup_controls' contains move / delete buttons (whole expression editor) |
| 618 | + * TODO: do not display 'left' button for the first token, do not display 'right' button for the last token |
| 619 | + */ |
| 620 | +CB_Token.prototype.createPopupControls = function() { |
| 621 | + var popupControls = document.createElement( 'div' ); |
| 622 | + var hint; |
| 623 | + popupControls.className = 'cb_popup_controls'; |
| 624 | + for ( var key in this.controlButtonsList ) { |
| 625 | + if ( this.controlButtonsList[ key ] !== null ) { |
| 626 | + // button was not removed, add it |
| 627 | + hint = ''; |
| 628 | + if ( typeof CB_ConditionEditor.localEditHints[ key ] !== 'undefined' ) { |
| 629 | + hint = CB_ConditionEditor.localEditHints[ key ]; |
| 630 | + } |
| 631 | + popupControls.appendChild( this.controlButton( key, this.controlButtonsList[ key ], hint ) ); |
| 632 | + } |
| 633 | + } |
| 634 | + return popupControls; |
| 635 | +} |
| 636 | +CB_Token.prototype.controlButton = function( op, text, hint ) { |
| 637 | + var button = CB_lib.divButton( op, text, hint ); |
| 638 | + CB_lib.addEvent( button, 'click', CB_Token.prototype.controlButtonClick ); |
| 639 | + return button; |
| 640 | +} |
| 641 | +/* |
| 642 | + * creates token control (container) |
| 643 | + * 2. div class='cb_token_inputs' contains dynamical selects / inputs (single token editor) |
| 644 | + */ |
| 645 | +CB_Token.prototype.createTokenInputs = function() { |
| 646 | + tokenInputs = document.createElement( 'div' ); |
| 647 | + tokenInputs.className = 'cb_token_inputs'; |
| 648 | + for ( var i = 0; i < arguments.length; i++ ) { |
| 649 | + tokenInputs.appendChild( arguments[i] ); |
| 650 | + } |
| 651 | + return tokenInputs; |
| 652 | +} |
| 653 | +/* |
| 654 | + * get current token's control container of className given |
| 655 | + * currently node has two childs (control containers): |
| 656 | + * 1. div class='cb_popup_controls' contains move / delete buttons (whole expression editor) |
| 657 | + * 2. div class='cb_token_inputs' contains dynamical selects / inputs (single token editor) |
| 658 | + */ |
| 659 | +CB_Token.prototype.getControlContainer = function( className ) { |
| 660 | + var node; |
| 661 | + for ( var i = 0; i < this.node.childNodes.length; i++ ) { |
| 662 | + var node = this.node.childNodes[i]; |
| 663 | + if ( node.nodeType == 1 && node.className == className ) { |
| 664 | + return node; |
| 665 | + } |
| 666 | + } |
| 667 | + alert( 'Cannot get control class='+className+' of node index=['+this.index+'] in CB_Token' ); |
| 668 | + return null; |
| 669 | +} |
| 670 | +/*** |
| 671 | + * display current token popup controls, hide another tokens popup controls |
| 672 | +**/ |
| 673 | +CB_Token.prototype.nodeMouseOver = function( event ) { |
| 674 | + var obj = CB_lib.getEventObj( event, true ); |
| 675 | + var lineInstanceName = CB_Token.prototype.findLineInstanceName( obj ); |
| 676 | + var index = CB_Token.prototype.findIndex( obj ); |
| 677 | + // {{{ switch the context |
| 678 | + if ( index < CB_ConditionEditor[ lineInstanceName ].tokens.length ) { |
| 679 | + CB_Token.prototype._nodeMouseOver.call( CB_ConditionEditor[ lineInstanceName ].tokens[ index ], obj ); |
| 680 | + } |
| 681 | + // switch the context }}} |
| 682 | +} |
| 683 | +/* |
| 684 | + * @param domObj is current node here (currently unused) |
| 685 | + */ |
| 686 | +CB_Token.prototype._nodeMouseOver = function( domObj ) { |
| 687 | + CB_ConditionEditor[ this.lineInstanceName ].showPopupControls( this.index ); |
| 688 | +} |
| 689 | +/*** |
| 690 | + * click edit buttons located inside popup controls |
| 691 | +**/ |
| 692 | +CB_Token.prototype.controlButtonClick = function( event ) { |
| 693 | + var obj = CB_lib.getEventObj( event, true ); |
| 694 | + var lineInstanceName = CB_Token.prototype.findLineInstanceName( obj ); |
| 695 | + var index = CB_Token.prototype.findIndex( obj ); |
| 696 | + // {{{ switch the context |
| 697 | + if ( index < CB_ConditionEditor[ lineInstanceName ].tokens.length ) { |
| 698 | + CB_Token.prototype._controlButtonClick.call( CB_ConditionEditor[ lineInstanceName ].tokens[ index ], obj ); |
| 699 | + } |
| 700 | + // switch the context }}} |
| 701 | +} |
| 702 | +/* |
| 703 | + * @param domObj is control button which was clicked |
| 704 | + */ |
| 705 | +CB_Token.prototype._controlButtonClick = function( domObj ) { |
| 706 | + var editOp = domObj.getAttribute( 'buttonoperation' ); |
| 707 | + CB_ConditionEditor[ this.lineInstanceName ].doEdit( this.index, editOp ); |
| 708 | +} |
| 709 | +/*** |
| 710 | + * dynamic elements ( text input, select/option) handling |
| 711 | + * this kind of selector appears dynamically only during mouseover event over the created span class='cb_virtual_select' element |
| 712 | + * and becomes the same div (with selected option value) back after the mouseout event over the selector |
| 713 | +***/ |
| 714 | +/* |
| 715 | + * creates dynamical (linked to current CB_Token object property) text input field |
| 716 | + */ |
| 717 | +CB_Token.prototype.dynamicTextInput = function() { |
| 718 | + var element = CB_lib.textInput( this.number ); |
| 719 | + CB_lib.addEvent( element, 'change', CB_Token.prototype.dynamicTextInputChange ); |
| 720 | + CB_lib.addEvent( element, 'keyup', CB_Token.prototype.dynamicTextInputChange ); |
| 721 | +// CB_lib.addEvent( element, 'mouseout', CB_Token.prototype.dynamicTextInputChange ); |
| 722 | + return element; |
| 723 | +} |
| 724 | +CB_Token.prototype.dynamicTextInputChange = function( event ) { |
| 725 | + var obj = CB_lib.getEventObj( event, true ); |
| 726 | + var lineInstanceName = CB_Token.prototype.findLineInstanceName( obj ); |
| 727 | + var index = CB_Token.prototype.findIndex( obj ); |
| 728 | + // {{{ switch the context |
| 729 | + if ( index < CB_ConditionEditor[ lineInstanceName ].tokens.length ) { |
| 730 | + CB_Token.prototype._dynamicTextInputChange.call( CB_ConditionEditor[ lineInstanceName ].tokens[ index ], obj ); |
| 731 | + } |
| 732 | + // switch the context }}} |
| 733 | +} |
| 734 | +/* domObj is a text input here */ |
| 735 | +CB_Token.prototype._dynamicTextInputChange = function( domObj ) { |
| 736 | + var number = parseInt( domObj.value ); |
| 737 | + this.number = isNaN( number ) ? 0 : Math.abs( number ); |
| 738 | + domObj.style.width = (this.number.toString().length) * 0.75 + "em"; |
| 739 | + // deselect highlighted sample value, because input value was changed |
| 740 | + // TODO: implement in cleaner way? |
| 741 | + if ( this.lineInstanceName == 'samplesLine' ) { |
| 742 | + CB_ConditionEditor.clearSelection(); |
| 743 | + } |
| 744 | +} |
| 745 | +/* |
| 746 | + * @param optionsList string property name of object instance in CB_ConditionEditor containing key / value pairs list |
| 747 | + * @param property string name of property in CB_Token object has to be changed by dynamicSelector ('op', 'field') |
| 748 | + * @param selectedOption selected key in optionsList |
| 749 | + */ |
| 750 | +CB_Token.prototype.dynamicSelector = function( optionsList, property, selectedOption ) { |
| 751 | + var element = document.createElement( 'span' ); |
| 752 | + element.className = 'cb_virtual_select'; |
| 753 | + element.appendChild( document.createTextNode( CB_ConditionEditor[ optionsList ][ selectedOption ] ) ); |
| 754 | + element.setAttribute( 'optionslist', optionsList ); |
| 755 | + element.setAttribute( 'tokenproperty', property ); |
| 756 | + CB_lib.addEvent( element, 'mouseover', CB_Token.prototype.dynamicSelectorMouseover ); |
| 757 | + return element; |
| 758 | +} |
| 759 | +CB_Token.prototype.dynamicSelectorMouseover = function( event ) { |
| 760 | + var obj = CB_lib.getEventObj( event, true ); |
| 761 | + var lineInstanceName = CB_Token.prototype.findLineInstanceName( obj ); |
| 762 | + var index = CB_Token.prototype.findIndex( obj ); |
| 763 | + // {{{ switch the context |
| 764 | + if ( index < CB_ConditionEditor[ lineInstanceName ].tokens.length ) { |
| 765 | + CB_Token.prototype._dynamicSelectorMouseover.call( CB_ConditionEditor[ lineInstanceName ].tokens[ index ], obj ); |
| 766 | + } |
| 767 | + // switch the context }}} |
| 768 | +} |
| 769 | +/* domObj is a span of cb_virtual_select' class here */ |
| 770 | +CB_Token.prototype._dynamicSelectorMouseover = function( domObj ) { |
| 771 | + var optionsList = domObj.getAttribute( 'optionslist' ); |
| 772 | + var property = domObj.getAttribute( 'tokenproperty' ); |
| 773 | + var selector = CB_lib.htmlSelector( CB_ConditionEditor[ optionsList ], this[ property ] ); |
| 774 | + selector.setAttribute( 'optionslist', optionsList ); |
| 775 | + selector.setAttribute( 'tokenproperty', property ); |
| 776 | + CB_lib.addEvent( selector, 'change', CB_Token.prototype.dynamicSelectorChange ); |
| 777 | + CB_lib.addEvent( selector, 'blur', CB_Token.prototype.dynamicSelectorBlur ); |
| 778 | + domObj.parentNode.replaceChild( selector, domObj ); // selector - new element, domObj - old element |
| 779 | + // refresh all another nodes but this one |
| 780 | + CB_ConditionEditor[ this.lineInstanceName ].viewCached( this.index ); |
| 781 | +} |
| 782 | +CB_Token.prototype.dynamicSelectorChange = function( event ) { |
| 783 | + var obj = CB_lib.getEventObj( event, true ); |
| 784 | + var lineInstanceName = CB_Token.prototype.findLineInstanceName( obj ); |
| 785 | + var index = CB_Token.prototype.findIndex( obj ); |
| 786 | + // {{{ switch the context |
| 787 | + if ( index < CB_ConditionEditor[ lineInstanceName ].tokens.length ) { |
| 788 | + CB_Token.prototype._dynamicSelectorChange.call( CB_ConditionEditor[ lineInstanceName ].tokens[ index ], obj ); |
| 789 | + } |
| 790 | + // switch the context }}} |
| 791 | +} |
| 792 | +/* domObj is a dynamically generated select/option here */ |
| 793 | +CB_Token.prototype._dynamicSelectorChange = function( domObj ) { |
| 794 | + var property = domObj.getAttribute( 'tokenproperty' ); |
| 795 | + var selectedOption, option; |
| 796 | + if ( typeof domObj.selectedIndex !== 'undefined' ) { |
| 797 | + CB_lib.log( 'property='+property ); |
| 798 | + CB_lib.log( 'selected index='+domObj.options[ domObj.selectedIndex ].value ); |
| 799 | + // note that is select tag index, not a this.index! |
| 800 | + if ( property == 'op' ) { |
| 801 | + this.op = domObj.options[ domObj.selectedIndex ].value; |
| 802 | + } else if ( property == 'field' ) { |
| 803 | + this.field = domObj.options[ domObj.selectedIndex ].value; |
| 804 | + } else { |
| 805 | + alert( 'Unknown property='+property+' in CB_Token._dynamicSelectorChange' ); |
| 806 | + } |
| 807 | + // deselect highlighted sample value, because selector's value was changed |
| 808 | + // TODO: implement in cleaner way? |
| 809 | + if ( this.lineInstanceName == 'samplesLine' ) { |
| 810 | + CB_ConditionEditor.clearSelection(); |
| 811 | + } |
| 812 | + CB_ConditionEditor[ this.lineInstanceName ].view(); |
| 813 | + } |
| 814 | +} |
| 815 | +CB_Token.prototype.dynamicSelectorBlur = function( event ) { |
| 816 | + var obj = CB_lib.getEventObj( event, true ); |
| 817 | + var lineInstanceName = CB_Token.prototype.findLineInstanceName( obj ); |
| 818 | + var index = CB_Token.prototype.findIndex( obj ); |
| 819 | + // {{{ switch the context |
| 820 | + if ( index < CB_ConditionEditor[ lineInstanceName ].tokens.length ) { |
| 821 | + CB_Token.prototype._dynamicSelectorBlur.call( CB_ConditionEditor[ lineInstanceName ].tokens[ index ], obj ); |
| 822 | + } |
| 823 | + // switch the context }}} |
| 824 | +} |
| 825 | +/* domObj is a dynamically generated select/option here */ |
| 826 | +CB_Token.prototype._dynamicSelectorBlur = function( domObj ) { |
| 827 | + // note that is select tag index, not a this.index! |
| 828 | + CB_lib.log( 'blur selected index='+domObj.options[ domObj.selectedIndex ].value ); |
| 829 | + CB_lib.log('this.type='+this.type); |
| 830 | + this.buildNode(); |
| 831 | +} |
| 832 | +/* |
| 833 | + * param @controlButtonsList - optional object key / value pair of controlButtons (old buttons will be removed) |
| 834 | + */ |
| 835 | +CB_Token.prototype.buildNode = function( controlButtonsList ) { |
| 836 | + if ( typeof controlButtonsList !== 'undefined' ) { |
| 837 | + this.controlButtonsList = controlButtonsList; |
| 838 | + } |
| 839 | + this.buildNodePoly(); |
| 840 | +} |
| 841 | +CB_Token.prototype.buildNodeCached = function() { |
| 842 | + this.buildNodePoly( true ); |
| 843 | +} |
| 844 | +/*** |
| 845 | + * buildNodePoly() polymorphic prototypes (initialized in the constructor of CB_Token) |
| 846 | +***/ |
| 847 | +CB_Token.prototype.buildSelectNode = function() { |
| 848 | + this.clearNode(); |
| 849 | + var tokenInputs = this.createTokenInputs( document.createTextNode( CB_ConditionEditor.localMessages[ 'all_op' ] ) ); |
| 850 | + this.node.appendChild( tokenInputs ); |
| 851 | +} |
| 852 | +CB_Token.prototype.buildBracketNode = function() { |
| 853 | + this.clearNode(); |
| 854 | + var tokenInputs = this.createTokenInputs( document.createTextNode( CB_ConditionEditor.localBrackets[ this.op ] ) ); |
| 855 | + this.node.appendChild( tokenInputs ); |
| 856 | +} |
| 857 | +CB_Token.prototype.buildLogicNode = function() { |
| 858 | + this.clearNode(); |
| 859 | + var tokenInputs = this.createTokenInputs( this.dynamicSelector( 'localBoolOps', 'op', this.op ) ); |
| 860 | + this.node.appendChild( tokenInputs ); |
| 861 | +} |
| 862 | +CB_Token.prototype.buildComparsionNode = function( cacheTextInput ) { |
| 863 | + // if cacheTextInput was passed, do not re-generate the dynamic text input |
| 864 | + var dynamicTextInput = ( typeof cacheTextInput == 'undefined' ) ? this.dynamicTextInput() : this.getControlContainer( 'cb_token_inputs' ).childNodes[2]; |
| 865 | + this.clearNode(); |
| 866 | + var positions = CB_ConditionEditor.localMessages[ 'op2_template' ].match( /\$\d/g ); |
| 867 | + var parameters = { |
| 868 | + '$1' : this.dynamicSelector( 'localDbFields', 'field', this.field ), |
| 869 | + '$2' : this.dynamicSelector( 'localCmpOps', 'op', this.op ), |
| 870 | + '$3' : dynamicTextInput |
| 871 | + } |
| 872 | + var tokenInputs = this.createTokenInputs( |
| 873 | + // this.getControlContainer( 'cb_token_inputs' ).childNodes[0] |
| 874 | + parameters[ positions[0] ], |
| 875 | + // this.getControlContainer( 'cb_token_inputs' ).childNodes[1] |
| 876 | + parameters[ positions[1] ], |
| 877 | + // this.getControlContainer( 'cb_token_inputs' ).childNodes[2] |
| 878 | + parameters[ positions[2] ] |
| 879 | + ); |
| 880 | + this.node.appendChild( tokenInputs ); |
| 881 | +} |
| 882 | +/*** |
| 883 | + * toString() polymorphic prototypes (initialized in the constructor of CB_Token) |
| 884 | +***/ |
| 885 | +/* operator */ |
| 886 | +CB_Token.prototype.OpToString = function() { |
| 887 | + return this.op; |
| 888 | +} |
| 889 | +/* decoded operator */ |
| 890 | +CB_Token.prototype.BracketToString = function() { |
| 891 | + if ( this.op == 'lbracket' ) { return '('; } |
| 892 | + if ( this.op == 'rbracket' ) { return ')'; } |
| 893 | + return 'error'; |
| 894 | +} |
| 895 | +/* two operands and operator */ |
| 896 | +CB_Token.prototype.Op2ToString = function() { |
| 897 | + return this.op+this.field+this.number; |
| 898 | +} |
| 899 | +/* |
| 900 | + * returns a paste type (which is the similar to token type, just l/r brackets are separated types) |
| 901 | + */ |
| 902 | +CB_Token.prototype.getPasteType = function() { |
| 903 | + if ( this.type != 'bracket' ) { |
| 904 | + return this.type; |
| 905 | + } else { |
| 906 | + return this.op; |
| 907 | + } |
| 908 | +} |
| 909 | +/* end of CB_Token */ |
| 910 | + |
| 911 | +/*** |
| 912 | + * condition edit line constructor |
| 913 | + * @param type - type of edit line ('condition', 'samples') used to initialize polymorphic methods |
| 914 | + * @param domContainer - visual DOM container of editor expression |
| 915 | + * @param lineInstanceName - name of property ( CB_ConditionEditor[lineInstanceName] ) |
| 916 | + */ |
| 917 | +function CB_EditLine( type, domContainer, lineInstanceName ) { |
| 918 | + this.type = type; |
| 919 | + // editor expression visual container |
| 920 | + this.node = domContainer; |
| 921 | + this.node.setAttribute( 'lineinstancename', lineInstanceName ); |
| 922 | + // array of generated token objects (currently edited expression) |
| 923 | + this.tokens = new Array(); |
| 924 | + // {{{ token highlighting (currently only single token, because the clipboard is single token) |
| 925 | + // note: currently implemented and used only for type='samples' |
| 926 | + this.hilitedIndex = -1; |
| 927 | + this.savedTokenColors = { 'color': '', 'bgr': '' }; |
| 928 | + // }}} |
| 929 | + switch ( type ) { |
| 930 | + case 'condition' : |
| 931 | + this.view = CB_EditLine.prototype.viewCondition; |
| 932 | + this.viewCached = CB_EditLine.prototype.viewCachedCondition; |
| 933 | + this.doEdit = CB_EditLine.prototype.doEditCondition; |
| 934 | + break; |
| 935 | + case 'samples' : |
| 936 | + this.view = CB_EditLine.prototype.viewSamples; |
| 937 | + this.viewCached = CB_EditLine.prototype.viewCachedSamples; |
| 938 | + this.doEdit = CB_EditLine.prototype.doEditSamples; |
| 939 | + break; |
| 940 | + default : |
| 941 | + alert( 'Unknown type='+type+' in CB_EditLine constructor' ); |
| 942 | + return; |
| 943 | + } |
| 944 | +} |
| 945 | +/* |
| 946 | + * view current expression (completely) |
| 947 | + * also re-indexes tokens, because they might have been moved in doEdit() calls |
| 948 | + * should be performing cleanly consequently |
| 949 | + * used for the visual updating after editing / moving / inserting / deleting / adding tokens in this.tokens array |
| 950 | + */ |
| 951 | +CB_EditLine.prototype.viewCondition = function() { |
| 952 | + var cbFirst, cbLast, cb, cbAll; |
| 953 | + // add paste buttons, when these are available |
| 954 | + if ( CB_ConditionEditor.clipboard == '' ) { |
| 955 | + cbFirst = { 'right': '→', 'remove': 'x' }; |
| 956 | + cbLast = { 'left': '←', 'remove': 'x' }; |
| 957 | + cb = { 'left': '←', 'right': '→', 'remove': 'x' }; |
| 958 | + cbAll = {}; |
| 959 | + } else { |
| 960 | + cbFirst = { 'right': '→', 'paste': '+' }; |
| 961 | + cbLast = { 'left': '←', 'paste': '+', 'paste_right': '>+' }; |
| 962 | + cb = { 'left': '←', 'right': '→', 'paste': '+' }; |
| 963 | + cbAll = { 'paste': '+' }; |
| 964 | + } |
| 965 | + var i; |
| 966 | + this.node.innerHTML = ''; |
| 967 | + for ( i = 0; i < this.tokens.length; i++ ) { |
| 968 | + this.tokens[i].setIndex( i ); |
| 969 | + if ( this.tokens[i].type == 'select' ) { |
| 970 | + this.tokens[i].buildNode( cbAll ); |
| 971 | + } else { |
| 972 | + if ( i == 0 ) { |
| 973 | + this.tokens[i].buildNode( cbFirst ); |
| 974 | + } else if ( i == this.tokens.length - 1 ) { |
| 975 | + this.tokens[i].buildNode( cbLast ); |
| 976 | + } else { |
| 977 | + this.tokens[i].buildNode( cb ); |
| 978 | + } |
| 979 | + } |
| 980 | + this.node.appendChild( this.tokens[i].node ); |
| 981 | + } |
| 982 | + var errorPos = this.validate(); |
| 983 | + this.applyButton( errorPos == -1 ); |
| 984 | +} |
| 985 | +CB_EditLine.prototype.viewSamples = function( cachedIndex ) { |
| 986 | + var i; |
| 987 | + this.node.innerHTML = ''; |
| 988 | + for ( i = 0; i < this.tokens.length; i++ ) { |
| 989 | + this.tokens[i].setIndex( i ); |
| 990 | + if ( this.tokens[i].type == 'select' ) { |
| 991 | + this.tokens[i].buildNode( { 'clear': '=' } ); |
| 992 | + } else { |
| 993 | + this.tokens[i].buildNode( { 'copy': '+', 'append': '>+' } ); |
| 994 | + } |
| 995 | + this.node.appendChild( this.tokens[i].node ); |
| 996 | + } |
| 997 | +} |
| 998 | +/*** prototypes of polymorphic viewCached method ***/ |
| 999 | +CB_EditLine.prototype.viewCachedCondition = function( cachedIndex ) { |
| 1000 | + if ( CB_Setup.isIE < 9 ) { |
| 1001 | + // cached rendering optimization does not works in IE 8, unfortunately |
| 1002 | + // TODO: check in IE9 |
| 1003 | + // https://developer.mozilla.org/en/Browser_Detection_and_Cross_Browser_Support |
| 1004 | + // http://msdn.microsoft.com/en-us/library/ms537509(VS.85).aspx |
| 1005 | + return; |
| 1006 | + } |
| 1007 | + this.viewCachedSamples( cachedIndex ); |
| 1008 | + var errorPos = this.validate(); |
| 1009 | + this.applyButton( errorPos == -1 ); |
| 1010 | +} |
| 1011 | +/* |
| 1012 | + * @param cachedIndex - optional index of token in this.tokens that |
| 1013 | + * should not be regenerated but taken from already built token node instead |
| 1014 | + * enables to view partially cached expression (during various events in token node) |
| 1015 | + * also does not re-build dynamic text input fields, otherwise these may lose their values |
| 1016 | + */ |
| 1017 | +CB_EditLine.prototype.viewCachedSamples = function( cachedIndex ) { |
| 1018 | + if ( CB_Setup.isIE < 9 ) { |
| 1019 | + // cached rendering optimization does not works in IE 8, unfortunately |
| 1020 | + // TODO: check in IE9 |
| 1021 | + // https://developer.mozilla.org/en/Browser_Detection_and_Cross_Browser_Support |
| 1022 | + // http://msdn.microsoft.com/en-us/library/ms537509(VS.85).aspx |
| 1023 | + return; |
| 1024 | + } |
| 1025 | + var i; |
| 1026 | + var idx = ( typeof cachedIndex == 'undefined' ) ? -1 : cachedIndex; |
| 1027 | + this.node.innerHTML = ''; |
| 1028 | + for ( i = 0; i < this.tokens.length; i++ ) { |
| 1029 | + if ( i != idx ) { |
| 1030 | + // cached (partial) node build (see buildNode prototypes) |
| 1031 | + this.tokens[i].buildNodeCached(); |
| 1032 | + } |
| 1033 | + this.node.appendChild( this.tokens[i].node ); |
| 1034 | + } |
| 1035 | +} |
| 1036 | +CB_EditLine.prototype.applyButton = function( isEnabled ) { |
| 1037 | + var div = document.createElement( 'div' ); |
| 1038 | + div.className = 'cb_token_container'; |
| 1039 | + div.style.marginBottom = '0.5em'; |
| 1040 | + var applyButton = document.createElement( 'input' ); |
| 1041 | + applyButton.setAttribute( 'type', 'button' ); |
| 1042 | + applyButton.setAttribute( 'id', 'cb_apply_button' ); |
| 1043 | + applyButton.setAttribute( 'value', CB_ConditionEditor.localMessages[ 'apply_button' ] ); |
| 1044 | + if ( !isEnabled ) { |
| 1045 | + div.style.color = CB_Setup.colors.error.color; |
| 1046 | + div.style.backgroundColor = CB_Setup.colors.error.bgr; |
| 1047 | + applyButton.disabled = !isEnabled; |
| 1048 | + } |
| 1049 | + CB_lib.addEvent( applyButton, 'click', CB_ConditionEditor.applyButtonClick ); |
| 1050 | + div.appendChild( applyButton ); |
| 1051 | + var buttonContainer = this.node.parentNode.lastChild; |
| 1052 | + buttonContainer.innerHTML = ''; |
| 1053 | + buttonContainer.appendChild( div ); |
| 1054 | +} |
| 1055 | +CB_EditLine.prototype.showPopupControls = function( currentIndex ) { |
| 1056 | + var popupControls; |
| 1057 | + for ( var i = 0; i < this.tokens.length; i++ ) { |
| 1058 | + popupControls = this.tokens[i].getControlContainer( 'cb_popup_controls' ); |
| 1059 | + popupControls.style.visibility = (i == currentIndex) ? 'visible' : 'hidden'; |
| 1060 | + } |
| 1061 | +} |
| 1062 | +/* |
| 1063 | + * set / reset currently highlighted token |
| 1064 | + * @param index - index of this.tokens[]; use value = -1 to reset highlighting completely |
| 1065 | + * @param colors - object { 'color':color, 'bgr':background} - color, background color of highlighted token |
| 1066 | + */ |
| 1067 | +CB_EditLine.prototype.setHighlight = function( index, colors ) { |
| 1068 | + // restore original colors of previousely highlighted token |
| 1069 | + if ( this.hilitedIndex >= 0 && this.hilitedIndex < this.tokens.length ) { |
| 1070 | + this.tokens[ this.hilitedIndex ].setColors( this.savedTokenColors ); |
| 1071 | + } |
| 1072 | + if ( index >= 0 && index < this.tokens.length ) { |
| 1073 | + // save original colors of new highlighted token |
| 1074 | + // we need a copy of properties, not a source object reference! |
| 1075 | + this.savedTokenColors.color = this.tokens[ index ].colors.color; |
| 1076 | + this.savedTokenColors.bgr = this.tokens[ index ].colors.bgr; |
| 1077 | + // set new highlighted token |
| 1078 | + this.hilitedIndex = index; |
| 1079 | + this.tokens[ index ].setColors( colors ); |
| 1080 | + } else { |
| 1081 | + this.hilitedIndex = -1; |
| 1082 | + } |
| 1083 | +} |
| 1084 | +/*** |
| 1085 | + * tokens move / remove section (expression editor) |
| 1086 | +***/ |
| 1087 | +CB_EditLine.prototype.doEditCondition = function( currentIndex, editOp ) { |
| 1088 | + CB_lib.log( 'editOp='+editOp+' token index='+currentIndex ); |
| 1089 | + // remove previous erroneous highlighted tokens, there will be new ones or no one |
| 1090 | + this.setHighlight( -1 ); |
| 1091 | + switch ( editOp ) { |
| 1092 | + case 'left' : this.tokenLeft( currentIndex ); break; |
| 1093 | + case 'right' : this.tokenRight( currentIndex ); break; |
| 1094 | + case 'remove' : this.tokenRemove( currentIndex ); break; |
| 1095 | + case 'paste': this.tokenPaste( currentIndex ); break; |
| 1096 | + case 'paste_right': this.tokenPaste( currentIndex + 1 ); break; |
| 1097 | + default : |
| 1098 | + alert( 'Unknown editOp='+editOp+' in CB_EditLine.prototype.doEditCondition' ); |
| 1099 | + } |
| 1100 | + // re-index & view is a MUST after edit operations |
| 1101 | + this.view(); |
| 1102 | +} |
| 1103 | +CB_EditLine.prototype.doEditSamples = function( currentIndex, editOp ) { |
| 1104 | + CB_lib.log( 'editOp='+editOp+' token index='+currentIndex ); |
| 1105 | + switch ( editOp ) { |
| 1106 | + case 'append' : |
| 1107 | + case 'clear' : // 'clear' is 'append' called with token 'select all', only edit hints are different |
| 1108 | + this.tokenCopy( currentIndex ); |
| 1109 | + CB_ConditionEditor.conditionLine.doEdit( CB_ConditionEditor.conditionLine.tokens.length, 'paste' ); |
| 1110 | + break; |
| 1111 | + case 'copy' : |
| 1112 | + this.tokenCopy( currentIndex ); |
| 1113 | + break; |
| 1114 | + default : |
| 1115 | + alert( 'Unknown editOp='+editOp+' in CB_EditLine.prototype.doEditSamples' ); |
| 1116 | + } |
| 1117 | + this.view(); |
| 1118 | +} |
| 1119 | +CB_EditLine.prototype.tokenCopy = function( currentIndex ) { |
| 1120 | + var tokenStr = this.tokens[ currentIndex ].toString(); |
| 1121 | + if ( CB_ConditionEditor.clipboard != tokenStr ) { |
| 1122 | + // highlight clicked token to indicate it's being copied |
| 1123 | + this.setHighlight( currentIndex, CB_Setup.colors.copy ); |
| 1124 | + // set clipboard string |
| 1125 | + CB_ConditionEditor.clipboard = tokenStr; |
| 1126 | + } else { |
| 1127 | + // attempt to paste the same value twice, clear the selection |
| 1128 | + CB_ConditionEditor.clearSelection(); |
| 1129 | + } |
| 1130 | + // refresh condition line to display / hide paste buttons (these are available only when clipboard is not empty) |
| 1131 | + CB_ConditionEditor.conditionLine.view(); |
| 1132 | +} |
| 1133 | +CB_EditLine.prototype.tokenPaste = function( currentIndex ) { |
| 1134 | + var pastedToken = new CB_Token( CB_ConditionEditor.clipboard, 'conditionLine', currentIndex ); |
| 1135 | + if ( this.doPaste( pastedToken ) ) { |
| 1136 | + // paste was successful, clear the selection |
| 1137 | + CB_ConditionEditor.clearSelection(); |
| 1138 | + } |
| 1139 | +} |
| 1140 | +CB_EditLine.allowedPaste = { |
| 1141 | +// paste is being performed between 'prev' and 'curr' |
| 1142 | +// key 'curr' may be equal to 'next' sometimes (when the token inbetween is being checked) |
| 1143 | +// allowedPaste key is PasteType of new (inserted) token |
| 1144 | +// see this.validate() for usage |
| 1145 | +// curr.notexists is lastpos, prev.notexists is pos0 |
| 1146 | + 'lbracket' : { |
| 1147 | + 'curr': { 'notexists': false, 'lbracket': true, 'rbracket': false, 'logic': false, 'comparsion': true }, |
| 1148 | + 'prev': { 'notexists': true, 'lbracket': true, 'rbracket': false, 'logic': true, 'comparsion': false } |
| 1149 | + }, |
| 1150 | + 'rbracket' : { |
| 1151 | + 'curr': { 'notexists': true, 'lbracket': false, 'rbracket': true, 'logic': true, 'comparsion': false }, |
| 1152 | + 'prev': { 'notexists': false, 'lbracket': false, 'rbracket': true, 'logic': false, 'comparsion': true } |
| 1153 | + }, |
| 1154 | + 'logic' : { |
| 1155 | + 'curr': { 'notexists': false, 'lbracket': true, 'rbracket': false, 'logic': false, 'comparsion': true }, |
| 1156 | + 'prev': { 'notexists': false, 'lbracket': false, 'rbracket': true, 'logic': false, 'comparsion': true } |
| 1157 | + }, |
| 1158 | + 'comparsion' : { |
| 1159 | + 'curr': { 'notexists': true, 'lbracket': false, 'rbracket': true, 'logic': true, 'comparsion': true }, |
| 1160 | + 'prev': { 'notexists': true, 'lbracket': true, 'rbracket': false, 'logic': true, 'comparsion': false } |
| 1161 | + }, |
| 1162 | + 'select' : { |
| 1163 | + 'curr': { 'notexists': true, 'lbracket': false, 'rbracket': false, 'logic': false, 'comparsion': false }, |
| 1164 | + 'prev': { 'notexists': true, 'lbracket': false, 'rbracket': false, 'logic': false, 'comparsion': false } |
| 1165 | + } |
| 1166 | +}; |
| 1167 | +/* |
| 1168 | + * validates current expression |
| 1169 | + * @return errorPos = -1, when there are no errors, otherwise errorPos contains index of erroneous token |
| 1170 | + */ |
| 1171 | +CB_EditLine.prototype.validate = function() { |
| 1172 | + var errorPos = -1; |
| 1173 | + var lastBracketPos = -1; |
| 1174 | + var bracketsLevel = 0; |
| 1175 | + var prevToken, nextToken, currToken; |
| 1176 | + var currType, prevType, currType; // paste types, not token.type ! |
| 1177 | + for ( var i = 0; i < this.tokens.length; i++ ) { |
| 1178 | + currToken = this.tokens[i]; |
| 1179 | + currType = currToken.getPasteType(); |
| 1180 | + prevToken = this.getToken( i - 1 ); |
| 1181 | + prevType = (prevToken == null) ? 'notexists' : prevToken.getPasteType(); |
| 1182 | + nextToken = this.getToken( i + 1 ); |
| 1183 | + nextType = (nextToken == null) ? 'notexists' : nextToken.getPasteType(); |
| 1184 | + // check brackets nesting |
| 1185 | + if ( currToken.type == 'bracket' ) { |
| 1186 | + lastBracketPos = i; |
| 1187 | + if ( currToken.op == 'lbracket' ) { |
| 1188 | + bracketsLevel++; |
| 1189 | + } else { // this.tokens[i].op == 'rbracket' |
| 1190 | + bracketsLevel--; |
| 1191 | + } |
| 1192 | + } |
| 1193 | + if ( errorPos == -1 && bracketsLevel < 0 ) { |
| 1194 | + errorPos = i; |
| 1195 | + } |
| 1196 | + // check, whether the token "fits" to the current position |
| 1197 | + if ( !CB_EditLine.allowedPaste[ currType ].curr[ nextType ] || |
| 1198 | + !CB_EditLine.allowedPaste[ currType ].prev[ prevType ] ) { |
| 1199 | + errorPos = i; |
| 1200 | + } |
| 1201 | + } |
| 1202 | + if ( errorPos == -1 && bracketsLevel != 0 ) { |
| 1203 | + errorPos = lastBracketPos; |
| 1204 | + } |
| 1205 | + this.setHighlight( errorPos, CB_Setup.colors.error ); |
| 1206 | + return errorPos; |
| 1207 | +} |
| 1208 | +/* |
| 1209 | + * try to paste new token at the selected position |
| 1210 | + * @param newToken - instance of CB_Token |
| 1211 | + * newToken.index indicates "desired" position (may be corrected in this method) |
| 1212 | + * @return true, when paste was successful, false otherwise |
| 1213 | + */ |
| 1214 | +CB_EditLine.prototype.doPaste = function( newToken ) { |
| 1215 | + var currToken = this.getToken( newToken.index ); |
| 1216 | + var prevToken = this.getToken( newToken.index - 1 ); |
| 1217 | + var currType = (currToken == null) ? 'notexists' : currToken.getPasteType(); |
| 1218 | + var prevType = (prevToken == null) ? 'notexists' : prevToken.getPasteType(); |
| 1219 | + var newType = newToken.getPasteType(); |
| 1220 | + switch ( newType ) { |
| 1221 | + case 'lbracket' : |
| 1222 | + case 'rbracket' : |
| 1223 | + case 'logic' : |
| 1224 | + case 'comparsion' : |
| 1225 | + break; |
| 1226 | + case 'select' : |
| 1227 | + // "select all" clears the whole condition line when being pasted |
| 1228 | + this.tokens = [ new CB_Token( 'all', 'conditionLine', 0 ) ]; |
| 1229 | + return true; |
| 1230 | + default : |
| 1231 | + alert( 'Unimplemented token type='+newToken.type+' in CB_EditLine.doPaste' ); |
| 1232 | + return true; |
| 1233 | + } |
| 1234 | + if ( currType == 'select' || prevType == 'select' ) { |
| 1235 | + // "select all" disappears when pasting over it |
| 1236 | + newToken.setIndex( 0 ); |
| 1237 | + this.tokens = [ newToken ]; |
| 1238 | + return true; |
| 1239 | + } |
| 1240 | + // allow to paste at 0 & last index of expression ( 'notexists' ) |
| 1241 | + // at all other indexes allow to paste only when newToken "fits" between prevToken and currToken |
| 1242 | + if ( currType == 'notexists' || prevType == 'notexists' || |
| 1243 | + ( CB_EditLine.allowedPaste[ newType ].curr[ currType ] && |
| 1244 | + CB_EditLine.allowedPaste[ newType ].prev[ prevType ] ) ) { |
| 1245 | + return this.insertToken( newToken ); |
| 1246 | + } |
| 1247 | + return false; |
| 1248 | +} |
| 1249 | +CB_EditLine.prototype.tokenLeft = function( currentIndex ) { |
| 1250 | + var movedToken = this.tokens[ currentIndex ]; |
| 1251 | + // move single token by default |
| 1252 | + var movedTypes = [ movedToken.getPasteType() ]; |
| 1253 | + var currToken, currType; |
| 1254 | + var prevToken, prevType; |
| 1255 | + var nextToken, nextType; |
| 1256 | + var i; |
| 1257 | + if ( movedTypes[ 0 ] == 'select' || currentIndex == 0 ) { |
| 1258 | + return; |
| 1259 | + } |
| 1260 | + switch ( movedTypes[ 0 ] ) { |
| 1261 | + case 'logic' : |
| 1262 | + nextToken = this.getToken( currentIndex + 1 ); |
| 1263 | + nextType = (nextToken == null) ? 'notexists' : nextToken.getPasteType(); |
| 1264 | + if ( nextType != 'comparsion' ) { |
| 1265 | + return; |
| 1266 | + } |
| 1267 | + // move current 'logic' and next 'comparsion' token |
| 1268 | + movedTypes.push( nextType ); |
| 1269 | + break; |
| 1270 | + case 'comparsion' : |
| 1271 | + prevToken = this.getToken( currentIndex - 1 ); |
| 1272 | + prevType = (prevToken == null) ? 'notexists' : prevToken.getPasteType(); |
| 1273 | + if ( prevType != 'logic' ) { |
| 1274 | + return; |
| 1275 | + } |
| 1276 | + // move previous 'logic' and current 'comparsion' token |
| 1277 | + movedTypes.unshift( prevType ); |
| 1278 | + // start to move from previous token |
| 1279 | + currentIndex--; |
| 1280 | + break; |
| 1281 | + } |
| 1282 | + for ( i = currentIndex - 1; i >= 0; i-- ) { |
| 1283 | + prevToken = this.getToken( i - 1 ); |
| 1284 | + currToken = this.getToken( i ); |
| 1285 | + currType = (currToken == null) ? 'notexists' : currToken.getPasteType(); |
| 1286 | + prevType = (prevToken == null) ? 'notexists' : prevToken.getPasteType(); |
| 1287 | + if ( CB_EditLine.allowedPaste[ movedTypes[ movedTypes.length - 1 ] ].curr[ currType ] && |
| 1288 | + CB_EditLine.allowedPaste[ movedTypes[ 0 ] ].prev[ prevType ] ) { |
| 1289 | + if ( this.moveTokens( currentIndex, i, movedTypes.length ) ) { return; } |
| 1290 | + } |
| 1291 | + } |
| 1292 | +} |
| 1293 | +CB_EditLine.prototype.tokenRight = function( currentIndex ) { |
| 1294 | + var movedToken = this.tokens[ currentIndex ]; |
| 1295 | + // move single token by default |
| 1296 | + var movedTypes = [ movedToken.getPasteType() ]; |
| 1297 | + var currToken, currType; |
| 1298 | + var prevToken, prevType; |
| 1299 | + var nextToken, nextType; |
| 1300 | + var i; |
| 1301 | + if ( movedTypes[ 0 ] == 'select' || currentIndex == this.tokens.length - 1 ) { |
| 1302 | + return; |
| 1303 | + } |
| 1304 | + switch ( movedTypes[ 0 ] ) { |
| 1305 | + case 'logic' : |
| 1306 | + nextToken = this.getToken( currentIndex + 1 ); |
| 1307 | + nextType = (nextToken == null) ? 'notexists' : nextToken.getPasteType(); |
| 1308 | + if ( nextType != 'comparsion' ) { |
| 1309 | + return; |
| 1310 | + } |
| 1311 | + // move current 'logic' and next 'comparsion' token |
| 1312 | + movedTypes.push( nextType ); |
| 1313 | + break; |
| 1314 | + case 'comparsion' : |
| 1315 | + prevToken = this.getToken( currentIndex - 1 ); |
| 1316 | + prevType = (prevToken == null) ? 'notexists' : prevToken.getPasteType(); |
| 1317 | + if ( prevType != 'logic' ) { |
| 1318 | + return; |
| 1319 | + } |
| 1320 | + // move previous 'logic' and current 'comparsion' token |
| 1321 | + movedTypes.unshift( prevType ); |
| 1322 | + // start to move from previous token |
| 1323 | + currentIndex--; |
| 1324 | + break; |
| 1325 | + } |
| 1326 | + for ( i = currentIndex + 1; i <= this.tokens.length; i++ ) { |
| 1327 | + prevToken = this.getToken( i - 1 ); |
| 1328 | + currToken = this.getToken( i ); |
| 1329 | + currType = (currToken == null) ? 'notexists' : currToken.getPasteType(); |
| 1330 | + prevType = (prevToken == null) ? 'notexists' : prevToken.getPasteType(); |
| 1331 | + if ( CB_EditLine.allowedPaste[ movedTypes[ movedTypes.length - 1 ] ].curr[ currType ] && |
| 1332 | + CB_EditLine.allowedPaste[ movedTypes[ 0 ] ].prev[ prevType ] ) { |
| 1333 | + if ( this.moveTokens( currentIndex, i, movedTypes.length ) ) { return; } |
| 1334 | + } |
| 1335 | + } |
| 1336 | +} |
| 1337 | +CB_EditLine.prototype.tokenRemove = function( currentIndex ) { |
| 1338 | + var token = this.tokens[ currentIndex ]; |
| 1339 | + var type = token.type; |
| 1340 | + var op = token.op; |
| 1341 | + var i, bracketSublevel; |
| 1342 | + switch ( type ) { |
| 1343 | + case 'bracket' : |
| 1344 | + bracketSublevel = 0; |
| 1345 | + if ( op == 'lbracket' ) { |
| 1346 | + // search for next matching rbracket, then delete the both |
| 1347 | + for ( i = currentIndex + 1; i < this.tokens.length; i++ ) { |
| 1348 | + if ( this.tokens[i].type == 'bracket' ) { |
| 1349 | + if ( this.tokens[i].op == 'lbracket' ) { |
| 1350 | + ++bracketSublevel; |
| 1351 | + } else { // this.tokens[i].op == 'rbracket' ) |
| 1352 | + if ( --bracketSublevel <= 0 ) { |
| 1353 | + // the order of next two operations is important |
| 1354 | + this.removeTokens( i, 1 ); |
| 1355 | + this.removeTokens( currentIndex, 1 ); |
| 1356 | + return; |
| 1357 | + } |
| 1358 | + } |
| 1359 | + } |
| 1360 | + } |
| 1361 | + } else { // op == 'rbracket' |
| 1362 | + // search for previous matching lbracket, then delete the both |
| 1363 | + for ( i = currentIndex - 1; i >= 0; i-- ) { |
| 1364 | + if ( this.tokens[i].type == 'bracket' ) { |
| 1365 | + if ( this.tokens[i].op == 'rbracket' ) { |
| 1366 | + --bracketSublevel; |
| 1367 | + } else { // this.tokens[i].op == 'lbracket' ) |
| 1368 | + if ( ++bracketSublevel >= 0 ) { |
| 1369 | + // the order of next two operations is important |
| 1370 | + this.removeTokens( currentIndex, 1 ); |
| 1371 | + this.removeTokens( i, 1 ); |
| 1372 | + return; |
| 1373 | + } |
| 1374 | + } |
| 1375 | + } |
| 1376 | + } |
| 1377 | + } |
| 1378 | + break; |
| 1379 | + case 'select' : |
| 1380 | + return; |
| 1381 | + case 'logic' : |
| 1382 | + if ( currentIndex == 0 || currentIndex == this.tokens.length - 1 ) { |
| 1383 | + break; |
| 1384 | + } |
| 1385 | + var p1 = this.tokens[currentIndex + 1]; |
| 1386 | + if ( p1.type == 'comparsion' ) { |
| 1387 | + // remove current logical token and the next comparsion right to it |
| 1388 | + this.removeTokens( currentIndex, 2 ); |
| 1389 | + return; |
| 1390 | + } |
| 1391 | + if ( p1.type != 'bracket' || p1.op != 'lbracket' ) { |
| 1392 | + // next (right) token is not lbracket |
| 1393 | + break; |
| 1394 | + } |
| 1395 | + // try to remove the whole subexpression right to current token |
| 1396 | + bracketSublevel = 0; |
| 1397 | + // search for next matching rbracket, then delete the whole subexpression and logic just before it (left side) |
| 1398 | + for ( i = currentIndex + 1; i < this.tokens.length; i++ ) { |
| 1399 | + if ( this.tokens[i].type == 'bracket' ) { |
| 1400 | + if ( this.tokens[i].op == 'lbracket' ) { |
| 1401 | + ++bracketSublevel; |
| 1402 | + } else { // this.tokens[i].op == 'rbracket' ) |
| 1403 | + if ( --bracketSublevel <= 0 ) { |
| 1404 | + this.removeTokens( currentIndex, i - currentIndex + 1 ); |
| 1405 | + return; |
| 1406 | + } |
| 1407 | + } |
| 1408 | + } |
| 1409 | + } |
| 1410 | + break; |
| 1411 | + case 'comparsion' : |
| 1412 | + if ( currentIndex == 0 ) { |
| 1413 | + break; |
| 1414 | + } |
| 1415 | + if ( this.tokens[currentIndex - 1].type == 'logic' ) { |
| 1416 | + // remove current comparsion and logical op before it |
| 1417 | + this.removeTokens( currentIndex - 1, 2 ); |
| 1418 | + return; |
| 1419 | + } |
| 1420 | + if ( currentIndex <= 1 || currentIndex >= this.tokens.length - 1 ) { |
| 1421 | + break; |
| 1422 | + } |
| 1423 | + var m2 = this.tokens[currentIndex - 2]; |
| 1424 | + var m1 = this.tokens[currentIndex - 1]; |
| 1425 | + var p1 = this.tokens[currentIndex + 1]; |
| 1426 | + if ( m2.type == 'logic' && |
| 1427 | + m1.type == 'bracket' && m1.op == 'lbracket' && |
| 1428 | + p1.type == 'bracket' && p1.op == 'rbracket' ) { |
| 1429 | + // remove current comparsion with brackets around it and also logical op before it |
| 1430 | + this.removeTokens( currentIndex - 2, 4 ); |
| 1431 | + } |
| 1432 | + break; |
| 1433 | + default : |
| 1434 | + alert( 'Unimplemented token type='+token.type+' in CB_EditLine.tokenRemove' ); |
| 1435 | + } |
| 1436 | + // "smart" remove was unsuccessful, delete only current token |
| 1437 | + this.removeTokens( currentIndex, 1 ); |
| 1438 | + if ( this.tokens.length == 0 ) { |
| 1439 | + // empty line becomes 'select' 'all' |
| 1440 | + this.tokens = [ new CB_Token( 'all', 'conditionLine', 0 ) ]; |
| 1441 | + } |
| 1442 | +} |
| 1443 | +CB_EditLine.prototype.getToken = function( index ) { |
| 1444 | + if ( index >= 0 && index < this.tokens.length ) { |
| 1445 | + return this.tokens[ index ]; |
| 1446 | + } else { |
| 1447 | + return null; |
| 1448 | + } |
| 1449 | +} |
| 1450 | +/*** |
| 1451 | + * psysically changes this.tokens, from source parameters provided in control buttons handlers (defined just above) |
| 1452 | +***/ |
| 1453 | +/* insert new token at selected position |
| 1454 | + * @param newToken |
| 1455 | + * newToken.index indicates "desired" position (may be corrected in this method) |
| 1456 | + * @return true, when paste was successful, false otherwise |
| 1457 | + */ |
| 1458 | +CB_EditLine.prototype.insertToken = function( newToken ) { |
| 1459 | + if ( newToken.index >=0 && newToken.index <= this.tokens.length ) { |
| 1460 | + this.tokens.splice( newToken.index, 0, newToken ); |
| 1461 | + return true; |
| 1462 | + } else { |
| 1463 | + alert( 'An attempt to insert token to non-defined index='+newToken.index+' in CB_EditLine.insertToken' ); |
| 1464 | + return false; |
| 1465 | + } |
| 1466 | +} |
| 1467 | +/* |
| 1468 | + * moves count of tokens from currentIndex to newIndex, tokens at newIndex are moved next to RIGHT |
| 1469 | + * @param currentIndex source index in this.tokens |
| 1470 | + * @param newIndex destination index in this.tokens |
| 1471 | + * @param count number of tokens to move |
| 1472 | + * @result true, move is complete; false, attempt to move into itself |
| 1473 | + */ |
| 1474 | +CB_EditLine.prototype.moveTokens = function( currentIndex, newIndex, count ) { |
| 1475 | + if ( currentIndex <= newIndex && newIndex - currentIndex <= count ) { |
| 1476 | + return false; |
| 1477 | + } |
| 1478 | + // save our tokens to temporary array |
| 1479 | + var ourTokens = this.tokens.slice( currentIndex, currentIndex + count ); |
| 1480 | + if ( ourTokens.length != count ) { |
| 1481 | + alert( 'Slice after the end of tokens array in CB_EditLine.moveTokens' ); |
| 1482 | + return true; |
| 1483 | + } |
| 1484 | + // remove our tokens from their current position |
| 1485 | + this.tokens.splice( currentIndex, count ); |
| 1486 | + if ( newIndex > currentIndex ) { |
| 1487 | + // correct newIndex, because we've already removed count of entries BEFORE newIndex |
| 1488 | + newIndex -= count; |
| 1489 | + } |
| 1490 | + // insert our tokens into their new position, more already existing ones to RIGHT |
| 1491 | + this.tokens = this.tokens.slice( 0, newIndex ).concat( ourTokens.concat( this.tokens.slice( newIndex ) ) ); |
| 1492 | + return true; |
| 1493 | +} |
| 1494 | +CB_EditLine.prototype.removeTokens = function( index, count ) { |
| 1495 | + this.tokens.splice( index, count ); |
| 1496 | +} |
| 1497 | +/*** |
| 1498 | + * end of tokens move / remove section (expression editor) |
| 1499 | +***/ |
| 1500 | +CB_EditLine.prototype.getEncodedExpr = function() { |
| 1501 | + var encodedExpr = ''; |
| 1502 | + var firstElem = true; |
| 1503 | + for ( i = 0; i < this.tokens.length; i++ ) { |
| 1504 | + if ( firstElem ) { |
| 1505 | + firstElem = false; |
| 1506 | + } else { |
| 1507 | + encodedExpr += '_'; |
| 1508 | + } |
| 1509 | + encodedExpr += this.tokens[i].toString(); |
| 1510 | + } |
| 1511 | + return encodedExpr; |
| 1512 | +} |
| 1513 | + |
| 1514 | +/*** |
| 1515 | + * condition editor |
| 1516 | + */ |
| 1517 | +var CB_ConditionEditor = { |
| 1518 | + |
| 1519 | + // local interface messages (object key/val pairs) |
| 1520 | + // also includes local 'all' op and one/two operands operators templates |
| 1521 | + localMessages : null, |
| 1522 | + // messages for tokens control buttons |
| 1523 | + localEditHints : null, |
| 1524 | + // local views of tokens |
| 1525 | + localDbFields : null, // local db fields |
| 1526 | + localBrackets : null, // local expression brackets |
| 1527 | + localBoolOps : null, // local boolean operator names |
| 1528 | + localCmpOps : null, // local comparsion operators |
| 1529 | + |
| 1530 | + // CB_EditLine instance of currently edited expression |
| 1531 | + conditionLine : null, |
| 1532 | + // CB_EditLine instance of samples to add to currently edited expression |
| 1533 | + samplesLine : null, |
| 1534 | + // token string clipboard (single token to copy/paste) |
| 1535 | + // TODO: implement clipboardLine? (multple tokens copy/paste with visual clipboard) |
| 1536 | + clipboard : '', |
| 1537 | + |
| 1538 | + setLocalNames : function( localMessages, localEditHints, localDbFields, localBrackets, localBoolOps, localCmpOps ) { |
| 1539 | + this.localMessages = localMessages; |
| 1540 | + this.localEditHints = localEditHints; |
| 1541 | + this.localDbFields = localDbFields; |
| 1542 | + this.localBrackets = localBrackets; |
| 1543 | + this.localBoolOps = localBoolOps; |
| 1544 | + this.localCmpOps = localCmpOps; |
| 1545 | + }, |
| 1546 | + |
| 1547 | + /* |
| 1548 | + * creates a visual editor from encoded infix expression given |
| 1549 | + */ |
| 1550 | + createExpr : function( encInfixQueue ) { |
| 1551 | + var i, ea, oToken; |
| 1552 | + CB_lib.log( 'createExpr encInfixQueue='+encInfixQueue); |
| 1553 | + ea = encInfixQueue.split( '_' ); |
| 1554 | + var cbEditorContainer = document.getElementById( 'cb_editor_container' ); |
| 1555 | + // show previousely hidden toolbox cell |
| 1556 | + cbEditorContainer.parentNode.style.display = (CB_Setup.isIE > 7) ? 'table-cell' : 'block'; |
| 1557 | + // condition editor does not work in IE versions less than 7 |
| 1558 | + // better to upgrade than try to fix (IE6 produced "memory read errors" while executing CB_Token / CB_EditLine code) |
| 1559 | + if ( CB_Setup.isIE < 7 ) { |
| 1560 | + cbEditorContainer.innerHTML = ''; |
| 1561 | + var textNode = document.createTextNode( this.localMessages[ 'ie6_warning' ] ); |
| 1562 | + cbEditorContainer.appendChild( textNode ); |
| 1563 | + return; |
| 1564 | + } |
| 1565 | + // pass property name to CB_EditLine constructor, |
| 1566 | + // otherwise event handlers won't be able to call() proper instance of CB_EditLine |
| 1567 | + this.conditionLine = new CB_EditLine( 'condition', cbEditorContainer, 'conditionLine' ); |
| 1568 | + if ( ea.length > 0 ) { |
| 1569 | + for ( i = 0; i < ea.length; i++ ) { |
| 1570 | + // pass lineInstanceName and index to CB_Token constructor, |
| 1571 | + // otherwise event handlers won't be able to call() proper instance of CB_Token |
| 1572 | + oToken = new CB_Token( ea[i], 'conditionLine', i ); |
| 1573 | + if ( oToken.type == 'undef' || oToken.type == 'select' ) { |
| 1574 | + this.conditionLine.tokens = [ new CB_Token( 'all', 'conditionLine', 0 ) ]; |
| 1575 | + break; |
| 1576 | + } |
| 1577 | + this.conditionLine.tokens.push( oToken ); |
| 1578 | + } |
| 1579 | + } else { |
| 1580 | + this.conditionLine.tokens = [ new CB_Token( 'all', 'conditionLine', 0 ) ]; |
| 1581 | + } |
| 1582 | + this.conditionLine.view(); |
| 1583 | + this.createEditSamples(); |
| 1584 | + }, |
| 1585 | + |
| 1586 | + createEditSamples : function() { |
| 1587 | + var i = 0; |
| 1588 | + var cbEditorControls = document.getElementById( 'cb_editor_controls' ); |
| 1589 | + // show previousely hidden toolbox cell |
| 1590 | + cbEditorControls.parentNode.style.display = (CB_Setup.isIE > 7) ? 'table-cell' : 'block'; |
| 1591 | + this.samplesLine = new CB_EditLine( 'samples', cbEditorControls, 'samplesLine' ); |
| 1592 | +// commented out because does not works in IE7 |
| 1593 | +// this.samplesLine.node.style.borderTopColor = 'lightgray'; |
| 1594 | +// this.samplesLine.node.style.borderTopWidth = '2px'; |
| 1595 | +// this.samplesLine.node.style.borderTopStyle = 'dashed'; |
| 1596 | + // pass lineInstanceName and index to CB_Token constructor, |
| 1597 | + // otherwise event handlers won't be able to call() proper instance of CB_Token |
| 1598 | + this.samplesLine.tokens.push( new CB_Token( '(', 'samplesLine', i++, CB_Setup.colors.samples ) ); |
| 1599 | + this.samplesLine.tokens.push( new CB_Token( ')', 'samplesLine', i++, CB_Setup.colors.samples ) ); |
| 1600 | + this.samplesLine.tokens.push( new CB_Token( 'or', 'samplesLine', i++, CB_Setup.colors.samples ) ); |
| 1601 | + this.samplesLine.tokens.push( new CB_Token( 'and', 'samplesLine', i++, CB_Setup.colors.samples ) ); |
| 1602 | + this.samplesLine.tokens.push( new CB_Token( 'ges1', 'samplesLine', i++, CB_Setup.colors.samples ) ); |
| 1603 | + this.samplesLine.tokens.push( new CB_Token( 'all', 'samplesLine', i++, CB_Setup.colors.samples ) ); |
| 1604 | + this.samplesLine.view(); |
| 1605 | + }, |
| 1606 | + |
| 1607 | + clearSelection : function() { |
| 1608 | + this.clipboard = ''; |
| 1609 | + this.samplesLine.setHighlight( -1 ); |
| 1610 | + }, |
| 1611 | + |
| 1612 | + applyButtonClick : function( event ) { |
| 1613 | + var obj = CB_lib.getEventObj( event, true ); |
| 1614 | + obj.blur(); |
| 1615 | + // {{{ switch the context |
| 1616 | + CB_ConditionEditor.submitExpr.call( CB_ConditionEditor ); |
| 1617 | + // switch the context }}} |
| 1618 | + }, |
| 1619 | + |
| 1620 | + submitExpr : function() { |
| 1621 | + this.conditionLine.view(); |
| 1622 | + var encInfixQueue = this.conditionLine.getEncodedExpr(); |
| 1623 | + var appliedOption = CB_lib.getSelectOption( document.getElementById( 'cb_expr_select' ), encInfixQueue, 'infixexpr' ); |
| 1624 | + var setCookie = appliedOption == null ? 1 : 0; |
| 1625 | + var param = [ encInfixQueue, CategoryBrowser.nameFilter, CategoryBrowser.nameFilterCI, setCookie ]; |
| 1626 | + if ( CategoryBrowser.pagerLimit !== null ) { |
| 1627 | + param.push( CategoryBrowser.pagerLimit ); |
| 1628 | + } |
| 1629 | + CB_lib.log('destination encodedExpr='+param[0]); |
| 1630 | + CB_lib.log('destination setCookie='+param[1]); |
| 1631 | + CB_lib.log('destination pagerLimit='+param[2]); |
| 1632 | + sajax_do_call( "CategoryBrowser::applyEncodedQueue", param, document.getElementById( 'cb_root_container' ) ); |
| 1633 | + if ( setCookie ) { |
| 1634 | + sajax_do_call( "CategoryBrowser::generateSelectedOption", [encInfixQueue], CB_ConditionEditor.appendSelectedOption ); |
| 1635 | + } |
| 1636 | + }, |
| 1637 | + |
| 1638 | + /* |
| 1639 | + * @param request.responsetext html representation of select's option for new expression value just applied |
| 1640 | + */ |
| 1641 | + appendSelectedOption : function( request ) { |
| 1642 | + // {{{ switch the context |
| 1643 | + CB_ConditionEditor._appendSelectedOption.call( CB_ConditionEditor, this, request ); |
| 1644 | + // switch the context }}} |
| 1645 | + }, |
| 1646 | + |
| 1647 | + _appendSelectedOption : function( eventObj, request ) { |
| 1648 | + if ( request.status != 200 ) { |
| 1649 | + alert( 'Invalid AJAX response in CB_ConditionEditor._appendSelectedOption, request.status='+request.status ); |
| 1650 | + return; |
| 1651 | + } |
| 1652 | + // cannot create option node from innerHTML in IE |
| 1653 | + var div = document.createElement( 'div' ); |
| 1654 | + div.innerHTML = request.responseText; |
| 1655 | + div = div.firstChild; // get div received from PHP via AJAX result |
| 1656 | + // cannot import innerHTML directly to select node in IE |
| 1657 | + var option = document.createElement( 'option' ); |
| 1658 | + CategoryBrowser.rootCond = div.getAttribute( 'value' ); |
| 1659 | + option.setAttribute( 'value', CategoryBrowser.rootCond ); |
| 1660 | + option.setAttribute( 'selected', div.getAttribute( 'selected' ) ); |
| 1661 | + option.setAttribute( 'infixexpr', div.getAttribute( 'infixexpr' ) ); |
| 1662 | + option.innerHTML = div.innerHTML; |
| 1663 | + document.getElementById( 'cb_expr_select' ).appendChild( option ); |
| 1664 | + } |
| 1665 | + |
| 1666 | +} /* end of CB_ConditionEditor */ |
Property changes on: trunk/extensions/CategoryBrowser/category_browser.js |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 1667 | + native |
Index: trunk/extensions/CategoryBrowser/CategoryBrowserMain.php |
— | — | @@ -0,0 +1,923 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of CategoryBrowser. |
| 6 | + * |
| 7 | + * CategoryBrowser is free software; you can redistribute it and/or modify |
| 8 | + * it under the terms of the GNU General Public License as published by |
| 9 | + * the Free Software Foundation; either version 2 of the License, or |
| 10 | + * (at your option) any later version. |
| 11 | + * |
| 12 | + * CategoryBrowser is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | + * GNU General Public License for more details. |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU General Public License |
| 18 | + * along with CategoryBrowser; if not, write to the Free Software |
| 19 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 20 | + * |
| 21 | + * ***** END LICENSE BLOCK ***** |
| 22 | + * |
| 23 | + * CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
| 24 | + * |
| 25 | + * To activate this extension : |
| 26 | + * * Create a new directory named CategoryBrowser into the directory "extensions" of MediaWiki. |
| 27 | + * * Place the files from the extension archive there. |
| 28 | + * * Add this line at the end of your LocalSettings.php file : |
| 29 | + * require_once "$IP/extensions/CategoryBrowser/CategoryBrowser.php"; |
| 30 | + * |
| 31 | + * @version 0.2.0 |
| 32 | + * @link http://www.mediawiki.org/wiki/Extension:CategoryBrowser |
| 33 | + * @author Dmitriy Sintsov <questpc@rambler.ru> |
| 34 | + * @addtogroup Extensions |
| 35 | + */ |
| 36 | + |
| 37 | +if( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is a part of MediaWiki extension.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +abstract class CB_AbstractPager { |
| 42 | + |
| 43 | + var $db; |
| 44 | + |
| 45 | + /* pager position (actual offset) |
| 46 | + * 0 means pager has no previous elements |
| 47 | + * -1 means pager has no elements at all |
| 48 | + */ |
| 49 | + var $offset = -1; |
| 50 | + /* provided "source" offset */ |
| 51 | + var $query_offset; |
| 52 | + /* indicates, whether the pager has further elements */ |
| 53 | + var $hasMoreEntries = false; |
| 54 | + /* maximal number of entries per page (actual number of entries on page) */ |
| 55 | + var $limit = 0; |
| 56 | + /* provided "source" limit */ |
| 57 | + var $query_limit; |
| 58 | + /* array of current entries */ |
| 59 | + var $entries; |
| 60 | + |
| 61 | + /* |
| 62 | + * abstract query (doesn't instantinate) |
| 63 | + * @param offset - suggested SQL offset |
| 64 | + * @param limit - suggested SQL limit |
| 65 | + */ |
| 66 | + function __construct( $offset, $limit ) { |
| 67 | + $this->db = & wfGetDB( DB_SLAVE ); |
| 68 | + $this->query_limit = intval( $limit ); |
| 69 | + $this->query_offset = intval( $offset ); |
| 70 | + } |
| 71 | + |
| 72 | + /* |
| 73 | + * |
| 74 | + * initializes hasMoreEntries and array of entries from DB result |
| 75 | + */ |
| 76 | + function setEntries( &$db_result ) { |
| 77 | + $this->hasMoreEntries = false; |
| 78 | + $this->entries = array(); |
| 79 | + $count = $this->db->numRows( $db_result ); |
| 80 | + if ( $count < 1 ) { return; } |
| 81 | + $this->offset = $this->query_offset; |
| 82 | + $this->limit = $this->query_limit; |
| 83 | + if ( $this->hasMoreEntries = $count > $this->limit ) { |
| 84 | + // do not include "overflow" entry, it belongs to the next page |
| 85 | + $count--; |
| 86 | + } |
| 87 | + // do not include last row (which was loaded only to set hasMoreEntries) |
| 88 | + for ( $i = 0; $i < $count; $i++ ) { |
| 89 | + $row = $this->db->fetchObject( $db_result ); |
| 90 | + $this->entries[] = $row; |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + // returns previous SQL select offset |
| 95 | + function getPrevOffset() { |
| 96 | + $prev_offset = $this->offset - $this->limit; |
| 97 | + return ( ($prev_offset >= 0) ? $prev_offset : 0); |
| 98 | + } |
| 99 | + |
| 100 | + // returns next SQL select offset |
| 101 | + function getNextOffset() { |
| 102 | + return ( ( $this->hasMoreEntries ) ? $this->offset + $this->limit : 0 ); |
| 103 | + } |
| 104 | + |
| 105 | + /* |
| 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 | + */ |
| 112 | + function getListType() { |
| 113 | + // it is not enough to check $this->entries[0], |
| 114 | + // because some broken tables might have just some (not all) cat_title = NULL or page_title = NULL |
| 115 | + foreach( $this->entries as &$entry ) { |
| 116 | + if ( isset( $entry->page_namespace ) && $entry->page_namespace == NS_FILE ) { return 'generateFilesList'; } |
| 117 | + if ( isset( $entry->page_title ) ) { return 'generatePagesList'; } |
| 118 | + if ( isset( $entry->cat_title ) ) { return 'generateCatList'; } |
| 119 | + if ( isset( $entry->cl_sortkey ) ) { return 'generateCatList'; } |
| 120 | + } |
| 121 | + throw new MWException( 'Entries not initialized in ' . __METHOD__ ); |
| 122 | + } |
| 123 | + |
| 124 | +} /* end of CB_AbstractPager class */ |
| 125 | + |
| 126 | +/* |
| 127 | + * subentries (subcategories, pages, files) pager |
| 128 | + * TODO: gracefully set offset = 0 when too large offset was given |
| 129 | + */ |
| 130 | +class CB_SubPager extends CB_AbstractPager { |
| 131 | + |
| 132 | + var $page_table; |
| 133 | + var $category_table; |
| 134 | + var $categorylinks_table; |
| 135 | + // database ID of parent category |
| 136 | + var $parentCatId; |
| 137 | + // database fields to query |
| 138 | + var $select_fields; |
| 139 | + // namespace SQL condition (WHERE part) |
| 140 | + var $ns_cond; |
| 141 | + // javascript function used to navigate between the pages |
| 142 | + var $js_nav_func; |
| 143 | + |
| 144 | + /* |
| 145 | + * creates subcategory list pager |
| 146 | + * |
| 147 | + * @param $parentCatId id of parent category |
| 148 | + * @param $offset SQL offset |
| 149 | + * @param $limit SQL limit |
| 150 | + * |
| 151 | + * TODO: query count of parentCatId subcategories/pages/files in category table for progress / percentage display |
| 152 | + */ |
| 153 | + function __construct( $parentCatId, $offset, $limit, $js_nav_func, $select_fields = '*', $ns_cond = '' ) { |
| 154 | + parent::__construct( $offset, $limit ); |
| 155 | + $this->page_table = $this->db->tableName( 'page' ); |
| 156 | + $this->category_table = $this->db->tableName( 'category' ); |
| 157 | + $this->categorylinks_table = $this->db->tableName( 'categorylinks' ); |
| 158 | + $this->parentCatId = $parentCatId; |
| 159 | + $this->select_fields = $select_fields; |
| 160 | + $this->ns_cond = $ns_cond; |
| 161 | + $this->js_nav_func = $js_nav_func; |
| 162 | + } |
| 163 | + |
| 164 | + /* |
| 165 | + * set offset, limit, hasMoreEntries and entries |
| 166 | + * @param $offset SQL offset |
| 167 | + * @param $limit SQL limit |
| 168 | + */ |
| 169 | + function getCurrentRows() { |
| 170 | + /* TODO: change the query to more optimal one (no subselects) |
| 171 | + * 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 |
| 172 | + */ |
| 173 | + $query_string = |
| 174 | + "SELECT {$this->select_fields} " . |
| 175 | + "FROM {$this->page_table} ". |
| 176 | + "INNER JOIN {$this->categorylinks_table} FORCE INDEX (cl_sortkey) ON cl_from = page_id " . |
| 177 | + "LEFT JOIN {$this->category_table} ON cat_title = page_title AND page_namespace = " . NS_CATEGORY . " " . |
| 178 | + "WHERE cl_to IN (" . |
| 179 | + "SELECT cat_title " . |
| 180 | + "FROM {$this->category_table} " . |
| 181 | + "WHERE cat_id = " . $this->db->addQuotes( $this->parentCatId ) . |
| 182 | + ") " . ( ($this->ns_cond == '') ? '' : "AND {$this->ns_cond} " ) . |
| 183 | + "ORDER BY cl_sortkey "; |
| 184 | + $res = $this->db->query( $query_string . "LIMIT {$this->query_offset}," . ($this->query_limit + 1), __METHOD__ ); |
| 185 | + $this->setEntries( $res ); |
| 186 | + } |
| 187 | + |
| 188 | + // returns JS function call used to navigate to the previous page of this pager |
| 189 | + function getPrevAjaxLink() { |
| 190 | + $result = (object) array( |
| 191 | + "call"=>"return CategoryBrowser.{$this->js_nav_func}(this," . $this->parentCatId . "," . $this->getPrevOffset() . ( ($this->limit == CB_PAGING_ROWS) ? '' : ',' . $this->limit ) . ')', |
| 192 | + "placeholders"=>false |
| 193 | + ); |
| 194 | + return $result; |
| 195 | + } |
| 196 | + |
| 197 | + // returns JS function call used to navigate to the next page of this pager |
| 198 | + function getNextAjaxLink() { |
| 199 | + $result = (object) array( |
| 200 | + "call"=>"return CategoryBrowser.{$this->js_nav_func}(this," . $this->parentCatId . ',' . $this->getNextOffset() . ( ($this->limit == CB_PAGING_ROWS) ? '' : ',' . $this->limit ) . ')', |
| 201 | + "placeholders"=>false |
| 202 | + ); |
| 203 | + return $result; |
| 204 | + } |
| 205 | + |
| 206 | +} /* end of CB_SubPager class */ |
| 207 | + |
| 208 | +/* |
| 209 | + * creates a root category pager |
| 210 | + * TODO: gracefully set offset = 0 when too large offset was given |
| 211 | + * TODO: with $conds == '' categories aren't always sorted alphabetically |
| 212 | + */ |
| 213 | +class CB_RootPager extends CB_AbstractPager { |
| 214 | + |
| 215 | + |
| 216 | + /* string paging conds aka filter (WHERE statement) */ |
| 217 | + var $conds; |
| 218 | + /* _optional_ instance of CB_SqlCond object used to construct this pager |
| 219 | + * (in case it's been provided in constructor call) |
| 220 | + */ |
| 221 | + var $sqlCond = null; |
| 222 | + |
| 223 | + // category name filter (LIKE) |
| 224 | + var $nameFilter = ''; |
| 225 | + // category name filter case-insensetive flag (when true, tries to use insensetive LIKE COLLATE) |
| 226 | + var $nameFilterCI = false; |
| 227 | + |
| 228 | + /* |
| 229 | + * formal constructor |
| 230 | + * real instantination should be performed by calling public static methods below |
| 231 | + */ |
| 232 | + function __construct( $offset, $limit ) { |
| 233 | + parent::__construct( $offset, $limit ); |
| 234 | + } |
| 235 | + |
| 236 | + /* |
| 237 | + * @param $conds - instanceof CB_SqlCond (parentized condition generator) |
| 238 | + * @param $offset - SQL OFFSET |
| 239 | + * @param $limit - SQL LIMIT |
| 240 | + */ |
| 241 | + public static function newFromSqlCond( CB_SqlCond $conds, $offset = 0, $limit = CB_PAGING_ROWS ) { |
| 242 | + $rp = new CB_RootPager( $offset, $limit ); |
| 243 | + $rp->conds = $conds->getCond(); |
| 244 | + $rp->sqlCond = &$conds; |
| 245 | + return $rp; |
| 246 | + } |
| 247 | + |
| 248 | + /* |
| 249 | + * @param $tokens - array of infix ops of sql condition |
| 250 | + * @param $offset - SQL OFFSET |
| 251 | + * @param $limit - SQL LIMIT |
| 252 | + */ |
| 253 | + public static function newFromInfixTokens( $tokens, $offset = 0, $limit = CB_PAGING_ROWS ) { |
| 254 | + if ( !is_array( $tokens ) ) { |
| 255 | + return null; |
| 256 | + } |
| 257 | + try { |
| 258 | + $sqlCond = CB_SqlCond::newFromInfixTokens( $tokens ); |
| 259 | + } catch( MWException $ex ) { |
| 260 | + return null; |
| 261 | + } |
| 262 | + return self::newFromSqlCond( $sqlCond, $offset, $limit ); |
| 263 | + } |
| 264 | + |
| 265 | + /* |
| 266 | + * create root pager from the largest non-empty category range |
| 267 | + * @param $ranges - array of "complete" token queues (range) |
| 268 | + * (every range is an stdobject of decoded infix queue and encoded reverse polish queue) |
| 269 | + */ |
| 270 | + public static function newFromCategoryRange( $ranges ) { |
| 271 | + $rp = null; |
| 272 | + foreach ( $ranges as &$range ) { |
| 273 | + $rp = CB_RootPager::newFromInfixTokens( $range->infix_decoded ); |
| 274 | + if ( is_object( $rp ) && $rp->offset != -1 ) { |
| 275 | + break; |
| 276 | + } |
| 277 | + } |
| 278 | + return $rp; |
| 279 | + } |
| 280 | + |
| 281 | + /* |
| 282 | + * filter catetories by names |
| 283 | + * @param $cat_name_filter - string category name begins from |
| 284 | + * @param $cat_name_filter_ci - boolean, true attempts to use case-insensetive search, when available |
| 285 | + */ |
| 286 | + function setNameFilter( $cat_name_filter, $cat_name_filter_ci ) { |
| 287 | + $this->nameFilter = ltrim( $cat_name_filter ); |
| 288 | + $this->nameFilterCI = $cat_name_filter_ci; |
| 289 | + } |
| 290 | + |
| 291 | + /* |
| 292 | + * performs range query and stores the results |
| 293 | + */ |
| 294 | + function getCurrentRows() { |
| 295 | + $conds = trim( $this->conds ); |
| 296 | + // use name filter, when available |
| 297 | + if ( $this->nameFilter != '' ) { |
| 298 | + if ( $conds != '' ) { |
| 299 | + $conds = "( $conds ) AND "; |
| 300 | + } |
| 301 | + $conds .= 'cat_title LIKE ' . $this->db->addQuotes( $this->nameFilter . '%' ); |
| 302 | + if ( $this->nameFilterCI && CB_Setup::$cat_title_CI != '' ) { |
| 303 | + // case insensetive search is active |
| 304 | + $conds .= ' COLLATE ' . $this->db->addQuotes( CB_Setup::$cat_title_CI ); |
| 305 | + } |
| 306 | + } |
| 307 | + $options = array( 'OFFSET' => $this->query_offset, 'ORDER BY' => 'cat_title', 'LIMIT' => $this->query_limit + 1 ); |
| 308 | + $res = $this->db->select( 'category', |
| 309 | + array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ), |
| 310 | + $conds, |
| 311 | + __METHOD__, |
| 312 | + $options |
| 313 | + ); |
| 314 | + /* set actual offset, limit, hasMoreEntries and entries */ |
| 315 | + $this->setEntries( $res ); |
| 316 | + } |
| 317 | + |
| 318 | + // returns JS function call used to navigate to the previous page of this pager |
| 319 | + function getPrevAjaxLink() { |
| 320 | + $result = (object) array( |
| 321 | + "call"=>'return CategoryBrowser.rootCats(\'' . Xml::escapeJsString( $this->sqlCond->getEncodedQueue( false ) ) . '\',' . $this->getPrevOffset() . ( ($this->limit == CB_PAGING_ROWS) ? '' : ',' . $this->limit ) . ')', |
| 322 | + "placeholders"=>true |
| 323 | + ); |
| 324 | + return $result; |
| 325 | + } |
| 326 | + |
| 327 | + // returns JS function call used to navigate to the next page of this pager |
| 328 | + function getNextAjaxLink() { |
| 329 | + $result = (object) array( |
| 330 | + "call"=>'return CategoryBrowser.rootCats(\'' . Xml::escapeJsString( $this->sqlCond->getEncodedQueue( false ) ) . '\',' . $this->getNextOffset() . ( ($this->limit == CB_PAGING_ROWS) ? '' : ',' . $this->limit ) . ')', |
| 331 | + "placeholders"=>false |
| 332 | + ); |
| 333 | + return $result; |
| 334 | + } |
| 335 | + |
| 336 | +} /* end of CB_RootPager class */ |
| 337 | + |
| 338 | +/* |
| 339 | + * browsing class - both for special page and AJAX calls |
| 340 | + */ |
| 341 | +class CategoryBrowser { |
| 342 | + |
| 343 | + function __construct() { |
| 344 | + CB_Setup::initUser(); |
| 345 | + } |
| 346 | + |
| 347 | + /* |
| 348 | + * include stylesheets and scripts; set javascript variables |
| 349 | + * @param $outputPage - an instance of OutputPage |
| 350 | + * @param $isRTL - whether the current language is RTL |
| 351 | + * currently set: cookie prefix; |
| 352 | + * localAllOp, local1opTemplate, local2opTemplate, localDbFields, localBrackets, localBoolOps, localCmpOps |
| 353 | + */ |
| 354 | + static function headScripts( &$outputPage, $isRTL ) { |
| 355 | + global $wgJsMimeType; |
| 356 | + $outputPage->addLink( |
| 357 | + array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => CB_Setup::$ScriptPath . '/category_browser.css?' . CB_Setup::$version ) |
| 358 | + ); |
| 359 | + if ( $isRTL ) { |
| 360 | + $outputPage->addLink( |
| 361 | + array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => CB_Setup::$ScriptPath . '/category_browser_rtl.css?' . CB_Setup::$version ) |
| 362 | + ); |
| 363 | + } |
| 364 | + $outputPage->addScript( |
| 365 | + '<script type="' . $wgJsMimeType . '" src="' . CB_Setup::$ScriptPath . '/category_browser.js?' . CB_Setup::$version . '"></script> |
| 366 | + <script type="' . $wgJsMimeType . '"> |
| 367 | + CB_lib.setCookiePrefix("' . CB_Setup::getJsCookiePrefix() . '"); |
| 368 | + CB_ConditionEditor.setLocalNames( ' . |
| 369 | + CategoryBrowser::getJsObject( 'cbLocalMessages', 'apply_button', 'all_op', 'op1_template', 'op2_template', 'ie6_warning' ) . ", \n\t\t\t" . |
| 370 | + CategoryBrowser::getJsObject( 'cbLocalEditHints', 'left', 'right', 'remove', 'copy', 'append', 'clear', 'paste', 'paste_right' ) . ", \n\t\t\t" . |
| 371 | + CategoryBrowser::getJsObject( 'cbLocalDbFields', 's', 'p', 'f' ) . ", \n\t\t\t" . |
| 372 | + CategoryBrowser::getJsObject( 'cbLocalOps', 'lbracket', 'rbracket' ) . ", \n\t\t\t" . |
| 373 | + CategoryBrowser::getJsObject( 'cbLocalOps', 'or', 'and' ) . ", \n\t\t\t" . |
| 374 | + CategoryBrowser::getJsObject( 'cbLocalOps', 'le', 'ge', 'eq' ) . |
| 375 | + ' );</script>' . "\n" ); |
| 376 | + } |
| 377 | + |
| 378 | + static function getJsObject( $method_name ) { |
| 379 | + $args = func_get_args(); |
| 380 | + array_shift( $args ); // remove $method_name from $args |
| 381 | + $result = '{ '; |
| 382 | + $firstElem = true; |
| 383 | + foreach( $args as &$arg ) { |
| 384 | + if ( $firstElem ) { |
| 385 | + $firstElem = false; |
| 386 | + } else { |
| 387 | + $result .= ', '; |
| 388 | + } |
| 389 | + $result .= $arg . ': "' . Xml::escapeJsString( call_user_func( array( 'self', $method_name), $arg ) ) . '"'; |
| 390 | + } |
| 391 | + $result .= ' }'; |
| 392 | + return $result; |
| 393 | + } |
| 394 | + |
| 395 | + /* |
| 396 | + * currently passed to Javascript: |
| 397 | + * localMessages, localDbFields, localBrackets, localBoolOps, localCmpOps |
| 398 | + */ |
| 399 | + /* |
| 400 | + * getJsObject callback |
| 401 | + */ |
| 402 | + static private function cbLocalMessages( $arg ) { |
| 403 | + return wfMsg( "cb_${arg}" ); |
| 404 | + } |
| 405 | + |
| 406 | + static private function cbLocalEditHints( $arg ) { |
| 407 | + return wfMsg( "cb_edit_${arg}_hint" ); |
| 408 | + } |
| 409 | + |
| 410 | + /* |
| 411 | + * getJsObject callback |
| 412 | + */ |
| 413 | + static private function cbLocalOps( $arg ) { |
| 414 | + return wfMsg( "cb_${arg}_op" ); |
| 415 | + } |
| 416 | + |
| 417 | + /* |
| 418 | + * getJsObject callback |
| 419 | + */ |
| 420 | + static private function cbLocalDbFields( $arg ) { |
| 421 | + return wfMsg( "cb_" . CB_SqlCond::$decoded_fields[ $arg ] ); |
| 422 | + } |
| 423 | + |
| 424 | + /* |
| 425 | + * generates "complete" ranges |
| 426 | + * @param $source_ranges source ranges which contain only decoded infix queue |
| 427 | + * @return "complete" ranges which contain decoded infix queue and encoded polish queue |
| 428 | + */ |
| 429 | + static function generateRanges( array &$source_ranges ) { |
| 430 | + $ranges = array(); |
| 431 | + foreach( $source_ranges as $infix_queue ) { |
| 432 | + $sqlCond = CB_SqlCond::newFromInfixTokens( $infix_queue ); |
| 433 | + $ranges[] = (object) array( 'infix_decoded'=>$infix_queue, 'polish_encoded'=> $sqlCond->getEncodedQueue( false ) ); |
| 434 | + } |
| 435 | + return $ranges; |
| 436 | + } |
| 437 | + |
| 438 | + /* |
| 439 | + * add new "complete" range to "complete" ranges list |
| 440 | + * @param $ranges "complete" ranges list (decoded infix, encoded polish) |
| 441 | + * @param $sqlCond will be added to $ranges only when no such queue already exists |
| 442 | + * @modifies $ranges |
| 443 | + */ |
| 444 | + static function addRange( array &$ranges, CB_SqlCond $sqlCond ) { |
| 445 | + $encPolishQueue = $sqlCond->getEncodedQueue( false ); |
| 446 | + $queueExists = false; |
| 447 | + foreach( $ranges as &$range ) { |
| 448 | + if ( $range->polish_encoded == $encPolishQueue ) { |
| 449 | + $queueExists = true; |
| 450 | + break; |
| 451 | + } |
| 452 | + } |
| 453 | + if ( !$queueExists ) { |
| 454 | + $sqlCond->getCond(); // build infix queue array |
| 455 | + $ranges[] = (object) array( 'infix_decoded'=>$sqlCond->infix_queue, 'polish_encoded'=>$encPolishQueue ); |
| 456 | + } |
| 457 | + } |
| 458 | + |
| 459 | + /* |
| 460 | + * generates SQL condition selector html code |
| 461 | + * @param $ranges - array of "complete" (decode infix/encoded polish) token queues |
| 462 | + * @param $rootPager - root pager currently used with this selector |
| 463 | + * @return selector html code |
| 464 | + */ |
| 465 | + static function generateSelector( array &$ranges, CB_RootPager $rootPager ) { |
| 466 | + # {{{ condition form/select template |
| 467 | + $condOptList = array(); |
| 468 | + // do not pass current pager's limit because it's meaningless |
| 469 | + // we need MAX (default) possible limit, not the current limit |
| 470 | + // also current limit is being calculated only during the call $pager->getCurrentRows() |
| 471 | + // TODO: implement the field to select pager's default limit |
| 472 | + $js_func_call = 'return CategoryBrowser.setExpr(this,' . CB_PAGING_ROWS . ')'; |
| 473 | + // FF doesn't always fire onchange, IE doesn't always fire onmouseup |
| 474 | + $condFormTpl = array ( |
| 475 | + array( '__tag'=>'noscript', 'class'=>'cb_noscript', 0=>wfMsg( 'cb_requires_javascript' ) ), |
| 476 | + array( '__tag'=>'form', '__end'=>"\n", |
| 477 | + array( '__tag'=>'select', 'id'=>'cb_expr_select', 'onmouseup'=>$js_func_call, 'onchange'=>$js_func_call, '__end'=>"\n", 0=>&$condOptList ) |
| 478 | + ) |
| 479 | + ); |
| 480 | + # }}} |
| 481 | + $queueFound = false; |
| 482 | + $selectedEncPolishQueue = $rootPager->sqlCond->getEncodedQueue( false ); |
| 483 | + foreach( $ranges as &$range ) { |
| 484 | + $condOptList[] = self::generateOption( $range, $selectedEncPolishQueue ); |
| 485 | + if ( $range->polish_encoded == $selectedEncPolishQueue ) { |
| 486 | + $queueFound = true; |
| 487 | + } |
| 488 | + } |
| 489 | + if ( !$queueFound ) { |
| 490 | + throw new MWException( 'Either the selected queue was not added to ranges list via CategoryBrowser::addRange(), or wrong ranges list passed to ' . __METHOD__ ); |
| 491 | + } |
| 492 | + return CB_XML::toText( $condFormTpl ); |
| 493 | + } |
| 494 | + |
| 495 | + static function generateOption( $range, $selectedValue, $nodeName = 'option' ) { |
| 496 | + # {{{ condition select's option template |
| 497 | + $condOptVal = ''; |
| 498 | + $condOptName = ''; |
| 499 | + $condOptInfix = ''; |
| 500 | + $condOptTpl = |
| 501 | + array( '__tag'=>$nodeName, 'value'=>&$condOptVal, 'infixexpr'=>&$condOptInfix, 0=>&$condOptName, '__end'=>"\n" ); |
| 502 | + # }}} |
| 503 | + $le = new CB_LocalExpr( $range->infix_decoded ); |
| 504 | + $condOptVal = CB_Setup::specialchars( $range->polish_encoded ); |
| 505 | + $sqlCond = CB_SqlCond::newFromEncodedPolishQueue( $range->polish_encoded ); |
| 506 | + $condOptInfix = CB_Setup::specialchars( $sqlCond->getEncodedQueue( true ) ); |
| 507 | + if ( $range->polish_encoded == $selectedValue ) { |
| 508 | + $condOptTpl['selected'] = null; |
| 509 | + } |
| 510 | + $condOptName = CB_Setup::entities( $le->toString() ); |
| 511 | + return CB_XML::toText( $condOptTpl ); |
| 512 | + } |
| 513 | + |
| 514 | + function initNavTpl() { |
| 515 | + # {{{ navigation link (prev,next) template |
| 516 | + $this->nav_link = ''; |
| 517 | + if ( !isset( $this->nav_link_tpl ) ) { |
| 518 | + $this->nav_link_tpl = |
| 519 | + array( '__tag'=>'div', 'class'=>'cb_cat_container', '__end'=>"\n", 0=>&$this->nav_link ); |
| 520 | + } |
| 521 | + # }}} |
| 522 | + } |
| 523 | + |
| 524 | + function initAjaxLinkTpl() { |
| 525 | + # {{{ ajax link template |
| 526 | + $this->ajax_onclick = ''; |
| 527 | + $this->ajax_link_text = ''; |
| 528 | + $this->ajax_link_comment = ''; |
| 529 | + if ( !isset( $this->ajax_link_tpl ) ) { |
| 530 | + $this->ajax_link_tpl = |
| 531 | + array( |
| 532 | + array( '__tag'=>'a', 'class'=>'cb_sublink', 'href'=>'', 'onclick'=>&$this->ajax_onclick, 0=>&$this->ajax_link_text ), |
| 533 | + array( '__tag'=>'span', 'class'=>'cb_comment', 0=>&$this->ajax_link_comment ) |
| 534 | + ); |
| 535 | + } |
| 536 | + # }}} |
| 537 | + } |
| 538 | + |
| 539 | + function initSortkeyTpl() { |
| 540 | + # {{{ category sortkey hint template |
| 541 | + $this->sortkey_hint = ''; |
| 542 | + if ( !isset( $this->sortkey_hint_tpl ) ) { |
| 543 | + $this->sortkey_hint_tpl = array( '__tag'=>'span', 'class'=>'cb_comment', 'style'=>'padding:0em 0.1em 0em 0.1em;', 0=>&$this->sortkey_hint ); |
| 544 | + } |
| 545 | + # }}} |
| 546 | + } |
| 547 | + |
| 548 | + function generateCatList( CB_AbstractPager $pager ) { |
| 549 | + if ( $pager->offset == -1 ) { |
| 550 | + return ''; // list has no entries |
| 551 | + } |
| 552 | + # {{{ one category container template |
| 553 | + $subcat_count_hint = ''; |
| 554 | + $cat_expand_sign = ''; |
| 555 | + $cat_link = ''; |
| 556 | + $cat_tpl = |
| 557 | + array( '__tag'=>'div', 'class'=>'cb_cat_container', '__end'=>"\n", |
| 558 | + array( '__tag'=>'div', 'class'=>'cb_cat_controls', |
| 559 | + array( '__tag'=>'span', 'title'=>&$subcat_count_hint, 'class'=>'cb_cat_expand', 0=>&$cat_expand_sign ), |
| 560 | + array( '__tag'=>'span', 'class'=>'cb_cat_item', 0=>&$cat_link ) |
| 561 | + ) |
| 562 | + ); |
| 563 | + # }}} |
| 564 | + $this->initNavTpl(); |
| 565 | + $this->initAjaxLinkTpl(); |
| 566 | + $this->initSortkeyTpl(); |
| 567 | + # create list of categories |
| 568 | + $catlist = array( |
| 569 | + array( '__tag'=>'noscript', 'class'=>'cb_noscript', 0=>wfMsg( 'cb_requires_javascript' ) ), |
| 570 | + ); |
| 571 | + # previous page AJAX link |
| 572 | + $this->nav_link = ''; |
| 573 | + $prev_link = ' '; // |
| 574 | + $link_obj = $pager->getPrevAjaxLink(); |
| 575 | + if ( $pager->offset != 0 ) { |
| 576 | + $this->ajax_onclick = $link_obj->call; |
| 577 | + $prev_offset = $pager->getPrevOffset() + 1; |
| 578 | + $this->ajax_link_text = wfMsg( 'cb_previous_items_link' ); |
| 579 | + $this->ajax_link_comment = wfMsg( 'cb_previous_items_stats', $prev_offset, $prev_offset + $pager->limit - 1 ); |
| 580 | + $this->nav_link = CB_XML::toText( $this->ajax_link_tpl ); |
| 581 | + $prev_link = CB_XML::toText( $this->nav_link_tpl); |
| 582 | + } |
| 583 | + if ( $link_obj->placeholders || $this->nav_link != '' ) { |
| 584 | + $catlist[] = $prev_link; |
| 585 | + } |
| 586 | + # generate entries list |
| 587 | + foreach ( $pager->entries as &$cat ) { |
| 588 | + // cat_title might be NULL sometimes - probably due to DB corruption? |
| 589 | + if ( ( $cat_title_str = $cat->cat_title ) == NULL ) { |
| 590 | + // weird, but occasionally may happen; |
| 591 | + if ( empty( $cat->cl_sortkey ) ) { |
| 592 | + continue; |
| 593 | + } |
| 594 | + $cat_title_str = $cat->cl_sortkey; |
| 595 | + $cat_title_obj = Title::newFromText( $cat_title_str, NS_CATEGORY ); |
| 596 | + } else { |
| 597 | + $cat_title_obj = Title::makeTitle( NS_CATEGORY, $cat_title_str ); |
| 598 | + } |
| 599 | + $this->ajax_link_comment = ''; |
| 600 | + |
| 601 | + # calculate exact number of pages alone |
| 602 | + $cat->pages_only = intval( $cat->cat_pages ) - intval( $cat->cat_subcats ) - intval( $cat->cat_files ); |
| 603 | + # generate tree "expand" sign |
| 604 | + if ( $cat->cat_subcats === NULL ) { |
| 605 | + $cat_expand_sign = 'x'; |
| 606 | + $subcat_count_hint = ''; |
| 607 | + } elseif ( $cat->cat_subcats > 0 ) { |
| 608 | + $this->ajax_onclick = 'return CategoryBrowser.subCatsPlus(this,' . $cat->cat_id . ')'; |
| 609 | + $this->ajax_link_text = '+'; |
| 610 | + $cat_expand_sign = CB_XML::toText( $this->ajax_link_tpl ); |
| 611 | + $subcat_count_hint = wfMsgExt( 'cb_has_subcategories', array( 'parsemag' ), $cat->cat_subcats ); |
| 612 | + } else { |
| 613 | + $cat_expand_sign = ' '; // |
| 614 | + $subcat_count_hint = ''; |
| 615 | + } |
| 616 | + |
| 617 | + # create AJAX links for viewing categories, pages, files, belonging to this category |
| 618 | + $ajax_links = ''; |
| 619 | + if ( !empty( $cat->cat_id ) ) { |
| 620 | + $this->ajax_onclick = 'return CategoryBrowser.subCatsLink(this,' . $cat->cat_id . ')'; |
| 621 | + $this->ajax_link_text = wfMsgExt( 'cb_has_subcategories', array( 'parsemag' ), $cat->cat_subcats ); |
| 622 | + $cat_subcats = ( ($cat->cat_subcats > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
| 623 | + |
| 624 | + $this->ajax_onclick = 'return CategoryBrowser.pagesLink(this,' . $cat->cat_id . ')'; |
| 625 | + $this->ajax_link_text = wfMsgExt( 'cb_has_pages', array( 'parsemag' ), $cat->pages_only ); |
| 626 | + $cat_pages = ( ( $cat->pages_only > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
| 627 | + |
| 628 | + $this->ajax_onclick = 'return CategoryBrowser.filesLink(this,' . $cat->cat_id . ')'; |
| 629 | + $this->ajax_link_text = wfMsgExt( 'cb_has_files', array( 'parsemag' ), $cat->cat_files ); |
| 630 | + $cat_files = ( ( $cat->cat_files > 0 ) ? ' | ' . CB_XML::toText( $this->ajax_link_tpl ) : '' ); |
| 631 | + $ajax_links .= $cat_subcats . $cat_pages . $cat_files; |
| 632 | + } |
| 633 | + $cat_link = CB_Setup::$skin->link( $cat_title_obj, $cat_title_obj->getText() ); |
| 634 | + # show the sortkey, when it does not match title name |
| 635 | + # note that cl_sortkey is empty for CB_RootCond pager |
| 636 | + $this->sortkey_hint = ''; |
| 637 | + if ( !empty( $cat->cl_sortkey ) && |
| 638 | + $cat_title_obj->getText() != $cat->cl_sortkey ) { |
| 639 | + $this->sortkey_hint = '(' . CategoryViewer::getSubcategorySortChar( $cat_title_obj, $cat->cl_sortkey ) . ')'; |
| 640 | + $cat_link .= CB_XML::toText( $this->sortkey_hint_tpl ); |
| 641 | + } |
| 642 | + $cat_link .= $ajax_links; |
| 643 | + # finally add generated $cat_tpl/$cat_link to $catlist |
| 644 | + $catlist[] = CB_XML::toText( $cat_tpl ); |
| 645 | + } |
| 646 | + # next page AJAX link |
| 647 | + $this->nav_link = ''; |
| 648 | + $next_link = ' '; // |
| 649 | + $link_obj = $pager->getNextAjaxLink(); |
| 650 | + if ( $pager->hasMoreEntries ) { |
| 651 | + $this->ajax_onclick = $link_obj->call; |
| 652 | + $this->ajax_link_text = wfMsg( 'cb_next_items_link' ); |
| 653 | + $this->ajax_link_comment = wfMsg( 'cb_next_items_stats', $pager->getNextOffset() + 1 ); |
| 654 | + $this->nav_link = CB_XML::toText( $this->ajax_link_tpl ); |
| 655 | + $next_link = CB_XML::toText( $this->nav_link_tpl); |
| 656 | + } |
| 657 | + if ( $link_obj->placeholders || $this->nav_link != '' ) { |
| 658 | + $catlist[] = $next_link; |
| 659 | + } |
| 660 | + return $catlist; |
| 661 | + } |
| 662 | + |
| 663 | + function generatePagesList( CB_SubPager $pager ) { |
| 664 | + if ( $pager->offset == -1 ) { |
| 665 | + return ''; // list has no entries |
| 666 | + } |
| 667 | + # {{{ one page container template |
| 668 | + $page_link = ''; |
| 669 | + $page_tpl = |
| 670 | + array( '__tag'=>'div', 'class'=>'cb_cat_container', '__end'=>"\n", |
| 671 | + array( '__tag'=>'div', 'class'=>'cb_cat_item', 0=>&$page_link ) |
| 672 | + ); |
| 673 | + # }}} |
| 674 | + $this->initNavTpl(); |
| 675 | + $this->initAjaxLinkTpl(); |
| 676 | + $this->initSortkeyTpl(); |
| 677 | + # create list of pages |
| 678 | + $pagelist = array(); |
| 679 | + # previous page AJAX link |
| 680 | + $this->nav_link = ''; |
| 681 | + $prev_link = ' '; // |
| 682 | + $link_obj = $pager->getPrevAjaxLink(); |
| 683 | + if ( $pager->offset != 0 ) { |
| 684 | + $this->ajax_onclick = $link_obj->call; |
| 685 | + $prev_offset = $pager->getPrevOffset() + 1; |
| 686 | + $this->ajax_link_text = wfMsg( 'cb_previous_items_link' ); |
| 687 | + $this->ajax_link_comment = wfMsg( 'cb_previous_items_stats', $prev_offset, $prev_offset + $pager->limit - 1 ); |
| 688 | + $this->nav_link = CB_XML::toText( $this->ajax_link_tpl ); |
| 689 | + $prev_link = CB_XML::toText( $this->nav_link_tpl); |
| 690 | + } |
| 691 | + if ( $link_obj->placeholders || $this->nav_link != '' ) { |
| 692 | + $pagelist[] = $prev_link; |
| 693 | + } |
| 694 | + foreach ( $pager->entries as &$page ) { |
| 695 | + $page_title = Title::makeTitle( $page->page_namespace, $page->page_title ); |
| 696 | + $page_link = CB_Setup::$skin->link( $page_title, $page_title->getPrefixedText() ); |
| 697 | + # show the sortkey, when it does not match title name |
| 698 | + # note that cl_sortkey is empty for CB_RootCond pager |
| 699 | + $this->sortkey_hint = ''; |
| 700 | + if ( !empty( $page->cl_sortkey ) && |
| 701 | + $page_title->getText() != $page->cl_sortkey ) { |
| 702 | + $this->sortkey_hint = '(' . CategoryViewer::getSubcategorySortChar( $page_title, $page->cl_sortkey ) . ')'; |
| 703 | + $page_link .= CB_XML::toText( $this->sortkey_hint_tpl ); |
| 704 | + } |
| 705 | + $pagelist[] = CB_XML::toText( $page_tpl ); |
| 706 | + } |
| 707 | + # next page AJAX link |
| 708 | + $this->nav_link = ''; |
| 709 | + $next_link = ' '; // |
| 710 | + $link_obj = $pager->getNextAjaxLink(); |
| 711 | + if ( $pager->hasMoreEntries ) { |
| 712 | + $this->ajax_onclick = $link_obj->call; |
| 713 | + $this->ajax_link_text = wfMsg( 'cb_next_items_link' ); |
| 714 | + $this->ajax_link_comment = wfMsg( 'cb_next_items_stats', $pager->getNextOffset() + 1 ); |
| 715 | + $this->nav_link = CB_XML::toText( $this->ajax_link_tpl ); |
| 716 | + $next_link = CB_XML::toText( $this->nav_link_tpl); |
| 717 | + } |
| 718 | + if ( $link_obj->placeholders || $this->nav_link != '' ) { |
| 719 | + $pagelist[] = $next_link; |
| 720 | + } |
| 721 | + return $pagelist; |
| 722 | + } |
| 723 | + |
| 724 | + function generateFilesList( CB_SubPager $pager ) { |
| 725 | + global $wgOut, $wgCategoryMagicGallery; |
| 726 | + // unstub $wgOut, otherwise $wgOut->mNoGallery may be unavailable |
| 727 | + // strange, but calling wfDebug() instead does not unstub successfully |
| 728 | + $wgOut->getHeadItems(); |
| 729 | + if ( $pager->offset == -1 ) { |
| 730 | + return ''; // list has no entries |
| 731 | + } |
| 732 | + # respect extension & core settings |
| 733 | + if ( CB_Setup::$imageGalleryPerRow < 1 || !$wgCategoryMagicGallery || $wgOut->mNoGallery ) { |
| 734 | + return $this->generatePagesList( $pager ); |
| 735 | + } |
| 736 | + $this->initNavTpl(); |
| 737 | + $this->initAjaxLinkTpl(); |
| 738 | + $this->initSortkeyTpl(); |
| 739 | + # {{{ gallery container template |
| 740 | + $gallery_html = ''; |
| 741 | + $gallery_tpl = array( '__tag'=>'div', 'class'=>'cb_files_container', 0=>&$gallery_html ); |
| 742 | + # }}} |
| 743 | + # create list of files (holder of prev/next AJAX links and generated image gallery) |
| 744 | + $filelist = array(); |
| 745 | + # create image gallery |
| 746 | + $gallery = new ImageGallery(); |
| 747 | + $gallery->setHideBadImages(); |
| 748 | + $gallery->setPerRow( CB_Setup::$imageGalleryPerRow ); |
| 749 | + # previous page AJAX link |
| 750 | + $prev_link = ' '; // |
| 751 | + $this->nav_link = ''; |
| 752 | + $link_obj = $pager->getPrevAjaxLink(); |
| 753 | + if ( $pager->offset != 0 ) { |
| 754 | + $this->ajax_onclick = $link_obj->call; |
| 755 | + $prev_offset = $pager->getPrevOffset() + 1; |
| 756 | + $this->ajax_link_text = wfMsg( 'cb_previous_items_link' ); |
| 757 | + $this->ajax_link_comment = wfMsg( 'cb_previous_items_stats', $prev_offset, $prev_offset + $pager->limit - 1 ); |
| 758 | + $this->nav_link = CB_XML::toText( $this->ajax_link_tpl ); |
| 759 | + } |
| 760 | + if ( $link_obj->placeholders || $this->nav_link != '' ) { |
| 761 | + $prev_link = CB_XML::toText( $this->nav_link_tpl); |
| 762 | + } |
| 763 | + foreach ( $pager->entries as &$file ) { |
| 764 | + $file_title = Title::makeTitle( $file->page_namespace, $file->page_title ); |
| 765 | + # show the sortkey, when it does not match title name |
| 766 | + # note that cl_sortkey is empty for CB_RootCond pager |
| 767 | + $this->sortkey_hint = ''; |
| 768 | + if ( !empty( $file->cl_sortkey ) && |
| 769 | + $file_title->getText() != $file->cl_sortkey ) { |
| 770 | + $this->sortkey_hint = '(' . CategoryViewer::getSubcategorySortChar( $file_title, $file->cl_sortkey ) . ')'; |
| 771 | + } |
| 772 | + $gallery->add( $file_title, ($this->sortkey_hint != '') ? CB_XML::toText( $this->sortkey_hint_tpl ) : '' ); |
| 773 | + } |
| 774 | + # next page AJAX link |
| 775 | + $next_link = ' '; // |
| 776 | + $this->nav_link = ''; |
| 777 | + $link_obj = $pager->getNextAjaxLink(); |
| 778 | + if ( $pager->hasMoreEntries ) { |
| 779 | + $this->ajax_onclick = $link_obj->call; |
| 780 | + $this->ajax_link_text = wfMsg( 'cb_next_items_link' ); |
| 781 | + $this->ajax_link_comment = wfMsg( 'cb_next_items_stats', $pager->getNextOffset() + 1 ); |
| 782 | + $this->nav_link = CB_XML::toText( $this->ajax_link_tpl ); |
| 783 | + } |
| 784 | + if ( $link_obj->placeholders || $this->nav_link != '' ) { |
| 785 | + $next_link = CB_XML::toText( $this->nav_link_tpl); |
| 786 | + } |
| 787 | + $filelist = $prev_link; |
| 788 | + if ( !$gallery->isEmpty() ) { |
| 789 | + $gallery_html = $gallery->toHTML(); |
| 790 | + $filelist .= CB_XML::toText( $gallery_tpl ); |
| 791 | + } |
| 792 | + $filelist .= $next_link; |
| 793 | + return $filelist; |
| 794 | + } |
| 795 | + |
| 796 | + /* |
| 797 | + * called via AJAX to get root list for specitied offset, limit |
| 798 | + * where condition will be read from the cookie previousely set |
| 799 | + * @param $args[0] : encoded reverse polish queue |
| 800 | + * @param $args[1] : category name filter string |
| 801 | + * @param $args[2] : category name filter case insensitive flag |
| 802 | + * @param $args[3] : offset (optional) |
| 803 | + * @param $args[4] : limit (optional) |
| 804 | + */ |
| 805 | + static function getRootOffsetHtml() { |
| 806 | + wfLoadExtensionMessages( 'CategoryBrowser' ); |
| 807 | + $args = func_get_args(); |
| 808 | + $limit = ( count( $args ) > 4 ) ? abs( intval( $args[4] ) ) : CB_PAGING_ROWS; |
| 809 | + $offset = ( count( $args ) > 3 ) ? abs( intval( $args[3] ) ) : 0; |
| 810 | + $nameFilterCI = ( count( $args ) > 2 ) ? $args[2] == 'true' : false; |
| 811 | + $nameFilter = ( count( $args ) > 1 ) ? $args[1] : ''; |
| 812 | + $encPolishQueue = ( count( $args ) > 0 ) ? $args[0] : 'all'; |
| 813 | + $cb = new CategoryBrowser(); |
| 814 | + $sqlCond = CB_SqlCond::newFromEncodedPolishQueue( $encPolishQueue ); |
| 815 | + $rootPager = CB_RootPager::newFromSqlCond( $sqlCond, $offset, $limit ); |
| 816 | + $rootPager->setNameFilter( $nameFilter, $nameFilterCI ); |
| 817 | + $rootPager->getCurrentRows(); |
| 818 | + $catlist = $cb->generateCatList( $rootPager ); |
| 819 | + return CB_XML::toText( $catlist ); |
| 820 | + } |
| 821 | + |
| 822 | + /* |
| 823 | + * called via AJAX to get list of (subcategories,pages,files) for specitied parent category id, offset, limit |
| 824 | + * @param $args[0] : type of pager ('subcats','pages','files') |
| 825 | + * @param $args[1] : parent category id |
| 826 | + * @param $args[2] : offset (optional) |
| 827 | + * @param $args[3] : limit (optional) |
| 828 | + */ |
| 829 | + static function getSubOffsetHtml() { |
| 830 | + $pager_types = array( |
| 831 | + 'subcats' => array( |
| 832 | + 'js_nav_func' => "subCatsNav", |
| 833 | + 'select_fields' => "cl_sortkey, cat_id, cat_title, cat_subcats, cat_pages, cat_files", |
| 834 | + 'ns_cond' => "page_namespace = " . NS_CATEGORY |
| 835 | + ), |
| 836 | + 'pages' => array( |
| 837 | + 'js_nav_func' => "pagesNav", |
| 838 | + 'select_fields' => "page_title, page_namespace, page_len, page_is_redirect", |
| 839 | + 'ns_cond' => "NOT page_namespace IN (" . NS_FILE . "," . NS_CATEGORY . ")" |
| 840 | + ), |
| 841 | + 'files' => array( |
| 842 | + 'js_nav_func' => "filesNav", |
| 843 | + 'select_fields' => "page_title, page_namespace, page_len, page_is_redirect", |
| 844 | + 'ns_cond' => "page_namespace = " . NS_FILE |
| 845 | + ) |
| 846 | + ); |
| 847 | + wfLoadExtensionMessages( 'CategoryBrowser' ); |
| 848 | + $args = func_get_args(); |
| 849 | + if ( count( $args ) < 2 ) { |
| 850 | + return 'Too few parameters in ' . __METHOD__; |
| 851 | + } |
| 852 | + if ( !isset( $pager_types[ $args[0] ] ) ) { |
| 853 | + return 'Unknown pager type in ' . __METHOD__; |
| 854 | + } |
| 855 | + $pager_type = & $pager_types[ $args[0] ]; |
| 856 | + $limit = ( count( $args ) > 3 ) ? abs( intval( $args[3] ) ) : CB_PAGING_ROWS; |
| 857 | + $offset = ( count( $args ) > 2 ) ? abs( intval( $args[2] ) ) : 0; |
| 858 | + $parentCatId = abs( intval( $args[1] ) ); |
| 859 | + $cb = new CategoryBrowser(); |
| 860 | + $pager = new CB_SubPager( $parentCatId, $offset, $limit, |
| 861 | + $pager_type[ 'js_nav_func' ], |
| 862 | + $pager_type[ 'select_fields' ], |
| 863 | + $pager_type[ 'ns_cond' ] ); |
| 864 | + $pager->getCurrentRows(); |
| 865 | + switch ( $pager->getListType() ) { |
| 866 | + case 'generateCatList' : |
| 867 | + $list = $cb->generateCatList( $pager ); |
| 868 | + break; |
| 869 | + case 'generatePagesList' : |
| 870 | + $list = $cb->generatePagesList( $pager ); |
| 871 | + break; |
| 872 | + case 'generateFilesList' : |
| 873 | + $list = $cb->generateFilesList( $pager ); |
| 874 | + break; |
| 875 | + default : |
| 876 | + return 'Unknown list type in ' . __METHOD__; |
| 877 | + } |
| 878 | + return CB_XML::toText( $list ); |
| 879 | + } |
| 880 | + |
| 881 | + /* |
| 882 | + * called via AJAX to setup custom edited expression cookie then display category root offset |
| 883 | + * @param $args[0] : encoded infix expression |
| 884 | + * @param $args[1] : category name filter string |
| 885 | + * @param $args[2] : category name filter case insensitive flag |
| 886 | + * @param $args[3] : 1 - cookie has to be set, 0 - cookie should not be set (expression is pre-defined or already was stored) |
| 887 | + * @param $args[4] : pager limit (optional) |
| 888 | + */ |
| 889 | + static function applyEncodedQueue() { |
| 890 | + CB_Setup::initUser(); |
| 891 | + $args = func_get_args(); |
| 892 | + $limit = ( (count( $args ) > 4) ? intval( $args[4] ) : CB_PAGING_ROWS ); |
| 893 | + $setCookie = ( (count( $args ) > 3) ? $args[3] != 0 : false ); |
| 894 | + $nameFilterCI = ( count( $args ) > 2 ) ? $args[2] == 'true' : false; |
| 895 | + $nameFilter = ( count( $args ) > 1 ) ? $args[1] : ''; |
| 896 | + $encInfixQueue = ( (count( $args ) > 0) ? $args[0] : 'all' ); |
| 897 | + $sqlCond = CB_SqlCond::newFromEncodedInfixQueue( $encInfixQueue ); |
| 898 | + $encPolishQueue = $sqlCond->getEncodedQueue( false ); |
| 899 | + if ( $setCookie ) { |
| 900 | + CB_Setup::setCookie( 'rootcond', $encPolishQueue, time()+60*60*24*7 ); |
| 901 | + } |
| 902 | + return self::getRootOffsetHtml( $encPolishQueue, $nameFilter, $nameFilterCI, 0, $limit ); |
| 903 | + } |
| 904 | + |
| 905 | + /* |
| 906 | + * called via AJAX to generate new selected option when the selected rootcond is new (the rootcond cookie was set) |
| 907 | + * @param $args[0] currently selected expression in encoded infix format |
| 908 | + */ |
| 909 | + static function generateSelectedOption() { |
| 910 | + wfLoadExtensionMessages( 'CategoryBrowser' ); |
| 911 | + CB_Setup::initUser(); |
| 912 | + $args = func_get_args(); |
| 913 | + if ( count( $args ) < 1 ) { |
| 914 | + throw new MWException( 'Argument 0 is missing in ' . __METHOD__ ); |
| 915 | + } |
| 916 | + $encInfixQueue = $args[0]; |
| 917 | + $sqlCond = CB_SqlCond::newFromEncodedInfixQueue( $encInfixQueue ); |
| 918 | + $ranges = array(); |
| 919 | + self::addRange( $ranges, $sqlCond ); |
| 920 | + # generate div instead of option to avoid innerHTML glitches in IE |
| 921 | + return self::generateOption( $ranges[0], $sqlCond->getEncodedQueue( false ), 'div' ); |
| 922 | + } |
| 923 | + |
| 924 | +} /* end of CategoryBrowser class */ |
Property changes on: trunk/extensions/CategoryBrowser/CategoryBrowserMain.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 925 | + native |
Index: trunk/extensions/CategoryBrowser/README |
— | — | @@ -0,0 +1,12 @@ |
| 2 | +MediaWiki extension CategoryBrowser, version 0.2.0 |
| 3 | + |
| 4 | +CategoryBrowser is an AJAX-enabled category filter and browser for MediaWiki. |
| 5 | +Categories can be filtered with pre-defined and also with used-defined conditions. |
| 6 | +User-defined conditions can be edited by using Javascript-based interface. |
| 7 | +Currently applied used-defined condition is stored in user's cookie for future use. |
| 8 | +Categories also can be filtered by first letters in their names. |
| 9 | +AJAX-browser implements recursive browsing for categories, but also shows pages |
| 10 | +and files as well. Categories, pages and files are browsed with limit count, to |
| 11 | +reduce the resource consumption. |
| 12 | + |
| 13 | +See http://www.mediawiki.org/wiki/Extension:CategoryBrowser for further details. |
\ No newline at end of file |