Difference between revisions of "Extension:TreeAndMenu"

From Organic Design wiki
m
(change render code to handle trees or menus)
Line 88: Line 88:
 
* Expand #tree parser-functions
 
* Expand #tree parser-functions
 
*/
 
*/
public function expandTree(&$parser) { return $this->expandTreeAndMenu($parser,'tree'); }
+
public function expandTree() {
 +
$args = func_get_args();
 +
return $this->expandTreeAndMenu('tree',$args);
 +
}
  
 
/**
 
/**
 
* Expand #menu parser-functions
 
* Expand #menu parser-functions
 
*/
 
*/
public function expandMenu(&$parser) { return $this->expandTreeAndMenu($parser,'menu'); }
+
public function expandMenu() {
 +
$args = func_get_args();
 +
return $this->expandTreeAndMenu('menu',$args);
 +
}
  
 
/**
 
/**
 
* Expand either kind of parser-function (reformats tree rows for matching later) and store args
 
* Expand either kind of parser-function (reformats tree rows for matching later) and store args
 
*/
 
*/
private function expandTreeAndMenu(&$parser,$magic) {
+
private function expandTreeAndMenu($magic,$args) {
 +
$parser = array_shift($args);
 
 
 
# Store args for this tree for later use
 
# Store args for this tree for later use
$args = array('type' => $magic);
+
foreach ($args as $arg) if (preg_match('/^(\\w+?)\\s*=\\s*(.+)$/s',$arg,$m)) $args[$m[1]] = $m[2]; else $text = $arg;
foreach (func_get_args() as $arg) if (!is_object($arg)) {
 
if (preg_match('/^(\\w+?)\\s*=\\s*(.+)$/s',$arg,$m)) $args[$m[1]] = $m[2]; else $text = $arg;
 
}
 
  
 
# Create a unique id for this tree or use id supplied in args and store args wrt id
 
# Create a unique id for this tree or use id supplied in args and store args wrt id
 
$this->id = isset($args['id']) ? $args['id'] : uniqid('');
 
$this->id = isset($args['id']) ? $args['id'] : uniqid('');
 +
$args['type'] = $magic;
 
$this->args[$this->id] = $args;
 
$this->args[$this->id] = $args;
  
Line 113: Line 118:
 
$text = preg_replace('/(?<=\\*)\\s*\\[\\[Image:(.+?)\\]\\]/',"{$this->uniq}3$1{$this->uniq}4",$text);
 
$text = preg_replace('/(?<=\\*)\\s*\\[\\[Image:(.+?)\\]\\]/',"{$this->uniq}3$1{$this->uniq}4",$text);
 
$text = preg_replace_callback('/^(\\*+)(.*?)$/m',array($this,'formatRow'),$text);
 
$text = preg_replace_callback('/^(\\*+)(.*?)$/m',array($this,'formatRow'),$text);
+
 
 
return $text;
 
return $text;
 
}
 
}
Line 133: Line 138:
 
* Called after parser has finished (ParserAfterTidy) so all transcluded parts can be assembled into final trees
 
* Called after parser has finished (ParserAfterTidy) so all transcluded parts can be assembled into final trees
 
*/
 
*/
public function renderTreeAndMenu(&$parser, &$text) {
+
public function renderTreeAndMenu(&$parser,&$text) {
 
global $wgJsMimeType;
 
global $wgJsMimeType;
 
$u = $this->uniq;
 
$u = $this->uniq;
Line 148: Line 153:
 
$rows  = array();
 
$rows  = array();
 
$depths = array('' => 0); # depth of each tree root
 
$depths = array('' => 0); # depth of each tree root
$rootId = '';           # the id of the current root-tree (used as tree id in PASS2)
+
$rootId = '';             # the id of the current root-tree (used as tree id in PASS2)
 
$lastId = '';
 
$lastId = '';
 
$lastDepth = 0;
 
$lastDepth = 0;
Line 170: Line 175:
 
$node++;
 
$node++;
 
list($id,$depth,$icon,$item,$start) = $info;
 
list($id,$depth,$icon,$item,$start) = $info;
$args = $this->args[$id];
+
$args = $this->args[$id];
 +
$type  = $args['type'];
 
if (!isset($args['root'])) $args['root'] = ''; # tmp - need to handle rootless trees
 
if (!isset($args['root'])) $args['root'] = ''; # tmp - need to handle rootless trees
$end = $node == count($rows) || $rows[$node][4];
+
$end   = $node == count($rows) || $rows[$node][4];
$add  = isset($args['root']) ? "tree.add(0,-1,'".$args['root']."');" : '';
+
 
+
# Append node script for this row
# Append the dTree JS to add a node for this row
+
if ($depth > $last) {
if ($depth > $last) $parents[$depth] = $node-1;
+
$parents[$depth] = $node-1;
 +
if ($type == 'menu') $nodes .= "<ul>\n";
 +
}
 +
elseif ($depth < $last && $type == 'menu' && !$start) $nodes .= str_repeat('</ul>',$last-$depth);
 
$parent = $parents[$depth];
 
$parent = $parents[$depth];
$last  = $depth;
+
$nodes .= $type == 'tree' ? "{$this->uniqname}$id.add($node,$parent,'$item');\n" : "<li>$item</li>\n";
$nodes .= "{$this->uniqname}$id.add($node,$parent,'$item');\n";
+
$last = $depth;
  
# Last row of current root-tree, surround nodes dtree JS and div etc
+
# Last row of current root, surround nodes dtree or menu script and div etc
 
if ($end) {
 
if ($end) {
+
$class = "{$this->uniqname}-$type";
# Open all and close all links
+
if ($type == 'tree') {
$top = $bottom = $root = '';
 
 
 
foreach ($args as $arg => $pos)
 
if (($pos == 'top' || $pos == 'bottom' || $pos == 'root') && ($arg == 'open' || $arg == 'close'))
 
$$pos .= "<a href=\"javascript: {$this->uniqname}$id.{$arg}All();\">&nbsp;{$arg} all</a>&nbsp;";
 
  
if ($top) $top = "<p>&nbsp;$top</p>";
+
# Finalise a tree
if ($bottom) $bottom = "<p>&nbsp;$bottom</p>";
+
$add = isset($args['root']) ? "tree.add(0,-1,'".$args['root']."');" : '';
if ($root) $add = "tree.add(0,-1,'$root');";  
+
$top = $bottom = $root = '';
 +
foreach ($args as $arg => $pos)
 +
if (($pos == 'top' || $pos == 'bottom' || $pos == 'root') && ($arg == 'open' || $arg == 'close'))
 +
$$pos .= "<a href=\"javascript: {$this->uniqname}$id.{$arg}All();\">&nbsp;{$arg} all</a>&nbsp;";
 +
if ($top) $top = "<p>&nbsp;$top</p>";
 +
if ($bottom) $bottom = "<p>&nbsp;$bottom</p>";
 +
if ($root) $add = "tree.add(0,-1,'$root');";  
 +
$html = "$top<div class='$class' id='$id'>
 +
<script type=\"$wgJsMimeType\">
 +
// TreeAndMenu{$this->version}
 +
tree = new dTree('{$this->uniqname}$id');
 +
for (i in tree.icon) tree.icon[i] = '{$this->baseUrl}/'+tree.icon[i];{$this->images}
 +
tree.config.useLines = {$this->useLines};
 +
$add
 +
{$this->uniqname}$id = tree;
 +
$nodes
 +
document.getElementById('$id').innerHTML = {$this->uniqname}$id.toString();
 +
</script>
 +
</div>$bottom";
 +
}
 +
else {
 +
if ($depth > 0) $nodes .= str_repeat('</ul>',$depth);
 +
$html = "<ul class='$class' id='$id'>$nodes</ul>\n"; # Finalise a menu
 +
}
  
# Build tree JS
+
$text  = preg_replace("/\x7f1$u\x7f$id\x7f.+?$/m",$html,$text,1); # replace first occurence of this trees root-id
$tree = "
 
$top
 
<div class='TreeAndMenu' id='$id'>
 
<script type=\"$wgJsMimeType\">
 
// TreeView{$this->version}
 
tree = new dTree('{$this->uniqname}$id');
 
for (i in tree.icon) tree.icon[i] = '{$this->baseUrl}/'+tree.icon[i];{$this->images}
 
tree.config.useLines = {$this->useLines};
 
$add
 
{$this->uniqname}$id = tree;
 
$nodes
 
document.getElementById('$id').innerHTML = {$this->uniqname}$id.toString();
 
</script>
 
</div>
 
$bottom
 
";
 
$text  = preg_replace("/\x7f1$u\x7f$id\x7f.+?$/m",$tree,$text,1); # replace first occurence of this trees root-id
 
 
$nodes = '';
 
$nodes = '';
 
}
 
}

Revision as of 03:08, 17 April 2008

<?php /**

Info.svg These are the MediaWiki extensions we're using and/or developing. Please refer to the information on the mediawiki.org wiki for installation and usage details. Extensions here which have no corresponding mediawiki article are either not ready for use or have been superseded. You can also browse our extension code in our local Subversion repository or our GitHub mirror.

if (!defined('MEDIAWIKI')) die('Not an entry point.');

define('TREEANDMENU_VERSION','1.0.0, 2008-04-17');

  1. Set any unset images to default titles

if (!isset($wgTreeViewImages) || !is_array($wgTreeViewImages)) $wgTreeViewImages = array();

$wgTreeMagic = "tree"; # the parser-function name for trees $wgMenuMagic = "menu"; # the parser-function name for dropdown menus $wgTreeViewShowLines = false; # whether to render the dotted lines joining nodes $wgExtensionFunctions[] = 'wfSetupTreeAndMenu'; $wgHooks['LanguageGetMagic'][] = 'wfTreeAndMenuLanguageGetMagic';

$wgExtensionCredits['parserhook'][] = array( 'name' => 'TreeAndMenu', 'author' => 'Nad, Sven', 'url' => 'http://www.mediawiki.org/wiki/Extension:Treeview', 'description' => 'Adds #tree and #menu parser functions which contain bullet-lists to be rendered as collapsible treeview\'s or dropdown menus. The treeview\'s use the dTree JavaScript tree menu, and the dropdown menu\'s use Son of Suckerfish', 'version' => TREEANDMENU_VERSION );

class TreeAndMenu {

var $version = TREEANDMENU_VERSION; var $uniq = ; # uniq part of all tree id's var $uniqname = 'tam'; # input name for uniqid var $id = ; # id for specific tree var $baseDir = ; # internal absolute path to treeview directory var $baseUrl = ; # external URL to treeview directory (relative to domain) var $images = ; # internal JS to update dTree images var $useLines = true; # internal variable determining whether to render connector lines var $args = array(); # args for each tree


/** * Constructor */ function __construct() { global $wgOut,$wgHooks,$wgParser,$wgScriptPath,$wgJsMimeType, $wgTreeMagic,$wgMenuMagic,$wgTreeViewImages,$wgTreeViewShowLines;

# Add hooks $wgParser->setFunctionHook($wgTreeMagic,array($this,'expandTree')); $wgParser->setFunctionHook($wgMenuMagic,array($this,'expandMenu')); $wgHooks['ParserAfterTidy'][] = array($this,'renderTreeAndMenu');

# Update general tree paths and properties $this->baseDir = dirname(__FILE__); $this->baseUrl = preg_replace('|^.+(?=[/\\\\]extensions)|',$wgScriptPath,$this->baseDir); $this->useLines = $wgTreeViewShowLines ? 'true' : 'false'; $this->uniq = uniqid($this->uniqname);

# Convert image titles to file paths and store as JS to update dTree foreach ($wgTreeViewImages as $k => $v) { $title = Title::newFromText($v,NS_IMAGE); $image = Image::newFromTitle($title); $v = $image && $image->exists() ? $image->getURL() : $wgTreeViewImages[$k]; $this->images .= "tree.icon['$k'] = '$v';"; }

# Add link to output to load dtree.js script $wgOut->addScript("<script type=\"$wgJsMimeType\" src=\"{$this->baseUrl}/dtree.js\"></script>\n"); $wgOut->addScript("<script type=\"$wgJsMimeType\"><![CDATA[//><"."!-- sfHover = function() { var sfEls = document.getElementById('nav').getElementsByTagName('li'); for (var i=0; i<sfEls.length; i++) { sfEls[i].onmouseover=function() { this.className+=' sfhover'; } sfEls[i].onmouseout=function() { this.className=this.className.replace(new RegExp(' sfhover\\b'),); } } } if (window.attachEvent) window.attachEvent('onload',sfHover); //--><!]]></script>"); }


/** * Expand #tree parser-functions */ public function expandTree() { $args = func_get_args(); return $this->expandTreeAndMenu('tree',$args); }

/** * Expand #menu parser-functions */ public function expandMenu() { $args = func_get_args(); return $this->expandTreeAndMenu('menu',$args); }

/** * Expand either kind of parser-function (reformats tree rows for matching later) and store args */ private function expandTreeAndMenu($magic,$args) { $parser = array_shift($args);

# Store args for this tree for later use foreach ($args as $arg) if (preg_match('/^(\\w+?)\\s*=\\s*(.+)$/s',$arg,$m)) $args[$m[1]] = $m[2]; else $text = $arg;

# Create a unique id for this tree or use id supplied in args and store args wrt id $this->id = isset($args['id']) ? $args['id'] : uniqid(); $args['type'] = $magic; $this->args[$this->id] = $args;

# Reformat tree rows for matching in ParserAfterStrip $text = preg_replace('/(?<=\\*)\\s*\\[\\[Image:(.+?)\\]\\]/',"{$this->uniq}3$1{$this->uniq}4",$text); $text = preg_replace_callback('/^(\\*+)(.*?)$/m',array($this,'formatRow'),$text);

return $text; }


/** * Reformat tree bullet structure recording row, depth and id in a format which is not altered by wiki-parsing * - format is: 1{uniq}-{id}-{depth}-{item}-2{uniq} * - sequences of this format will be matched in ParserAfterTidy and converted into dTree JavaScript * - NOTE: we can't encode a unique row-id because if the same tree instranscluded twice a cached version * may be used (even if parser-cache disabled) this also means that tree id's may be repeated */ private function formatRow($m) { return "\x7f1{$this->uniq}\x7f{$this->id}\x7f".(strlen($m[1])-1)."\x7f$m[2]\x7f2{$this->uniq}"; }


/** * Called after parser has finished (ParserAfterTidy) so all transcluded parts can be assembled into final trees */ public function renderTreeAndMenu(&$parser,&$text) { global $wgJsMimeType; $u = $this->uniq;

# Determine which trees are sub trees # - there should be a more robust way to do this, # it's just based on the fact that all sub-tree's have a minus preceding their row data if (!preg_match_all("/\x7f\x7f1$u\x7f(.+?)\x7f/",$text,$subs)) $subs = array(1 => array());

# Extract all the formatted tree rows in the page and if any, replace with dTree JavaScript if (preg_match_all("/\x7f1$u\x7f(.+?)\x7f([0-9]+)\x7f({$u}3(.+?){$u}4)?(.*?)(?=\x7f[12]$u)/",$text,$matches,PREG_SET_ORDER)) {

# PASS-1: build $rows array containing depth, and tree start/end information $rows = array(); $depths = array( => 0); # depth of each tree root $rootId = ; # the id of the current root-tree (used as tree id in PASS2) $lastId = ; $lastDepth = 0; foreach ($matches as $match) { list(,$id,$depth,,$icon,$item) = $match; $start = false; if ($id != $lastId) { if (!isset($depths[$id])) $depths[$id] = $depths[$lastId]+$lastDepth; if ($start = $rootId != $id && !in_array($id,$subs[1])) $depths[$rootId = $id] = 0; } if ($item) $rows[] = array($rootId,$depth+$depths[$id],$icon,addslashes($item),$start); $lastId = $id; $lastDepth = $depth; }

# PASS-2: build the JavaScript and replace into $text $parents = array(); # parent node for each depth $last = -1; $nodes = ; foreach ($rows as $node => $info) { $node++; list($id,$depth,$icon,$item,$start) = $info; $args = $this->args[$id]; $type = $args['type']; if (!isset($args['root'])) $args['root'] = ; # tmp - need to handle rootless trees $end = $node == count($rows) || $rows[$node][4];

# Append node script for this row if ($depth > $last) { $parents[$depth] = $node-1;

if ($type == 'menu') $nodes .= "

    \n"; } elseif ($depth < $last && $type == 'menu' && !$start) $nodes .= str_repeat('

',$last-$depth);

$parent = $parents[$depth];

$nodes .= $type == 'tree' ? "{$this->uniqname}$id.add($node,$parent,'$item');\n" : "

  • $item
  • \n";

    $last = $depth;

    # Last row of current root, surround nodes dtree or menu script and div etc if ($end) { $class = "{$this->uniqname}-$type"; if ($type == 'tree') {

    # Finalise a tree $add = isset($args['root']) ? "tree.add(0,-1,'".$args['root']."');" : ; $top = $bottom = $root = ; foreach ($args as $arg => $pos) if (($pos == 'top' || $pos == 'bottom' || $pos == 'root') && ($arg == 'open' || $arg == 'close')) $$pos .= "<a href=\"javascript: {$this->uniqname}$id.{$arg}All();\"> {$arg} all</a> ";

    if ($top) $top = "

     $top

    "; if ($bottom) $bottom = "

     $bottom

    ";

    if ($root) $add = "tree.add(0,-1,'$root');";

    $html = "$top

    <script type=\"$wgJsMimeType\"> // TreeAndMenu{$this->version} tree = new dTree('{$this->uniqname}$id'); for (i in tree.icon) tree.icon[i] = '{$this->baseUrl}/'+tree.icon[i];{$this->images} tree.config.useLines = {$this->useLines}; $add {$this->uniqname}$id = tree; $nodes document.getElementById('$id').innerHTML = {$this->uniqname}$id.toString(); </script>

    $bottom";

    } else {

    if ($depth > 0) $nodes .= str_repeat('',$depth); $html = "

      $nodes

    \n"; # Finalise a menu

    }

    $text = preg_replace("/\x7f1$u\x7f$id\x7f.+?$/m",$html,$text,1); # replace first occurence of this trees root-id $nodes = ; } } }

    $text = preg_replace("/\x7f1$u\x7f.+?[\\r\\n]+/m",,$text); # Remove all unreplaced row information return true; }

    }


    /**

    * Called from $wgExtensionFunctions array when initialising extensions
    */
    

    function wfSetupTreeAndMenu() { global $wgTreeAndMenu; $wgTreeAndMenu = new TreeAndMenu(); }


    /**

    * Needed in MediaWiki >1.8.0 for magic word hooks to work properly
    */
    

    function wfTreeAndMenuLanguageGetMagic(&$magicWords,$langCode = 0) { global $wgTreeMagic,$wgMenuMagic; $magicWords[$wgTreeMagic] = array($langCode,$wgTreeMagic); $magicWords[$wgMenuMagic] = array($langCode,$wgMenuMagic); return true; }