r71652 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r71651‎ | r71652 | r71653 >
Date:19:17, 25 August 2010
Author:questpc
Status:deferred
Tags:
Comment:
Initial commit of CategoryBrowser extension
Modified paths:
  • /trunk/extensions/CategoryBrowser (added) (history)
  • /trunk/extensions/CategoryBrowser/COPYING (added) (history)
  • /trunk/extensions/CategoryBrowser/CategoryBrowser.php (added) (history)
  • /trunk/extensions/CategoryBrowser/CategoryBrowserBasic.php (added) (history)
  • /trunk/extensions/CategoryBrowser/CategoryBrowserMain.php (added) (history)
  • /trunk/extensions/CategoryBrowser/CategoryBrowserPage.php (added) (history)
  • /trunk/extensions/CategoryBrowser/CategoryBrowser_i18n.php (added) (history)
  • /trunk/extensions/CategoryBrowser/INSTALL (added) (history)
  • /trunk/extensions/CategoryBrowser/README (added) (history)
  • /trunk/extensions/CategoryBrowser/category_browser.css (added) (history)
  • /trunk/extensions/CategoryBrowser/category_browser.js (added) (history)
  • /trunk/extensions/CategoryBrowser/category_browser_rtl.css (added) (history)

Diff [purge]

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
1154 + 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
1119 + 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
1183 + 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
1194 + 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
1195 + 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
1676 + 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
11667 + 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 = '&#160;'; // &nbsp;
 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 = '&#160;'; // &nbsp;
 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 = '&#160;'; // &nbsp;
 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 = '&#160;'; // &nbsp;
 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 = '&#160;'; // &nbsp;
 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 = '&#160;'; // &nbsp;
 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 = '&#160;'; // &nbsp;
 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
1925 + 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

Follow-up revisions

RevisionCommit summaryAuthorDate
r71659Post-import fixes and todos (r71652).siebrand20:19, 25 August 2010

Status & tagging log