You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by br...@apache.org on 2009/06/14 21:11:09 UTC

svn commit: r784605 [2/6] - in /incubator/jspwiki/trunk: ./ src/WebContent/ src/WebContent/WEB-INF/classes/templates/ src/WebContent/scripts/ src/WebContent/templates/default/ src/WebContent/templates/default/editors/ src/WebContent/templates/default/i...

Modified: incubator/jspwiki/trunk/src/WebContent/scripts/jspwiki-common.js
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/WebContent/scripts/jspwiki-common.js?rev=784605&r1=784604&r2=784605&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/WebContent/scripts/jspwiki-common.js (original)
+++ incubator/jspwiki/trunk/src/WebContent/scripts/jspwiki-common.js Sun Jun 14 19:11:08 2009
@@ -1,4 +1,4 @@
-/* 
+/*! 
     JSPWiki - a JSP-based WikiWiki clone.
 
     Licensed to the Apache Software Foundation (ASF) under one
@@ -17,163 +17,331 @@
     KIND, either express or implied.  See the License for the
     specific language governing permissions and limitations
     under the License.  
- */
- 
+*/
+
 /*
-Javascript routines to support JSPWiki
-Since v.2.6.0
+Script: jspwiki-common.js
+	Javascript routines to support JSPWiki, a JSP-based WikiWiki clone.
+
+License:
+	http://www.apache.org/licenses/LICENSE-2.0
 
-Uses mootools v1.1, with following components:  
-*	Core, Class,  Native, Element(ex. Dimensions), Window,
-*	Effects(ex. Scroll), Drag(Base), Remote, Plugins(Hash.Cookie, Tips, Accordion)
-
-Core JS Routine
-*	100 Wiki object (page parms, UserPrefs and setting focus) 
-*	140 SearchBox object: remember 10 most recent search topics
-*	290 HighlightWords in the page-content
-
-Core Dynamic Styles
-	Wiki.addPageRender( XYZ )
-	Wiki.renderPage(page-element, page-name)
-
-*	110 WikiSlimbox (attachment viewer): dynamic style 
-*	130 TabbedSection object: dynamic style
-*	150 Colors, GraphBar object: dynamic style
-*	200 Collapsible list items: dynamic style
-*	230 Sortable: dynamic style 
-*	240 Table-filter (excel like column filters): dynamic style
-*	280 ZebraTable (color odd/even rows): dynmaic style
-
-Complementary Dynamic Styles (see jspwiki-commonstyles.js)
-*	114 Reflection (adds reflection to images): dynamic style 
-*	116 WikiCoverflow (based on MooFlow) : dynamic style
-*	118 Google Chart: dynamic style
-*	132 Accordion object: dynamic style
-*	220 RoundedCorners: dynamic style
-*	260 WikiTips: dynamic style 
-*	270 WikiColumns: dynamic style
-*	300 Prettify: dynamic style
+Since:
+	v.2.6.0
+
+Dependencies:
+	Based on http://mootools.net/ v1.11
+	*	Core, Class,  Native, Element(ex. Dimensions), Window,
+	*	Effects(ex. Scroll), Drag(Base), Remote, Plugins(Hash.Cookie, Tips, Accordion)
+
+Core JS Routines:
+	*	[Wiki] object (page parms, UserPrefs and setting focus) 
+	*	[WikiSlimbox]
+	*	[SearchBox]: remember 10 most recent search topics
+	*	[HighlightWords] 
+
+Core Wiki plugins, implementing JSPWiki Dynamic Styles:
+	*	Wiki.registerPlugin( <js-plugin: function(dom-element, page-name){...} >)
+	*	Wiki.renderPage(page-element, page-name)
+
+	*	[WikiSlimbox] (attachment viewer)
+	*	[TabbedSection] including all accordion variants
+	*	[Colors], [GraphBar]
+	*	[Collapsible] list and box 
+	*	[TableAdds] with %%sortable, %%tablefilter (excel like column filters) and %%zebra-table
+	* 	[Popup] DOM based popup, replacing alert(), prompt(), confirm()
 
 */
 
-/* extend mootools */
+/*
+Global: $getText
+	Global functions to get the text of a dom node
+*/
+function $getText(el) {
+	return el.innerText || el.textContent || '';
+}
+
+/*
+Global: mootools-extensions
+*/
 String.extend({
+
+	/*
+	Function: deCamelize
+		Convert camelCase string to space-separated set of words.
+	
+	Example:
+	> "CamelCase".deCamelize(); /returns "Camel Case"
+	*/
 	deCamelize: function(){
 		return this.replace(/([a-z])([A-Z])/g,"$1 $2");
 	},
-	trunc: function(size,elips){
-		if( !elips ) elips="...";
-		return (this.length<size) ? this : this.substring(0,size)+elips;
+
+	/*
+	Function: trunc 
+		Truncate a string to a maximum length
+	
+	Arguments:
+		size - maximum length of the string
+		elips - (optional) the elips indicates when the string was truncacted (defaults to '...')
+
+	Example:
+	> "this is a long string".trunc(7); //returns "this is..."
+	*/
+	trunc: function(size, elips){
+
+		return (this.length<size) ? this : this.slice(0, size) + (elips||'...');
+
 	},
+
+	/*
+	Function: stripScipts
+		Strips the String of its <script> tags and anything in between them.
+
+	Examples:
+	> var myString = "<script>alert('Hello')</script>Hello, World.";
+	> myString.stripScripts(); //Returns "Hello, World."	
+	*/
 	stripScripts: function(){
-		var text = this.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, function(){
-			return '';
-		});
-		return text;
+		return this.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, '');
 	}
-})
+});
 
-// get text of a dhtml node
-function $getText(el) {
-	return el.innerText || el.textContent || '';
+/*
+Function: getLast
+	Returns the last item from the array.
+	(credit: mootools v1.2, bugfix doesn't work when last element is '')
+
+Examples:
+> ['Cow', 'Pig', 'Dog', 'Cat'].getLast(); //returns 'Cat'
+*/
+//Array.extend seems not to overwrite the native getLast function
+Array.prototype.getLast = function(){
+	return (this.length) ? this[this.length - 1] : null;
 }
+
+
 Element.extend({
 
-	/* wrapper = new Element('div').injectWrapper(node); */
-	wrapChildren: function(el){
+	/*
+	Function: wraps
+		This method moves this Element around its target.
+		The Element is moved to the position of the passed element and becomes the parent.
+		(credit: mootools v1.2)
+
+	Arguments:
+		el - (mixed) The id of an element or an Element.
+
+	Returns:
+		(element) This Element.
+
+	Examples:
+	HTML
+	>	<div id="myFirstElement"></div>
+	JavaScript
+	>	var mySecondElement = new Element('div', {id: 'mySecondElement'});
+	>	mySecondElement.wraps($('myFirstElement'));
+	Resulting HTML
+	>	<div id="mySecondElement">
+    >		<div id="myFirstElement"></div>
+	>	</div>	
+	*/
+	wraps: function(el){
 		while( el.firstChild ) this.appendChild( el.firstChild );
 		el.appendChild( this ) ;
 		return this;
 	},
 
+	/*
+	Function: visible
+		Check if the current element and all its parents are visible.
+
+	Retuns:
+		(boolean) - true when element is visible.
+
+	Examples:
+	>	$('thisElement').visible; //returns true when visible
+	*/
 	visible: function() {
 		var el = this;
-		while($type(el)=='element'){
-			if(el.getStyle('visibility') == 'hidden') return false;
-			if(el.getStyle('display') == 'none' ) return false;
+		while( $type(el)=='element' ){
+			if((el.getStyle('visibility') == 'hidden') || (el.getStyle('display') == 'none' )) return false;
 			el = el.getParent();
 		}
 		return true;
 	},
 
-	hide: function() {
+	/*
+	Function: hide
+		Hide the element: set 'display' style to 'none'.
+
+	Returns:
+		(element) - This Element
+
+	Examples:
+	>	$('thisElement').hide(); 
+	*/
+ 	hide: function() {
 		return this.setStyle('display','none');
 	},
 
+	/*
+	Function: show
+		Show the element: set 'display' style to '' (default display style)
+
+	Returns:
+		(element) - This Element
+
+	Examples:
+	>	$('thisElement').show(); 
+	*/
 	show: function() {
 		return this.setStyle('display','');
 	},
 
+	/*
+	Function: toggle
+		Toggle the 'display' style of the element.
+
+	Returns:
+		(element) - This Element
+
+	Examples:
+	>	$('thisElement').toggle(); 
+	*/
 	toggle: function() {
 		return this.visible() ? this.hide() : this.show();  
 	},
-
-	scrollTo: function(x, y){
-		this.scrollLeft = x;
-		this.scrollTop = y;
+	
+	/*
+	Function: addHover
+		Shortcut function to add 'hover' css class to an element.
+		This allow to support hover effects on all elements, also in IE.
+
+	Arguments
+		clazz - (optional) hover class-name, default is {{hover}}
+
+	Returns:
+		(element) - This Element
+
+	Examples:
+	>	$('thisElement').addHover(); 
+	*/
+	addHover: function(clazz){
+		if(!clazz) clazz = 'hover';
+		return this.addEvents({
+			'mouseenter': function(){ this.addClass(clazz) },
+			'mouseleave': function(){ this.removeClass(clazz) }
+		});
+	},
+	
+	/*
+	Function: getDefaultValue
+		Returns the default value of a form element.
+		Inspired by getValue() of mootools, v1.1
+
+	Returns:
+		(element) - This Element
+
+	Examples:
+	> $('thisElement').getDefaultValue(); 
+	*/
+	getDefaultValue: function(){
+		switch(this.getTag()){
+			case 'select':
+				var values = [];
+				$each(this.options, function(option){
+					if( option.defaultSelected ) values.push($pick(option.value, option.text));
+				});
+				return (this.multiple) ? values : values[0];
+			case 'input': if (!(this.defaultChecked && ['checkbox', 'radio'].contains(this.type)) && !['hidden', 'text', 'password'].contains(this.type)) break;
+			case 'textarea': return this.defaultValue;
+		}
+		return false;
 	},
 
-	/* dimensions.js */
+	/*
+	Property: getPosition
+		Returns the real offsets of the element.
+
+	Arguments:
+		overflown - optional, an array of nested scrolling containers for 
+		  scroll offset calculation, use this if your element is inside 
+		  any element containing scrollbars
+
+	Example:
+	> $('element').getPosition();
+
+	Returns:
+	> {x: 100, y:500}
+	*/
 	getPosition: function(overflown){
 		overflown = overflown || [];
 		var el = this, left = 0, top = 0;
+
 		do {
 			left += el.offsetLeft || 0;
 			top += el.offsetTop || 0;
+			//if( el.getStyle('position')=='relative') alert( 'found relative\n'+el.innerHTML.slice(0,40) );
 			el = el.offsetParent;
-		} while (el);
+		} while( el && (el.getStyle('position')=='static') );
+
 		overflown.each(function(element){
 			left -= element.scrollLeft || 0;
 			top -= element.scrollTop || 0;
 		});
-		return {'x': left, 'y': top};
-	},
 
-	getDefaultValue: function(){
-		switch(this.getTag()){
-			case 'select':
-				var values = [];
-				$each(this.options, function(option){
-					if (option.defaultSelected) values.push($pick(option.value, option.text));
-				});
-				return (this.multiple) ? values : values[0];
-			case 'input': if (!(this.defaultChecked && ['checkbox', 'radio'].contains(this.type)) && !['hidden', 'text', 'password'].contains(this.type)) break;
-			case 'textarea': return this.defaultValue;
-		}
-		return false;
+		return {'x': left, 'y': top};
 	}
 
 });
 
+
+/*
+Class: Observer
+	Class to observe a dom element for changes.
+	
+Example:
+>	$(formInput).observe(function(){
+>		alert('my value changed to '+this.getValue() );
+>	});
+
+*/
 var Observer = new Class({
-	initialize: function(el, fn, options){
-		this.options = Object.extend({
-	   	    event: 'keyup',
-			delay: 300
-		}, options || {});
-		this.element = $(el);
-		this.callback = fn;
-		this.timeout = null;
-		this.listener = this.fired.bind(this);
-		this.value = this.element.getValue();
-		this.element.setProperty('autocomplete','off').addEvent(this.options.event, this.listener);
+	options: {
+		event: 'keyup',
+		delay: 300
 	},
-	fired: function() {
-		if (this.value == this.element.value) return;
-		this.clear();
-		this.value = this.element.value;
-		this.timeout = this.callback.delay(this.options.delay, null, [this.element]);
+	initialize: function(el, fn, options){
+		var self = this;
+		self.setOptions(options);
+		self.element = el = $(el);
+		self.callback = fn;
+		self.timeout = null;
+		self.listener = self.fired.bind(self);
+		self.value = el.getValue();
+		el.set({autocomplete:'off'}).addEvent(self.options.event, self.listener);
+	},
+	fired: function(){
+		var self = this,
+			el = self.element,
+			value = el.value;
+
+		if( self.value == value ) return;
+		self.clear();
+		self.value = value;
+		self.timeout = self.callback.delay(self.options.delay, null, [el]);
 	},
-	clear: function() {
+	clear: function(){
 		this.timeout = $clear(this.timeout);
 	},
-	stop: function() {
+	stop: function(){
 		this.element.removeEvent(this.options.event, this.listener);
 		this.clear();
 	}
 });
+Observer.implement(new Options);
 
-/* Observable class: observe any form element for changes */
 Element.extend({
 	observe: function(fn, options){
 		return new Observer(this, fn, options);
@@ -181,13 +349,35 @@
 });
 
 
-/* I18N Support
- * LocalizedStrings takes form { "javascript.some.resource.key":"localised resource key {0}" }
- * Examples:
- * "moreInfo".localize();
- * "imageInfo".localize(2,4); => "Image {0} of {1}" becomes "Image 2 of 4 
- */
-var LocalizedStrings = LocalizedStrings || []; //defensive
+/* 
+Global: LocalizedString 
+	The __LocalizedStrings__ object is the store of all {{name:value}} pairs
+	with the localisation values. 
+	The name of each entry starts with the prefix {{javascript}}.
+	The value of each entry may contain parameters like this: {{ {0},{1},... }}
+	
+Examples:
+	(start code)
+	LocalizedStrings = { 
+		"javascript.some.resource.key":"localised resource key {0}" ,
+		"javascript.moreInfo":"More",
+		"javascript.imageInfo":"Image {0} of {1}"
+	}
+	(end)
+
+*/
+var LocalizedStrings = LocalizedStrings || [];
+
+/*
+Function: localize
+	Localize a string; with or without parameters.
+	Uses the [LocalideString] global hash.
+
+Examples:
+>	"moreInfo".localize() =='More';
+>	"imageInfo".localize(2,4); => "Image {0} of {1}" becomes "Image 2 of 4
+
+*/
 String.extend({
 	localize: function(){
 		var s = LocalizedStrings["javascript."+this], 
@@ -201,237 +391,516 @@
 	}
 });
 
-/* FIXME parse number anywhere inside a string */
-Number.REparsefloat = new RegExp( "([+-]?\\d+(:?\\.\\d+)?(:?e[-+]?\\d+)?)", "i");
 
-/** TABLE stuff **/
-function $T(el) {
-	var t = $(el); 
-	return (t && t.tBodies[0]) ? $(t.tBodies[0]) : t;
-};
+/*
+Class: Wiki
+	The main javascript class to support basic jspwiki functions.
+*/
+var Wiki = {
 
-/* FIXME */
-// find first ancestor element with tagName
-function getAncestorByTagName( node, tagName ) {
-	if( !node) return null;
-	if( node.nodeType == 1 && (node.tagName.toLowerCase() == tagName.toLowerCase())){ 
-		return node; 
-	} else { 
-		return getAncestorByTagName( node.parentNode, tagName ); 
-	}
-}
+	/*
+	Function: initialize
+		Initialize main Wiki properties.
+	
+	It reads all the "meta" dom elements, prefixed with "wiki",
+	such as 
+	* wikiContext : jspwiki requestcontext variable (view, edit, info, ...)
+	* wikiBaseUrl 
+	* wikiPageUrl: page url template with dummy pagename "%23%24%25"
+	* wikiEditUrl : edit page url
+	* wikiJsonUrl : JSON-RPC url
+	* wikiPageName : pagename without blanks
+	* wikiUserName 
+	* wikiTemplateUrl : path of the jsp template
+	* wikiApplicationName
+
+	It parses the {{JSPWikiUserPrefs}} cookie.
+	
+	All registered js plugins are invoked for both the main page and
+	the favorites block.
+	
+	When the 'referrer' url (previous page) contains a "section=" parameter, 
+	the wiki page will be scrolled to the right section.
+	
+	*/
+	initialize: function(){
 
+		var self = this,
+			host = location.host;
 
-/** 100 Wiki functions **/
-var Wiki = {
+		if(self.prefs) return; //already initialised
 
-	onPageLoad: function(){
-		if(this.prefs) return; //already initialised
-		//read all meta elements starting with wiki
-		$$('meta').each(function(el){
+		// read all meta elements starting with wiki
+		$$('meta').each( function(el){
 			var n = el.getProperty('name') || '';
 			if( n.indexOf('wiki') == 0 ) this[n.substr(4)] = el.getProperty('content');
-		},this);
-
-		var h = location.host;
-		this.BasePath = this.BaseUrl.slice(this.BaseUrl.indexOf(h)+h.length,-1);
-
-		// If JSPWiki is installed in the root, then we have to make sure that
-		// the cookie-cutter works properly here.
-		
-		if( this.BasePath == '' ) this.BasePath = '/';
-		
-		this.prefs = new Hash.Cookie('JSPWikiUserPrefs', {path:Wiki.BasePath, duration:20});
-		
-		this.PermissionEdit = !!$$('a.edit')[0]; //deduct permission level
-		this.url = null;
-		this.parseLocationHash.periodical(500);
+		}, self);
 
-		this.makeMenuFx('morebutton', 'morepopup');
-		this.addEditLinks();
-
-		var p = $('page'),
-			f = $('favorites'); 
-		
-		if(p) this.renderPage(p, Wiki.PageName);
-		if(f) this.renderPage(f, "Favorites");
-		
-		this.addCollapsableFavs();
-	},
+		self.BasePath = (self.BaseUrl) ? 
+			self.BaseUrl.slice(self.BaseUrl.indexOf(host)+host.length,-1) : '';
 
+		// if JSPWiki is installed in the root, then we have to make sure that
+		// the cookie-cutter works properly here.		
+		if(self.BasePath == '') self.BasePath = '/';
+		
+		self.prefs = new Hash.Cookie('JSPWikiUserPrefs', {path:Wiki.BasePath, duration:20});
+		
+		self.allowEdit = !!$E('a.edit'); //deduct permission level
+		self.url = null;
+		self.parseHash.periodical(500);
+		
+		/* reusable dialog object for alert, prompt, confirm */
+		//FIXME: lazy creation ???
+		self.dialog = new Dialog({
+			caption:self.ApplicationName || 'JSPWiki',
+			showNow:false,
+			relativeTo:$('query')
+		});
+
+		//FIXME
+		self.makeMenuFx('morebutton', 'morepopup');
+		self.addEditLinks();
+
+		self.renderPage( $('page'), Wiki.PageName);
+		self.renderPage( $('favorites'), "Favorites");
+		
+		//self.addCollapsableFavs();
+		self.splitbar();
+
+		//jump back to the section previously being edited
+		if( document.referrer.test( /\&section=(\d+)$/ ) ){
+			var section = RegExp.$1.toInt(),
+				els = this.getSections();
+			if( els && els[section] ){
+				var el = els[section];
+				window.scrollTo( el.getLeft(), el.getTop() );
+			}
+		}
 
-	/* show popup alert, which allows any html msg to be displayed */
+		SearchBox.initialize();
+		HighlightWord.initialize();
+		self.setFocus();
+	},
+
+	/*
+	Function: getSections
+		Returns the list of all section headers, excluding the header of the 
+		Table Of Contents.
+	*/
+	getSections: function(){
+		return $$('#pagecontent *[id^=section]').filter( 
+			function(item){ return(item.id != 'section-TOC') }
+		);
+	},
+
+	/* 
+	Function: alert
+		Show the alert popup. Any html fragment can be displayed.
+
+	Arguments:
+		msg - html text fragment
+		
+	Example:
+	> Wiki.alert( "alert message");
+	*/
 	alert: function(msg){
-		return alert(msg); //standard js
-		
-	},
-	/* show popup prompt, which allows any html msg to be displayed and replied to */
+		//return alert(msg); //standard js
+
+		this.dialog
+			.setBody( new Element('p').setHTML(msg) )
+			.setButtons({ Ok:Class.empty })
+			.show();
+	},
+
+	/*
+	Function: confirm
+		Replaces the standard confirm dialog, supporting any html fragment.
+		If the user clicks "OK", the box returns true. 
+		If the user clicks "Cancel", the box returns false.
+		The return value (true/false) is handled via the callback function.
+
+	Example:
+	> Wiki.confirm("sometext", callback-function(true/false) );
+	*/
+	confirm: function(msg, callack){
+		//return callback( confirm(msg) ); //standard js
+
+		this.dialog
+			.setBody( new Element('p').setHTML(msg) )
+			.setButtons({ 
+				Ok:function(){ callback(true); },
+				Cancel:function(){ callback(false); }
+			})
+			.show();
+	},
+
+	/* 
+	Function: prompt
+		Show the prompt prompt, with standard 'Ok' and 'Cancel' buttons. 
+		Replaces the standard prompt handling, supporting any html fragment.
+		The return value is handled via the callback function.
+
+	Arguments:
+		msg - html text fragment
+		defaultreply - (string) default answer
+		callback - (function) invoke when pressing the Ok button
+
+	Example:
+		> Wiki.prompt("sometext","defaultvalue", callback-function(result) );		
+	*/
 	prompt: function(msg, defaultreply, callback){
-		return callback( prompt(msg,defaultreply) ); //standard js
-		
-	},
+		//return callback( prompt( msg, defaultreply ) ); //standard js
 
+		var input;
+			
+		this.dialog.setBody([
+				new Element('p').setHTML(msg),
+				new Element('form').adopt( 
+					input = new Element('input',{ 
+						name:'prompt', 
+						type:'text', 
+						value: defaultreply, 
+						size: 24 
+					}) 
+				)
+			])
+			.setButtons({
+				Ok:function(){ callback( input.getValue() ) },
+				Cancel:Class.empty
+			})
+			.show();
+		input.focus();
+	},
+
+	/*
+	Function: registerPlugin
+		Register a jspwiki javascript plugin.	
+	*/
+	registerPlugin: function(fn){
+		if(!this.plugins) this.plugins = [];
+		this.plugins.push(fn);
+	},
+
+	/*
+	Function: renderPage
+		Invoke all registered wiki js plugins.
+	
+	Arguments:
+		page - dom element 
+		name - wiki page of the page (pagename, or 'Favorites)
+	*/
 	renderPage: function(page, name){
-		this.$pageHandlers.each(function(obj){
-			obj.render(page, name)
-		});
-	},
-	addPageRender: function(fn){
-		if(!this.$pageHandlers) this.$pageHandlers = [];
-		this.$pageHandlers.push(fn);
+		if( page ){
+			this.plugins.each( function(obj){
+				//obj(page, name);
+				($type(obj)=='function') ? obj(page, name) : obj.render(page, name);
+			});
+		}
 	},
 
+	/*
+	Function: setFocus
+		Set the focus of certain form elements, depending on the context of the page.
+		Protect agains IE6: you can't set the focus on invisible elements.
+	*/
 	setFocus: function(){
 		/* plain.jsp,   login.jsp,   prefs/profile, prefs/prefs, find */
 		['editorarea','j_username','loginname','assertedName','query2'].some(function(el){
 			el = $(el);
-			if(el && el.visible()) { el.focus(); return true; }
-			return false;
+			return el && el.visible() ? !!el.focus() : false;
 		});
 	},
 
-	getUrl: function(pagename){
+	/*
+	Property: toUrl
+		Turn a wiki pagename into a full wiki-url 
+	*/
+	toUrl: function(pagename){
+
 		return this.PageUrl.replace(/%23%24%25/, pagename);
+
 	},	
 
-	/* retrieve pagename from any wikipage url format */
-	getPageName: function(url){
+	/*
+	Property: toPageName
+		Parse a wiki-url and return the corresponding wiki pagename 
+	*/
+	toPageName: function(url){
+
 		var s = this.PageUrl.escapeRegExp().replace(/%23%24%25/, '(.+)'),
-			res = url.match(new RegExp(s));
+			res = url.match( new RegExp(s) );
 		return (res ? res[1] : false);
+
 	},
 
-	//ref org.apache.wiki.parser.MarkupParser.cleanLink()
-	//trim repeated whitespace
-	//allow letters, digits and punctuation chars: ()&+,-=._$ 
-	cleanLink: function(p){
+	/*
+	Property: cleanPageName
+		Remove all not-allowed chars from a *candidate* pagename.
+		Trim repeated whitespace, allow letters, digits and 
+		following punctuation chars: ()&+,-=._$ 
+		Ref com.ecyrd.jspwiki.parser.MarkupParser.cleanPageName()
+	*/
+	cleanPageName: function(p){
+
 		return p.trim().replace(/\s+/g,' ')
 				.replace(/[^A-Za-z0-9()&+,-=._$ ]/g, '');
-	},
 
-	changeOrientation: function(){
-		var fav = $('prefOrientation').getValue();
-		$('wikibody')
-			.removeClass('fav-left').removeClass('fav-right')
-			.addClass(fav);
 	},
 
-	/* make hover menu with fade effect */
+	/*
+	Function: makeMenuFx
+		Make hover menu with fade effect. 
+	*/
 	makeMenuFx: function(btn, menu){
-		var btn = $(btn), menu = $(menu);
+		btn = $(btn); 
+		menu = $(menu);
 		if(!btn || !menu) return;
 
 		var	popfx = menu.effect('opacity', {wait:false}).set(0);
 		btn.adopt(menu).set({
-			'href':'#',
-			'events':{
+			href:'#',
+			events:{
 				'mouseout': function(){ popfx.start(0) },
 				'mouseover': function(){ Wiki.locatemenu(btn,menu); popfx.start(0.9) }
 			}
 		});
 	},
 	
-	//FIXME
+	/*
+	Function: locatemenu
+		TODO
+	*/
 	locatemenu: function(base,el){
+
 		var win = {'x': window.getWidth(), 'y': window.getHeight()},
 			scroll = {'x': window.getScrollLeft(), 'y': window.getScrollTop()},
 			corner = base.getPosition(),
 			offset = {'x': base.offsetWidth-el.offsetWidth, 'y': base.offsetHeight },
 			popup = {'x': el.offsetWidth, 'y': el.offsetHeight},
-			prop = {'x': 'left', 'y': 'top'};
+			prop = {'x': 'left', 'y': 'top'},
+			z, pos;
 
-		for (var z in prop){
-			var pos = corner[z] + offset[z]; /*top-left corner of base */
+		for( z in prop ){
+			pos = corner[z] + offset[z]; /*top-left corner of base */
 			if ((pos + popup[z] - scroll[z]) > win[z]) pos = win[z] - popup[z] + scroll[z];
 			el.setStyle(prop[z], pos);
 		};
 	},
 
-	parseLocationHash: function(){
+	/*
+	Function: parseHash
+		TODO: periodic screening of the #hash to ensure all screen sections are displayed properly 
+	
+	FIXME:
+		add handling of BACK button for tabs ??
+	*/
+	parseHash: function(){
+
 		if(this.url && this.url == location.href ) return;
 		this.url = location.href;
 		var h = location.hash; 
-		if( h=="" ) return;
-		h = h.replace(/^#/,'');
+		if( !h || h=='' ) return;
+		h = $( h.slice(1) );
+
+		while( $type( h ) == 'element' ){
+		
+			if( h.hasClass('hidetab') ){
+
+				TabbedSection.click.apply($('menu-' + h.id));
+
+			} else if( h.hasClass('tab') ){
 
-		var el = $(h);
-		while( $type(el) == 'element' ){
-			if( el.hasClass('hidetab') ){
-				TabbedSection.click.apply($('menu-'+el.id));
-			} else if( el.hasClass('tab') ){
 				/* accordion -- need to find accordion toggle object */
-				el.fireEvent('onShow');
+				h.fireEvent('onShow');
 				
-			} else if( el.hasClass('collapsebody') ){
+			} else if( h.hasClass('collapsebody') ){
+
 				/* collapsible box -- need to find the toggle button */
-			} else if(!el.visible() ){
+
+			} else if( h.hasClass('collapsebox') ){
+
+				var bullet = h.getFirst();
+				if(bullet) bullet.fireEvent('click');
+				
+			} else if( !h.visible() ){
 				//alert('not visible'+el.id);
 				//fixme need to find the correct toggler
 				//el.show(); //eg collapsedBoxes: fixme
 			}
-			el = el.getParent();
+			h = h.getParent();
 		}
 
 		location = location.href; /* now jump to the #hash */
 	},
 	
-	/* SubmitOnce: disable all buttons to avoid double submit */
+	/*
+	Function: SubmitOnce
+		Disable all form buttons to avoid double submits.
+		At the start, overwrite any {{onbeforeunload}} handler installed by
+		eg. the WikiEdit class.
+
+	Fixme:
+		Replaced by stripes in v3.0 ??
+	*/
 	submitOnce: function(form){
+
 		window.onbeforeunload = null; /* regular exit of this page -- see jspwiki-edit.js */
 		(function(){ 
 			$A(form.elements).each(function(e){
 				if( (/submit|button/i).test(e.type)) e.disabled = true;
 			});
 		}).delay(10);
+
 		return true;
 	},
 
+	/*
+	Function: submitUpload
+		Support for the upload progressbar.
+		Attached to the attachment upload submit form.
+		
+	Returns:
+		Returns via the submitOnce() function.
+	*/
 	submitUpload: function(form, progress){
-		$('progressbar').setStyle('visibility','visible');
-		this.progressbar =
-		Wiki.jsonrpc.periodical(1000, this, ["progressTracker.getProgress",[progress],function(result){
-			result = result.stripScripts(); //xss vulnerability
-			if(!result.code) $('progressbar').getFirst().setStyle('width',result+'%').setHTML(result+'%');
-		}]);
 
-		return Wiki.submitOnce(form);
-	},
+		var self = this,
+			bar = $('progressbar').setStyle('visibility','visible');
+			
+		self.progressbar = self.jsonrpc.periodical(
+			1000, 
+			self, 
+			["progressTracker.getProgress",[progress],function(result){
+				result = result.stripScripts(); //xss vulnerability
+				if(!result.code) bar.getFirst().setStyle('width',result+'%').setHTML(result+'%');
+			}]
+		);
+		return self.submitOnce(form);
+	},
+
+	/*
+	Property: splitbar
+		Add a toggle bar next to the main page content block, to 
+		show/hide the favorites block with a click of the mouse.
+		The open/close status is saved in the Wiki.prefs cookie.
+		When the user hovers the mouse over the toggle bar, an arrow
+		image is shown to indicate the collapsable effect.
+
+	Note: 
+		The toggle bar has css-id 'collapseFavs'.
+		The toggle bar gets a .hover class when the mouse hovers over it.
+		The mouse-pointer image has css-id 'collapseFavsPointer'.
+		The DOM body gets a .fav-slide class when the favorites are collapsed (hidden)
+		
+		
+	DOM structure:
+	(start code)
+	<div id='content'>
+		<div id='page'>
+			<div class='tabmenu'> ... </div>
+			<div class='tabs'>
+				<div class='splitbar'></div> <== injected splitbar
+				<div id='pagecontent'> ... </div>
+				<div id='attach'> ... </div>
+				<div id='info'> ... </div>
+			</div>
+		</div>
+		<div id='favorites'> ... </div>
+	</div>
+	(end)
+	*/
+	splitbar: function(){
+
+		//inject toggle block :: can be done at server already
+		var splitbar = 'splitbar',
+			pref = 'hidefav',
+			body = $E('body'),
+			pointer = new Element('div', {'id':'splitPointer'}).inject(body),
+			pointerFn = function(e){
+				this.addClass('hover');
+				pointer.setStyles({ left:$('page').getPosition().x, top: (e.pageY || e.clientY) }).show();
+			};
+
+		// The cookie is not saved to the server's Preferences automatically, (HTTP GET)
+		// so the body class will not be set yet.
+		// Should better move server side, for faster rendering. wf-stripes
+		body.addClass( Wiki.prefs.get( pref )||'' );
+					
+		new Element('div',{
+			'class':splitbar,
+			'events':{
+				'click': function(){
+					body.toggleClass( pref );
+					Wiki.prefs.set( pref, body.hasClass(pref) ? pref:'' );
+				},
+				'mouseenter': pointerFn,
+				'mousemove': pointerFn,
+				'mouseleave': function(){ this.removeClass('hover'); pointer.hide() }
+			}
+		}).injectTop( $('page') );
+
+	},	
 
 	addCollapsableFavs: function(){
 
 		var body = $('wikibody'),
 			$pref = 'fav-slide',
 			pref = Wiki.prefs.get('ToggleFav'),
-			renderBullet = function(el){
-				if(el.hasClass('collapseOpen')){
-					el.setProperty('title','favorites.show'.localize()).setHTML('-'); /* &raquo; */
-				} else {
-					el.setProperty('title','favorites.hide'.localize()).setHTML('+'); /* &laquo; */
-				}
-			};
+			tabs = $E('#page .tabs'); 
+			
+		if( !tabs ) return;
 		
-		//FIXME: cookie is not loaded into server Preferences automatically, so body class not yet set
-		//Should better move server side, for faster rendering. wf-stripes
+		// The cookie is not saved to the server's Preferences automatically, 
+		// so the body class will not be set yet.
+		// Should better move server side, for faster rendering. wf-stripes
 		(pref==$pref) ? body.addClass($pref) : body.removeClass($pref);		
-		
-		renderBullet( new Element('a', { 
-			'id':'favoriteToggle',
-			'class': (pref==$pref) ? 'collapseOpen':'collapseClose',
+
+		/* Be careful.
+		   The .tabs block can not be made relative, cause then the .tabmenu doesn't
+		   stack properly with the .tabs block.
+		   Therefore, insert a relative DIV container to contain the clickable vertical bar.
+		   TODO: needs probably another IE hack ;-)
+		 */
+
+		tabs = new Element('div', { 
+			'styles': { 
+				'position':'relative', 
+				'padding': tabs.getStyle('padding') // take over padding from original .tabs 
+			} 
+		}).wraps(tabs.setStyle('padding','0'));			
+
+		var pointer = new Element('div', {'id':'collapseFavsPointer'}).hide().inject(body),
+			movePointer = function(e){
+				this.addClass('hover');
+				pointer.setStyles({ left:this.getPosition().x, top: (e.pageY || e.clientY) }).show();
+			};
+					
+		new Element('div', { 
+			'id':'collapseFavs',
 			'events': {
 				'click': function(){
-					this.toggleClass('collapseOpen').toggleClass('collapseClose');
 					body.toggleClass($pref);
-					renderBullet(this);
 					Wiki.prefs.set('ToggleFav', body.hasClass($pref) ? $pref:'' );
-
+				},
+				'mouseenter': movePointer,
+				'mousemove': movePointer,
+				'mouseleave': function(){ 
+					this.removeClass('hover');
+					pointer.hide();
 				}
 			}
-		}).injectTop('page') );
- 
+		}).injectTop(tabs);
+
 	},
 
+	// fixme: should move server side
 	addEditLinks: function(){
-		if( $("previewcontent") || !this.PermissionEdit || this.prefs.get('SectionEditing') != 'on') return;
+
+
+//fixme: SectionEditing is not properly save when updating userprefs !
+//alert(this.prefs.keys()+"\n"+this.prefs.values());
+
+		if( $("previewcontent") || !this.allowEdit || this.prefs.get('SectionEditing') != 'on') return;
 
 		var url = this.EditUrl;
 		url = url + (url.contains('?') ? '&' : '?') + 'section=';
@@ -439,87 +908,171 @@
 		var aa = new Element('a',{'class':'editsection'}).setHTML('quick.edit'.localize()), 
 			i = 0;
 
-		$$('#pagecontent *[id^=section]').each(function(el){
-			if(el.id=='section-TOC') return;
+		this.getSections().each( function(el){
 			el.adopt(aa.set({'href':url + i++ }).clone());
 		});
+		
 	},
 
-	$jsonid : 10000,
-	jsonrpc: function(method, params, fn) {	
-		new Ajax( Wiki.JsonUrl, {
-			postBody: Json.toString({"id":Wiki.$jsonid++, "method":method, "params":params}), 
-			method: 'post', 
-			onComplete: function(result){ 
-				var r = Json.evaluate(result,true);
-				if(r){
-					if(r.result){ fn(r.result) }
-					else if(r.error){ fn(r.error) }
-				}
+	/*
+	Function: ajax
+		todo
+		FIXME: to be refactored based on Stripes approach to AJAX.
+
+	Arguments:
+	
+	Options:
+		action - stripes event name
+		params - DOM-element or js-object. Will be converted to a query-string
+
+	Example:
+		(start code)
+		Wiki.ajax('Search.jsp', {
+			action:'ajaxSearch',
+			params:{query:'some-text', maxItems:20},
+			update:$('dom-element'),
+			onComplete:function(){
+				alert('ajax - done');
+			}
+		});
+		(end)	
+	*/
+	ajax: function(url, options){
+
+		//FIXME: under contstruction	
+		var params = options.action+'=&'+options.params.toQueryString();		
+
+  		new Ajax( url, {
+			postBody: params,
+			method: 'post',
+			update: options.update,
+			onComplete: function( result ){ 
+				options.onComplete( result );
 			}
 		}).request();
+	
+	},
+
+	/*
+	Function: jsonrpc
+		Generic json-rpc routines to talk to the jspwiki-engine backend.
+		FIXME: to be refactored based on Stripes approach to AJAX.
+
+	Note:
+		Uses the JsonUrl which is read from the page as meta tag
+		{{{ <meta name="wikiJsonUrl" content='/JSPWiki-pipo/JSON-RPC' /> }}}
+
+	Supported rpc calls:
+		- {{search.findPages}} gets the list of pagenames with partial match
+		- {{progressTracker.getProgress}}  
+		- {{search.getSuggestions}} gets the list of pagenames with partial match
+
+	Example:
+		(start code)
+		Wiki.jsonrpc('search.findPages', ['Janne',20], function(result){
+			//do something with the result json object
+		});
+
+		//stripes approach
+		Wiki.jsonrpc('Search.jsp', {
+			action: 'findPages',
+			params: js-object, 
+			onComplete: function(result){
+			...json result object...
+		});
+
+		(end)
+	*/
+	$jsonid : 10000,
+	jsonrpc: function(method, params, fn){
+
+		if(this.JsonUrl){
+
+			new Ajax(this.JsonUrl, {
+				postBody: Json.toString({
+					'id':this.$jsonid++, 
+					'method':method, 
+					'params':params
+				}), 
+				method: 'post', 
+				onComplete: function(result){ 
+					var r = Json.evaluate(result,true);
+					fn(r.result || r.error || null);
+					/*if( r ){
+						if(r.result){ fn(r.result) }
+						else if(r.error){ fn(r.error) }
+					}*/
+				}
+			}).request();
+
+		}
 	}	
+
 }
 
 
-/** 110 WikiSlimbox
- ** Inspired http://www.digitalia.be/software/slimbox by Christophe Bleys
- ** 	%%slimbox [...] %%
- ** 	%%slimbox-img  [some-image.jpg] %%
- ** 	%%slimbox-ajax [some-page links] %%
- **/
-var WikiSlimbox = {
 
-	render: function(page, name){
-		var i = 0,
-			lnk = new Element('a',{'class':'slimbox'}).setHTML('&raquo;');
+/*
+Plugin: WikiSlimbox
+
+Credits:
+	Inspired by Slimbox by Christophe Bleys. (see http://www.digitalia.be/software/slimbox) 
+
+Example:
+	> %%slimbox [...] %%
+	> %%slimbox-img  [some-image.jpg] %%
+	> %%slimbox-ajax [some-page links] %%
+*/
+Wiki.registerPlugin( function(page,name){
+	var i = 0,
+		lnk = new Element('a',{'class':'slimbox'}).setHTML('&raquo;');
 			
-		$ES('*[class^=slimbox]',page).each(function(slim){
-			var group = 'lightbox'+ i++,
-				parm = slim.className.split('-')[1] || 'img ajax',
-				filter = [];
-			if(parm.test('img')) filter.extend(['img.inline', 'a.attachment']); 
-			if(parm.test('ajax')) filter.extend(['a.wikipage', 'a.external']); 
-
-			$ES(filter.join(','),slim).each(function(el){
-				var href = el.src||el.href,
-					rel = (el.className.test('inline|attachment')) ? 'img' : 'ajax';
-
-				if((rel=='img') && !href.test('(.bmp|.gif|.png|.jpg|.jpeg)(\\?.*)?$','i')) return;
-
-				lnk.clone().setProperties({
-					'href':href, 
-					'rel':group+' '+rel,
-					'title':el.alt||el.getText()
-				}).injectAfter(el);//.injectBefore(el);
-
-				if(el.src) el.replaceWith(new Element('a',{
-					'class':'attachment',
-					'href':el.src
-				}).setHTML(el.alt||el.getText()));
-			});
-		});
-		if(i) Lightbox.init();
-		//new Asset.javascript(Wiki.TemplateUrl+'scripts/slimbox.js');
-	}
-}
-Wiki.addPageRender(WikiSlimbox);
+	$ES('*[class^=slimbox]',page).each(function(slim){
+		var group = 'lightbox'+ i++,
+			parm = slim.className.split('-')[1] || 'img ajax',
+			filter = [];
+		if(parm.test('img')) filter.extend(['img.inline', 'a.attachment']); 
+		if(parm.test('ajax')) filter.extend(['a.wikipage', 'a.external']); 
+
+		$ES(filter.join(','),slim).each(function(el){
+			var href = el.src||el.href,
+				rel = (el.className.test('inline|attachment')) ? 'img' : 'ajax';
+
+			if((rel=='img') && !href.test('(.bmp|.gif|.png|.jpg|.jpeg)(\\?.*)?$','i')) return;
+
+			lnk.clone().setProperties({
+				'href':href, 
+				'rel':group+' '+rel,
+				'title':el.alt||el.getText()
+			}).injectAfter(el);//.injectBefore(el);
+
+			if(el.src) el.replaceWith(new Element('a',{
+				'class':'attachment',
+				'href':el.src
+			}).setHTML(el.alt||el.getText()));
+		});
+	});
+	if(i) Lightbox.init();
+	//new Asset.javascript(Wiki.TemplateUrl+'scripts/slimbox.js');
+})
+
 
 /*
+Class: Slimbox
 	Slimbox v1.31 - The ultimate lightweight Lightbox clone
 	by Christophe Beyls (http://www.digitalia.be) - MIT-style license.
 	Inspired by the original Lightbox v2 by Lokesh Dhakar.
 
 	Updated by Dirk Frederickx to fit JSPWiki needs
-	- minimum size of image canvas DONE
-	- add maximum size of image w.r.t window size DONE
-	- CLOSE icon -> close x text iso icon DONE
-	- <<prev, next>> links added in visible part of screen DONE
-	- add size of picture to info window DONE
-	- spacebor, down arrow, enter : next image DONE
-	- up arrow : prev image DONE
-	- allow the same picture occuring several times DONE
-	- add support for external page links  => slimbox_ex DONE
+	- minimum size of image canvas
+	- add maximum size of image w.r.t window size
+	- CLOSE icon -> close x text iso icon
+	- {{prev, next}} links added in visible part of screen
+	- add size of picture to info window
+	- spacebar, down arrow, enter : next image
+	- up arrow : prev image
+	- allow the same picture occuring several times
+	- add support for external page links  => slimbox_ex 
 */
 var Lightbox = {
 
@@ -668,6 +1221,15 @@
 		if( this.images[imageNum][2] == 'img' ){
 			this.preload.onload = this.nextEffect.bind(this);
 			this.preload.src = this.images[imageNum][0];
+		} else if( this.images[imageNum][2] == 'element' ){
+			/**/
+			this.so = this.images[imageNum][0];
+			this.so.setProperties({
+				width: '120px', 
+				height: '120px' 
+			});
+			this.so.inject(this.image);
+			this.nextEffect();
 		} else {			
 			this.iframeId = "lbFrame_"+new Date().getTime();	// Safari would not update iframe content that has static id.
 			this.so = new Element('iframe').setProperties({
@@ -679,7 +1241,6 @@
 				src:this.images[imageNum][0]
 			}).inject(this.image);
 			this.nextEffect();	//asynchronous loading?
-
 		}
 		return false;
 	},
@@ -707,7 +1268,7 @@
 				h = Math.max(this.options.initialHeight,this.preload.height),
 				ww = Window.getWidth()-10,
 				wh = Window.getHeight()-120;
-			if(this.images[this.activeImage][2]!='img' &&!this.ajaxFailed){ w = 6000; h = 3000; }
+			//if(this.images[this.activeImage][2]!='img' &&!this.ajaxFailed){ w = 6000; h = 3000; }
 			if(w > ww) { h = Math.round(h * ww/w); w = ww; }
 			if(h > wh) { w = Math.round(w * wh/h); h = wh; }
 
@@ -768,26 +1329,41 @@
 };
 
 
-/** Class: Tabbed Section (130)
+/*
+Class: TabbedSection
 	Creates tabs, based on some css-class information
-	Use in jspwiki: %%tabbedSection  %%tab-FirstTab .. %% %%
+	Use in jspwiki: 
+	> %%tabbedSection  
+	> 	%%tab-FirstTab .. %% 
+	> /%
+
+	Alternative syntax based on header markup
+	> %%tabbedSection  
+	> 	!# First tab title .. 
+	> 	!# Second tab title .. 
+	> /%
 
-	Following markup:
+
+Example:
+	(start code)
 	<div class="tabbedSection">
 		<div class="tab-FirstTab">..<div>
 		<div class="tab-SecondTab">..<div>
 	</div>
-
+	(end)
 	is changed into
+	(start code)
 	<div class="tabmenu"><span><a activetab>..</a></span>..</div>
 	<div class="tabbedSection tabs">
 		<div class="tab-firstTab ">
 		<div class="tab-SecondTab hidetab">
 	</div>
- **/
+	(end)
+*/
 var TabbedSection = {
 
 	render: function(page, name){
+
 		// add click handlers to existing tabmenu's, generated by <wiki:tabbedSection>
 		$ES('.tabmenu a',page).each(function(el){
 			if(!el.href) el.addEvent('click', this.click);
@@ -800,6 +1376,18 @@
 			
 			var menu = new Element('div',{'class':'tabmenu'}).injectBefore(tt);
 
+			/*
+			var menuItems = this.getTabs(tt);
+			for( var m in menuItems ){
+				new Element('a', {
+					//'id':'menu-'+tab.id, 
+					'class':(i==0) ? 'activetab' : '',  //fixme
+					'events':{ 
+						'click': this.click(this,m) 
+					}
+				}).appendText( menuItems[m] ).inject(menu);				
+			}
+			*/
 			tt.getChildren().each(function(tab,i) {
 				//find nested %%tab-XXX
 				var clazz = tab.className;
@@ -821,6 +1409,98 @@
 			},this);
 		}, this);
 	},
+	
+	/* 
+	Function: getTabs
+		Helper function for TabbedSection. Also used by Accordion.
+		It will scan the 
+		modifies DOM to ensure each TAB section is conform <div class="tab">...</div>
+		return a set of menu-items : {id:'menu1-title', id:'menu2-title'}
+
+		(1) %%tab-tabName : <div class="tab">...</div>
+		(2) !# tab name: <div class="tab"><h2>tab name</h2> ... </div>
+		
+	Arguments:
+		el - DOM content container
+		
+	Returns:
+		{id:'menu1 title', id:'menu2 title', ...} or null
+	*/
+	$id: 1000,
+	getTabs : function( tt ){
+		var tabs = [], titles=[];
+		
+		var isTabClass = true, isTabHeads = true;		
+		
+		
+		el.getChildren().each(function(tab) {
+			
+			if( !tab.className.test('^tab-') ) return;
+			var clazz = tab.className;
+			if( !clazz.test('^tab-') ) return; 
+
+			if( !tab.id || (tab.id=="") ) tab.id = clazz; //unique id
+
+			(i==0) ? tab.removeClass('hidetab') : tab.addClass('hidetab');
+
+			new Element('div',{'class':'clearbox'}).inject(tab);
+
+			var title = clazz.substr(4).deCamelize(); //drop 'tab-' prefix
+
+			//FIXME use class to make tabs visible during printing 
+			//(i==0) ? tab.removeClass('hidetab'): tab.addClass('hidetab');
+
+			var title = tab.className.substr(4).deCamelize(),
+				t = toggle.clone().appendText(title);
+			if(togglemenu) {
+				toggles.push(t.inject(togglemenu));
+			} else {
+				toggles.push(t.adopt(bullet.clone()).injectBefore(tab));
+			}        
+			titles.push(title);
+			tabs.push(tab.addClass('tab'));
+		});
+			
+		return {'titles':titles,'tabs':tabs};
+
+
+/**tabbed-section*******/
+			if(tt.hasClass('tabs')) return;
+
+			el.addClass('tabs'); //css styling on tabs contents
+			
+			var tt = this.getTabs(el);
+			var menu = new Element('div',{'class':'tabmenu'}).injectBefore(tt);
+			var toggle = new Element('a',{'events':{ 'click': this.click }});
+
+			tt.titles.each(function(title,i) {
+				var t = toggle.clone().appendText(title).inject(menu);
+				if(i==0) t.addClass('activetab');
+				t.id='menu-'+tt.tabs[i].id;
+			},this);
+
+
+/**accordion*******/		
+
+			var toggles=[], menu=false;
+
+			/*if(tt.hasCalss('accordion')){ no separate menu block needed } */
+			if(tt.hasClass('tabbedAccordion')){ menu = 'togglemenu'; }
+			else if(tt.hasClass('leftAccordion')){ menu = 'sidemenu left'; }
+			else if(tt.hasClass('rightAccordion')){	menu = 'sidemenu right'; }
+
+			if(menu) menu = new (Element('div'),{'class':menu }).injectBefore(tt);			
+			
+			var tt = this.getTabs(el);
+			/*build togglemenu*/
+			tt.titles.each( function(title,i){
+				var t = toggle.clone().appendText(title);
+				menu ? t.inject(menu) : t.adopt(bullet.clone()).injectBefore(tt.tabs[i]);
+				toggles.push(t);
+			});
+			new Accordion(toggles, tt.tabs, { /* ... */});		
+		
+	}, 
 
 	click: function(){
 		var menu = $(this).getParent(),
@@ -829,23 +1509,31 @@
 		menu.getChildren().removeClass('activetab');
 		this.addClass('activetab');
 
+		//skip possible relative wrapper element 
+		var rel = tabs.getFirst();
+		if(rel.getStyle('position')=='relative') tabs = rel;
+
 		tabs.getChildren().addClass('hidetab');
-		tabs.getElementById(this.id.substr(5)).removeClass('hidetab');		
+
+		//fixme: id needs to be unique , should not be the TAB name
+		tabs.getElementById( this.id.slice(5) ).removeClass('hidetab');		
 	}
 	
 }
-Wiki.addPageRender(TabbedSection);
-
+Wiki.registerPlugin( TabbedSection );
+//FIXME: convert to class
+//Wiki.registerPlugin( function(page,name){
 
 
-/* 140 SearchBox
+/*
+Class: SearchBox
  * FIXME: remember 10 most recent search topics (cookie based)
  * Extended with quick links for view, edit and clone (ref. idea of Ron Howard - Nov 05)
  * Refactored for mootools, April 07
- */
+*/
 var SearchBox = {
 
-	onPageLoad: function(){
+	initialize: function(){
 		this.onPageLoadQuickSearch();
 		this.onPageLoadFullSearch();
 	},
@@ -931,18 +1619,21 @@
 			if (option.value == match) option.selected = true;
 		});
 
-		new Ajax(Wiki.BasePath+'Search.action', {
-			postBody: "ajaxSearch=&"+$('searchform2').toQueryString(),
-			update: 'searchResult2',
-			method: 'post',
+		Wiki.ajax('Search.jsp',{
+			action:'ajaxSearch',
+			params:$('searchform2'),
+			update: 'searchResult2', 
 			onComplete: function() { 
 				$('spin').hide(); 
+				//FIXME: stripes generates a whole web-page iso of page fragment with searchresults.
+				var x = $E('#searchResult2',$('searchResult2'));
+				$('searchResult2').replaceWith( x );
 				GraphBar.render($('searchResult2')); 
 				Wiki.prefs.set('PrevQuery', q2); 
 			} 
-		}).request();
-
+		});
 		location.hash = '#'+q2+":"+$('start').value;  /* push the query into the url history */
+
 	},
 
 	submit: function(){ 
@@ -978,7 +1669,7 @@
 				
 				result.list.each(function(el){
 					new Element('li').adopt( 
-						new Element('a',{'href':Wiki.getUrl(el.map.page) }).setHTML(el.map.page), 
+						new Element('a',{'href':Wiki.toUrl(el.map.page) }).setHTML(el.map.page), 
 						new Element('span',{'class':'small'}).setHTML(" ("+el.map.score+")")
 					).inject(frag);
 				});
@@ -996,13 +1687,13 @@
 
 		var handleResult = function(s){
 			if(s == '') return;
-			if(!search)	s = Wiki.cleanLink(s);//remove invalid chars from the pagename
+			if(!search)	s = Wiki.cleanPageName(s);//remove invalid chars from the pagename
 		
 			p=encodeURIComponent(p);
 			s=encodeURIComponent(s);
 			if(clone && (s != p)) s += '&clone=' + p;
 
-			location.href = url.replace('Main', s );
+			location.href = url.replace('__PAGEHERE__', s );
 		};
 		
 		if(s!='') {
@@ -1014,44 +1705,127 @@
 }
 
 
-/**
- ** 150 GraphBar Object: also used on the findpage
- ** %%graphBars ... %%
- ** convert numbers inside %%gBar ... %% tags to graphic horizontal bars
- ** no img needed.
- ** supported parameters: bar-color and bar-maxsize
- ** e.g. %%graphBars-e0e0e0 ... %%  use color #e0e0e0, default size 120
- ** e.g. %%graphBars-blue-red ... %%  blend colors from blue to red
- ** e.g. %%graphBars-red-40 ... %%  use color red, maxsize 40 chars
- ** e.g. %%graphBars-vertical ... %%  vertical bars
- ** e.g. %%graphBars-progress ... %%  progress bars in 2 colors
- ** e.g. %%graphBars-gauge ... %%  gauge bars in gradient colors
- **/
+/* 
+Class: Color
+	Class for creating and manipulating colors in JavaScript. 
+	Minimal variant of the Color class, inspired by mootools 
+ 
+Syntax:
+	>var myColor = new Color(color[, type]);
+
+Arguments:
+	# color - (mixed) A string or an array representation of a color.
+	# type - (string, optional) A string representing the type of the color to create.
+
+Color:
+	There are 2 representations of color: String, RGB. 
+	For String representation see Element:setStyle for more information.
+
+Examples:
+	String representation:
+	> '#fff'
+	RGB representation:
+	> [255, 255, 255]
+	Or
+	> [255, 255, 255, 1] //(For transparency.)
+
+Returns:
+	(array) A new Color instance.
+
+Examples:
+	> var black = new Color('#000');
+	> var purple = new Color([255,0,255]);
+	> var azure = new Color('azure');
 
-/* minimal variant of the Color class, inspired by mootools */
+Credit:
+  mootools 1.11
+*/
 var Color = new Class({
 
-	_HTMLColors: {
-		black  :"000000", green :"008000", silver :"c0c0c0", lime  :"00ff00",
-		gray   :"808080", olive :"808000", white  :"ffffff", yellow:"ffff00",
-		maroon :"800000", navy  :"000080", red    :"ff0000", blue  :"0000ff",
-		purple :"800080", teal  :"008080", fuchsia:"ff00ff", aqua  :"00ffff" 
+	colors: {
+		aqua:[0,255,255],
+		azure:[240,255,255],
+		beige:[245,245,220],
+		black:[0,0,0],
+		blue:[0,0,255],
+		brown:[165,42,42],
+		cyan:[0,255,255],
+		darkblue:[0,0,139],
+		darkcyan:[0,139,139],
+		darkgrey:[169,169,169],
+		darkgreen:[0,100,0],
+		darkkhaki:[189,183,107],
+		darkmagenta:[139,0,139],
+		darkolivegreen:[85,107,47],
+		darkorange:[255,140,0],
+		darkorchid:[153,50,204],
+		darkred:[139,0,0],
+		darksalmon:[233,150,122],
+		darkviolet:[148,0,211],
+		fuchsia:[255,0,255],
+		gold:[255,215,0],
+		green:[0,128,0],
+		indigo:[75,0,130],
+		khaki:[240,230,140],
+		lightblue:[173,216,230],
+		lightcyan:[224,255,255],
+		lightgreen:[144,238,144],
+		lightgrey:[211,211,211],
+		lightpink:[255,182,193],
+		lightyellow:[255,255,224],
+		lime:[0,255,0],
+		magenta:[255,0,255],
+		maroon:[128,0,0],
+		navy:[0,0,128],
+		olive:[128,128,0],
+		orange:[255,165,0],
+		pink:[255,192,203],
+		purple:[128,0,128],
+		violet:[128,0,128],
+		red:[255,0,0],
+		silver:[192,192,192],
+		white:[255,255,255],
+		yellow:[255,255,0]
 	},
 	
 	initialize: function(color, type){
+
 		if(!color) return false;
 		type = type || (color.push ? 'rgb' : 'hex');
-		if(this._HTMLColors[color]) color = this._HTMLColors[color];
-		var rgb = (type=='rgb') ? color : color.toString().hexToRgb(true);
+		var rgb = (type=='rgb') ? color : this.colors[color.trim().toLowerCase()] || color.toString().hexToRgb(true);
 		if(!rgb) return false;
 		rgb.hex = rgb.rgbToHex();
 		return $extend(rgb, Color.prototype);
+
 	},
 
+
+	/*
+	Method: mix
+		Mixes two or more colors with the Color.
+
+	Syntax:
+		> var myMix = myColor.mix(color[, color2[, color3[, ...][, alpha]);
+
+	Arguments:
+		# color - (mixed) A single or many colors, in hex or rgb representation, to mix with this Color.
+		# alpha - (number, optional) If the last argument is a number, it will be treated as the amount of the color to mix.
+
+	Returns:
+		(array) A new Color instance.
+
+	Examples:
+	(code)
+	// mix black with white and purple, each time at 10% of the new color
+	var darkpurple = new Color('#000').mix('#fff', [255, 0, 255], 10);
+ 
+	$('myDiv').setStyle('background-color', darkpurple);
+	(end)
+	*/
 	mix: function(){
 		var colors = $A(arguments),
 			rgb = this.copy(),
-			alpha = (($type(colors[colors.length - 1]) == 'number') ? colors.pop() : 50)/100,
+			alpha = (($type(colors.getLast()) == 'number') ? colors.pop() : 50)/100,
 			alphaI = 1-alpha;
 		
 		colors.each(function(color){
@@ -1060,15 +1834,48 @@
 		});
 		return new Color(rgb, 'rgb');
 	},
+	
+	/*
+	Method: invert
+		Inverts the Color.
+	
+	Syntax:
+		> var myInvert = myColor.invert();
+
+	Returns:
+		* (array) A new Color instance.
 
+	Examples:
+		(code)
+		var white = new Color('#fff');
+		var black = white.invert();
+		(end)
+	*/
 	invert: function(){
-		return new Color(this.map(function(value){
+		return new Color( this.map( function(value){
 			return 255 - value;
 		}));
 	}
-
 });
 
+/*
+Class: GraphBar
+	Object: also used on the findpage
+ ** %%graphBars ... %%
+ ** convert numbers inside %%gBar ... %% tags to graphic horizontal bars
+ ** no img needed.
+
+ ** supported parameters: bar-color and bar-maxsize
+
+Examples:
+> %%graphBars-e0e0e0 ... %%  use color #e0e0e0, default size 120
+> %%graphBars-blue-red ... %%  blend colors from blue to red
+> %%graphBars-red-40 ... %%  use color red, maxsize 40 chars
+> %%graphBars-vertical ... %%  vertical bars
+> %%graphBars-progress ... %%  progress bars in 2 colors
+> %%graphBars-gauge ... %%  gauge bars in gradient colors
+*/
+
 var GraphBar =
 {
 	render: function(page, name){
@@ -1124,7 +1931,7 @@
 						bar1.set(border+'Width',ubound-barData[j]).set('marginLeft','-1ex'); 
 					}					
 				} else { // isVertical
-					if(pb.getTag()=='td') { pb = new Element('div').wrapChildren(pb); }
+					if(pb.getTag()=='td') { pb = new Element('div').wraps(pb); }
 
 					pb.setStyles({'height':ubound+b.getStyle('lineHeight').toInt(), 'position':'relative'});
 					b.setStyle('position', 'relative'); //needed for inserted spans ;-)) hehe
@@ -1155,22 +1962,26 @@
 		},this);
 	},
 
-	// parse bar data types and scale according to lbound and size
+	/*
+	Function: parseBarData
+		Parse bar data types and scale according to lbound and size
+	*/
 	parseBarData: function(nodes, lbound, size){
 		var barData=[], 
 			maxValue=Number.MIN_VALUE, 
 			minValue=Number.MAX_VALUE,
-			num=date=true;
+			num=true,
+			ddd=num;
 	
 		nodes.each(function(n,i){
-			var s = n.getText();
-			barData.push(s);
-			if(num) num = !isNaN(parseFloat( s.match(Number.REparsefloat) ) );
-			if(date) date = !isNaN(Date.parse(s));
+			var v = n.getText();
+			barData.push(v);
+			num &= !isNaN(v.toFloat());
+			ddd &= !isNaN(Date.parse(v));
 		});
 		barData = barData.map(function(b){
-			if(date)     { b = new Date(Date.parse(b) ).valueOf();  }
-			else if(num) { b = parseFloat( b.match(Number.REparsefloat) ); }
+			if( ddd ){ b = new Date(Date.parse(b) ).valueOf(); }
+			else if( num ){ b = b.match(/([+-]?\d+(:?\.\d+)?(:?e[-+]?\d+)?)/)[0].toFloat(); }
 			
 			maxValue = Math.max(maxValue, b);
 			minValue = Math.min(minValue, b);
@@ -1184,42 +1995,49 @@
 		});
 	},
 
-	/* Fetch set of gBar values from a table
-	 * Check first-row to match field-name: return array with col values
-	 * Check first-column to match field-name: return array with row values
-	 * insert SPANs as place-holder of the missing gBars
-	 */
+	/* 
+	Function: getTableValues
+		Fetch set of gBar values from a table
+		* check first-row to match field-name: return array with col values
+		* check first-column to match field-name: return array with row values
+		* insert SPANs as place-holder of the missing gBars
+	*/
 	getTableValues: function(node, fieldName){
 		var table = $E('table', node); if(!table) return false;
-		var tlen = table.rows.length;
+		var tlen = table.rows.length, h, r, result, i;
 
 		if( tlen > 1 ){ /* check for COLUMN based table */
-			var r = table.rows[0];
-			for( var h=0; h < r.cells.length; h++ ){
+			r = table.rows[0];
+			for( h=0; h < r.cells.length; h++ ){
 				if( $getText( r.cells[h] ).trim() == fieldName ){
-					var result = [];
-					for( var i=1; i< tlen; i++)
-						result.push( new Element('span').wrapChildren(table.rows[i].cells[h]) );
+					result = [];
+					for( i=1; i< tlen; i++)
+						result.push( new Element('span').wraps(table.rows[i].cells[h]) );
 					return result;
 				}
 			}
 		}
-		for( var h=0; h < tlen; h++ ){  /* check for ROW based table */
-			var r = table.rows[h];
+		for( h=0; h < tlen; h++ ){  /* check for ROW based table */
+			r = table.rows[h];
 			if( $getText( r.cells[0] ).trim() == fieldName ){
-				var result = [];
-				for( var i=1; i< r.cells.length; i++)
-					result.push( new Element('span').wrapChildren(r.cells[i]) );
+				result = [];
+				for( i=1; i< r.cells.length; i++)
+					result.push( new Element('span').wraps(r.cells[i]) );
 				return result;
 			}
 		}
 		return false;
 	}
 }
-Wiki.addPageRender(GraphBar);
+Wiki.registerPlugin(GraphBar);
+//FIXME:convert to class
 
 
-/** 200 Collapsible list and boxes **/
+/*
+Class: Collapsible
+	Provides support for collapsible list and boxes.
+	The collapse status is stored in a browser cookie.
+*/
 var Collapsible =
 {
 	pims : [], // all me cookies
@@ -1227,7 +2045,8 @@
 	render: function(page, name){
 		page = $(page); if(!page) return;
 
-		var cookie = Wiki.Context.test(/view|edit|comment/) ? "JSPWikiCollapse"+ name: "";
+		var cookie = Wiki.Context.test(/view|edit|comment/) ? "JSPWiki"+name : "";
+		//var cookie = "";  //activate this line if you want to deactivatie cookie handling
 
 		if(!this.bullet) {
 			this.bullet = new Element('div',{'class':'collapseBullet'}).setHTML('&bull;');		
@@ -1243,7 +2062,7 @@
 		}, this);
 		$ES('.collapsebox,.collapsebox-closed', page).each(function(el){ 
 			this.collapseBox(el); 
-		}, this);	
+		}, this);
 	},
 
 	collapseBox: function(el){
@@ -1262,9 +2081,12 @@
 		this.newBullet(bullet, body, !isclosed, title );
 	},
 
-	// Modifies the list such that sublists can be hidden/shown by clicking the listitem bullet
-	// The listitem bullet is a node inserted into the DOM tree as the first child of the
-	// listitem containing the sublist.
+	/*
+	Function: collapseNode
+		Modifies the list such that sublists can be hidden/shown by clicking 
+		the listitem bullet. The listitem bullet is a node inserted into the 
+		DOM tree as the first child of the listitem contained by the sublist.
+	*/
 	collapseNode: function(node){
 		$ES('li',node).each(function(li){
 			var ulol = $E('ul',li) || $E('ol',li);
@@ -1279,7 +2101,7 @@
 			}
 			if( emptyLI ) return;
 			
-			new Element('div',{'class':'collapsebody'}).wrapChildren(li);
+			new Element('div',{'class':'collapsebody'}).wraps(li);
 			var bullet = this.bullet.clone().injectTop(li);
 			if(ulol) this.newBullet(bullet, ulol, (ulol.getTag()=='ul'));
 		},this);
@@ -1299,8 +2121,7 @@
 
 		bullet.className = (isopen ? 'collapseClose' : 'collapseOpen'); //ready for rendering
 		clicktarget.addEvent('click', this.clickBullet.bind(bullet, [ck, ck.value.length-1, bodyfx]))
-			.addEvent('mouseenter', function(){ clicktarget.addClass('hover')} )
-			.addEvent('mouseleave', function(){ clicktarget.removeClass('hover')} );
+			.addHover();
 			  
 		bodyfx.fireEvent('onStart');
 		if(!isopen) bodyfx.set(0);	
@@ -1324,8 +2145,12 @@
 		if(ck.name) Cookie.set(ck.name, ck.value, {path:Wiki.BasePath, duration:20});
 	},
 
-	// parse initial cookie versus actual document 
-	// returns true if collapse status is open
+	/*
+	Function: parseCookie
+		Parse the initial cookie versus actual document 
+	Returns:
+		true if collapse status is open
+	*/
 	parseCookie: function( isopen ){
 		var ck = this.pims.getLast(),
 			cursor = ck.value.length,
@@ -1344,242 +2169,490 @@
 		return(token == 'o');
 	}
 };
-Wiki.addPageRender(Collapsible);
+Wiki.registerPlugin(Collapsible);
+//Wiki.registerPlugin( Collapsible.render );
 
 
-/** 230 Sortable -- Sort tables **/
-//TODO cache table ok, cache datatype for each column
-var Sortable =
-{
-	render: function(page,name){
-		this.DefaultTitle = "sort.click".localize();
-		this.AscendingTitle = "sort.ascending".localize();
-		this.DescendingTitle = "sort.descending".localize();
-		
-		$ES('.sortable table',page).each(function(table){
-			if( table.rows.length < 2 ) return;
-
-			$A(table.rows[0].cells).each(function(th){
-				th=$(th);
-				if(th.getTag()=='th'){
-					th.addEvent('click', this.sort.bind(this,[th]) )
-						.addClass('sort')
-						.title=this.DefaultTitle;
-				}
-			},this);
-		},this);
+/*
+Plugin: commentbox
+	This wiki-plugin supports the commentbox dynamic style.
+
+Example:  
+>  %%commentbox ... /% : floating box to the right
+>  %%commentbox-LegendTitle .... /% : make it a legend box
+
+DOM structure  
+>	<div class="commentbox"> ... </div>
+>	<fieldset class="commentbox">
+>		<legend>LegendTitle</legend>
+>		....
+>	</fieldset>
+
+*/
+Wiki.registerPlugin( function(page,name){
+
+	$ES('*[class^=commentbox]',page).each(function(el){
+		var legend = el.className.split('-')[1];
+  		if( legend ){
+  			new Element('fieldset',{'class':'commentbox'}).adopt( 
+  				new Element('legend').setHTML( legend.deCamelize() )
+  			).wraps(el).injectBefore(el);
+  			el.remove();
+  		}
+	});
+});
+
+
+/*
+Class: TableAdds
+	Add support for sorting, filtering and zebra-striping of tables.
+	TODO: add support for row-grouping
+
+Credit:
+	Filters inspired by http://www.codeproject.com/jscript/filter.asp
+*/
+var TableAdds = new Class({
+
+	options: {
+		//sort: true,
+		//filter: true,
+		//zebra: [color1, color2],
+		title : {
+			all: "(All)",
+			sort: "Click to sort",
+			ascending: "Ascending sort. Click to reverse",
+			descending: "Descending sort. Click to reverse"
+		}
 	},
 
-	sort: function(th){
-		var table = getAncestorByTagName(th, "table" ),
-			filter = (table.filterStack),
-			rows = (table.sortCache || []),
-			colidx = 0, //target column to sort
-			body = $T(table); 
-		th = $(th);
-
-		//todo add spinner while sorting
-		//validate header row
-		$A(body.rows[0].cells).each(function(thi, i){
-			if(thi.getTag() != 'th') return;
-			if(th == thi) { colidx=i; return; }
-			thi.removeClass('sortAscending').removeClass('sortDescending')
-				.addClass('sort').title = Sortable.DefaultTitle;
-		});
-
-		if(rows.length == 0){  //if data not yet cached
-			$A(body.rows).each(function(r,i){
-				if((i==0) || ((i==1) && (filter))) return;
-				rows.push( r );
+
+	initialize: function( table, options ){
+
+		if( table.rows.length < 3 ) return null;
+		table = $(table);
+
+		var self = table.TableAdds;  //max one TableAdds instance per table
+		if( !self) {
+			this.table = table;
+			this.head = $A(table.rows[0].cells).filter(function(el){
+							return el.getTag()=='th'; 
+						}); 
+			table.TableAdds = self = this;
+		}
+		self.setOptions(options);
+		options = self.options;
+		var head = self.head;
+		
+		if(!self.sorted && options.sort){
+			head.each( function(th,i){
+				th.set({
+					'class': 'sort',
+					title: options.title.sort,
+					events: { 
+						'click': self.sort.bind(self,[i]) 
+					}
+				});
 			});
-		};		
-		var datatype = Sortable.guessDataType(rows,colidx);
+			self.sorted = true;
+		};
 
-		//do the actual sorting
-		if(th.hasClass('sort')){ 
-			rows.sort( Sortable.createCompare(colidx, datatype) )
-		}
-		else rows.reverse(); 
-		
-		var fl=th.hasClass('sortDescending');
-		th.removeClass('sort').removeClass('sortAscending').removeClass('sortDescending');
-		th.addClass(fl ? 'sortAscending': 'sortDescending')
-			.title= fl ? Sortable.DescendingTitle: Sortable.AscendingTitle;
-		
-		var frag = document.createDocumentFragment();
-		rows.each( function(r,i){ frag.appendChild(r); });
-		body.appendChild(frag);
-		table.sortCache = rows;
-		if(table.zebra) table.zebra();
-	},
-
-	guessDataType: function(rows, colidx){
-		var num=date=ip4=euro=kmgt=true;
-		rows.each(function(r,i){
-			var v = $getText(r.cells[colidx]).clean().toLowerCase();
-			if(num)  num  = !isNaN(parseFloat(v));
-			if(date) date = !isNaN(Date.parse(v));
-			if(ip4)  ip4  = v.test(/(?:\\d{1,3}\\.){3}\\d{1,3}/); //169.169.0.1
-			if(euro) euro = v.test(/^[£$€][0-9.,]+/);
-			if(kmgt) kmgt = v.test(/(?:[0-9.,]+)\s*(?:[kmgt])b/);  //2 MB, 4GB, 1.2kb, 8Tb
-		});
-		return (kmgt) ? 'kmgt': (euro) ? 'euro': (ip4) ? 'ip4': (date) ? 'date': (num) ? 'num': 'string';
-	},
-
-	convert: function(val, datatype){
-		switch(datatype){
-			case "num" : return parseFloat( val.match( Number.REparsefloat ) );
-			case "euro": return parseFloat( val.replace(/[^0-9.,]/g,'') );
-			case "date": return new Date( Date.parse( val ) );
-			case "ip4" : 
-				var octet = val.split( "." );
-				return parseInt(octet[0]) * 1000000000 + parseInt(octet[1]) * 1000000 + parseInt(octet[2]) * 1000 + parseInt(octet[3]);
-			case "kmgt":
-				var v = val.toString().toLowerCase().match(/([0-9.,]+)\s*([kmgt])b/);
-				if(!v) return 0;
-				var z=v[2];
-				z = (z=='m') ? 3 : (z=='g') ? 6 : (z=='t') ? 9 : 0;
-				return v[1].toFloat()*Math.pow(10,z);
-			default: return val.toString().toLowerCase();
+		if( !self.filters && options.filter){
+			head.each( function(th,i){
+				th.adopt( new Element('select',{ 
+					'class':'filter',
+					events: { 
+						'click': function(e){ new Event(e).stopPropagation() },
+						'change': self.filter.bind(self,[i])
+					} 
+				}) );
+			});
+			self.filters = [];
+			self.buildFilters();
 		}
+
+		if( !self.zebra && options.zebra ){
+			(self.zebra = self.zebrafy.bind(self,options.zebra) )();
+		}
+		return self;
 	},
+	
+	/*
+	Function: sort
+		This is a ''click'' event handler to sort tables by a certain column.
+		Css styling is applied 
+		to change the appearance of the sortAscending/sortDescending clickable
+		controls inside the table header. 
+		The data-type of the column is auto-recognized to avoid extensive
+		parameterisation.
+
+		# Copy the table body rows into a javascript array and guess 
+		  the data-type of the column to be sorted.
+		# Do the actual sort or reverse sort
+		# Apply css format to the header cells.
+		# Put the sorted array back into the DOM tree of the document
+
+		The [guessDataType] and [doSort] are helper functions for data-type 
+		conversion and caching.
+
+		Following CSS selectors can be changed if needed ..
+		* clickable column headers, not yet sorted: css class = ''.sort''
+		* column headers, sorted ascending: css class = ''.sortAscending'' 
+		* column headers, sorted descending: css clas = ''.sortDescending''
+
+
+	Credits:
+		The implementation was inspired by the excellent javascript created by 
+		Erik Arvidsson. See http://webfx.eae.net/dhtml/sortabletable/sortabletable.html.
+
+	Arguments:
+		column - index of the column to be used as sort key
+	*/
+	sort: function( column ){
+
+		//todo: add spinner while sorting
+		var th = this.head[column],
+			rows = this.getRows(); //table.sortCache,
+
+		this.guessDataType(rows, column);
+
+		th.hasClass('sort') ? this.doSort(rows, column) : rows.reverse(); 
+
+		// format the header cell
+		var isdesc = th.hasClass('sortDescending'), 
+			title = this.options.title;
+
+		new Elements(this.head).addClass('sort').removeClass('sortAscending').removeClass('sortDescending');
+
+		th.removeClass('sort')
+			.addClass( isdesc ? 'sortAscending' : 'sortDescending')
+			.title = isdesc ? title.descending : title.ascending;
 
-	createCompare: function(i, datatype) {
-		return function(row1, row2) {
-			var val1 = Sortable.convert( $getText(row1.cells[i]), datatype );
-			var val2 = Sortable.convert( $getText(row2.cells[i]), datatype );
+		//put sorted rows back into the table		
+		var frag = document.createDocumentFragment();
+		rows.each( function(r){ frag.appendChild(r); });
+		this.table.appendChild(frag);
 
-			if(val1<val2){ return -1 } else if(val1>val2){ return 1 } else return 0;
-		}
-	}
-}
-Wiki.addPageRender(Sortable);
+		var zebra = this.zebra; 
+		if( zebra ) zebra();
+	},
 
-/** 240 table-filters 
- ** inspired by http://www.codeproject.com/jscript/filter.asp
- **/
-var TableFilter =
-{
-	render: function(page,name){
-		this.All = "filter.all".localize();
-		this.FilterRow = 1; //row number of filter dropdowns
-		
-		$ES('.table-filter table',page).each( function(table){
-			if( table.rows.length < 2 ) return;
+	/*
+	Function: filter
+		Filter the table based on the filter column and (selected) filter value. 
+		This function is also an onChange event handler linked with a 'select' element.
+	
+	Arguments
+		column - index of the column to be used as sort key
+		filtervalue - (optional) the value to be filtered, if not present, 
+			take the selected value of the dropdown filter 
+	*/
+	filter: function( column, filtervalue ){ 
+
+		var rows = this.getRows(),
+			select = this.head[column].getLast(), //get select element
+			value = filtervalue || select.getValue(),
+			isAll = (value == this.options.title.all),
+			filters = this.filters;
+
+		// First check if the column is allready in the filters stack.
+		// If so, store the new filter-value in the filters stack.
+		// Otherwise, add a new entry at the end of the filters stack.
+		if( filters.every( function( filter ,i ){
 
-			/*
-			$A(table.rows[0].cells).each(function(e,i){
-				var s = new Element('select',{ 
-					'events': { 
-						'click':function(event){ event.stop(); }.bindWithEvent(), 
-						'change':TableFilter.filter 
-					} 
-				});
-				s.fcol = i; //store index
-				e.adopt(s);	        
-			},this);
-			*/
-			
-			var r = $(table.insertRow(TableFilter.FilterRow)).addClass('filterrow');
-			for(var j=0; j < table.rows[0].cells.length; j++ ){
-				var s = new Element('select',{ 
-					'events': { 
-						'change':TableFilter.filter 
-					} 
-				});
-				s.fcol = j; //store index
-				
-				new Element('th').adopt(s).inject(r);
+			if( filter.column != column ) return true;
+			isAll ? filters.splice(i, 1) : filter.value = value;  
+			return false;
+
+		}) ) filters.push( {value:value, column:column} );
+
+		//reset visibility of all rows, and then apply the filters
+		rows.each( function(r){ r.style.display=''; });
+
+		filters.each( function(filter){
+
+			var value = filter.value, 
+				column = filter.column;
+
+			this.buildFilter(column, value);
+
+			rows.each( function(r){ 
+				if( value != r.data[column] ) r.style.display = 'none';
+			});
+
+		},this);
+
+		this.buildFilters(); //fill remaining dropdowns
+	},
+
+	/*
+	Function: zebrafy
+		Add odd/even coloring to the table.		
+
+	Arguments:
+		color1 - color specified in hex(without #) or as html color name.
+		color1 - color specified in hex(without #) or as html color name.
+
+		When the first color == 'table' of '' the predefined css class ''.odd''
+		is used to color the alternative rows.
+	*/
+	zebrafy: function(color1, color2){
+
+		var j=0,
+			isDefault = color1.test('table') || color1=='';
+		color1 = (!isDefault && color1) ? (new Color(color1,'hex')) : '';
+		color2 = (!isDefault && color2) ? (new Color(color2,'hex')) : '';
+
+		this.getRows().each( function(r){
+			if( r.style.display!='none' ){
+				if( isDefault ) $(r)[ (j++ % 2) ? 'addClass' : 'removeClass']('odd');
+				else $(r).setStyle('background-color', (j++ % 2) ? color1 : color2 );
 			}
-			table.filterStack = [];
-			TableFilter.buildEmptyFilters(table);
 		});
+
 	},
 
-	buildEmptyFilters: function(table){
-		for(var i=0; i < table.rows[0].cells.length; i++){
-			var ff = table.filterStack.some(function(f){ return f.fcol==i });
-			if(!ff) TableFilter.buildFilter(table, i);
-		}
-		if(table.zebra) table.zebra();			
-	},
-
-	// this function initialises a column dropdown filter
-	buildFilter: function(table, col, selectedValue){
-		// Get a reference to the dropdownbox.
-		var select = table.rows[TableFilter.FilterRow].cells[col].firstChild;
-		//var select = $(table.rows[0].cells[col]).getLast();
-		if(!select) return; //empty dropdown
-		select.options.length = 0;
-
-		var rows=[];
-		$A(table.rows).each(function(r,i){
-			if((i==0) || (i==TableFilter.FilterRow)) return;
-			if(r.style.display == 'none') return;
-			rows.push( r );
-		});
-		rows.sort(Sortable.createCompare(col, Sortable.guessDataType(rows,col)));
-
-		//add only unique strings to the dropdownbox
-		select.options[0]= new Option(this.All, this.All);
-		var value;
-		rows.each(function(r,i){
-			var v = $getText(r.cells[col]).clean().toLowerCase();
-			if(v == value) return;
-			value = v;
-			//if(v.length > 32) v = v.substr(0,32)+ "...";
-			//select.options[select.options.length] = new Option(v, value);
-			select.options[select.options.length] = new Option(v.trunc(32), value);
-		});
-		(select.options.length <= 2) ? select.hide() : select.show();
-		if(selectedValue != undefined) {
-			select.value = selectedValue;
-		} else {
-			select.options[0].selected = true;
-		}
+	/*
+	Function: getRows
+		Retrieve the set of data rows of the table.
+		Cache the result for subsequent calls.
+
+	Returns:
+		Array with all data rows of the table. (excluding the header row)
+	*/
+	getRows: function(){
+
+		if( !this.rows ) this.rows = $A(this.table.rows).copy(1);
+		return this.rows;
+
 	},
 
-	filter: function(){ //onchange handler of filter dropdowns
-		var col   = this.fcol,
-			value = this.value,
-			table = getAncestorByTagName(this, 'table');
-		if( !table || table.style.display == 'none') return;
 
-		// First check if the column is allready in the filter.
-		if(table.filterStack.every(function(f,i){
-			if(f.fcol != col) return true;
-			if(value == TableFilter.All) table.filterStack.splice(i, 1);
-			else f.fValue = value;
-			return false;
-		}) ) table.filterStack.push( {fValue:value, fcol:col} );
+	/*
+	Function:  buildFilters
+		Build for each unfitered column a new filter dropdown.
+	*/
+	buildFilters: function( ){
+
+		this.head.each( function(th, column){
+
+			var filtered = this.filters.some( function(f){ return f.column==column });
+			if( !filtered ) this.buildFilter( column );
+
+		},this);
+
+		var zebra = this.zebra;
+		if( zebra ) zebra();
+	},
 
-		$A(table.rows).each(function(r,i){ //show all
-			r.style.display='';
+	/*
+	Function: buildFilter
+		Build a single column dropdown filter. Only the column values of the 
+		visible rows will be part of the filter dropdown.
+	
+	Arguments:
+		table - table
+		col - column index
+		filterValue - normalised value of the selected item (optional)
+	*/
+	buildFilter: function( column, filterValue ){
+
+		var select = this.head[column].getLast();
+		if(!select) return; //empty dropdown ????
+
+		var dropdown = select.options,
+			rows = this.getRows(),
+			all = this.options.title.all,
+			rr = [],
+			unique;
+
+		this.guessDataType(rows, column);
+
+		//collect only the visible rows
+		rows.each( function(r){
+			if( r.style.display !='none' ) rr.push(r);
+		});
+		
+		this.doSort(rr, column);
+
+		//build dropdown with all unique column values
+		dropdown.length = 0;
+		dropdown[0]= new Option(all, all);
+
+		rr.each( function(r){
+			var d = r.data[column];
+			if( d && d != unique ){
+				unique = d;
+				dropdown[dropdown.length] = new Option( $getText(r.cells[column]).clean().trunc(32), d);
+			}
 		});
+		select.value = filterValue || dropdown[0].value;
 
-		table.filterStack.each(function(f){ //now filter the right rows
-			var v = f.fValue, c = f.fcol;
-			TableFilter.buildFilter(table, c, v);
+		// disable the dropdown if only one value 
+		select.disabled = (dropdown.length <= 2);
+	},
 
-			var j=0;
-			$A(table.rows).each(function(r,i){
-				if((i==0) || (i==TableFilter.FilterRow)) return;
-				if(v != $getText(r.cells[c]).clean().toLowerCase()) r.style.display = 'none';
-			});
+	/*
+	Function: guessDataType
+		Parse the column and guess its data-type.
+		Then convert all values according to that data-type.
+		The result is cached in rows~[n].data.
+
+	Supported data-types:
+		numeric - numeric value, with . as decimal separator
+		date - dates as supported by javascript Date.parse
+		  See https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Date/parse
+		ip4 - ip addresses (like 169.169.0.1)
+		euro - currency values (like £10.4, $50, €0.5)
+		kmgt - storage values (like 2 MB, 4GB, 1.2kb, 8Tb)
+
+	Arguments:
+		rows - array of rows each pointing to a DOM tr element
+			and caching previously ''guessed'' converted data.
+		column - index (0..n) of the processed column
+	*/
+	guessDataType: function(rows, column){
+
+		//check cached table data
+		if( rows[0].data && rows[0].data[column] ) return;
+
+		var num=true, ddd=num, ip4=num, euro=num, kmgt=num;
+
+		rows.each( function( r,iii ){
+
+			var v = r.cells[column];
+			if( v ){
+				v = v.getAttribute('jspwiki:sortvalue') || $getText(v);
+				v = v.clean().toLowerCase();
+
+				if( !r.data ) r.data=[];
+				r.data[column] = v;
+
+				num &= !isNaN(v.toFloat());
+				ddd &= !isNaN(Date.parse(v));
+				ip4 &= v.test(/(?:\d{1,3}\.){3}\d{1,3}/); //169.169.0.1
+				euro &= v.test(/^[£$€][0-9.,]+/);
+				kmgt &= v.test(/(?:[0-9.,]+)\s*(?:[kmgt])b/); //2 MB, 4GB, 1.2kb, 8Tb
+			}
+		});
+
+		//now convert all cells to sortable values according to the datatype
+		rows.each( function( r ){ 
+		
+			var val = r.data[column], z;
+
+			if( kmgt ){
+
+				val = val.match(/([0-9.,]+)\s*([kmgt])b/) || [0,0,''];
+				z = {m:3, g:6, t:9}[ val[2] ] || 0;
+				val = val[1].replace(/,/g,'').toFloat() * Math.pow(10, z);
+				
+			} else if( euro ){
+			
+				val = val.replace(/[^0-9.]/g,'').toFloat();
+
+			} else if( ip4 ){
+
+				val = val.split( '.' );
+				val = ((val[0].toInt() * 256 + val[1].toInt()) * 256 + val[2].toInt()) * 256 + val[3].toInt();
+
+			} else if( ddd ){
+
+				val = Date.parse( val );
+				
+			} else if( num ){
+
+				val = val.match(/([+-]?\d+(:?\.\d+)?(:?e[-+]?\d+)?)/)[0].toFloat();
+				
+			}		
+
+			r.data[column] = val;
+		
 		});
-		TableFilter.buildEmptyFilters(table); //fill remaining dropdowns
+
+	},
+
+	/*
+	Function: doSort
+		Helper function to sort an array, previoulsy prepared by
+		[guessDataType]
+	
+	Arguments:
+		array - target array to be sorted
+		column - column index to use a sorting key
+	*/
+	doSort: function( array, column){	
+
+		array.sort( function(a, b){ 
+			a = a.data[column];
+			b = b.data[column];
+			return (a<b) ? -1 : (a>b) ? 1 : 0; 
+		}); 
 	}
-}
-Wiki.addPageRender(TableFilter);
+	
+});
+TableAdds.implement(new Options);
+
+/*
+Script: TableAdds
+	Register a wiki page renderer, invoking the TableAdds class
+	where needed.
+
+Table sorting:
+	All tables inside a JSPWiki {{%%sortable}} style are retrieved and processed.
+	An onclick() handler is added to each column header which points to the 
+	heart of the javascript: the [TableAdds.sort] function.
+
+Table filtering:
+	Add excel like filter dropdowns to all tables inside a JSPWiki {{%%filtertable}} style.
+
+
+Odd/Even coloring of tables (zebra style): 
+	- odd rows get css class odd (ref. jspwiki.css )
+	> %%zebra-table ... /%
+	- odd rows get css style='background=<color>'
+	> %%zebra-<odd-color> ... /%
+	- odd rows get odd-color, even rows get even-color	
+	> %%zebra-<odd-color>-<even-color> ... /%
+
+*/
+Wiki.registerPlugin( function(page, name){
+
+	var title = {
+		all: "filter.all".localize(),
+		sort: "sort.click".localize(),
+		ascending: "sort.ascending".localize(),
+		descending: "sort.descending".localize()
+	};
+
+	$ES('.sortable table',page).each( function(table){
+		new TableAdds(table, {sort:true, title: title});
+	});
+
+	$ES('.table-filter table',page).each( function(table){
+		new TableAdds(table, {filter:true, title: title});
+	});
+
+	$ES('*[class^=zebra]',page).each( function(el){
+		var parms = el.className.split('-').splice(1);
+		$ES('table',el).each(function(table){
+			new TableAdds(table, {zebra: parms});
+		});
+	});
+
+});
 
 
-/** 250 Categories: turn wikipage link into AJAXed popup **/
+/*
+Class: Categories
+	Turn wikipage links into AJAXed popups.
+*/
 var Categories =
 {
-	render: function (page,name){
+	render: function(page, name){
 
 		$ES('.category a.wikipage',page).each(function(link){
-			var page = Wiki.getPageName(link.href); 
+
+			var page = Wiki.toPageName(link.href); 
 			if(!page) return;
 			var wrap = new Element('span').injectBefore(link).adopt(link),
 				popup = new Element('div',{'class':'categoryPopup'}).inject(wrap),
@@ -1595,121 +2668,342 @@
 					update: popup,
 					onComplete: function(){
 						link.setProperty('title', '').removeEvent('click');
-						wrap.addEvent('mouseover', function(e){ popfx.start(0.9); })
-							.addEvent('mouseout', function(e){ popfx.start(0); });
-						popup.setStyle('left', link.getPosition().x);
-						popup.setStyle('top', link.getPosition().y+16);
-						popfx.start(0.9); 
-						
-						$ES('li,div.categoryTitle',popup).each(function(el){
-							el.addEvent('mouseout',function(){ this.removeClass('hover')})
-							  .addEvent('mouseover',function(){ this.addClass('hover')});
+						wrap.addEvents({
+							'mouseover': function(e){ popfx.start(0.9); },
+							'mouseout': function(e){ popfx.start(0); }
+						});
+						popup.setStyles({
+							'left': link.getPosition().x,
+							'top': link.getPosition().y+16
 						});
+						popfx.start(0.9); 
 
-						
+						$ES('li,div.categoryTitle',popup).addHover();						
 					}
 				}).request();
 			});
 		});
 	} 
 }
-Wiki.addPageRender(Categories);
+Wiki.registerPlugin( Categories );
 
 
-/** 280 ZebraTable
- ** Color odd/even rows of table differently
- ** 1) odd rows get css class odd (ref. jspwiki.css )
- **   %%zebra-table ... %%
- **
- ** 2) odd rows get css style='background=<color>'
- ** %%zebra-<odd-color> ... %%
- **
- ** 3) odd rows get odd-color, even rows get even-color
- ** %%zebra-<odd-color>-<even-color> ... %%
- **
- ** colors are specified in HEX (without #) format or html color names (red, lime, ...)
- **/
-var ZebraTable = {
-	render: function(page,name){
-		$ES('*[class^=zebra]',page).each(function(z){
-			var parms = z.className.split('-'), 
-				isDefault = parms[1].test('table'),
-				c1 = '', 
-				c2 = '';
-			if(parms[1]) c1= new Color(parms[1],'hex');
-			if(parms[2]) c2= new Color(parms[2],'hex');
-			$ES('table',z).each(function(t){
-				t.zebra = this.zebrafy.pass([isDefault, c1,c2],t);
-				t.zebra();
-			},this);
-		},this);
-	},
-	zebrafy: function(isDefault, c1, c2){
-		var j=0;
-		$A($T(this).rows).each(function(r,i){
-			if(i==0 || (r.style.display=='none')) return;
-			if(isDefault) (j++ % 2) ? $(r).addClass('odd') : $(r).removeClass('odd');
-			else $(r).setStyle('background-color', (j++ % 2) ? c1 : c2 );
-		});
-	}
-}
-Wiki.addPageRender(ZebraTable);
+/*
+Class: HighlightWord
+
+Credit:
+	Inspired by http://www.kryogenix.org/code/browser/searchhi/
 
-/** Highlight Word
- ** Inspired by http://www.kryogenix.org/code/browser/searchhi/
- ** Modified 21006 to fix query string parsing and add case insensitivity
- ** Modified 20030227 by sgala@hisitech.com to skip words
- **                   with "-" and cut %2B (+) preceding pages
- ** Refactored for JSPWiki -- now based on regexp
- **/
+History:
+	- Modified 21006 to fix query string parsing and add case insensitivity
+	- Modified 20030227 by sgala@hisitech.com to skip words
+	  with "-" and cut %2B (+) preceding pages
+	- Refactored for JSPWiki -- now based on regexp
+*/
 var HighlightWord =
 {
-	onPageLoad: function (){
+	initialize: function (){
 		var q = Wiki.prefs.get('PrevQuery');
 		Wiki.prefs.set('PrevQuery', '');
 		if( !q && document.referrer.test("(?:\\?|&)(?:q|query)=([^&]*)","g") ) q = RegExp.$1;
-		if( !q ) return;
-
-		var words = decodeURIComponent(q).stripScripts(); //xss vulnerability
-		words = words.replace( /\+/g, " " );
-		words = words.replace( /\s+-\S+/g, "" );
-		words = words.replace( /([\(\[\{\\\^\$\|\)\?\*\.\+])/g, "\\$1" ); //escape metachars
-		words = words.trim().split(/\s+/).join("|");
-		this.reMatch = new RegExp( "(" + words + ")" , "gi");
+		
+		if( q ){
+			var words = decodeURIComponent(q).stripScripts(); //xss vulnerability
+			words = words.replace( /\+/g, " " );
+			words = words.replace( /\s+-\S+/g, "" );
+			words = words.replace( /([\(\[\{\\\^\$\|\)\?\*\.\+])/g, "\\$1" ); //escape metachars
+			words = words.trim().split(/\s+/).join("|");
+			
+			this.reMatch = new RegExp( "(" + words + ")" , "gi");
 
-		this.walkDomTree( $("pagecontent") );
+			this.walkDomTree( $("pagecontent") );
+		}
 	},
 
-	// recursive tree walk matching all text nodes
-	walkDomTree: function(node){
+	/*
+	Function: walkDomTree
+		Recursive tree walk to match all text nodes
+	*/
+	walkDomTree: function( node ){
+
 		if( !node ) return;
-		for(var nn=null, n = node.firstChild; n ; n = nn) {
-			nn = n. nextSibling; /* prefetch nextSibling cause the tree will be modified */
+
+		for( var nn=null, n = node.firstChild; n ; n = nn ){
+			// prefetch nextSibling cause the tree will be modified 
+			nn = n. nextSibling;
 			this.walkDomTree(n);
 		}
+
 		// continue on text-nodes, not yet highlighted, with a word match
 		if( node.nodeType != 3 ) return;
 		if( node.parentNode.className == "searchword" ) return;
 		var s = node.innerText || node.textContent || '';
 
-		s = s.replace(/</g,'&lt;'); /* pre text elements may contain <xml> element */
+		s = s.replace(/</g,'&lt;'); // pre text elements may contain <xml> element 
 
-		if( !this.reMatch.test( s ) ) return;
-		var tmp = new Element('span').setHTML(s.replace(this.reMatch,"<span class='searchword'>$1</span>"));
+		if( this.reMatch.test( s ) ){
+			var tmp = new Element('span').setHTML(s.replace(this.reMatch,"<span class='searchword'>$1</span>")),
+				f = document.createDocumentFragment();
 
-		var f = document.createDocumentFragment();
-		while( tmp.firstChild ) f.appendChild( tmp.firstChild );
+			while( tmp.firstChild ) f.appendChild( tmp.firstChild );
 
-		node.parentNode.replaceChild( f, node );
+			node.parentNode.replaceChild( f, node );
+		}
 	}
 }
 
-window.addEvent('load', function(){
-	Wiki.onPageLoad();
+/*
+Class: Dialog
+	Simplified implementation of a Dialog box. Acts as a base class
+	for other dialog classes.
+	It is based on mootools v1.11, depending on the Drag.Base class.
+
+Arguments:
+	options - optional, see options below
+
+Options:
+	className - css class for dialog box, default is 'dialog'
+	style - (optional) additional css style for the dialog box
+	relativeTo - DOM element to position the dialog box
+	modal - ffs
+	resize - resize callback, default null which implies no resize.
+	showNow - (default true) show the dialogbox at initialisation time
+	draggable - (default true) make the dialogbox draggable
+	buttons - (array) set of DOM elements will get a click handler attached
+	onShow - (eventhandler) called when dialogbox is displayed
+	onHide - (eventhandler) called when dialogbox is hidden
+	onResize - (eventhandler) called when dialogbox is resized
+
+Events:
+	onShow - fires when the dialog is shown
+	onHide - fires when the dialog is hidden
+	onResize - fires when the dialog is resized
+
+Example:
+	(start code)
+	<script>
+	var button = $('colorButton');			
+	var cd = new ColorDialog({
+		relativeTo:button,
+		onChange:function(color){ $('target').setStyle('background',color);}
+	});	
+	button.addEvent('click', cd.toggle.bind(cd));
+	//button.addEvent('click', function(){cd.toggle()});
+	
+	</script>
+	(end code)
+*/
+var Dialog = new Class({
+
+	options:{
+		className: 'dialog',
+		//style:{ dialog-box overrule css styles}
+		relativeTo: document.body,
+		//modal: false,
+		//resize: false, //true or {x:[min,max],y:[min,max]}
+		showNow: true,
+		draggable: true,
+		//buttons: { 'button-label': callbackfn-when-clicked }
+		//onShow: Class.empty,
+		//onHide: Class.empty,
+		//onResize: Class.empty
+	},
+
+	initialize:function(options){
+	
+		this.setOptions(options);
+	
+		var caption, body, self = this;
+		options = self.options;
+
+		var el = this.element = new Element('div',{
+			'class':'dialog',
+			'styles': $extend({display:'none'},options.style||{})
+		}).adopt(
+			caption = new Element('div',{'class':'caption'}).adopt(
+				new Element('a',{
+					'class':'closer',
+					'events':{ 'click': this.hide.bind(this) }
+				}).setHTML('&#215;')
+			),
+			body = new Element('div',{'class':'body'}),
+			buttons = new Element('div',{'class':'buttons'})
+		);
+		
+		self.caption = caption;
+		self.body = body;
+		self.buttons = buttons;
+		
+		if( options.caption ) caption.appendText(options.caption); 
+		self.setBody( options.body );
+		self.setButtons( options.buttons );
+		
+		if( options.draggable ){
+			new Drag.Base(el); 
+			el.setStyle('cursor','move');
+		}
+		
+		if( options.resize ){
+			var resize = new Element('div',{'class':'resize'}).inject(el);
+			body.makeResizable({
+				handle:resize, 
+				limit:options.resize,
+				onDrag: function(){	self.resize(this.value.now.x) }
+			})
+		}		
+
+		el.injectInside(document.body);
+		self.resetPosition();
+		if( options.showNow ) self.show();
+	},
+	
+	/*
+	Function: toElement
+	*/
+	toElement: function(){ 
+		return this.element; 
+	},
+
+	/*
+	Function: show
+		Show the dialog box and fire the 'onShow' event.
+	*/
+	show: function(){ 
+		this.element.setStyle('display',''); 
+		this.fireEvent('onShow', [this]);
+		return this; 
+	},
+	/*
+	Function: hide
+		Hide the dialog box and fire the 'onHide' event.
+	*/
+	hide: function(){
+		this.element.setStyle('display','none'); 
+		this.fireEvent('onHide');
+		return this; 
+	},
+	/*
+	Function: toggle
+		Toggle the visibility of the dialog box.
+	*/
+	toggle: function(){
+		var isVisible = this.element.getStyle('display')!='none';
+		return this[isVisible ? 'hide' : 'show']();
+	},
+	/*
+	Function: resetPosition
+		Set the position of the dialog box back to its initialization value.
+	*/
+	resetPosition: function(){
+		this.setPosition(this.options.relativeTo);
+	},
+	/*
+	Function: setBody
+		Set the body of the dialog box
+	Arguments:
+		content - string or DOM element
+	Example:
+		> setBody( "this is a new dialog content");
+		> setBody( new Element('span',{'class','error'}).setHTML('Error encountered') );
+	*/
+	setBody: function(content){
+
+		var body = this.body.empty(),
+			type = $type(content);
+
+		if( type=='string'){
+			body.setHTML(content)		
+		} else if( type=='element'){
+			body.adopt(content.show());
+		};
+		
+		return this;
+	},
+	/*
+	Function: setButtons
+		this.buttons.empty();
+		if( buttons ){
+		
+		}
+	*/
+	setButtons: function(buttons){
+
+		var self = this,
+			buttonDiv = this.buttons.empty();
+			
+		if( buttons ){
+		
+			for(var btn in buttons){
+				new Element('a',{
+					'class':'btn',
+					'events':{
+						click: buttons[btn],
+						mouseup: function(){self.hide();}
+					}
+				}).adopt(
+					new Element('span').adopt(
+						new Element('span').setHTML(btn.localize())
+					)
+				).inject(buttonDiv);
+			}
+		}
+		return this;
+	},
+	/*
+	Function: setPosition
+		Position the dialog box. (absolute positioning)
+		
+	Arguments:
+		relativeTo: (optional) DOM element. Default is the center of the window.
+	*/
+	setPosition: function( relativeTo ){
+
+		if( relativeTo ){
+
+			//new Element('span',{'styles':{'position':'relative'}}).injectAfter(relativeTo).adopt(relativeTo,el);
+			
+			if( $type(relativeTo) == 'element' ) relativeTo = relativeTo.getCoordinates();
+			var pos = {left:relativeTo.left, top:relativeTo.top + relativeTo.height}
+
+		} else {
+

[... 48 lines stripped ...]