Restructuring and revamping; more messages, more options, better config, more flexible ajax interface. Should be fully backwards compatible. It will hopefully even work for people with old JS code cached.

This commit is contained in:
Daniel Kinzler 2008-06-28 20:13:20 +00:00
parent 0bffdb6902
commit dbfd3e4833
8 changed files with 356 additions and 276 deletions

View file

@ -14,11 +14,23 @@ class CategoryTreeCategoryPage extends CategoryPage {
class CategoryTreeCategoryViewer extends CategoryViewer {
var $child_titles;
function getCategoryTree() {
global $wgOut, $wgCategoryTreeCategoryPageOptions;
if ( ! isset($this->categorytree) ) {
CategoryTree::setHeaders( $wgOut );
$this->categorytree = new CategoryTree( $wgCategoryTreeCategoryPageOptions );
}
return $this->categorytree;
}
/**
* Add a subcategory to the internal lists
*/
function addSubcategory( $title, $sortkey, $pageLength ) {
global $wgContLang, $wgOut, $wgRequest, $wgCategoryTreeCategoryPageMode;
global $wgContLang, $wgOut, $wgRequest;
if ( $wgRequest->getCheck( 'notree' ) ) {
return parent::addSubcategory( $title, $sortkey, $pageLength );
@ -29,18 +41,15 @@ class CategoryTreeCategoryViewer extends CategoryViewer {
return parent::addSubcategory( $title, $sortkey, $pageLength );
}
if ( ! isset($this->categorytree) ) {
CategoryTree::setHeaders( $wgOut );
$this->categorytree = new CategoryTree;
}
$tree = $this->getCategoryTree();
$this->children[] = $this->categorytree->renderNode( $title, $wgCategoryTreeCategoryPageMode );
$this->children[] = $tree->renderNode( $title );
$this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey );
}
function getSubcategorySection() {
global $wgOut, $wgRequest, $wgCookiePrefix, $wgCategoryTreeCategoryPageMode;
global $wgOut, $wgRequest, $wgCookiePrefix;
if ( $wgRequest->getCheck( 'notree' ) ) {
return parent::getSubcategorySection();
@ -98,11 +107,10 @@ class CategoryTreeCategoryViewer extends CategoryViewer {
if ( $showAs == 'list' ) {
$r .= $this->formatList( $this->children, $this->children_start_char );
} else {
CategoryTree::setHeaders( $wgOut );
$ct = new CategoryTree;
$ct = getCategoryTree();
foreach ( $this->child_titles as $title ) {
$r .= $ct->renderNode( $title, $wgCategoryTreeCategoryPageMode );
$r .= $ct->renderNode( $title );
}
}
return $r;

View file

@ -16,11 +16,13 @@
margin-left: 1.5ex;
}
.CategoryTreeBullet a:link {
.CategoryTreeBullet a,
.CategoryTreeBullet a:link,
.CategoryTreeBullet a:active,
.CategoryTreeBullet a:visited {
text-decoration: none;
color: inherit;
font-weight: bold;
speak: none;
speak: none;
}
.CategoryTreeLabelPage {

View file

@ -17,7 +17,7 @@ $messages['en'] = array(
'categorytree' => 'CategoryTree',
'categorytree-tab' => 'Tree',
'categorytree-legend' => 'Show category tree',
'categorytree-desc' => 'AJAX based gadget to display the [[Special:CategoryTree|category structure]] of a wiki',
'categorytree-desc' => 'Dynamically navigate the [[Special:CategoryTree|category structure]]',
'categorytree-header' => 'Enter a category name to see its contents as a tree structure.
Note that this requires advanced JavaScript functionality known as AJAX.
If you have a very old browser, or have JavaScript disabled, it will not work.',
@ -32,6 +32,10 @@ If you have a very old browser, or have JavaScript disabled, it will not work.',
'categorytree-collapse' => 'collapse',
'categorytree-expand' => 'expand',
'categorytree-collapse-bullet' => '[<b>&#x2212;</b>]',
'categorytree-expand-bullet' => '[<b>+</b>]',
'categorytree-page-bullet' => '&nbsp;',
'categorytree-load' => 'load',
'categorytree-loading' => 'loading…',
'categorytree-nothing-found' => 'nothing found',
@ -586,7 +590,7 @@ $messages['de'] = array(
'categorytree' => 'Kategorienbaum',
'categorytree-tab' => 'Baum',
'categorytree-legend' => 'Zeige Kategorienbaum',
'categorytree-desc' => 'Ajax-basiertes Gadget, um die [[Special:CategoryTree|Kategorien-Struktur]] eines Wikis anzuzeigen',
'categorytree-desc' => 'Dynamische Navigation für die [[Special:CategoryTree|Kategorien-Struktur]]',
'categorytree-header' => 'Zeigt für die angegebene Kategorie die Unterkategorien in einer Baumstruktur.
Diese Seite benötigt bestimmte JavaScript-Funktionen (Ajax) und funktioniert möglicherweise nicht, wenn JavaScript ausgeschaltet ist oder ein sehr alter Browser verwendet wird.',
'categorytree-category' => 'Kategorie:',

View file

@ -26,48 +26,88 @@ var categoryTreeRetryMsg = "Please wait a moment and try again.";
return n;
}
function categoryTreeExpandNode(cat, mode, lnk) {
function categoryTreeExpandNode(cat, options, lnk) {
var div= categoryTreeNextDiv( lnk.parentNode.parentNode );
div.style.display= 'block';
lnk.innerHTML= '&ndash;';
lnk.innerHTML= categoryTreeCollapseBulletMsg;
lnk.title= categoryTreeCollapseMsg;
lnk.onclick= function() { categoryTreeCollapseNode(cat, mode, lnk) }
lnk.onclick= function() { categoryTreeCollapseNode(cat, options, lnk) }
if (lnk.className != "CategoryTreeLoaded") {
categoryTreeLoadNode(cat, mode, lnk, div);
categoryTreeLoadNode(cat, options, lnk, div);
}
}
function categoryTreeCollapseNode(cat, mode, lnk) {
function categoryTreeCollapseNode(cat, options, lnk) {
var div= categoryTreeNextDiv( lnk.parentNode.parentNode );
div.style.display= 'none';
lnk.innerHTML= '+';
lnk.innerHTML= categoryTreeExpandBulletMsg;
lnk.title= categoryTreeExpandMsg;
lnk.onclick= function() { categoryTreeExpandNode(cat, mode, lnk) }
lnk.onclick= function() { categoryTreeExpandNode(cat, options, lnk) }
}
function categoryTreeLoadNode(cat, mode, lnk, div) {
function categoryTreeLoadNode(cat, options, lnk, div) {
div.style.display= 'block';
lnk.className= 'CategoryTreeLoaded';
lnk.innerHTML= '&ndash;';
lnk.innerHTML= categoryTreeCollapseBulletMsg;
lnk.title= categoryTreeCollapseMsg;
lnk.onclick= function() { categoryTreeCollapseNode(cat, mode, lnk) }
lnk.onclick= function() { categoryTreeCollapseNode(cat, options, lnk) }
categoryTreeLoadChildren(cat, mode, div)
categoryTreeLoadChildren(cat, options, div)
}
function categoryTreeLoadChildren(cat, mode, div) {
function categoryTreeEncodeOptions(options) {
var opt = "";
for (k in options) {
v = options[k];
switch (typeof v) {
case 'string':
v = '"' + v.replace(/([\\"'])/g, "\\$1") + '"';
break;
case 'number':
case 'boolean':
case 'null':
v = String(v);
break;
case 'object':
if ( !v ) v = 'null';
else throw new Error("categoryTreeLoadChildren can not encode complex types");
break;
default:
throw new Error("categoryTreeLoadChildren encountered strange variable type " + (typeof v));
}
if ( opt != "" ) opt += ", ";
opt += k;
opt += ":";
opt += v;
}
opt = "{"+opt+"}";
return opt;
}
function categoryTreeLoadChildren(cat, options, div) {
div.innerHTML= '<i class="CategoryTreeNotice">' + categoryTreeLoadingMsg + '</i>';
if ( typeof options == "string" ) { //hack for backward compatibility
options = { mode : options };
}
function f( request ) {
if (request.status != 200) {
div.innerHTML = '<i class="CategoryTreeNotice">' + categoryTreeErrorMsg + ' </i>';
var retryLink = document.createElement('a');
retryLink.innerHTML = categoryTreeRetryMsg;
retryLink.onclick = function() {
categoryTreeLoadChildren(cat, mode, div);
categoryTreeLoadChildren(cat, options, div, enc);
}
div.appendChild(retryLink);
return;
@ -79,8 +119,8 @@ var categoryTreeRetryMsg = "Please wait a moment and try again.";
if ( result == '' ) {
result= '<i class="CategoryTreeNotice">';
if ( mode == 0 ) result= categoryTreeNoSubcategoriesMsg;
else if ( mode == 10 ) result= categoryTreeNoPagesMsg;
if ( options.mode == 0 ) result= categoryTreeNoSubcategoriesMsg;
else if ( options.mode == 10 ) result= categoryTreeNoPagesMsg;
else result= categoryTreeNothingFoundMsg;
result+= '</i>';
@ -90,5 +130,6 @@ var categoryTreeRetryMsg = "Please wait a moment and try again.";
div.innerHTML= result;
}
sajax_do_call( "efCategoryTreeAjaxWrapper", [cat, mode] , f );
var opt = categoryTreeEncodeOptions(options);
sajax_do_call( "efCategoryTreeAjaxWrapper", [cat, opt, 'json'] , f );
}

View file

@ -6,7 +6,7 @@
*
* @addtogroup Extensions
* @author Daniel Kinzler, brightbyte.de
* @copyright © 2006-2007 Daniel Kinzler
* @copyright © 2006-2008 Daniel Kinzler and others
* @license GNU General Public Licence 2.0 or later
*/
@ -46,12 +46,21 @@ $wgCategoryTreeDisableCache = true;
$wgCategoryTreeDynamicTag = false;
$wgCategoryTreeHTTPCache = false;
$wgCategoryTreeUnifiedView = true;
$wgCategoryTreeOmitNamespace = false;
$wgCategoryTreeMaxDepth = array(CT_MODE_PAGES => 1, CT_MODE_ALL => 1, CT_MODE_CATEGORIES => 2);
$wgCategoryTreeExtPath = '/extensions/CategoryTree';
$wgCategoryTreeVersion = '2'; #NOTE: bump this when you change the CSS or JS files!
$wgCategoryTreeOmitNamespace = false;
$wgCategoryTreeDefaultMode = CT_MODE_CATEGORIES;
$wgCategoryTreeDefaultOptions = array(); #Default values for most options. ADD NEW OPTIONS HERE!
$wgCategoryTreeDefaultOptions['mode'] = NULL; # will be set to $wgCategoryTreeDefaultMode in efCategoryTree(); compatibility quirk
$wgCategoryTreeDefaultOptions['hideprefix'] = NULL; # will be set to $wgCategoryTreeDefaultMode in efCategoryTree(); compatibility quirk
#TODO: hideprefix: always, never, catonly, catonly_if_onlycat
$wgCategoryTreeCategoryPageMode = CT_MODE_CATEGORIES;
$wgCategoryTreeVersion = '1'; #NOTE: bump this when you change the CSS or JS files!
$wgCategoryTreeCategoryPageOptions = array(); #Options to be used for category pages
$wgCategoryTreeCategoryPageOptions['mode'] = NULL; # will be set to $wgCategoryTreeDefaultMode in efCategoryTree(); compatibility quirk
/**
* Register extension setup hook and credits
@ -63,7 +72,7 @@ $wgExtensionCredits['specialpage'][] = array(
'svn-revision' => '$LastChangedRevision$',
'author' => 'Daniel Kinzler',
'url' => 'http://www.mediawiki.org/wiki/Extension:CategoryTree',
'description' => 'AJAX based gadget to display the category structure of a wiki',
'description' => 'Dynamically navigate the category structure',
'descriptionmsg' => 'categorytree-desc',
);
$wgExtensionCredits['parserhook'][] = array(
@ -72,7 +81,7 @@ $wgExtensionCredits['parserhook'][] = array(
'svn-revision' => '$LastChangedRevision$',
'author' => 'Daniel Kinzler',
'url' => 'http://www.mediawiki.org/wiki/Extension:CategoryTree',
'description' => 'AJAX based gadget to display the category structure of a wiki',
'description' => 'Dynamically navigate the category structure',
'descriptionmsg' => 'categorytree-desc',
);
@ -90,6 +99,8 @@ $wgSpecialPageGroups['CategoryTree'] = 'pages';
$wgHooks['OutputPageParserOutput'][] = 'efCategoryTreeParserOutput';
$wgHooks['ArticleFromTitle'][] = 'efCategoryTreeArticleFromTitle';
$wgHooks['LanguageGetMagic'][] = 'efCategoryTreeGetMagic';
/**
* register Ajax function
*/
@ -100,6 +111,8 @@ $wgAjaxExportList[] = 'efCategoryTreeAjaxWrapper';
*/
function efCategoryTree() {
global $wgUseAjax, $wgHooks;
global $wgCategoryTreeDefaultOptions, $wgCategoryTreeDefaultMode, $wgCategoryTreeOmitNamespace;
global $wgCategoryTreeCategoryPageOptions, $wgCategoryTreeCategoryPageMode;
# Abort if AJAX is not enabled
if ( !$wgUseAjax ) {
@ -113,6 +126,17 @@ function efCategoryTree() {
efCategoryTreeSetHooks();
}
if ( !isset( $wgCategoryTreeDefaultOptions['mode'] ) || is_null( $wgCategoryTreeDefaultOptions['mode'] ) ) {
$wgCategoryTreeDefaultOptions['mode'] = $wgCategoryTreeDefaultMode;
}
if ( !isset( $wgCategoryTreeDefaultOptions['hideprefix'] ) || is_null( $wgCategoryTreeDefaultOptions['hideprefix'] ) ) {
$wgCategoryTreeDefaultOptions['hideprefix'] = $wgCategoryTreeOmitNamespace;
}
if ( !isset( $wgCategoryTreeCategoryPageOptions['mode'] ) || is_null( $wgCategoryTreeCategoryPageOptions['mode'] ) ) {
$wgCategoryTreeCategoryPageOptions['mode'] = $wgCategoryTreeCategoryPageMode;
}
}
function efCategoryTreeSetHooks() {
@ -140,13 +164,23 @@ function efCategoryTreeGetMagic( &$magicWords, $langCode ) {
/**
* Entry point for Ajax, registered in $wgAjaxExportList.
* The $enc parameter determins how the $options is decoded into a PHP array.
* If $enc is not given, '' is asumed, which simulates the old call interface,
* namely, only providing the mode name or number.
* This loads CategoryTreeFunctions.php and calls CategoryTree::ajax()
*/
function efCategoryTreeAjaxWrapper( $category, $mode ) {
function efCategoryTreeAjaxWrapper( $category, $options, $enc = '' ) {
global $wgCategoryTreeHTTPCache, $wgSquidMaxAge, $wgUseSquid;
$ct = new CategoryTree;
$response = $ct->ajax( $category, $mode ); //FIXME: would need to pass on depth parameter here.
if ( is_string( $options ) ) {
$options = CategoryTree::decodeOptions( $options, $enc );
}
$depth = isset( $options['depth'] ) ? (int)$options['depth'] : 1;
$ct = new CategoryTree( $options, true );
$depth = efCategoryTreeCapDepth( $ct->getOption('mode'), $depth );
$response = $ct->ajax( $category, $depth );
if ( $wgCategoryTreeHTTPCache && $wgSquidMaxAge && $wgUseSquid ) {
$response->setCacheDuration( $wgSquidMaxAge );
@ -180,25 +214,6 @@ function efCategoryTreeCapDepth( $mode, $depth ) {
return min($depth, $max);
}
/**
* Helper function to convert a string to a boolean value.
* Perhaps make this a global function in MediaWiki proper
*/
function efCategoryTreeAsBool( $s ) {
if ( is_null( $s ) || is_bool( $s ) ) return $s;
$s = trim( strtolower( $s ) );
if ( $s === '1' || $s === 'yes' || $s === 'on' || $s === 'true' ) {
return true;
}
else if ( $s === '0' || $s === 'no' || $s === 'off' || $s === 'false' ) {
return false;
}
else {
return NULL;
}
}
/**
* Entry point for the {{#categorytree}} tag parser function.
* This is a wrapper around efCategoryTreeParserHook
@ -242,51 +257,21 @@ function efCategoryTreeParserHook( $cat, $argv, &$parser ) {
static $initialized = false;
$ct = new CategoryTree( $argv );
$divAttribs = Sanitizer::validateTagAttributes( $argv, 'div' );
$style = isset( $divAttribs['style'] ) ? $divAttribs['style'] : null;
$mode = isset( $argv[ 'mode' ] ) ? $argv[ 'mode' ] : null;
if ( $mode !== NULL ) {
$mode= trim( strtolower( $mode ) );
if ( $mode == 'all' ) $mode = CT_MODE_ALL;
else if ( $mode == 'pages' ) $mode = CT_MODE_PAGES;
else if ( $mode == 'categories' ) $mode = CT_MODE_CATEGORIES;
}
else {
$mode = $wgCategoryTreeDefaultMode;
}
$hideroot = isset( $argv[ 'hideroot' ] ) ? efCategoryTreeAsBool( $argv[ 'hideroot' ] ) : null;
$onlyroot = isset( $argv[ 'onlyroot' ] ) ? efCategoryTreeAsBool( $argv[ 'onlyroot' ] ) : null;
$hideroot = isset( $argv[ 'hideroot' ] ) ? CategoryTree::decodeBoolean( $argv[ 'hideroot' ] ) : null;
$onlyroot = isset( $argv[ 'onlyroot' ] ) ? CategoryTree::decodeBoolean( $argv[ 'onlyroot' ] ) : null;
$depthArg = isset( $argv[ 'depth' ] ) ? $argv[ 'depth' ] : null;
$depth = efCategoryTreeCapDepth($mode, $depthArg);
$depth = efCategoryTreeCapDepth( $ct->getOption( 'mode' ), $depthArg );
if ( $onlyroot ) $depth = 0;
$ct = new CategoryTree;
return $ct->getTag( $parser, $cat, $mode, $hideroot, $style, $depth );
return $ct->getTag( $parser, $cat, $hideroot, $style, $depth );
}
/**
* Hook callback that installs a tab for CategoryTree on Category pages
*/
/*
function efCategoryTreeInstallTabs( &$skin, &$content_actions ) {
global $wgTitle;
if ( $wgTitle->getNamespace() != NS_CATEGORY ) return true;
$special = Title::makeTitle( NS_SPECIAL, 'CategoryTree' );
$content_actions['categorytree'] = array(
'class' => false,
'text' => htmlspecialchars( CategoryTree::msg( 'tab' ) ),
'href' => $special->getLocalUrl() . '/' . $wgTitle->getPartialURL() );
return true;
}*/
/**
* Hook callback that injects messages and things into the <head> tag
* Does nothing if $parserOutput->mCategoryTreeTag is not set

View file

@ -17,6 +17,62 @@ if( !defined( 'MEDIAWIKI' ) ) {
class CategoryTree {
var $mIsAjaxRequest = false;
var $mOptions = array();
function __construct( $options, $ajax = false ) {
global $wgCategoryTreeDefaultOptions;
$this->mIsAjaxRequest = $ajax;
#ensure default values and order of options. Order may become important, it may influence the cache key!
foreach ( $wgCategoryTreeDefaultOptions as $option => $default ) {
if ( isset( $options[$option] ) && !is_null( $options[$option] ) ) $this->mOptions[$option] = $options[$option];
else $this->mOptions[$option] = $default;
}
$this->mOptions['mode'] = self::decodeMode( $this->mOptions['mode'] );
$this->mOptions['hideprefix'] = self::decodeBoolean( $this->mOptions['hideprefix'] );
}
function getOption( $name ) {
return $this->mOptions[$name];
}
static function decodeMode( $mode ) {
global $wgCategoryTreeDefaultOptions;
if ( is_null( $mode ) ) return $wgCategoryTreeDefaultOptions['mode'];
if ( is_int( $mode ) ) return $mode;
$mode = trim( strtolower( $mode ) );
if ( is_numeric( $mode ) ) return (int)$mode;
if ( $mode == 'all' ) $mode = CT_MODE_ALL;
else if ( $mode == 'pages' ) $mode = CT_MODE_PAGES;
else if ( $mode == 'categories' ) $mode = CT_MODE_CATEGORIES;
else if ( $mode == 'default' ) $mode = $wgCategoryTreeDefaultOptions['mode'];
return (int)$mode;
}
/**
* Helper function to convert a string to a boolean value.
* Perhaps make this a global function in MediaWiki proper
*/
static function decodeBoolean( $value ) {
if ( is_null( $value ) ) return NULL;
if ( is_bool( $value ) ) return $value;
if ( is_int( $value ) ) return ( $value > 0 );
$value = trim( strtolower( $value ) );
if ( is_numeric( $value ) ) return ( (int)$mode > 0 );
if ( $value == 'yes' || $value == 'y' || $value == 'true' || $value == 't' || $value == 'on' ) return true;
else if ( $value == 'no' || $value == 'n' || $value == 'false' || $value == 'f' || $value == 'off' ) return false;
else if ( $value == 'null' || $value == 'default' || $value == 'none' || $value == 'x' ) return NULL;
else return false;
}
/**
* Set the script tags in an OutputPage object
@ -57,6 +113,8 @@ class CategoryTree {
" <script type=\"{$wgJsMimeType}\">
var categoryTreeCollapseMsg = \"".Xml::escapeJsString(self::msg('collapse'))."\";
var categoryTreeExpandMsg = \"".Xml::escapeJsString(self::msg('expand'))."\";
var categoryTreeCollapseBulletMsg = \"".Xml::escapeJsString(self::msg('collapse-bullet'))."\";
var categoryTreeExpandBulletMsg = \"".Xml::escapeJsString(self::msg('expand-bullet'))."\";
var categoryTreeLoadMsg = \"".Xml::escapeJsString(self::msg('load'))."\";
var categoryTreeLoadingMsg = \"".Xml::escapeJsString(self::msg('loading'))."\";
var categoryTreeNothingFoundMsg = \"".Xml::escapeJsString(self::msg('nothing-found'))."\";
@ -68,16 +126,101 @@ class CategoryTree {
);
}
static function getJsonCodec() {
static $json = NULL;
if (!$json) {
$json = new Services_JSON(); #recycle API's JSON codec implementation
}
return $json;
}
static function encodeOptions( $options, $enc ) {
if ( $enc == 'mode' || $enc == '' ) {
$opt =$options['mode'];
} elseif ( $enc == 'json' ) {
$json = self::getJsonCodec(); //XXX: this may be a bit heavy...
$opt = $json->encode( $options );
} else {
throw new MWException( 'Unknown encoding for CategoryTree options: ' . $enc );
}
return $opt;
}
static function decodeOptions( $options, $enc ) {
if ( $enc == 'mode' || $enc == '' ) {
$opt = array( "mode" => $options );
} elseif ( $enc == 'json' ) {
$json = self::getJsonCodec(); //XXX: this may be a bit heavy...
$opt = $json->decode( $options );
$opt = get_object_vars( $opt );
} /* elseif () {
foreach ( $oo as $o ) {
if ($o === "") continue;
if ( preg_match( '!^(.*?)=(.*)$!', $o, $m ) {
$n = $m[1];
$opt[$n] = $m[2];
} else {
$opt[$o] = true;
}
}
} */ else {
throw new MWException( 'Unknown encoding for CategoryTree options: ' . $enc );
}
return $opt;
}
function getOptionsAsCacheKey( $depth = NULL ) {
$key = "";
foreach ( $this->mOptions as $k => $v ) {
$key .= $k . ':' . $v . ';';
}
if ( !is_null( $depth ) ) $key .= ";depth=" . $depth;
return $key;
}
function getOptionsAsJsStructure( $depth = NULL ) {
if ( !is_null( $depth ) ) {
$opt = $this->mOptions;
$opt['depth'] = $depth;
$s = self::encodeOptions( $opt, 'json' );
} else {
$s = self::encodeOptions( $this->mOptions, 'json' );
}
return $s;
}
function getOptionsAsJsString( $depth = NULL ) {
return Xml::escapeJsString( $s );
}
function getOptionsAsUrlParameters() {
$u = '';
foreach ( $this->mOptions as $k => $v ) {
if ( $u != '' ) $u .= '&';
$u .= $k . '=' . urlencode($v) ;
}
return $u;
}
/**
* Ajax call. This is called by efCategoryTreeAjaxWrapper, which is used to
* load CategoryTreeFunctions.php on demand.
*/
function ajax( $category, $mode ) {
function ajax( $category, $depth = 1 ) {
global $wgDBname;
$title = self::makeTitle( $category );
if ( ! $title ) return false; #TODO: error message?
$this->mIsAjaxRequest = true;
# Retrieve page_touched for the category
$dbkey = $title->getDBkey();
@ -88,7 +231,7 @@ class CategoryTree {
'page_title' => $dbkey,
), __METHOD__ );
$mckey = "$wgDBname:categorytree($mode):$dbkey"; //FIXME: would need to add depth parameter.
$mckey = "$wgDBname:categorytree(" . $this->getOptionsAsCacheKey( $depth ) . "):$dbkey";
$response = new AjaxResponse();
@ -100,7 +243,7 @@ class CategoryTree {
return $response;
}
$html = $this->renderChildren( $title, $mode ); //FIXME: would need to pass depth parameter.
$html = $this->renderChildren( $title, $depth );
if ( $html == '' ) $html = ' '; #HACK: Safari doesn't like empty responses.
#see Bug 7219 and http://bugzilla.opendarwin.org/show_bug.cgi?id=10716
@ -116,11 +259,10 @@ class CategoryTree {
* Custom tag implementation. This is called by efCategoryTreeParserHook, which is used to
* load CategoryTreeFunctions.php on demand.
*/
function getTag( &$parser, $category, $mode, $hideroot = false, $style = '', $depth=1 ) {
function getTag( &$parser, $category, $hideroot = false, $style = '', $depth=1 ) {
global $wgCategoryTreeDisableCache, $wgCategoryTreeDynamicTag;
static $uniq = 0;
$this->mIsAjaxRequest = false;
$category = trim( $category );
if ( $category === '' ) {
return false;
@ -141,14 +283,14 @@ class CategoryTree {
$html .= Xml::closeElement( 'span' );
}
else {
if ( !$hideroot ) $html .= CategoryTree::renderNode( $title, $mode, $depth>0, $wgCategoryTreeDynamicTag, $depth-1 );
else if ( !$wgCategoryTreeDynamicTag ) $html .= $this->renderChildren( $title, $mode, $depth-1 );
else { //FIXME: depth would need to be propagated here. this would imact the cache key, too
if ( !$hideroot ) $html .= CategoryTree::renderNode( $title, $depth, $wgCategoryTreeDynamicTag );
else if ( !$wgCategoryTreeDynamicTag ) $html .= $this->renderChildren( $title, $depth );
else {
$uniq += 1;
$load = 'ct-' . $uniq . '-' . mt_rand( 1, 100000 );
$html .= Xml::openElement( 'script', array( 'type' => 'text/javascript', 'id' => $load ) );
$html .= 'categoryTreeLoadChildren("' . Xml::escapeJsString( $title->getDBkey() ) . '", "' . $mode . '", document.getElementById("' . $load . '").parentNode );';
$html .= 'categoryTreeLoadChildren("' . Xml::escapeJsString( $title->getDBkey() ) . '", ' . $this->getOptionsAsJsStructure( $depth ) . ', document.getElementById("' . $load . '").parentNode);';
$html .= Xml::closeElement( 'script' );
}
}
@ -163,8 +305,8 @@ class CategoryTree {
* Returns a string with an HTML representation of the children of the given category.
* $title must be a Title object
*/
function renderChildren( &$title, $mode = NULL, $depth=0 ) {
global $wgCategoryTreeMaxChildren, $wgCategoryTreeDefaultMode;
function renderChildren( &$title, $depth=1 ) {
global $wgCategoryTreeMaxChildren;
if( $title->getNamespace() != NS_CATEGORY ) {
// Non-categories can't have children. :)
@ -178,7 +320,7 @@ class CategoryTree {
$transJoin = '';
$transWhere = '';
if ( $mode === NULL ) $wgCategoryTreeDefaultMode;
$mode = $this->getOption('mode');
#namespace filter. Should be configurable
if ( $mode == CT_MODE_ALL ) $nsmatch = '';
@ -210,7 +352,7 @@ class CategoryTree {
#TODO: translation support; ideally added to Title object
$t = Title::makeTitle( $row['page_namespace'], $row['page_title'] );
$s = $this->renderNode( $t, $mode, $depth>0, false, $depth-1 );
$s = $this->renderNode( $t, $depth-1, false );
$s .= "\n\t\t";
if ($row['page_namespace'] == NS_CATEGORY) $categories .= $s;
@ -226,7 +368,7 @@ class CategoryTree {
* Returns a string with an HTML representation of the parents of the given category.
* $title must be a Title object
*/
static function renderParents( &$title, $mode ) {
function renderParents( &$title ) {
global $wgCategoryTreeMaxChildren;
$dbr =& wfGetDB( DB_SLAVE );
@ -262,7 +404,7 @@ class CategoryTree {
$label = htmlspecialchars( $t->getText() );
if ( $trans && $trans!=$label ) $label.= ' ' . Xml::element( 'i', array( 'class' => 'translation'), $trans );
$wikiLink = $special->getLocalURL( 'target=' . $t->getPartialURL() . '&mode=' . $mode );
$wikiLink = $special->getLocalURL( 'target=' . $t->getPartialURL() . '&' . $this->getOptionsAsUrlParameters() );
if ( $s !== '' ) $s .= ' | ';
@ -282,18 +424,17 @@ class CategoryTree {
* Returns a string with a HTML represenation of the given page.
* $title must be a Title object
*/
function renderNode( &$title, $mode = NULL, $children = false, $loadchildren = false, $depth = 1 ) {
global $wgCategoryTreeOmitNamespace, $wgCategoryTreeDefaultMode;
function renderNode( &$title, $children = 0, $loadchildren = false ) {
global $wgCategoryTreeDefaultMode;
static $uniq = 0;
if ( $mode === NULL ) $wgCategoryTreeDefaultMode;
$mode = $this->getOption('mode');
$load = false;
if ( $children && $loadchildren ) {
if ( $children > 0 && $loadchildren ) {
$uniq += 1;
$load = 'ct-' . $uniq . '-' . mt_rand( 1, 100000 );
$children = false;
}
$ns = $title->getNamespace();
@ -304,7 +445,7 @@ class CategoryTree {
#when showing only categories, omit namespace in label unless we explicitely defined the configuration setting
#patch contributed by Manuel Schneider <manuel.schneider@wikimedia.ch>, Bug 8011
if ( $wgCategoryTreeOmitNamespace || $mode == CT_MODE_CATEGORIES ) $label = htmlspecialchars( $title->getText() );
if ( $this->getOption('hideprefix') || $mode == CT_MODE_CATEGORIES ) $label = htmlspecialchars( $title->getText() );
else $label = htmlspecialchars( $title->getPrefixedText() );
if ( $trans && $trans!=$label ) $label.= ' ' . Xml::element( 'i', array( 'class' => 'translation'), $trans );
@ -321,23 +462,6 @@ class CategoryTree {
if ( ( $ns % 2 ) > 0 ) $labelClass .= ' CategoryTreeLabelTalk';
$linkattr= array( 'href' => '#' );
if ( $load ) $linkattr[ 'id' ] = $load;
if ( !$children ) {
$txt = '+';
$linkattr[ 'onclick' ] = "this.href='javascript:void(0)'; categoryTreeExpandNode('".Xml::escapeJsString($key)."','".$mode."',this);";
# Don't load this message for ajax requests, so that we don't have to initialise $wgLang
$linkattr[ 'title' ] = $this->mIsAjaxRequest ? '##LOAD##' : self::msg('expand');
}
else {
$txt = ''; #NOTE: that's not a minus but a unicode ndash!
$linkattr[ 'onclick' ] = "this.href='javascript:void(0)'; categoryTreeCollapseNode('".Xml::escapeJsString($key)."','".$mode."',this);";
$linkattr[ 'title' ] = self::msg('collapse');
$linkattr[ 'class' ] = 'CategoryTreeLoaded';
}
$s = '';
#NOTE: things in CategoryTree.js rely on the exact order of tags!
@ -347,27 +471,48 @@ class CategoryTree {
$s .= Xml::openElement( 'div', array( 'class' => 'CategoryTreeSection' ) );
$s .= Xml::openElement( 'div', array( 'class' => 'CategoryTreeItem' ) );
$attr = array( 'class' => 'CategoryTreeBullet' );
$s .= Xml::openElement( 'span', $attr );
if ( $ns == NS_CATEGORY ) {
$s .= Xml::openElement( 'span', array( 'class' => 'CategoryTreeBullet' ) );
$s .= '[' . Xml::element( 'a', $linkattr, $txt ) . '] ';
$s .= Xml::closeElement( 'span' );
$linkattr= array( 'href' => '#' );
if ( $load ) $linkattr[ 'id' ] = $load;
$linkattr[ 'class' ] = "CategoryTreeToggle";
if ( $children == 0 || $loadchildren ) {
$txt = $this->msg('expand-bullet');
$linkattr[ 'onclick' ] = "this.href='javascript:void(0)'; categoryTreeExpandNode('".Xml::escapeJsString($key)."',".$this->getOptionsAsJsStructure().",this);";
# Don't load this message for ajax requests, so that we don't have to initialise $wgLang
$linkattr[ 'title' ] = $this->mIsAjaxRequest ? '##LOAD##' : self::msg('expand');
}
else {
$txt = $this->msg('collapse-bullet');
$linkattr[ 'onclick' ] = "this.href='javascript:void(0)'; categoryTreeCollapseNode('".Xml::escapeJsString($key)."',".$this->getOptionsAsJsStructure().",this);";
$linkattr[ 'title' ] = self::msg('collapse');
$linkattr[ 'class' ] .= ' CategoryTreeLoaded';
}
$s .= Xml::openElement( 'a', $linkattr ) . $txt . Xml::closeElement( 'a' ) . ' ';
} else {
$s .= ' ';
$s .= $this->msg('page-bullet');
}
$s .= Xml::closeElement( 'span' );
$s .= Xml::openElement( 'a', array( 'class' => $labelClass, 'href' => $wikiLink ) ) . $label . Xml::closeElement( 'a' );
$s .= Xml::closeElement( 'div' );
$s .= "\n\t\t";
$s .= Xml::openElement( 'div', array( 'class' => 'CategoryTreeChildren', 'style' => $children ? "display:block" : "display:none" ) );
//HACK here?
if ( $children ) $s .= $this->renderChildren( $title, $mode, $depth );
$s .= Xml::openElement( 'div', array( 'class' => 'CategoryTreeChildren', 'style' => $children > 0 ? "display:block" : "display:none" ) );
if ( $children > 0 && !$loadchildren) $s .= $this->renderChildren( $title, $children );
$s .= Xml::closeElement( 'div' );
$s .= Xml::closeElement( 'div' );
if ( $load ) {
$s .= "\n\t\t";
$s .= Xml::openElement( 'script', array( 'type' => 'text/javascript' ) );
$s .= 'categoryTreeExpandNode("'.Xml::escapeJsString($key).'", "'.$mode.'", document.getElementById("'.$load.'") );';
$s .= 'categoryTreeExpandNode("'.Xml::escapeJsString($key).'", '.$this->getOptionsAsJsStructure($children).', document.getElementById("'.$load.'"));';
$s .= Xml::closeElement( 'script' );
}

View file

@ -17,7 +17,7 @@ if( !defined( 'MEDIAWIKI' ) ) {
class CategoryTreePage extends SpecialPage {
var $target = '';
var $mode = CT_MODE_CATEGORIES;
var $tree = NULL;
/**
* Constructor
@ -28,12 +28,19 @@ class CategoryTreePage extends SpecialPage {
wfLoadExtensionMessages( 'CategoryTree' );
}
function getOption( $name ) {
global $wgCategoryTreeDefaultOptions;
if ( $this->tree ) return $this->tree->getOption( $name );
else return $wgCategoryTreeDefaultOptions[$name];
}
/**
* Main execution function
* @param $par Parameters passed to the page
*/
function execute( $par ) {
global $wgRequest, $wgOut, $wgMakeBotPrivileged, $wgUser;
global $wgRequest, $wgOut, $wgMakeBotPrivileged, $wgUser, $wgCategoryTreeDefaultOptions;
$this->setHeaders();
@ -45,13 +52,14 @@ class CategoryTreePage extends SpecialPage {
#HACK for undefined root category
if ( $this->target == '<rootcategory>' || $this->target == '&lt;rootcategory&gt;' ) $this->target = NULL;
$this->mode = $wgRequest->getVal( 'mode', CT_MODE_CATEGORIES );
$options = array();
if ( $this->mode == 'all' ) $this->mode = CT_MODE_ALL;
else if ( $this->mode == 'pages' ) $this->mode = CT_MODE_PAGES;
else if ( $this->mode == 'categories' ) $this->mode = CT_MODE_CATEGORIES;
# grab all known options from the request. Normalization is done by the CategoryTree class
foreach ( $wgCategoryTreeDefaultOptions as $option => $default ) {
$options[$option] = $wgRequest->getVal( $option, $default );
}
$this->mode = (int)$this->mode;
$this->tree = new CategoryTree( $options );
$wgOut->addWikiText( wfMsgNoTrans( 'categorytree-header' ) );
@ -64,12 +72,9 @@ class CategoryTreePage extends SpecialPage {
if ( $title && $title->getArticleID() ) {
$html = Xml::openElement( 'div', array( 'class' => 'CategoryTreeParents' ) );
Xml::element( 'span',
array( 'class' => 'CategoryTreeParents' ),
wfMsg( 'categorytree-parents' ) ) . ': ';
$html .= wfMsg( 'categorytree-parents' ) . ': ';
$ct = new CategoryTree;
$parents = $ct->renderParents( $title, $this->mode );
$parents = $this->tree->renderParents( $title );
if ( $parents == '' ) {
$html .= wfMsg( 'categorytree-nothing-found' );
@ -77,10 +82,12 @@ class CategoryTreePage extends SpecialPage {
$html .= $parents;
}
$html .= Xml::closeElement( 'div' ) .
Xml::openElement( 'div', array( 'class' => 'CategoryTreeResult' ) ) .
$ct->renderNode( $title, $this->mode, true, false ) .
Xml::closeElement( 'div' );
$html .= Xml::closeElement( 'div' );
$html .= Xml::openElement( 'div', array( 'class' => 'CategoryTreeResult' ) );
$html .= $this->tree->renderNode( $title, 1 ) .
$html .= Xml::closeElement( 'div' );
$wgOut->addHtml( $html );
}
else {
@ -98,6 +105,7 @@ class CategoryTreePage extends SpecialPage {
function makeInputForm() {
global $wgScript;
$thisTitle = Title::makeTitle( NS_SPECIAL, $this->getName() );
$mode = $this->getOption('mode');
$form = Xml::openElement( 'form', array( 'name' => 'categorytree', 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-categorytree-form' ) ) .
Xml::openElement( 'fieldset' ) .
@ -105,9 +113,9 @@ class CategoryTreePage extends SpecialPage {
Xml::hidden( 'title', $thisTitle->getPrefixedDbKey() ) .
Xml::inputLabel( wfMsg( 'categorytree-category' ), 'target', 'target', 20, $this->target ) . ' ' .
Xml::openElement( 'select', array( 'name' => 'mode' ) ) .
Xml::option( wfMsg( 'categorytree-mode-categories' ), 'categories', $this->mode == CT_MODE_CATEGORIES ? true : false ) .
Xml::option( wfMsg( 'categorytree-mode-pages' ), 'pages', $this->mode == CT_MODE_PAGES ? true : false ) .
Xml::option( wfMsg( 'categorytree-mode-all' ), 'all', $this->mode == CT_MODE_ALL ? true : false ) .
Xml::option( wfMsg( 'categorytree-mode-categories' ), 'categories', $mode == CT_MODE_CATEGORIES ? true : false ) .
Xml::option( wfMsg( 'categorytree-mode-pages' ), 'pages', $mode == CT_MODE_PAGES ? true : false ) .
Xml::option( wfMsg( 'categorytree-mode-all' ), 'all', $mode == CT_MODE_ALL ? true : false ) .
Xml::closeElement( 'select' ) . ' ' .
Xml::submitButton( wfMsg( 'categorytree-go' ), array( 'name' => 'dotree' ) ) .
Xml::closeElement( 'fieldset' ) .

119
README
View file

@ -1,6 +1,6 @@
--------------------------------------------------------------------------
README for the CategoryTree extension
Copyright © 2006-2007 Daniel Kinzler
Copyright © 2006-2008 Daniel Kinzler and others
Licenses: GNU General Public Licence (GPL)
GNU Free Documentation License (GFDL)
--------------------------------------------------------------------------
@ -8,125 +8,12 @@ Licenses: GNU General Public Licence (GPL)
The CategoryTree extension provides a dynamic view of the wiki's category
structure as a tree. It uses AJAX to load parts of the tree on demand.
<http://meta.wikimedia.org/wiki/CategoryTree_extension>
The CategoryTree extension was originally written by Daniel Kinzler in
2006 and is released under the GNU General Public Licence (GPL). The
internationalization files contain contributions by several people;
they are mentioned in each file individually. Also thanks to Tim Starling
for his contributions.
Instructions on installing and using this extension are available at
<http://www.mediawiki.org/wiki/Extension:CategoryTree>
INSTALLING
--------------------------------------------------------------------------
Copy the CategoryTree directory into the extensions folder of your
MediaWiki installation. Then add the following lines to your
LocalSettings.php file (near the end):
$wgUseAjax = true;
require_once( "{$IP}/extensions/CategoryTree/CategoryTree.php" );
Note that $wgUseAjax = true; will enable the Ajax framework in MediaWiki,
which is required by the CategoryTree extension. Ajax is a term for using
JavaScript to load parts of a page on demand. It is supported by all
recent graphic web browsers. For more information about Ajax see
<http://en.wikipedia.org/wiki/Ajax_%28programming%29>.
USAGE
--------------------------------------------------------------------------
CategoryTree can be used in three ways: directly on the category pages,
as a "custom tag" to show a category structure inline on a wiki page,
and as a special page.
The CategoryTree extension replaces the subcategory section of category
pages with a dynamic tree view. If Javascript is disabled, this appears
as a plain list. The dynamic subcategory entries can be disabled using
the URL parameter "notree" - this is intended for bots that rely on
parsing the HTML of category pages.
The custom tag is called <categorytree>. For example, if you put
<categorytree mode="pages">Foo</categorytree> on a wiki page, it will show
the contents of category Foo as a dynamic tree on that page. The tag accepts
the following attributes, using a HTML-like syntax:
* hideroot - set this to "on" to hide the "root" node of the tree, i.e.
the mention of category Foo from the example.
* onlyroot - set this to "on" show only the "root" node of the tree initially
* mode - can be "categories", "pages" or "all". See the Modes section below.
The default for this attribute is controlled by
$wgCategoryTreeDefaultMode, and is initially set to CT_MODE_CATEGORIES,
the equivalent of setting the mode attribute to "categories".
* style - can be used to specify any CSS styles you would like for the
tree.
Alternatively to the <categorytree> tag, parser-function syntax can also be
used, e.g {{#categorytree:Foo|hideroot=yes}}. This syntax allows the use of
magic variables, templates and template parameters for the category name.
The special page is called Special:CategoryTree; there you can enter the
name of a category and then browse it's content. The CategoryTree
extension also adds a tab for this special page to every category page.
MODES
--------------------------------------------------------------------------
The category tree can be shown in different modes, determining what types of
"leaves" the "tree" has:
* categories (constant CT_MODE_CATEGORIES): show subcategories only
* pages (constant CT_MODE_PAGES): show subcategories and pages, except images
* all (constant CT_MODE_ALL): show all pages, subcategories, images, etc
The CT_MODE_XXX constants can be used with configuration optiosn (see below).
OPTIONS
--------------------------------------------------------------------------
There are some options you can specify in your LocalSettings.php file:
$wgCategoryTreeMaxChildren - maximum number of children shown in a tree
node. Default is 200
$wgCategoryTreeAllowTag - enable <categorytree> tag. Default is true.
$wgCategoryTreeDynamicTag - loads the first level of the tree in a
<categorytag> dynamically. This way, the cache
does not need to be disabled. Default is false
$wgCategoryTreeDisableCache - disabled the parser cache for pages with a
<categorytree> tag. Default is true.
$wgCategoryTreeHTTPCache - enable HTTP cache for anon users. Default is
false.
$wgCategoryTreeOmitNamespace - never show namespace prefix. Default is
false. Patch contributed by Manuel Schneider
<manuel.schneider@wikimedia.ch>, Bug 8011
$wgCategoryMaxDepth - maximum value for depth argument; can be an integer,
or an associative array, mapping CT_MODE_XXX constants
to the maximum depth for that mode.
Ignored if $wgCategoryTreeDynamicTag is true. Introduced
by Steve Sanbeg.
$wgCategoryTreeExtPath - the (URL-) path where the extension is installed,
relative to $wgScriptPath, with leading "/". Default is
"/extensions/CategoryTree".
$wgCategoryTreeDefaultMode - the default mode to use when no mode attribute
is specified in a <categorytree> tag. May be
CT_MODE_CATEGORIES (the default), CT_MODE_PAGES, or
CT_MODE_ALL.
$wgCategoryTreeCategoryPageMode - the mode to use when rendering trees on
category pages. May be CT_MODE_CATEGORIES (the default),
CT_MODE_PAGES, or CT_MODE_ALL.
--------------------------------------------------------------------------
EOF