Index: qPloneTabs/tags/0.3.1/Extensions/Install.py
===================================================================
--- qPloneTabs/tags/0.3.1/Extensions/Install.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/Extensions/Install.py	(revision 32)
@@ -0,0 +1,122 @@
+import string
+from cStringIO import StringIO
+
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.DirectoryView import addDirectoryViews
+
+from Products.CMFPlone.migrations.migration_util import safeEditProperty
+from Products.qPloneTabs.config import *
+
+configlets = ({'id':PROJECTNAME,
+    'name':'Plone Tabs',
+    'action':'string:${portal_url}/prefs_tabs_form',
+    'condition':'',
+    'category':'Products',
+    'visible':1,
+    'appId':PROJECTNAME,
+    'permission':VIEW_PERMISSION,
+    'imageUrl':'qplonetabs.gif' },)
+
+def addPropertySheet(self, out):
+    """ Add tabs_properties property sheet to portal_properties and some needed field to it """
+    portal_props = getToolByName(self, 'portal_properties')
+    if not hasattr(portal_props, PROPERTY_SHEET):
+        portal_props.addPropertySheet(PROPERTY_SHEET, SHEET_TITLE)
+        out.write('Added %s property sheet to portal_properties\n' % PROPERTY_SHEET)
+    else:
+        out.write('Skipped adding %s property sheet to portal_properties\n' % PROPERTY_SHEET)
+    sheet = getattr(portal_props, PROPERTY_SHEET)
+
+    if not hasattr(sheet, FIELD_NAME):
+        safeEditProperty(sheet, FIELD_NAME, PROPERTY_FIELD, 'lines')
+        out.write('Added %s property field to %s property sheet\n' % (FIELD_NAME, PROPERTY_SHEET))
+    else:
+        out.write('Skipped adding %s property field to %s property sheet\n' % (FIELD_NAME, PROPERTY_SHEET))
+
+def setupSkin(self, out, skinFolder):
+    """ Setup product skin layer """
+
+    skinstool=getToolByName(self, 'portal_skins')
+    for skin in skinstool.getSkinSelections():
+        path = skinstool.getSkinPath(skin)
+        path = map(string.strip, string.split(path,','))
+        if not skinFolder in path:
+            try:
+                path.insert( path.index('custom')+1, skinFolder)
+            except ValueError:
+                path.append(skinFolder)
+            path = string.join(path, ', ')
+            skinstool.addSkinSelection(skin, path)
+            out.write('  %s layer sucessfully installed into skin %s.\n' % (skinFolder, skin))
+        else:
+            out.write('  %s layer was already installed into skin %s.\n' % (skinFolder, skin))
+
+def removeSkin(self, skins=[]):
+    """ Setup product skin layer """
+
+    if skins:
+        skinstool = getToolByName(self, 'portal_skins')
+        for skinName in skinstool.getSkinSelections():
+            path = skinstool.getSkinPath(skinName)
+            path = [i.strip() for i in  path.split(',')]
+            for s in skins:
+                if s in path:
+                    path.remove(s)
+                s += '/'
+                for layer in path:
+                    if layer.startswith(s):
+                        path.remove(layer)
+            path = ','.join(path)
+            skinstool.addSkinSelection(skinName, path)
+
+def addConfiglet(self, out):
+    """ Add tabs configlet to portal control panel """
+
+    configTool = getToolByName(self, 'portal_controlpanel', None)
+    if configTool:
+        for conf in configlets:
+            configTool.registerConfiglet(**conf)
+            out.write('Added configlet %s\n' % conf['id'])
+
+def removeConfiglet(self, out):
+    """ Remove tabs configlet from portal control panel """
+
+    configTool = getToolByName(self, 'portal_controlpanel', None)
+    if configTool:
+        for conf in configlets:
+            configTool.unregisterConfiglet(conf['id'])
+            out.write('Removed configlet %s\n' % conf['id'])
+
+def install(self):
+    """ Product installation """
+
+    out = StringIO()
+
+    out.write('setupSkin... \n')
+    skinstool = getToolByName(self, 'portal_skins')
+    addDirectoryViews(skinstool, SKINS_DIR, GLOBALS)
+    setupSkin(self, out, PROJECTNAME)
+
+    mtool = getToolByName(self, 'portal_migration')
+    plone_version = mtool.getFileSystemVersion()
+    if plone_version == '2.0.5':
+         setupSkin(self, out, PROJECTNAME+'/2.0.5')
+         out.write('Added %s/2.0.5 Layer to portal_skins\n' % PROJECTNAME)
+
+    addPropertySheet(self, out)
+
+    addConfiglet(self, out)
+
+    return out.getvalue()
+
+def uninstall(self):
+    """ Product uninstallation """
+
+    out = StringIO()
+
+    removeConfiglet(self, out)
+
+    out.write('removeSkin... \n')
+    removeSkin(self, [PROJECTNAME, PROJECTNAME+'/2.0.5'])
+
+    return out.getvalue()
Index: qPloneTabs/tags/0.3.1/Extensions/__init__.py
===================================================================
--- qPloneTabs/tags/0.3.1/Extensions/__init__.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/Extensions/__init__.py	(revision 32)
@@ -0,0 +1,1 @@
+# I'm a package ;-)
Index: qPloneTabs/tags/0.3.1/HISTORY.txt
===================================================================
--- qPloneTabs/tags/0.3.1/HISTORY.txt	(revision 32)
+++ qPloneTabs/tags/0.3.1/HISTORY.txt	(revision 32)
@@ -0,0 +1,54 @@
+0.3.1
+
+  * fixed js bug: after cancel of sort action innerHTML return not actual data for updating action to previous state,
+                  now instead of innerHTML property used DOM creation of actions list
+
+  * fixed css bug: when action's url is too long it overlap drag handle on sorting thus disabling drug&drop of this action
+
+0.3.0
+
+  * added support for any actions category
+
+  * fixed reordering bug on server with empty list
+  
+  * hidden reordering controls when the list of actions is empty
+
+0.2.2
+
+  * possibility to change portal_tabs visibility
+  
+  * better url processing
+  
+  * fixed editing bug
+  
+  * corrected some piece of css
+  
+  * fully remade elements state css logic
+  
+  * fixed adding and editing javascript bugs in IE5.5
+  
+  * corrected IE hovering
+  
+  * don't collapse editing and adding forms after sorting
+  
+  * possibility to edit any number of items at the same time
+  
+  * display url in right side near the tab name
+  
+  * generated tabs functionality
+  
+  * sliding 'advanced' section & everything that push down forward content on the page
+
+0.2.0
+
+  * fixed bug with reordering actions after delete
+  
+  * fixed bug with 'Advanced' section
+  
+  * compatible with Plone 2.0.5
+  
+  * focus 'name' text field on editing
+
+0.1.0
+
+  * initial release
Index: qPloneTabs/tags/0.3.1/README.txt
===================================================================
--- qPloneTabs/tags/0.3.1/README.txt	(revision 32)
+++ qPloneTabs/tags/0.3.1/README.txt	(revision 32)
@@ -0,0 +1,20 @@
+Quintagroup Plone Tabs
+
+  The product allows Plone websites to modify portal_tabs
+  actions from portal_control_panel form.
+
+  (c) "Quintagroup":http://quintagroup.com/ , 2006. 
+
+  support@quintagroup.com * quintessence of modern business
+
+Compatible with
+
+  * Firefox
+
+  * IE 5.5 - 6.0
+
+Authors
+
+  * Myroslav Opyr
+
+  * Vitaliy Podoba
Index: qPloneTabs/tags/0.3.1/TODO.txt
===================================================================
--- qPloneTabs/tags/0.3.1/TODO.txt	(revision 32)
+++ qPloneTabs/tags/0.3.1/TODO.txt	(revision 32)
@@ -0,0 +1,3 @@
+  * Opera compatibility
+  * 'portal-globalnav' visualization without page reloading
+  * using kukit framework
Index: qPloneTabs/tags/0.3.1/__init__.py
===================================================================
--- qPloneTabs/tags/0.3.1/__init__.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/__init__.py	(revision 32)
@@ -0,0 +1,11 @@
+from AccessControl import allow_module
+from Products.CMFCore.DirectoryView import registerDirectory
+
+from Products.qPloneTabs.config import SKINS_DIR, GLOBALS
+
+allow_module('Products.qPloneTabs.utils')
+allow_module('Products.qPloneTabs.config')
+registerDirectory(SKINS_DIR, GLOBALS)
+
+from Products.qPloneTabs.utils import getPortalActions
+from Products.qPloneTabs.utils import getRootTabs
Index: qPloneTabs/tags/0.3.1/config.py
===================================================================
--- qPloneTabs/tags/0.3.1/config.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/config.py	(revision 32)
@@ -0,0 +1,13 @@
+from Products.CMFCore import CMFCorePermissions
+
+VIEW_PERMISSION = CMFCorePermissions.ManagePortal
+
+PROJECTNAME = 'qPloneTabs'
+SKINS_DIR = 'skins'
+
+GLOBALS = globals()
+
+PROPERTY_SHEET = 'tabs_properties'
+SHEET_TITLE    = 'Portal Tabs Properties'
+FIELD_NAME = 'titles'
+PROPERTY_FIELD = ['portal_tabs|Portal Tabs Configuration', 'portal_footer|Portal Footer Configuration']
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/2.0.5/prefs_main_template.pt
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/2.0.5/prefs_main_template.pt	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/2.0.5/prefs_main_template.pt	(revision 32)
@@ -0,0 +1,43 @@
+<metal:page define-macro="master">
+  <tal:block metal:use-macro="here/main_template/macros/master">
+
+  <head metal:use-macro="here/header/macros/html_header">
+
+    <metal:fillbase fill-slot="base">
+      <metal:baseslot define-slot="base">
+      </metal:baseslot>
+    </metal:fillbase>
+
+    <metal:headslot fill-slot="head_slot">
+      <metal:headslot define-slot="head_slot" />
+      <tal:override-slot define="global sl python:True" />
+    </metal:headslot>
+
+    <metal:cssslot fill-slot="css_slot">
+      <metal:cssslot define-slot="css_slot" />
+    </metal:cssslot>
+
+    <metal:javascriptslot fill-slot="javascript_head_slot">
+      <metal:javascriptslot define-slot="javascript_head_slot" />
+    </metal:javascriptslot>
+
+  </head>
+
+    <metal:block fill-slot="top_slot"
+                 tal:define="global dummy python:request.set('disable_border',1)" />
+
+    <metal:override fill-slot="portlets_one_slot">
+      <metal:prefs use-macro="here/portlet_prefs/macros/portlet" />
+    </metal:override>
+
+    <metal:override fill-slot="column_two_slot" />
+
+    <div metal:fill-slot="main">
+      <metal:bodytext metal:define-slot="prefs_configlet_main" tal:content="nothing">
+        The content of a normal configlet.
+      </metal:bodytext>
+
+    </div>
+
+  </tal:block>
+</metal:page>
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/2.0.5/prefs_main_template.pt.metadata
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/2.0.5/prefs_main_template.pt.metadata	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/2.0.5/prefs_main_template.pt.metadata	(revision 32)
@@ -0,0 +1,5 @@
+[default]
+title=Group Properties
+
+[security]
+View=0:Authenticated
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/getPageTitle.py
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/getPageTitle.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/getPageTitle.py	(revision 32)
@@ -0,0 +1,33 @@
+## Script (Python) "getPageTitle"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= category='portal_tabs'
+##title=
+##
+
+from Products.CMFCore.utils import getToolByName
+from Products.qPloneTabs.config import PROPERTY_SHEET, FIELD_NAME
+
+portal_props = getToolByName(context, 'portal_properties')
+
+default_title = 'Plone \'%s\' Configuration' % category
+
+if not hasattr(portal_props, PROPERTY_SHEET):
+    return default_title
+
+sheet = getattr(portal_props, PROPERTY_SHEET)
+
+if not hasattr(sheet, FIELD_NAME):
+    return default_title
+
+field = sheet.getProperty(FIELD_NAME)
+dict = {}
+
+for line in field:
+    cat, title = line.split('|', 2)
+    dict[cat] = title
+
+return dict.get(category, None) or default_title
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/behaviour.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/behaviour.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/behaviour.js	(revision 32)
@@ -0,0 +1,254 @@
+/*
+   Behaviour v1.1 by Ben Nolan, June 2005. Based largely on the work
+   of Simon Willison (see comments by Simon below).
+
+   Description:
+   	
+   	Uses css selectors to apply javascript behaviours to enable
+   	unobtrusive javascript in html documents.
+   	
+   Usage:   
+   
+	var myrules = {
+		'b.someclass' : function(element){
+			element.onclick = function(){
+				alert(this.innerHTML);
+			}
+		},
+		'#someid u' : function(element){
+			element.onmouseover = function(){
+				this.innerHTML = "BLAH!";
+			}
+		}
+	};
+	
+	Behaviour.register(myrules);
+	
+	// Call Behaviour.apply() to re-apply the rules (if you
+	// update the dom, etc).
+
+   License:
+   
+   	This file is entirely BSD licensed.
+   	
+   More information:
+   	
+   	http://ripcord.co.nz/behaviour/
+   
+*/   
+
+var Behaviour = {
+	list : new Array,
+	
+	register : function(sheet){
+		Behaviour.list.push(sheet);
+	},
+	
+	start : function(){
+		Behaviour.addLoadEvent(function(){
+			Behaviour.apply();
+		});
+	},
+	
+	apply : function(){
+		for (h=0;sheet=Behaviour.list[h];h++){
+			for (selector in sheet){
+				list = document.getElementsBySelector(selector);
+				
+				if (!list){
+					continue;
+				}
+
+				for (i=0;element=list[i];i++){
+					sheet[selector](element);
+				}
+			}
+		}
+	},
+	
+	addLoadEvent : function(func){
+		var oldonload = window.onload;
+		
+		if (typeof window.onload != 'function') {
+			window.onload = func;
+		} else {
+			window.onload = function() {
+				oldonload();
+				func();
+			}
+		}
+	}
+}
+
+Behaviour.start();
+
+/*
+   The following code is Copyright (C) Simon Willison 2004.
+
+   document.getElementsBySelector(selector)
+   - returns an array of element objects from the current document
+     matching the CSS selector. Selectors can contain element names, 
+     class names and ids and can be nested. For example:
+     
+       elements = document.getElementsBySelect('div#main p a.external')
+     
+     Will return an array of all 'a' elements with 'external' in their 
+     class attribute that are contained inside 'p' elements that are 
+     contained inside the 'div' element which has id="main"
+
+   New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
+   See http://www.w3.org/TR/css3-selectors/#attribute-selectors
+
+   Version 0.4 - Simon Willison, March 25th 2003
+   -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
+   -- Opera 7 fails 
+*/
+
+function getAllChildren(e) {
+  // Returns all children of element. Workaround required for IE5/Windows. Ugh.
+  return e.all ? e.all : e.getElementsByTagName('*');
+}
+
+document.getElementsBySelector = function(selector) {
+  // Attempt to fail gracefully in lesser browsers
+  if (!document.getElementsByTagName) {
+    return new Array();
+  }
+  // Split selector in to tokens
+  var tokens = selector.split(' ');
+  var currentContext = new Array(document);
+  for (var i = 0; i < tokens.length; i++) {
+    token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
+    if (token.indexOf('#') > -1) {
+      // Token is an ID selector
+      var bits = token.split('#');
+      var tagName = bits[0];
+      var id = bits[1];
+      var element = document.getElementById(id);
+      if (tagName && element.nodeName.toLowerCase() != tagName) {
+        // tag with that ID not found, return false
+        return new Array();
+      }
+      // Set currentContext to contain just this element
+      currentContext = new Array(element);
+      continue; // Skip to next token
+    }
+    if (token.indexOf('.') > -1) {
+      // Token contains a class selector
+      var bits = token.split('.');
+      var tagName = bits[0];
+      var className = bits[1];
+      if (!tagName) {
+        tagName = '*';
+      }
+      // Get elements matching tag, filter them for class selector
+      var found = new Array;
+      var foundCount = 0;
+      for (var h = 0; h < currentContext.length; h++) {
+        var elements;
+        if (tagName == '*') {
+            elements = getAllChildren(currentContext[h]);
+        } else {
+            elements = currentContext[h].getElementsByTagName(tagName);
+        }
+        for (var j = 0; j < elements.length; j++) {
+          found[foundCount++] = elements[j];
+        }
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      for (var k = 0; k < found.length; k++) {
+        if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
+          currentContext[currentContextIndex++] = found[k];
+        }
+      }
+      continue; // Skip to next token
+    }
+    // Code to deal with attribute selectors
+    if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
+      var tagName = RegExp.$1;
+      var attrName = RegExp.$2;
+      var attrOperator = RegExp.$3;
+      var attrValue = RegExp.$4;
+      if (!tagName) {
+        tagName = '*';
+      }
+      // Grab all of the tagName elements within current context
+      var found = new Array;
+      var foundCount = 0;
+      for (var h = 0; h < currentContext.length; h++) {
+        var elements;
+        if (tagName == '*') {
+            elements = getAllChildren(currentContext[h]);
+        } else {
+            elements = currentContext[h].getElementsByTagName(tagName);
+        }
+        for (var j = 0; j < elements.length; j++) {
+          found[foundCount++] = elements[j];
+        }
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      var checkFunction; // This function will be used to filter the elements
+      switch (attrOperator) {
+        case '=': // Equality
+          checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
+          break;
+        case '~': // Match one of space seperated words 
+          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
+          break;
+        case '|': // Match start with value followed by optional hyphen
+          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
+          break;
+        case '^': // Match starts with value
+          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
+          break;
+        case '$': // Match ends with value - fails with "Warning" in Opera 7
+          checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
+          break;
+        case '*': // Match ends with value
+          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
+          break;
+        default :
+          // Just test for existence of attribute
+          checkFunction = function(e) { return e.getAttribute(attrName); };
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      for (var k = 0; k < found.length; k++) {
+        if (checkFunction(found[k])) {
+          currentContext[currentContextIndex++] = found[k];
+        }
+      }
+      // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
+      continue; // Skip to next token
+    }
+    
+    if (!currentContext[0]){
+    	return;
+    }
+    
+    // If we get here, token is JUST an element (not a class or ID selector)
+    tagName = token;
+    var found = new Array;
+    var foundCount = 0;
+    for (var h = 0; h < currentContext.length; h++) {
+      var elements = currentContext[h].getElementsByTagName(tagName);
+      for (var j = 0; j < elements.length; j++) {
+        found[foundCount++] = elements[j];
+      }
+    }
+    currentContext = found;
+  }
+  return currentContext;
+}
+
+/* That revolting regular expression explained 
+/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
+  \---/  \---/\-------------/    \-------/
+    |      |         |               |
+    |      |         |           The value
+    |      |    ~,|,^,$,* or =
+    |   Attribute 
+   Tag
+*/
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/builder.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/builder.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/builder.js	(revision 32)
@@ -0,0 +1,101 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// See scriptaculous.js for full license.
+
+var Builder = {
+  NODEMAP: {
+    AREA: 'map',
+    CAPTION: 'table',
+    COL: 'table',
+    COLGROUP: 'table',
+    LEGEND: 'fieldset',
+    OPTGROUP: 'select',
+    OPTION: 'select',
+    PARAM: 'object',
+    TBODY: 'table',
+    TD: 'table',
+    TFOOT: 'table',
+    TH: 'table',
+    THEAD: 'table',
+    TR: 'table'
+  },
+  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
+  //       due to a Firefox bug
+  node: function(elementName) {
+    elementName = elementName.toUpperCase();
+    
+    // try innerHTML approach
+    var parentTag = this.NODEMAP[elementName] || 'div';
+    var parentElement = document.createElement(parentTag);
+    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
+    } catch(e) {}
+    var element = parentElement.firstChild || null;
+      
+    // see if browser added wrapping tags
+    if(element && (element.tagName != elementName))
+      element = element.getElementsByTagName(elementName)[0];
+    
+    // fallback to createElement approach
+    if(!element) element = document.createElement(elementName);
+    
+    // abort if nothing could be created
+    if(!element) return;
+
+    // attributes (or text)
+    if(arguments[1])
+      if(this._isStringOrNumber(arguments[1]) ||
+        (arguments[1] instanceof Array)) {
+          this._children(element, arguments[1]);
+        } else {
+          var attrs = this._attributes(arguments[1]);
+          if(attrs.length) {
+            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+              parentElement.innerHTML = "<" +elementName + " " +
+                attrs + "></" + elementName + ">";
+            } catch(e) {}
+            element = parentElement.firstChild || null;
+            // workaround firefox 1.0.X bug
+            if(!element) {
+              element = document.createElement(elementName);
+              for(attr in arguments[1]) 
+                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
+            }
+            if(element.tagName != elementName)
+              element = parentElement.getElementsByTagName(elementName)[0];
+            }
+        } 
+
+    // text, or array of children
+    if(arguments[2])
+      this._children(element, arguments[2]);
+
+     return element;
+  },
+  _text: function(text) {
+     return document.createTextNode(text);
+  },
+  _attributes: function(attributes) {
+    var attrs = [];
+    for(attribute in attributes)
+      attrs.push((attribute=='className' ? 'class' : attribute) +
+          '="' + attributes[attribute].toString().escapeHTML() + '"');
+    return attrs.join(" ");
+  },
+  _children: function(element, children) {
+    if(typeof children=='object') { // array can hold nodes and text
+      children.flatten().each( function(e) {
+        if(typeof e=='object')
+          element.appendChild(e)
+        else
+          if(Builder._isStringOrNumber(e))
+            element.appendChild(Builder._text(e));
+      });
+    } else
+      if(Builder._isStringOrNumber(children)) 
+         element.appendChild(Builder._text(children));
+  },
+  _isStringOrNumber: function(param) {
+    return(typeof param=='string' || typeof param=='number');
+  }
+}
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/controls.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/controls.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/controls.js	(revision 32)
@@ -0,0 +1,750 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+//  Rob Wills
+// 
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality 
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least, 
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method 
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most 
+// useful when one of the tokens is \n (a newline), as it 
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+  baseInitialize: function(element, update, options) {
+    this.element     = $(element); 
+    this.update      = $(update);  
+    this.hasFocus    = false; 
+    this.changed     = false; 
+    this.active      = false; 
+    this.index       = 0;     
+    this.entryCount  = 0;
+
+    if (this.setOptions)
+      this.setOptions(options);
+    else
+      this.options = options || {};
+
+    this.options.paramName    = this.options.paramName || this.element.name;
+    this.options.tokens       = this.options.tokens || [];
+    this.options.frequency    = this.options.frequency || 0.4;
+    this.options.minChars     = this.options.minChars || 1;
+    this.options.onShow       = this.options.onShow || 
+    function(element, update){ 
+      if(!update.style.position || update.style.position=='absolute') {
+        update.style.position = 'absolute';
+        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+      }
+      Effect.Appear(update,{duration:0.15});
+    };
+    this.options.onHide = this.options.onHide || 
+    function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+    if (typeof(this.options.tokens) == 'string') 
+      this.options.tokens = new Array(this.options.tokens);
+
+    this.observer = null;
+    
+    this.element.setAttribute('autocomplete','off');
+
+    Element.hide(this.update);
+
+    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+  },
+
+  show: function() {
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix && 
+      (navigator.appVersion.indexOf('MSIE')>0) &&
+      (navigator.userAgent.indexOf('Opera')<0) &&
+      (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
+       '<iframe id="' + this.update.id + '_iefix" '+
+       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+      this.iefix = $(this.update.id+'_iefix');
+    }
+    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+  },
+  
+  fixIEOverlapping: function() {
+    Position.clone(this.update, this.iefix);
+    this.iefix.style.zIndex = 1;
+    this.update.style.zIndex = 2;
+    Element.show(this.iefix);
+  },
+
+  hide: function() {
+    this.stopIndicator();
+    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+    if(this.iefix) Element.hide(this.iefix);
+  },
+
+  startIndicator: function() {
+    if(this.options.indicator) Element.show(this.options.indicator);
+  },
+
+  stopIndicator: function() {
+    if(this.options.indicator) Element.hide(this.options.indicator);
+  },
+
+  onKeyPress: function(event) {
+    if(this.active)
+      switch(event.keyCode) {
+       case Event.KEY_TAB:
+       case Event.KEY_RETURN:
+         this.selectEntry();
+         Event.stop(event);
+       case Event.KEY_ESC:
+         this.hide();
+         this.active = false;
+         Event.stop(event);
+         return;
+       case Event.KEY_LEFT:
+       case Event.KEY_RIGHT:
+         return;
+       case Event.KEY_UP:
+         this.markPrevious();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+       case Event.KEY_DOWN:
+         this.markNext();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+      }
+     else 
+      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
+        return;
+
+    this.changed = true;
+    this.hasFocus = true;
+
+    if(this.observer) clearTimeout(this.observer);
+      this.observer = 
+        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+  },
+
+  onHover: function(event) {
+    var element = Event.findElement(event, 'LI');
+    if(this.index != element.autocompleteIndex) 
+    {
+        this.index = element.autocompleteIndex;
+        this.render();
+    }
+    Event.stop(event);
+  },
+  
+  onClick: function(event) {
+    var element = Event.findElement(event, 'LI');
+    this.index = element.autocompleteIndex;
+    this.selectEntry();
+    this.hide();
+  },
+  
+  onBlur: function(event) {
+    // needed to make click events working
+    setTimeout(this.hide.bind(this), 250);
+    this.hasFocus = false;
+    this.active = false;     
+  }, 
+  
+  render: function() {
+    if(this.entryCount > 0) {
+      for (var i = 0; i < this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),"selected") : 
+          Element.removeClassName(this.getEntry(i),"selected");
+        
+      if(this.hasFocus) { 
+        this.show();
+        this.active = true;
+      }
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+  
+  markPrevious: function() {
+    if(this.index > 0) this.index--
+      else this.index = this.entryCount-1;
+  },
+  
+  markNext: function() {
+    if(this.index < this.entryCount-1) this.index++
+      else this.index = 0;
+  },
+  
+  getEntry: function(index) {
+    return this.update.firstChild.childNodes[index];
+  },
+  
+  getCurrentEntry: function() {
+    return this.getEntry(this.index);
+  },
+  
+  selectEntry: function() {
+    this.active = false;
+    this.updateElement(this.getCurrentEntry());
+  },
+
+  updateElement: function(selectedElement) {
+    if (this.options.updateElement) {
+      this.options.updateElement(selectedElement);
+      return;
+    }
+
+    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    var lastTokenPos = this.findLastToken();
+    if (lastTokenPos != -1) {
+      var newValue = this.element.value.substr(0, lastTokenPos + 1);
+      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+      if (whitespace)
+        newValue += whitespace[0];
+      this.element.value = newValue + value;
+    } else {
+      this.element.value = value;
+    }
+    this.element.focus();
+    
+    if (this.options.afterUpdateElement)
+      this.options.afterUpdateElement(this.element, selectedElement);
+  },
+
+  updateChoices: function(choices) {
+    if(!this.changed && this.hasFocus) {
+      this.update.innerHTML = choices;
+      Element.cleanWhitespace(this.update);
+      Element.cleanWhitespace(this.update.firstChild);
+
+      if(this.update.firstChild && this.update.firstChild.childNodes) {
+        this.entryCount = 
+          this.update.firstChild.childNodes.length;
+        for (var i = 0; i < this.entryCount; i++) {
+          var entry = this.getEntry(i);
+          entry.autocompleteIndex = i;
+          this.addObservers(entry);
+        }
+      } else { 
+        this.entryCount = 0;
+      }
+
+      this.stopIndicator();
+
+      this.index = 0;
+      this.render();
+    }
+  },
+
+  addObservers: function(element) {
+    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+  },
+
+  onObserverEvent: function() {
+    this.changed = false;   
+    if(this.getToken().length>=this.options.minChars) {
+      this.startIndicator();
+      this.getUpdatedChoices();
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+
+  getToken: function() {
+    var tokenPos = this.findLastToken();
+    if (tokenPos != -1)
+      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+    else
+      var ret = this.element.value;
+
+    return /\n/.test(ret) ? '' : ret;
+  },
+
+  findLastToken: function() {
+    var lastTokenPos = -1;
+
+    for (var i=0; i<this.options.tokens.length; i++) {
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos > lastTokenPos)
+        lastTokenPos = thisTokenPos;
+    }
+    return lastTokenPos;
+  }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+  initialize: function(element, update, url, options) {
+	  this.baseInitialize(element, update, options);
+    this.options.asynchronous  = true;
+    this.options.onComplete    = this.onComplete.bind(this);
+    this.options.defaultParams = this.options.parameters || null;
+    this.url                   = url;
+  },
+
+  getUpdatedChoices: function() {
+    entry = encodeURIComponent(this.options.paramName) + '=' + 
+      encodeURIComponent(this.getToken());
+
+    this.options.parameters = this.options.callback ?
+      this.options.callback(this.element, entry) : entry;
+
+    if(this.options.defaultParams) 
+      this.options.parameters += '&' + this.options.defaultParams;
+
+    new Ajax.Request(this.url, this.options);
+  },
+
+  onComplete: function(request) {
+    this.updateChoices(request.responseText);
+  }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+//                    text only at the beginning of strings in the 
+//                    autocomplete array. Defaults to true, which will
+//                    match text at the beginning of any *word* in the
+//                    strings in the autocomplete array. If you want to
+//                    search anywhere in the string, additionally set
+//                    the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+//                   a partial match (unlike minChars, which defines
+//                   how many characters are required to do any match
+//                   at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+//                 Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector' 
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+  initialize: function(element, update, array, options) {
+    this.baseInitialize(element, update, options);
+    this.options.array = array;
+  },
+
+  getUpdatedChoices: function() {
+    this.updateChoices(this.options.selector(this));
+  },
+
+  setOptions: function(options) {
+    this.options = Object.extend({
+      choices: 10,
+      partialSearch: true,
+      partialChars: 2,
+      ignoreCase: true,
+      fullSearch: false,
+      selector: function(instance) {
+        var ret       = []; // Beginning matches
+        var partial   = []; // Inside matches
+        var entry     = instance.getToken();
+        var count     = 0;
+
+        for (var i = 0; i < instance.options.array.length &&  
+          ret.length < instance.options.choices ; i++) { 
+
+          var elem = instance.options.array[i];
+          var foundPos = instance.options.ignoreCase ? 
+            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
+            elem.indexOf(entry);
+
+          while (foundPos != -1) {
+            if (foundPos == 0 && elem.length != entry.length) { 
+              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
+                elem.substr(entry.length) + "</li>");
+              break;
+            } else if (entry.length >= instance.options.partialChars && 
+              instance.options.partialSearch && foundPos != -1) {
+              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+                  foundPos + entry.length) + "</li>");
+                break;
+              }
+            }
+
+            foundPos = instance.options.ignoreCase ? 
+              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
+              elem.indexOf(entry, foundPos + 1);
+
+          }
+        }
+        if (partial.length)
+          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+        return "<ul>" + ret.join('') + "</ul>";
+      }
+    }, options || {});
+  }
+});
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+  setTimeout(function() {
+    Field.activate(field);
+  }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+  initialize: function(element, url, options) {
+    this.url = url;
+    this.element = $(element);
+
+    this.options = Object.extend({
+      okText: "ok",
+      cancelText: "cancel",
+      savingText: "Saving...",
+      clickToEditText: "Click to edit",
+      okText: "ok",
+      rows: 1,
+      onComplete: function(transport, element) {
+        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+      },
+      onFailure: function(transport) {
+        alert("Error communicating with the server: " + transport.responseText.stripTags());
+      },
+      callback: function(form) {
+        return Form.serialize(form);
+      },
+      handleLineBreaks: true,
+      loadingText: 'Loading...',
+      savingClassName: 'inplaceeditor-saving',
+      loadingClassName: 'inplaceeditor-loading',
+      formClassName: 'inplaceeditor-form',
+      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+      highlightendcolor: "#FFFFFF",
+      externalControl:	null,
+      ajaxOptions: {}
+    }, options || {});
+
+    if(!this.options.formId && this.element.id) {
+      this.options.formId = this.element.id + "-inplaceeditor";
+      if ($(this.options.formId)) {
+        // there's already a form with that name, don't specify an id
+        this.options.formId = null;
+      }
+    }
+    
+    if (this.options.externalControl) {
+      this.options.externalControl = $(this.options.externalControl);
+    }
+    
+    this.originalBackground = Element.getStyle(this.element, 'background-color');
+    if (!this.originalBackground) {
+      this.originalBackground = "transparent";
+    }
+    
+    this.element.title = this.options.clickToEditText;
+    
+    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+    Event.observe(this.element, 'click', this.onclickListener);
+    Event.observe(this.element, 'mouseover', this.mouseoverListener);
+    Event.observe(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.observe(this.options.externalControl, 'click', this.onclickListener);
+      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  },
+  enterEditMode: function(evt) {
+    if (this.saving) return;
+    if (this.editing) return;
+    this.editing = true;
+    this.onEnterEditMode();
+    if (this.options.externalControl) {
+      Element.hide(this.options.externalControl);
+    }
+    Element.hide(this.element);
+    this.createForm();
+    this.element.parentNode.insertBefore(this.form, this.element);
+    Field.scrollFreeActivate(this.editField);
+    // stop the event to avoid a page refresh in Safari
+    if (evt) {
+      Event.stop(evt);
+    }
+    return false;
+  },
+  createForm: function() {
+    this.form = document.createElement("form");
+    this.form.id = this.options.formId;
+    Element.addClassName(this.form, this.options.formClassName)
+    this.form.onsubmit = this.onSubmit.bind(this);
+
+    this.createEditField();
+
+    if (this.options.textarea) {
+      var br = document.createElement("br");
+      this.form.appendChild(br);
+    }
+
+    okButton = document.createElement("input");
+    okButton.type = "submit";
+    okButton.value = this.options.okText;
+    this.form.appendChild(okButton);
+
+    cancelLink = document.createElement("a");
+    cancelLink.href = "#";
+    cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+    cancelLink.onclick = this.onclickCancel.bind(this);
+    this.form.appendChild(cancelLink);
+  },
+  hasHTMLLineBreaks: function(string) {
+    if (!this.options.handleLineBreaks) return false;
+    return string.match(/<br/i) || string.match(/<p>/i);
+  },
+  convertHTMLLineBreaks: function(string) {
+    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+  },
+  createEditField: function() {
+    var text;
+    if(this.options.loadTextURL) {
+      text = this.options.loadingText;
+    } else {
+      text = this.getText();
+    }
+    
+    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+      this.options.textarea = false;
+      var textField = document.createElement("input");
+      textField.type = "text";
+      textField.name = "value";
+      textField.value = text;
+      textField.style.backgroundColor = this.options.highlightcolor;
+      var size = this.options.size || this.options.cols || 0;
+      if (size != 0) textField.size = size;
+      this.editField = textField;
+    } else {
+      this.options.textarea = true;
+      var textArea = document.createElement("textarea");
+      textArea.name = "value";
+      textArea.value = this.convertHTMLLineBreaks(text);
+      textArea.rows = this.options.rows;
+      textArea.cols = this.options.cols || 40;
+      this.editField = textArea;
+    }
+    
+    if(this.options.loadTextURL) {
+      this.loadExternalText();
+    }
+    this.form.appendChild(this.editField);
+  },
+  getText: function() {
+    return this.element.innerHTML;
+  },
+  loadExternalText: function() {
+    Element.addClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = true;
+    new Ajax.Request(
+      this.options.loadTextURL,
+      Object.extend({
+        asynchronous: true,
+        onComplete: this.onLoadedExternalText.bind(this)
+      }, this.options.ajaxOptions)
+    );
+  },
+  onLoadedExternalText: function(transport) {
+    Element.removeClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = false;
+    this.editField.value = transport.responseText.stripTags();
+  },
+  onclickCancel: function() {
+    this.onComplete();
+    this.leaveEditMode();
+    return false;
+  },
+  onFailure: function(transport) {
+    this.options.onFailure(transport);
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+      this.oldInnerHTML = null;
+    }
+    return false;
+  },
+  onSubmit: function() {
+    // onLoading resets these so we need to save them away for the Ajax call
+    var form = this.form;
+    var value = this.editField.value;
+    
+    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+    // to be displayed indefinitely
+    this.onLoading();
+    
+    new Ajax.Updater(
+      { 
+        success: this.element,
+         // don't update on failure (this could be an option)
+        failure: null
+      },
+      this.url,
+      Object.extend({
+        parameters: this.options.callback(form, value),
+        onComplete: this.onComplete.bind(this),
+        onFailure: this.onFailure.bind(this)
+      }, this.options.ajaxOptions)
+    );
+    // stop the event to avoid a page refresh in Safari
+    if (arguments.length > 1) {
+      Event.stop(arguments[0]);
+    }
+    return false;
+  },
+  onLoading: function() {
+    this.saving = true;
+    this.removeForm();
+    this.leaveHover();
+    this.showSaving();
+  },
+  showSaving: function() {
+    this.oldInnerHTML = this.element.innerHTML;
+    this.element.innerHTML = this.options.savingText;
+    Element.addClassName(this.element, this.options.savingClassName);
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+  },
+  removeForm: function() {
+    if(this.form) {
+      if (this.form.parentNode) Element.remove(this.form);
+      this.form = null;
+    }
+  },
+  enterHover: function() {
+    if (this.saving) return;
+    this.element.style.backgroundColor = this.options.highlightcolor;
+    if (this.effect) {
+      this.effect.cancel();
+    }
+    Element.addClassName(this.element, this.options.hoverClassName)
+  },
+  leaveHover: function() {
+    if (this.options.backgroundColor) {
+      this.element.style.backgroundColor = this.oldBackground;
+    }
+    Element.removeClassName(this.element, this.options.hoverClassName)
+    if (this.saving) return;
+    this.effect = new Effect.Highlight(this.element, {
+      startcolor: this.options.highlightcolor,
+      endcolor: this.options.highlightendcolor,
+      restorecolor: this.originalBackground
+    });
+  },
+  leaveEditMode: function() {
+    Element.removeClassName(this.element, this.options.savingClassName);
+    this.removeForm();
+    this.leaveHover();
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+    if (this.options.externalControl) {
+      Element.show(this.options.externalControl);
+    }
+    this.editing = false;
+    this.saving = false;
+    this.oldInnerHTML = null;
+    this.onLeaveEditMode();
+  },
+  onComplete: function(transport) {
+    this.leaveEditMode();
+    this.options.onComplete.bind(this)(transport, this.element);
+  },
+  onEnterEditMode: function() {},
+  onLeaveEditMode: function() {},
+  dispose: function() {
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+    }
+    this.leaveEditMode();
+    Event.stopObserving(this.element, 'click', this.onclickListener);
+    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  }
+};
+
+// Delayed observer, like Form.Element.Observer, 
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+  initialize: function(element, delay, callback) {
+    this.delay     = delay || 0.5;
+    this.element   = $(element);
+    this.callback  = callback;
+    this.timer     = null;
+    this.lastValue = $F(this.element); 
+    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+  },
+  delayedListener: function(event) {
+    if(this.lastValue == $F(this.element)) return;
+    if(this.timer) clearTimeout(this.timer);
+    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+    this.lastValue = $F(this.element);
+  },
+  onTimerEvent: function() {
+    this.timer = null;
+    this.callback(this.element, $F(this.element));
+  }
+};
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/dragdrop.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/dragdrop.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/dragdrop.js	(revision 32)
@@ -0,0 +1,559 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// See scriptaculous.js for full license.
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+  drops: [],
+
+  remove: function(element) {
+    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+  },
+
+  add: function(element) {
+    element = $(element);
+    var options = Object.extend({
+      greedy:     true,
+      hoverclass: null  
+    }, arguments[1] || {});
+
+    // cache containers
+    if(options.containment) {
+      options._containers = [];
+      var containment = options.containment;
+      if((typeof containment == 'object') && 
+        (containment.constructor == Array)) {
+        containment.each( function(c) { options._containers.push($(c)) });
+      } else {
+        options._containers.push($(containment));
+      }
+    }
+    
+    if(options.accept) options.accept = [options.accept].flatten();
+
+    Element.makePositioned(element); // fix IE
+    options.element = element;
+
+    this.drops.push(options);
+  },
+
+  isContained: function(element, drop) {
+    var parentNode = element.parentNode;
+    return drop._containers.detect(function(c) { return parentNode == c });
+  },
+
+  isAffected: function(pX, pY, element, drop) {
+    return (
+      (drop.element!=element) &&
+      ((!drop._containers) ||
+        this.isContained(element, drop)) &&
+      ((!drop.accept) ||
+        (Element.classNames(element).detect( 
+          function(v) { return drop.accept.include(v) } ) )) &&
+      Position.within(drop.element, pX, pY) );
+  },
+
+  deactivate: function(drop) {
+    if(drop.hoverclass)
+      Element.removeClassName(drop.element, drop.hoverclass);
+    this.last_active = null;
+  },
+
+  activate: function(drop) {
+    if(this.last_active) this.deactivate(this.last_active);
+    if(drop.hoverclass)
+      Element.addClassName(drop.element, drop.hoverclass);
+    this.last_active = drop;
+  },
+
+  show: function(event, element) {
+    if(!this.drops.length) return;
+    var pX = Event.pointerX(event);
+    var pY = Event.pointerY(event);
+    Position.prepare();
+
+    var i = this.drops.length-1; do {
+      var drop = this.drops[i];
+      if(this.isAffected(pX, pY, element, drop)) {
+        if(drop.onHover)
+           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+        if(drop.greedy) { 
+          this.activate(drop);
+          return;
+        }
+      }
+    } while (i--);
+    
+    if(this.last_active) this.deactivate(this.last_active);
+  },
+
+  fire: function(event, element) {
+    if(!this.last_active) return;
+    Position.prepare();
+
+    if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
+      if (this.last_active.onDrop) 
+        this.last_active.onDrop(element, this.last_active.element, event);
+  },
+
+  reset: function() {
+    if(this.last_active)
+      this.deactivate(this.last_active);
+  }
+}
+
+var Draggables = {
+  observers: [],
+  addObserver: function(observer) {
+    this.observers.push(observer);
+    this._cacheObserverCallbacks();
+  },
+  removeObserver: function(element) {  // element instead of observer fixes mem leaks
+    this.observers = this.observers.reject( function(o) { return o.element==element });
+    this._cacheObserverCallbacks();
+  },
+  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
+    if(this[eventName+'Count'] > 0)
+      this.observers.each( function(o) {
+        if(o[eventName]) o[eventName](eventName, draggable, event);
+      });
+  },
+  _cacheObserverCallbacks: function() {
+    ['onStart','onEnd','onDrag'].each( function(eventName) {
+      Draggables[eventName+'Count'] = Draggables.observers.select(
+        function(o) { return o[eventName]; }
+      ).length;
+    });
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable.prototype = {
+  initialize: function(element) {
+    var options = Object.extend({
+      handle: false,
+      starteffect: function(element) { 
+        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
+      },
+      reverteffect: function(element, top_offset, left_offset) {
+        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+        new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
+      },
+      endeffect: function(element) { 
+         new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
+      },
+      zindex: 1000,
+      revert: false,
+      snap: false   // false, or xy or [x,y] or function(x,y){ return [x,y] }
+    }, arguments[1] || {});
+
+    this.element      = $(element);
+    if(options.handle && (typeof options.handle == 'string'))
+      this.handle = Element.childrenWithClassName(this.element, options.handle)[0];
+      
+    if(!this.handle) this.handle = $(options.handle);
+    if(!this.handle) this.handle = this.element;
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.offsetX      = 0;
+    this.offsetY      = 0;
+    this.originalLeft = this.currentLeft();
+    this.originalTop  = this.currentTop();
+    this.originalX    = this.element.offsetLeft;
+    this.originalY    = this.element.offsetTop;
+
+    this.options      = options;
+
+    this.active       = false;
+    this.dragging     = false;   
+
+    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+    this.eventMouseMove = this.update.bindAsEventListener(this);
+    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+    
+    this.registerEvents();
+  },
+  destroy: function() {
+    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+    this.unregisterEvents();
+  },
+  registerEvents: function() {
+    Event.observe(document, "mouseup", this.eventMouseUp);
+    Event.observe(document, "mousemove", this.eventMouseMove);
+    Event.observe(document, "keypress", this.eventKeypress);
+    Event.observe(this.handle, "mousedown", this.eventMouseDown);
+  },
+  unregisterEvents: function() {
+    //if(!this.active) return;
+    //Event.stopObserving(document, "mouseup", this.eventMouseUp);
+    //Event.stopObserving(document, "mousemove", this.eventMouseMove);
+    //Event.stopObserving(document, "keypress", this.eventKeypress);
+  },
+  currentLeft: function() {
+    return parseInt(this.element.style.left || '0');
+  },
+  currentTop: function() {
+    return parseInt(this.element.style.top || '0')
+  },
+  startDrag: function(event) {
+    if(Event.isLeftClick(event)) {
+      
+      // abort on form elements, fixes a Firefox issue
+      var src = Event.element(event);
+      if(src.tagName && (
+        src.tagName=='INPUT' ||
+        src.tagName=='SELECT' ||
+        src.tagName=='BUTTON' ||
+        src.tagName=='TEXTAREA')) return;
+      
+      // this.registerEvents();
+      this.active = true;
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var offsets = Position.cumulativeOffset(this.element);
+      this.offsetX =  (pointer[0] - offsets[0]);
+      this.offsetY =  (pointer[1] - offsets[1]);
+      Event.stop(event);
+    }
+  },
+  finishDrag: function(event, success) {
+    // this.unregisterEvents();
+
+    this.active = false;
+    this.dragging = false;
+
+    if(this.options.ghosting) {
+      Position.relativize(this.element);
+      Element.remove(this._clone);
+      this._clone = null;
+    }
+
+    if(success) Droppables.fire(event, this.element);
+    Draggables.notify('onEnd', this, event);
+
+    var revert = this.options.revert;
+    if(revert && typeof revert == 'function') revert = revert(this.element);
+
+    if(revert && this.options.reverteffect) {
+      this.options.reverteffect(this.element, 
+      this.currentTop()-this.originalTop,
+      this.currentLeft()-this.originalLeft);
+    } else {
+      this.originalLeft = this.currentLeft();
+      this.originalTop  = this.currentTop();
+    }
+
+    if(this.options.zindex)
+      this.element.style.zIndex = this.originalZ;
+
+    if(this.options.endeffect) 
+      this.options.endeffect(this.element);
+
+
+    Droppables.reset();
+  },
+  keyPress: function(event) {
+    if(this.active) {
+      if(event.keyCode==Event.KEY_ESC) {
+        this.finishDrag(event, false);
+        Event.stop(event);
+      }
+    }
+  },
+  endDrag: function(event) {
+    if(this.active && this.dragging) {
+      this.finishDrag(event, true);
+      Event.stop(event);
+    }
+    this.active = false;
+    this.dragging = false;
+  },
+  draw: function(event) {
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    var offsets = Position.cumulativeOffset(this.element);
+    offsets[0] -= this.currentLeft();
+    offsets[1] -= this.currentTop();
+    var style = this.element.style;
+    
+    var pos = [
+      (pointer[0] - offsets[0] - this.offsetX),
+      (pointer[1] - offsets[1] - this.offsetY)];
+    
+    if(this.options.snap) {
+      if(typeof this.options.snap == 'function') {
+        pos = this.options.snap(pos[0],pos[1]);
+      } else {
+      var draggable = this;
+      if(this.options.snap instanceof Array) {
+        pos = pos.collect( function(v, i) {
+          return Math.round(v/draggable.options.snap[i])*draggable.options.snap[i] })
+      } else {
+        pos = pos.collect( function(v) {
+          return Math.round(v/draggable.options.snap)*draggable.options.snap })
+      }
+    }}
+    
+    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+      style.left = pos[0] + "px";
+    if((!this.options.constraint) || (this.options.constraint=='vertical'))
+      style.top  = pos[1] + "px";
+    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+  },
+  update: function(event) {
+   if(this.active) {
+      if(!this.dragging) {
+        var style = this.element.style;
+        this.dragging = true;
+        
+        if(Element.getStyle(this.element,'position')=='') 
+          style.position = "relative";
+        
+        if(this.options.zindex) {
+          this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+          style.zIndex = this.options.zindex;
+        }
+
+        if(this.options.ghosting) {
+          this._clone = this.element.cloneNode(true);
+          Position.absolutize(this.element);
+          this.element.parentNode.insertBefore(this._clone, this.element);
+        }
+
+        Draggables.notify('onStart', this, event);
+        if(this.options.starteffect) this.options.starteffect(this.element);
+      }
+
+      Droppables.show(event, this.element);
+      Draggables.notify('onDrag', this, event);
+      this.draw(event);
+      if(this.options.change) this.options.change(this);
+
+      // fix AppleWebKit rendering
+      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 
+
+      Event.stop(event);
+   }
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+  initialize: function(element, observer) {
+    this.element   = $(element);
+    this.observer  = observer;
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  onStart: function() {
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  onEnd: function() {
+    Sortable.unmark();
+    if(this.lastValue != Sortable.serialize(this.element))
+      this.observer(this.element)
+  }
+}
+
+var Sortable = {
+  sortables: new Array(),
+  options: function(element){
+    element = $(element);
+    return this.sortables.detect(function(s) { return s.element == element });
+  },
+  destroy: function(element){
+    element = $(element);
+    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
+      Draggables.removeObserver(s.element);
+      s.droppables.each(function(d){ Droppables.remove(d) });
+      s.draggables.invoke('destroy');
+    });
+    this.sortables = this.sortables.reject(function(s) { return s.element == element });
+  },
+  create: function(element) {
+    element = $(element);
+    var options = Object.extend({ 
+      element:     element,
+      tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,      // fixme: unimplemented
+      overlap:     'vertical', // one of 'vertical', 'horizontal'
+      constraint:  'vertical', // one of 'vertical', 'horizontal', false
+      containment: element,    // also takes array of elements (or id's); or false
+      handle:      false,      // or a CSS class
+      only:        false,
+      hoverclass:  null,
+      ghosting:    false,
+      format:      null,
+      onChange:    Prototype.emptyFunction,
+      onUpdate:    Prototype.emptyFunction
+    }, arguments[1] || {});
+
+    // clear any old sortable with same element
+    this.destroy(element);
+
+    // build options for the draggables
+    var options_for_draggable = {
+      revert:      true,
+      ghosting:    options.ghosting,
+      constraint:  options.constraint,
+      handle:      options.handle };
+
+    if(options.starteffect)
+      options_for_draggable.starteffect = options.starteffect;
+
+    if(options.reverteffect)
+      options_for_draggable.reverteffect = options.reverteffect;
+    else
+      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+        element.style.top  = 0;
+        element.style.left = 0;
+      };
+
+    if(options.endeffect)
+      options_for_draggable.endeffect = options.endeffect;
+
+    if(options.zindex)
+      options_for_draggable.zindex = options.zindex;
+
+    // build options for the droppables  
+    var options_for_droppable = {
+      overlap:     options.overlap,
+      containment: options.containment,
+      hoverclass:  options.hoverclass,
+      onHover:     Sortable.onHover,
+      greedy:      !options.dropOnEmpty
+    }
+
+    // fix for gecko engine
+    Element.cleanWhitespace(element); 
+
+    options.draggables = [];
+    options.droppables = [];
+
+    // make it so
+
+    // drop on empty handling
+    if(options.dropOnEmpty) {
+      Droppables.add(element,
+        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
+      options.droppables.push(element);
+    }
+
+    (this.findElements(element, options) || []).each( function(e) {
+      // handles are per-draggable
+      var handle = options.handle ? 
+        Element.childrenWithClassName(e, options.handle)[0] : e;    
+      options.draggables.push(
+        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+      Droppables.add(e, options_for_droppable);
+      options.droppables.push(e);      
+    });
+
+    // keep reference
+    this.sortables.push(options);
+
+    // for onupdate
+    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+  },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    if(!element.hasChildNodes()) return null;
+    var elements = [];
+    $A(element.childNodes).each( function(e) {
+      if(e.tagName && e.tagName==options.tag.toUpperCase() &&
+        (!options.only || (Element.hasClassName(e, options.only))))
+          elements.push(e);
+      if(options.tree) {
+        var grandchildren = this.findElements(e, options);
+        if(grandchildren) elements.push(grandchildren);
+      }
+    });
+
+    return (elements.length>0 ? elements.flatten() : null);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(overlap>0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, dropon);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    } else {
+      Sortable.mark(dropon, 'after');
+      var nextElement = dropon.nextSibling || null;
+      if(nextElement != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, nextElement);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    }
+  },
+
+  onEmptyHover: function(element, dropon) {
+    if(element.parentNode!=dropon) {
+      var oldParentNode = element.parentNode;
+      dropon.appendChild(element);
+      Sortable.options(oldParentNode).onChange(element);
+      Sortable.options(dropon).onChange(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Element.hide(Sortable._marker);
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable && !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = $('dropmarker') || document.createElement('DIV');
+      Element.hide(Sortable._marker);
+      Element.addClassName(Sortable._marker, 'dropmarker');
+      Sortable._marker.style.position = 'absolute';
+      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.style.left = offsets[0] + 'px';
+    Sortable._marker.style.top = offsets[1] + 'px';
+    
+    if(position=='after')
+      if(sortable.overlap == 'horizontal') 
+        Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
+      else
+        Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+    
+    Element.show(Sortable._marker);
+  },
+
+  serialize: function(element) {
+    element = $(element);
+    var sortableOptions = this.options(element);
+    var options = Object.extend({
+      tag:  sortableOptions.tag,
+      only: sortableOptions.only,
+      name: element.id,
+      format: sortableOptions.format || /^[^_]*_(.*)$/
+    }, arguments[1] || {});
+    return $(this.findElements(element, options) || []).collect( function(item) {
+      return (encodeURIComponent(options.name) + "[]=" + 
+              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
+    }).join("&");
+  }
+} 
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/effects.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/effects.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/effects.js	(revision 32)
@@ -0,0 +1,904 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+//  Justin Palmer (http://encytemedia.com/)
+//  Mark Pilgrim (http://diveintomark.org/)
+//  Martin Bialasinki
+// 
+// See scriptaculous.js for full license.  
+
+/* ------------- element ext -------------- */  
+ 
+// converts rgb() and #xxx to #xxxxxx format,  
+// returns self (or first argument) if not convertable  
+String.prototype.parseColor = function() {  
+  color = "#";  
+  if(this.slice(0,4) == "rgb(") {  
+    var cols = this.slice(4,this.length-1).split(',');  
+    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
+  } else {  
+    if(this.slice(0,1) == '#') {  
+      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
+      if(this.length==7) color = this.toLowerCase();  
+    }  
+  }  
+  return(color.length==7 ? color : (arguments[0] || this));  
+}  
+
+Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {  
+  var children = $(element).childNodes;  
+  var text     = "";  
+  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");  
+ 
+  for (var i = 0; i < children.length; i++) {  
+    if(children[i].nodeType==3) {  
+      text+=children[i].nodeValue;  
+    } else {  
+      if((!children[i].className.match(classtest)) && children[i].hasChildNodes())  
+        text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);  
+    }  
+  }  
+ 
+  return text;
+}
+
+Element.setContentZoom = function(element, percent) {  
+  element = $(element);  
+  element.style.fontSize = (percent/100) + "em";   
+  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);  
+}
+
+Element.getOpacity = function(element){  
+  var opacity;  
+  if (opacity = Element.getStyle(element, "opacity"))  
+    return parseFloat(opacity);  
+  if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/))  
+    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
+  return 1.0;  
+}
+
+Element.setOpacity = function(element, value){  
+  element= $(element);  
+  var els = element.style;  
+  if (value == 1){  
+    els.opacity = '0.999999';  
+    if(/MSIE/.test(navigator.userAgent))  
+      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'');  
+  } else {  
+    if(value < 0.00001) value = 0;  
+    els.opacity = value;  
+    if(/MSIE/.test(navigator.userAgent))  
+      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +  
+        "alpha(opacity="+value*100+")";  
+  }   
+}  
+ 
+Element.getInlineOpacity = function(element){  
+  element= $(element);  
+  var op;  
+  op = element.style.opacity;  
+  if (typeof op != "undefined" && op != "") return op;  
+  return "";  
+}  
+ 
+Element.setInlineOpacity = function(element, value){  
+  element= $(element);  
+  var els = element.style;  
+  els.opacity = value;  
+}  
+ 
+Element.childrenWithClassName = function(element, className) {  
+  return $A($(element).getElementsByTagName('*')).select(
+    function(c) { return Element.hasClassName(c, className) });
+}  
+ 
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+  tagifyText: function(element) {
+    var tagifyStyle = "position:relative";
+    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1";
+    element = $(element);
+    $A(element.childNodes).each( function(child) {
+      if(child.nodeType==3) {
+        child.nodeValue.toArray().each( function(character) {
+          element.insertBefore(
+            Builder.node('span',{style: tagifyStyle},
+              character == " " ? String.fromCharCode(160) : character), 
+              child);
+        });
+        Element.remove(child);
+      }
+    });
+  },
+  multiple: function(element, effect) {
+    var elements;
+    if(((typeof element == 'object') || 
+        (typeof element == 'function')) && 
+       (element.length))
+      elements = element;
+    else
+      elements = $(element).childNodes;
+      
+    var options = Object.extend({
+      speed: 0.1,
+      delay: 0.0
+    }, arguments[2] || {});
+    var speed = options.speed;
+    var delay = options.delay;
+
+    $A(elements).each( function(element, index) {
+      new effect(element, Object.extend(options, { delay: delay + index * speed }));
+    });
+  }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {}
+
+Effect.Transitions.linear = function(pos) {
+  return pos;
+}
+Effect.Transitions.sinoidal = function(pos) {
+  return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+Effect.Transitions.reverse  = function(pos) {
+  return 1-pos;
+}
+Effect.Transitions.flicker = function(pos) {
+  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+}
+Effect.Transitions.wobble = function(pos) {
+  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+Effect.Transitions.pulse = function(pos) {
+  return (Math.floor(pos*10) % 2 == 0 ? 
+    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+Effect.Transitions.none = function(pos) {
+  return 0;
+}
+Effect.Transitions.full = function(pos) {
+  return 1;
+}
+
+/* ------------- core effects ------------- */
+
+Effect.Queue = {
+  effects:  [],
+  _each: function(iterator) {
+    this.effects._each(iterator);
+  },
+  interval: null,
+  add: function(effect) {
+    var timestamp = new Date().getTime();
+    
+    switch(effect.options.queue) {
+      case 'front':
+        // move unstarted effects after this effect  
+        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+            e.startOn  += effect.finishOn;
+            e.finishOn += effect.finishOn;
+          });
+        break;
+      case 'end':
+        // start effect after last queued effect has finished
+        timestamp = this.effects.pluck('finishOn').max() || timestamp;
+        break;
+    }
+    
+    effect.startOn  += timestamp;
+    effect.finishOn += timestamp;
+    this.effects.push(effect);
+    if(!this.interval) 
+      this.interval = setInterval(this.loop.bind(this), 40);
+  },
+  remove: function(effect) {
+    this.effects = this.effects.reject(function(e) { return e==effect });
+    if(this.effects.length == 0) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+  },
+  loop: function() {
+    var timePos = new Date().getTime();
+    this.effects.invoke('loop', timePos);
+  }
+}
+Object.extend(Effect.Queue, Enumerable);
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+  position: null,
+  setOptions: function(options) {
+    this.options = Object.extend({
+      transition: Effect.Transitions.sinoidal,
+      duration:   1.0,   // seconds
+      fps:        25.0,  // max. 25fps due to Effect.Queue implementation
+      sync:       false, // true for combining
+      from:       0.0,
+      to:         1.0,
+      delay:      0.0,
+      queue:      'parallel'
+    }, options || {});
+  },
+  start: function(options) {
+    this.setOptions(options || {});
+    this.currentFrame = 0;
+    this.state        = 'idle';
+    this.startOn      = this.options.delay*1000;
+    this.finishOn     = this.startOn + (this.options.duration*1000);
+    this.event('beforeStart');
+    if(!this.options.sync) Effect.Queue.add(this);
+  },
+  loop: function(timePos) {
+    if(timePos >= this.startOn) {
+      if(timePos >= this.finishOn) {
+        this.render(1.0);
+        this.cancel();
+        this.event('beforeFinish');
+        if(this.finish) this.finish(); 
+        this.event('afterFinish');
+        return;  
+      }
+      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
+      var frame = Math.round(pos * this.options.fps * this.options.duration);
+      if(frame > this.currentFrame) {
+        this.render(pos);
+        this.currentFrame = frame;
+      }
+    }
+  },
+  render: function(pos) {
+    if(this.state == 'idle') {
+      this.state = 'running';
+      this.event('beforeSetup');
+      if(this.setup) this.setup();
+      this.event('afterSetup');
+    }
+    if(this.options.transition) pos = this.options.transition(pos);
+    pos *= (this.options.to-this.options.from);
+    pos += this.options.from;
+    this.position = pos;
+    this.event('beforeUpdate');
+    if(this.update) this.update(pos);
+    this.event('afterUpdate');
+  },
+  cancel: function() {
+    if(!this.options.sync) Effect.Queue.remove(this);
+    this.state = 'finished';
+  },
+  event: function(eventName) {
+    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+    if(this.options[eventName]) this.options[eventName](this);
+  }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+  initialize: function(effects) {
+    this.effects = effects || [];
+    this.start(arguments[1]);
+  },
+  update: function(position) {
+    this.effects.invoke('render', position);
+  },
+  finish: function(position) {
+    this.effects.each( function(effect) {
+      effect.render(1.0);
+      effect.cancel();
+      effect.event('beforeFinish');
+      if(effect.finish) effect.finish(position);
+      effect.event('afterFinish');
+    });
+  }
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    // make this work on IE on elements without 'layout'
+    if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
+      this.element.style.zoom = 1;
+    var options = Object.extend({
+      from: Element.getOpacity(this.element) || 0.0,
+      to:   1.0
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  update: function(position) {
+    Element.setOpacity(this.element, position);
+  }
+});
+
+Effect.MoveBy = Class.create();
+Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
+  initialize: function(element, toTop, toLeft) {
+    this.element      = $(element);
+    this.toTop        = toTop;
+    this.toLeft       = toLeft;
+    this.start(arguments[3]);
+  },
+  setup: function() {
+    // Bug in Opera: Opera returns the "real" position of a static element or
+    // relative element that does not have top/left explicitly set.
+    // ==> Always set top and left for position relative elements in your stylesheets 
+    // (to 0 if you do not need them)
+    
+    Element.makePositioned(this.element);
+    this.originalTop  = parseFloat(Element.getStyle(this.element,'top')  || '0');
+    this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
+  },
+  update: function(position) {
+    var topd  = this.toTop  * position + this.originalTop;
+    var leftd = this.toLeft * position + this.originalLeft;
+    this.setPosition(topd, leftd);
+  },
+  setPosition: function(topd, leftd) {
+    this.element.style.top  = topd  + "px";
+    this.element.style.left = leftd + "px";
+  }
+});
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+  initialize: function(element, percent) {
+    this.element = $(element)
+    var options = Object.extend({
+      scaleX: true,
+      scaleY: true,
+      scaleContent: true,
+      scaleFromCenter: false,
+      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
+      scaleFrom: 100.0,
+      scaleTo:   percent
+    }, arguments[2] || {});
+    this.start(options);
+  },
+  setup: function() {
+    var effect = this;
+    
+    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+    this.elementPositioning = Element.getStyle(this.element,'position');
+    
+    effect.originalStyle = {};
+    ['top','left','width','height','fontSize'].each( function(k) {
+      effect.originalStyle[k] = effect.element.style[k];
+    });
+      
+    this.originalTop  = this.element.offsetTop;
+    this.originalLeft = this.element.offsetLeft;
+    
+    var fontSize = Element.getStyle(this.element,'font-size') || "100%";
+    ['em','px','%'].each( function(fontSizeType) {
+      if(fontSize.indexOf(fontSizeType)>0) {
+        effect.fontSize     = parseFloat(fontSize);
+        effect.fontSizeType = fontSizeType;
+      }
+    });
+    
+    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+    
+    this.dims = null;
+    if(this.options.scaleMode=='box')
+      this.dims = [this.element.clientHeight, this.element.clientWidth];
+    if(/^content/.test(this.options.scaleMode))
+      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+    if(!this.dims)
+      this.dims = [this.options.scaleMode.originalHeight,
+                   this.options.scaleMode.originalWidth];
+  },
+  update: function(position) {
+    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+    if(this.options.scaleContent && this.fontSize)
+      this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType;
+    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+  },
+  finish: function(position) {
+    if (this.restoreAfterFinish) {
+      var effect = this;
+      ['top','left','width','height','fontSize'].each( function(k) {
+        effect.element.style[k] = effect.originalStyle[k];
+      });
+    }
+  },
+  setDimensions: function(height, width) {
+    var els = this.element.style;
+    if(this.options.scaleX) els.width = width + 'px';
+    if(this.options.scaleY) els.height = height + 'px';
+    if(this.options.scaleFromCenter) {
+      var topd  = (height - this.dims[0])/2;
+      var leftd = (width  - this.dims[1])/2;
+      if(this.elementPositioning == 'absolute') {
+        if(this.options.scaleY) els.top = this.originalTop-topd + "px";
+        if(this.options.scaleX) els.left = this.originalLeft-leftd + "px";
+      } else {
+        if(this.options.scaleY) els.top = -topd + "px";
+        if(this.options.scaleX) els.left = -leftd + "px";
+      }
+    }
+  }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    var options = Object.extend({
+      startcolor:   "#ffff99"
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Prevent executing on elements not in the layout flow
+    if(this.element.style.display=='none') { this.cancel(); return; }
+    // Disable background image during the effect
+    this.oldBgImage = this.element.style.backgroundImage;
+    this.element.style.backgroundImage = "none";
+    if(!this.options.endcolor)
+      this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
+    if (typeof this.options.restorecolor == "undefined")
+      this.options.restorecolor = this.element.style.backgroundColor;
+    // init color calculations
+    this.colors_base = [
+      parseInt(this.options.startcolor.slice(1,3),16),
+      parseInt(this.options.startcolor.slice(3,5),16),
+      parseInt(this.options.startcolor.slice(5),16) ];
+    this.colors_delta = [
+      parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
+      parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
+      parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
+  },
+  update: function(position) {
+    var effect = this; var colors = $R(0,2).map( function(i){ 
+      return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position))
+    });
+    this.element.style.backgroundColor = "#" +
+      colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
+  },
+  finish: function() {
+    this.element.style.backgroundColor = this.options.restorecolor;
+    this.element.style.backgroundImage = this.oldBgImage;
+  }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    this.start(arguments[1] || {});
+  },
+  setup: function() {
+    Position.prepare();
+    var offsets = Position.cumulativeOffset(this.element);
+    var max = window.innerHeight ? 
+      window.height - window.innerHeight :
+      document.body.scrollHeight - 
+        (document.documentElement.clientHeight ? 
+          document.documentElement.clientHeight : document.body.clientHeight);
+    this.scrollStart = Position.deltaY;
+    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+  },
+  update: function(position) {
+    Position.prepare();
+    window.scrollTo(Position.deltaX, 
+      this.scrollStart + (position*this.delta));
+  }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+  var oldOpacity = Element.getInlineOpacity(element);
+  var options = Object.extend({
+  from: Element.getOpacity(element) || 1.0,
+  to:   0.0,
+  afterFinishInternal: function(effect) 
+    { if (effect.options.to == 0) {
+        Element.hide(effect.element);
+        Element.setInlineOpacity(effect.element, oldOpacity);
+      }  
+    }
+  }, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+  var options = Object.extend({
+  from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0),
+  to:   1.0,
+  beforeSetup: function(effect)  
+    { Element.setOpacity(effect.element, effect.options.from);
+      Element.show(effect.element); }
+  }, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+  element = $(element);
+  var oldOpacity = Element.getInlineOpacity(element);
+  var oldPosition = element.style.position;
+  return new Effect.Parallel(
+   [ new Effect.Scale(element, 200, 
+      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
+     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
+     Object.extend({ duration: 1.0, 
+      beforeSetupInternal: function(effect) 
+       { effect.effects[0].element.style.position = 'absolute'; },
+      afterFinishInternal: function(effect)
+       { Element.hide(effect.effects[0].element);
+         effect.effects[0].element.style.position = oldPosition;
+         Element.setInlineOpacity(effect.effects[0].element, oldOpacity); }
+     }, arguments[1] || {})
+   );
+}
+
+Effect.BlindUp = function(element) {
+  element = $(element);
+  Element.makeClipping(element);
+  return new Effect.Scale(element, 0, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      restoreAfterFinish: true,
+      afterFinishInternal: function(effect)
+        { 
+          Element.hide(effect.element);
+          Element.undoClipping(effect.element);
+        } 
+    }, arguments[1] || {})
+  );
+}
+
+Effect.BlindDown = function(element) {
+  element = $(element);
+  var oldHeight = element.style.height;
+  var elementDimensions = Element.getDimensions(element);
+  return new Effect.Scale(element, 100, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false,
+      scaleFrom: 0,
+      scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+      restoreAfterFinish: true,
+      afterSetup: function(effect) {
+        Element.makeClipping(effect.element);
+        effect.element.style.height = "0px";
+        Element.show(effect.element); 
+      },  
+      afterFinishInternal: function(effect) {
+        Element.undoClipping(effect.element);
+        effect.element.style.height = oldHeight;
+      }
+    }, arguments[1] || {})
+  );
+}
+
+Effect.SwitchOff = function(element) {
+  element = $(element);
+  var oldOpacity = Element.getInlineOpacity(element);
+  return new Effect.Appear(element, { 
+    duration: 0.4,
+    from: 0,
+    transition: Effect.Transitions.flicker,
+    afterFinishInternal: function(effect) {
+      new Effect.Scale(effect.element, 1, { 
+        duration: 0.3, scaleFromCenter: true,
+        scaleX: false, scaleContent: false, restoreAfterFinish: true,
+        beforeSetup: function(effect) { 
+          Element.makePositioned(effect.element); 
+          Element.makeClipping(effect.element);
+        },
+        afterFinishInternal: function(effect) { 
+          Element.hide(effect.element); 
+          Element.undoClipping(effect.element);
+          Element.undoPositioned(effect.element);
+          Element.setInlineOpacity(effect.element, oldOpacity);
+        }
+      })
+    }
+  });
+}
+
+Effect.DropOut = function(element) {
+  element = $(element);
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  var oldOpacity = Element.getInlineOpacity(element);
+  return new Effect.Parallel(
+    [ new Effect.MoveBy(element, 100, 0, { sync: true }), 
+      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+    Object.extend(
+      { duration: 0.5,
+        beforeSetup: function(effect) { 
+          Element.makePositioned(effect.effects[0].element); },
+        afterFinishInternal: function(effect) { 
+          Element.hide(effect.effects[0].element); 
+          Element.undoPositioned(effect.effects[0].element);
+          effect.effects[0].element.style.left = oldLeft;
+          effect.effects[0].element.style.top = oldTop;
+          Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } 
+      }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+  element = $(element);
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  return new Effect.MoveBy(element, 0, 20, 
+    { duration: 0.05, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, -40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, 40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, -40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, 40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, -20, 
+    { duration: 0.05, afterFinishInternal: function(effect) {
+        Element.undoPositioned(effect.element);
+        effect.element.style.left = oldLeft;
+        effect.element.style.top = oldTop;
+  }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+  element = $(element);
+  Element.cleanWhitespace(element);
+  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+  var oldInnerBottom = element.firstChild.style.bottom;
+  var elementDimensions = Element.getDimensions(element);
+  return new Effect.Scale(element, 100, 
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleFrom: 0,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},    
+    restoreAfterFinish: true,
+    afterSetup: function(effect) {
+      Element.makePositioned(effect.element.firstChild);
+      if (window.opera) effect.element.firstChild.style.top = "";
+      Element.makeClipping(effect.element);
+      element.style.height = '0';
+      Element.show(element); 
+    },  
+    afterUpdateInternal: function(effect) { 
+      effect.element.firstChild.style.bottom = 
+        (effect.dims[0] - effect.element.clientHeight) + 'px'; },
+    afterFinishInternal: function(effect) { 
+      Element.undoClipping(effect.element); 
+      Element.undoPositioned(effect.element.firstChild);
+      effect.element.firstChild.style.bottom = oldInnerBottom; }
+    }, arguments[1] || {})
+  );
+}
+  
+Effect.SlideUp = function(element) {
+  element = $(element);
+  Element.cleanWhitespace(element);
+  var oldInnerBottom = element.firstChild.style.bottom;
+  return new Effect.Scale(element, 0, 
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleMode: 'box',
+    scaleFrom: 100,
+    restoreAfterFinish: true,
+    beforeStartInternal: function(effect) { 
+      Element.makePositioned(effect.element.firstChild);
+      if (window.opera) effect.element.firstChild.style.top = "";
+      Element.makeClipping(effect.element);
+      Element.show(element); 
+    },  
+    afterUpdateInternal: function(effect) { 
+     effect.element.firstChild.style.bottom = 
+       (effect.dims[0] - effect.element.clientHeight) + 'px'; },
+    afterFinishInternal: function(effect) { 
+        Element.hide(effect.element);
+        Element.undoClipping(effect.element); 
+        Element.undoPositioned(effect.element.firstChild);
+        effect.element.firstChild.style.bottom = oldInnerBottom; }
+   }, arguments[1] || {})
+  );
+}
+
+Effect.Squish = function(element) {
+  // Bug in opera makes the TD containing this element expand for a instance after finish 
+  return new Effect.Scale(element, window.opera ? 1 : 0, 
+    { restoreAfterFinish: true,
+      beforeSetup: function(effect) { 
+        Element.makeClipping(effect.element); },  
+      afterFinishInternal: function(effect) { 
+        Element.hide(effect.element); 
+        Element.undoClipping(effect.element); } 
+  });
+}
+
+Effect.Grow = function(element) {
+  element = $(element);
+  var options = arguments[1] || {};
+  
+  var elementDimensions = Element.getDimensions(element);
+  var originalWidth = elementDimensions.width;
+  var originalHeight = elementDimensions.height;
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  var oldHeight = element.style.height;
+  var oldWidth = element.style.width;
+  var oldOpacity = Element.getInlineOpacity(element);
+  
+  var direction = options.direction || 'center';
+  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
+  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
+  var opacityTransition = options.opacityTransition || Effect.Transitions.full;
+  
+  var initialMoveX, initialMoveY;
+  var moveX, moveY;
+  
+  switch (direction) {
+    case 'top-left':
+      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      break;
+    case 'top-right':
+      initialMoveX = originalWidth;
+      initialMoveY = moveY = 0;
+      moveX = -originalWidth;
+      break;
+    case 'bottom-left':
+      initialMoveX = moveX = 0;
+      initialMoveY = originalHeight;
+      moveY = -originalHeight;
+      break;
+    case 'bottom-right':
+      initialMoveX = originalWidth;
+      initialMoveY = originalHeight;
+      moveX = -originalWidth;
+      moveY = -originalHeight;
+      break;
+    case 'center':
+      initialMoveX = originalWidth / 2;
+      initialMoveY = originalHeight / 2;
+      moveX = -originalWidth / 2;
+      moveY = -originalHeight / 2;
+      break;
+  }
+  
+  return new Effect.MoveBy(element, initialMoveY, initialMoveX, { 
+    duration: 0.01, 
+    beforeSetup: function(effect) { 
+      Element.hide(effect.element);
+      Element.makeClipping(effect.element);
+      Element.makePositioned(effect.element);
+    },
+    afterFinishInternal: function(effect) {
+      new Effect.Parallel(
+        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
+          new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }),
+          new Effect.Scale(effect.element, 100, {
+            scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, 
+            sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true})
+        ], Object.extend({
+             beforeSetup: function(effect) {
+              effect.effects[0].element.style.height = 0;
+              Element.show(effect.effects[0].element);
+             },              
+             afterFinishInternal: function(effect) {
+               var el = effect.effects[0].element;
+               var els = el.style;
+               Element.undoClipping(el); 
+               Element.undoPositioned(el);
+               els.top = oldTop;
+               els.left = oldLeft;
+               els.height = oldHeight;
+               els.width = originalWidth + 'px';
+               Element.setInlineOpacity(el, oldOpacity);
+             }
+           }, options)
+      )
+    }
+  });
+}
+
+Effect.Shrink = function(element) {
+  element = $(element);
+  var options = arguments[1] || {};
+  
+  var originalWidth = element.clientWidth;
+  var originalHeight = element.clientHeight;
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  var oldHeight = element.style.height;
+  var oldWidth = element.style.width;
+  var oldOpacity = Element.getInlineOpacity(element);
+
+  var direction = options.direction || 'center';
+  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
+  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
+  var opacityTransition = options.opacityTransition || Effect.Transitions.none;
+  
+  var moveX, moveY;
+  
+  switch (direction) {
+    case 'top-left':
+      moveX = moveY = 0;
+      break;
+    case 'top-right':
+      moveX = originalWidth;
+      moveY = 0;
+      break;
+    case 'bottom-left':
+      moveX = 0;
+      moveY = originalHeight;
+      break;
+    case 'bottom-right':
+      moveX = originalWidth;
+      moveY = originalHeight;
+      break;
+    case 'center':  
+      moveX = originalWidth / 2;
+      moveY = originalHeight / 2;
+      break;
+  }
+  
+  return new Effect.Parallel(
+    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
+      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}),
+      new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition })
+    ], Object.extend({            
+         beforeStartInternal: function(effect) { 
+           Element.makePositioned(effect.effects[0].element);
+           Element.makeClipping(effect.effects[0].element);
+         },
+         afterFinishInternal: function(effect) {
+           var el = effect.effects[0].element;
+           var els = el.style;
+           Element.hide(el);
+           Element.undoClipping(el); 
+           Element.undoPositioned(el);
+           els.top = oldTop;
+           els.left = oldLeft;
+           els.height = oldHeight;
+           els.width = oldWidth;
+           Element.setInlineOpacity(el, oldOpacity);
+         }
+       }, options)
+  );
+}
+
+Effect.Pulsate = function(element) {
+  element = $(element);
+  var options    = arguments[1] || {};
+  var oldOpacity = Element.getInlineOpacity(element);
+  var transition = options.transition || Effect.Transitions.sinoidal;
+  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
+  reverser.bind(transition);
+  return new Effect.Opacity(element, 
+    Object.extend(Object.extend({  duration: 3.0, from: 0,
+      afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); }
+    }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+  element = $(element);
+  var originalTop = element.style.top;
+  var originalLeft = element.style.left;
+  var originalWidth = element.style.width;
+  var originalHeight = element.style.height;
+  Element.makeClipping(element);
+  return new Effect.Scale(element, 5, Object.extend({   
+    scaleContent: false,
+    scaleX: false,
+    afterFinishInternal: function(effect) {
+    new Effect.Scale(element, 1, { 
+      scaleContent: false, 
+      scaleY: false,
+      afterFinishInternal: function(effect) { 
+        Element.hide(effect.element);  
+        Element.undoClipping(effect.element); 
+        effect.element.style.top = originalTop;
+        effect.element.style.left = originalLeft;
+        effect.element.style.width = originalWidth;
+        effect.element.style.height = originalHeight;
+      } });
+  }}, arguments[1] || {}));
+}
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/prototype.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/prototype.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/prototype.js	(revision 32)
@@ -0,0 +1,1758 @@
+/*  Prototype JavaScript framework, version 1.4.0_rc3
+ *  (c) 2005 Sam Stephenson <sam@conio.net>
+ *
+ *  THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
+ *  against the source tree, available from the Prototype darcs repository.
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *
+ *  For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.4.0_rc3',
+  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+
+  emptyFunction: function() {},
+  K: function(x) {return x}
+}
+
+var Class = {
+  create: function() {
+    return function() {
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.inspect = function(object) {
+  try {
+    if (object == undefined) return 'undefined';
+    if (object == null) return 'null';
+    return object.inspect ? object.inspect() : object.toString();
+  } catch (e) {
+    if (e instanceof RangeError) return '...';
+    throw e;
+  }
+}
+
+Function.prototype.bind = function(object) {
+  var __method = this;
+  return function() {
+    return __method.apply(object, arguments);
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this;
+  return function(event) {
+    return __method.call(object, event || window.event);
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    var digits = this.toString(16);
+    if (this < 16) return '0' + digits;
+    return digits;
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  }
+});
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0; i < arguments.length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) {}
+    }
+
+    return returnValue;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback();
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+function $() {
+  var elements = new Array();
+
+  for (var i = 0; i < arguments.length; i++) {
+    var element = arguments[i];
+    if (typeof element == 'string')
+      element = document.getElementById(element);
+
+    if (arguments.length == 1)
+      return element;
+
+    elements.push(element);
+  }
+
+  return elements;
+}
+Object.extend(String.prototype, {
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(eval);
+  },
+
+  escapeHTML: function() {
+    var div = document.createElement('div');
+    var text = document.createTextNode(this);
+    div.appendChild(text);
+    return div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
+  },
+
+  toQueryParams: function() {
+    var pairs = this.match(/^\??(.*)$/)[1].split('&');
+    return pairs.inject({}, function(params, pairString) {
+      var pair = pairString.split('=');
+      params[pair[0]] = pair[1];
+      return params;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  camelize: function() {
+    var oStringList = this.split('-');
+    if (oStringList.length == 1) return oStringList[0];
+
+    var camelizedString = this.indexOf('-') == 0
+      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
+      : oStringList[0];
+
+    for (var i = 1, len = oStringList.length; i < len; i++) {
+      var s = oStringList[i];
+      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
+    }
+
+    return camelizedString;
+  },
+
+  inspect: function() {
+    return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
+  }
+});
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var $break    = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        try {
+          iterator(value, index++);
+        } catch (e) {
+          if (e != $continue) throw e;
+        }
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+  },
+
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      if (result = !!(iterator || Prototype.K)(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(iterator(value, index));
+    });
+    return results;
+  },
+
+  detect: function (iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.collect(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (value >= (result || value))
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (value <= (result || value))
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator) {
+    return this.collect(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.collect(Prototype.K);
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      iterator(value = collections.pluck(index));
+      return value;
+    });
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+  if (iterable.toArray) {
+    return iterable.toArray();
+  } else {
+    var results = [];
+    for (var i = 0; i < iterable.length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0; i < this.length; i++)
+      iterator(this[i]);
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != undefined || value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value.constructor == Array ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  indexOf: function(object) {
+    for (var i = 0; i < this.length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function() {
+    var result = [];
+    for (var i = this.length; i > 0; i--)
+      result.push(this[i-1]);
+    return result;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  }
+});
+var Hash = {
+  _each: function(iterator) {
+    for (key in this) {
+      var value = this[key];
+      if (typeof value == 'function') continue;
+
+      var pair = [key, value];
+      pair.key = key;
+      pair.value = value;
+      iterator(pair);
+    }
+  },
+
+  keys: function() {
+    return this.pluck('key');
+  },
+
+  values: function() {
+    return this.pluck('value');
+  },
+
+  merge: function(hash) {
+    return $H(hash).inject($H(this), function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  toQueryString: function() {
+    return this.map(function(pair) {
+      return pair.map(encodeURIComponent).join('=');
+    }).join('&');
+  },
+
+  inspect: function() {
+    return '#<Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}>';
+  }
+}
+
+function $H(object) {
+  var hash = Object.extend({}, object || {});
+  Object.extend(hash, Enumerable);
+  Object.extend(hash, Hash);
+  return hash;
+}
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    do {
+      iterator(value);
+      value = value.succ();
+    } while (this.include(value));
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')},
+      function() {return new XMLHttpRequest()}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responderToAdd) {
+    if (!this.include(responderToAdd))
+      this.responders.push(responderToAdd);
+  },
+
+  unregister: function(responderToRemove) {
+    this.responders = this.responders.without(responderToRemove);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (responder[callback] && typeof responder[callback] == 'function') {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) {}
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate: function() {
+    Ajax.activeRequestCount++;
+  },
+
+  onComplete: function() {
+    Ajax.activeRequestCount--;
+  }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+  },
+
+  responseIsSuccess: function() {
+    return this.transport.status == undefined
+        || this.transport.status == 0
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  responseIsFailure: function() {
+    return !this.responseIsSuccess();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    var parameters = this.options.parameters || '';
+    if (parameters.length > 0) parameters += '&_=';
+
+    try {
+      this.url = url;
+      if (this.options.method == 'get' && parameters.length > 0)
+        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
+
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.options.method, this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous) {
+        this.transport.onreadystatechange = this.onStateChange.bind(this);
+        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
+      }
+
+      this.setRequestHeaders();
+
+      var body = this.options.postBody ? this.options.postBody : parameters;
+      this.transport.send(this.options.method == 'post' ? body : null);
+
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  setRequestHeaders: function() {
+    var requestHeaders =
+      ['X-Requested-With', 'XMLHttpRequest',
+       'X-Prototype-Version', Prototype.Version];
+
+    if (this.options.method == 'post') {
+      requestHeaders.push('Content-type',
+        'application/x-www-form-urlencoded');
+
+      /* Force "Connection: close" for Mozilla browsers to work around
+       * a bug where XMLHttpReqeuest sends an incorrect Content-length
+       * header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType)
+        requestHeaders.push('Connection', 'close');
+    }
+
+    if (this.options.requestHeaders)
+      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
+
+    for (var i = 0; i < requestHeaders.length; i += 2)
+      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState != 1)
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  header: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) {}
+  },
+
+  evalJSON: function() {
+    try {
+      return eval(this.header('X-JSON'));
+    } catch (e) {}
+  },
+
+  evalResponse: function() {
+    try {
+      return eval(this.transport.responseText);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  respondToReadyState: function(readyState) {
+    var event = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (event == 'Complete') {
+      try {
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      if (this.header('Content-type') == 'text/javascript')
+        this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + event, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
+    if (event == 'Complete')
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+  initialize: function(container, url, options) {
+    this.containers = {
+      success: container.success ? $(container.success) : $(container),
+      failure: container.failure ? $(container.failure) :
+        (container.success ? null : $(container))
+    }
+
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function(transport, object) {
+      this.updateContent();
+      onComplete(transport, object);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.responseIsSuccess() ?
+      this.containers.success : this.containers.failure;
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts)
+      response = response.stripScripts();
+
+    if (receiver) {
+      if (this.options.insertion) {
+        new this.options.insertion(receiver, response);
+      } else {
+        Element.update(receiver, response);
+      }
+    }
+
+    if (this.responseIsSuccess()) {
+      if (this.onComplete)
+        setTimeout(this.onComplete.bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = request.responseText;
+    }
+    this.timer = setTimeout(this.onTimerEvent.bind(this),
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+document.getElementsByClassName = function(className, parentElement) {
+  var children = ($(parentElement) || document.body).getElementsByTagName('*');
+  return $A(children).inject([], function(elements, child) {
+    if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+      elements.push(child);
+    return elements;
+  });
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) {
+  var Element = new Object();
+}
+
+Object.extend(Element, {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      Element[Element.visible(element) ? 'hide' : 'show'](element);
+    }
+  },
+
+  hide: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = 'none';
+    }
+  },
+
+  show: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = '';
+    }
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+  },
+
+  update: function(element, html) {
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+  },
+
+  getHeight: function(element) {
+    element = $(element);
+    return element.offsetHeight;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return Element.classNames(element).include(className);
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return Element.classNames(element).add(className);
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return Element.classNames(element).remove(className);
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    for (var i = 0; i < element.childNodes.length; i++) {
+      var node = element.childNodes[i];
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        Element.remove(node);
+    }
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.match(/^\s*$/);
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var x = element.x ? element.x : element.offsetLeft,
+        y = element.y ? element.y : element.offsetTop;
+    window.scrollTo(x, y);
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    var value = element.style[style.camelize()];
+    if (!value) {
+      if (document.defaultView && document.defaultView.getComputedStyle) {
+        var css = document.defaultView.getComputedStyle(element, null);
+        value = css ? css.getPropertyValue(style) : null;
+      } else if (element.currentStyle) {
+        value = element.currentStyle[style.camelize()];
+      }
+    }
+
+    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+
+    return value == 'auto' ? null : value;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    if (Element.getStyle(element, 'display') != 'none')
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = '';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = 'none';
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return;
+    element._overflow = element.style.overflow;
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return;
+    element.style.overflow = element._overflow;
+    element._overflow = undefined;
+  }
+});
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content.stripScripts();
+
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      try {
+        this.element.insertAdjacentHTML(this.adjacency, this.content);
+      } catch (e) {
+        if (this.element.tagName.toLowerCase() == 'tbody') {
+          this.insertContent(this.contentFromAnonymousTable());
+        } else {
+          throw e;
+        }
+      }
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.insertContent([this.range.createContextualFragment(this.content)]);
+    }
+
+    setTimeout(function() {content.evalScripts()}, 10);
+  },
+
+  contentFromAnonymousTable: function() {
+    var div = document.createElement('div');
+    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+    return $A(div.childNodes[0].childNodes[0].childNodes);
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment, this.element);
+    }).bind(this));
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+
+  insertContent: function(fragments) {
+    fragments.reverse().each((function(fragment) {
+      this.element.insertBefore(fragment, this.element.firstChild);
+    }).bind(this));
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.appendChild(fragment);
+    }).bind(this));
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment,
+        this.element.nextSibling);
+    }).bind(this));
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set(this.toArray().concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set(this.select(function(className) {
+      return className != classNameToRemove;
+    }).join(' '));
+  },
+
+  toString: function() {
+    return this.toArray().join(' ');
+  }
+}
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Field = {
+  clear: function() {
+    for (var i = 0; i < arguments.length; i++)
+      $(arguments[i]).value = '';
+  },
+
+  focus: function(element) {
+    $(element).focus();
+  },
+
+  present: function() {
+    for (var i = 0; i < arguments.length; i++)
+      if ($(arguments[i]).value == '') return false;
+    return true;
+  },
+
+  select: function(element) {
+    $(element).select();
+  },
+
+  activate: function(element) {
+    element = $(element);
+    element.focus();
+    if (element.select)
+      element.select();
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Form = {
+  serialize: function(form) {
+    var elements = Form.getElements($(form));
+    var queryComponents = new Array();
+
+    for (var i = 0; i < elements.length; i++) {
+      var queryComponent = Form.Element.serialize(elements[i]);
+      if (queryComponent)
+        queryComponents.push(queryComponent);
+    }
+
+    return queryComponents.join('&');
+  },
+
+  getElements: function(form) {
+    form = $(form);
+    var elements = new Array();
+
+    for (tagName in Form.Element.Serializers) {
+      var tagElements = form.getElementsByTagName(tagName);
+      for (var j = 0; j < tagElements.length; j++)
+        elements.push(tagElements[j]);
+    }
+    return elements;
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name)
+      return inputs;
+
+    var matchingInputs = new Array();
+    for (var i = 0; i < inputs.length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) ||
+          (name && input.name != name))
+        continue;
+      matchingInputs.push(input);
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.blur();
+      element.disabled = 'true';
+    }
+  },
+
+  enable: function(form) {
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.disabled = '';
+    }
+  },
+
+  findFirstElement: function(form) {
+    return Form.getElements(form).find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    Field.activate(Form.findFirstElement(form));
+  },
+
+  reset: function(form) {
+    $(form).reset();
+  }
+}
+
+Form.Element = {
+  serialize: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter)
+      return encodeURIComponent(parameter[0]) + '=' +
+        encodeURIComponent(parameter[1]);
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter)
+      return parameter[1];
+  }
+}
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'submit':
+      case 'hidden':
+      case 'password':
+      case 'text':
+        return Form.Element.Serializers.textarea(element);
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+    }
+    return false;
+  },
+
+  inputSelector: function(element) {
+    if (element.checked)
+      return [element.name, element.value];
+  },
+
+  textarea: function(element) {
+    return [element.name, element.value];
+  },
+
+  select: function(element) {
+    return Form.Element.Serializers[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var value = '', opt, index = element.selectedIndex;
+    if (index >= 0) {
+      opt = element.options[index];
+      value = opt.value;
+      if (!value && !('value' in opt))
+        value = opt.text;
+    }
+    return [element.name, value];
+  },
+
+  selectMany: function(element) {
+    var value = new Array();
+    for (var i = 0; i < element.length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) {
+        var optValue = opt.value;
+        if (!optValue && !('value' in opt))
+          optValue = opt.text;
+        value.push(optValue);
+      }
+    }
+    return [element.name, value];
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+  initialize: function(element, frequency, callback) {
+    this.frequency = frequency;
+    this.element   = $(element);
+    this.callback  = callback;
+
+    this.lastValue = this.getValue();
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    var elements = Form.getElements(this.element);
+    for (var i = 0; i < elements.length; i++)
+      this.registerCallback(elements[i]);
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        case 'password':
+        case 'text':
+        case 'textarea':
+        case 'select-one':
+        case 'select-multiple':
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) {
+  var Event = new Object();
+}
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+
+  element: function(event) {
+    return event.target || event.srcElement;
+  },
+
+  isLeftClick: function(event) {
+    return (((event.which) && (event.which == 1)) ||
+            ((event.button) && (event.button == 1)));
+  },
+
+  pointerX: function(event) {
+    return event.pageX || (event.clientX +
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
+  },
+
+  pointerY: function(event) {
+    return event.pageY || (event.clientY +
+      (document.documentElement.scrollTop || document.body.scrollTop));
+  },
+
+  stop: function(event) {
+    if (event.preventDefault) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else {
+      event.returnValue = false;
+      event.cancelBubble = true;
+    }
+  },
+
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode && (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
+  },
+
+  observers: false,
+
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
+  },
+
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0; i < Event.observers.length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.attachEvent))
+      name = 'keydown';
+
+    this._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.detachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      element.detachEvent('on' + name, observer);
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  offsetParent: function(element) {
+    if (element.offsetParent) return element.offsetParent;
+    if (element == document.body) return element;
+
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return element;
+
+    return document.body;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = this.realOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = this.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  clone: function(source, target) {
+    source = $(source);
+    target = $(target);
+    target.style.position = 'absolute';
+    var offsets = this.cumulativeOffset(source);
+    target.style.top    = offsets[1] + 'px';
+    target.style.left   = offsets[0] + 'px';
+    target.style.width  = source.offsetWidth + 'px';
+    target.style.height = source.offsetHeight + 'px';
+  },
+
+  page: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent==document.body)
+        if (Element.getStyle(element,'position')=='absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      valueT -= element.scrollTop  || 0;
+      valueL -= element.scrollLeft || 0;
+    } while (element = element.parentNode);
+
+    return [valueL, valueT];
+  },
+
+  clone: function(source, target) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || {})
+
+    // find page position of source
+    source = $(source);
+    var p = Position.page(source);
+
+    // find coordinate system to use
+    target = $(target);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(target,'position') == 'absolute') {
+      parent = Position.offsetParent(target);
+      delta = Position.page(parent);
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
+    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.style.position == 'absolute') return;
+    Position.prepare();
+
+    var offsets = Position.positionedOffset(element);
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';;
+    element.style.left   = left + 'px';;
+    element.style.width  = width + 'px';;
+    element.style.height = height + 'px';;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.style.position == 'relative') return;
+    Position.prepare();
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+  }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned.  For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return [valueL, valueT];
+  }
+}
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/qplonetabs.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/qplonetabs.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/qplonetabs.js	(revision 32)
@@ -0,0 +1,671 @@
+/* Global variables */
+
+var gBeforeReorderData = [];        // storage for actions before sorting
+var gBeforeReorderFragment = null;  // document fragment for insertion in UL after clicking cancel button after reorder
+var gBeforeEditData = {};           // hash for storage tabs fields before editing
+var category = 'portal_tabs';
+
+/* Main part - rules for element on our form */
+
+var myrules = {
+  '#app #reorder' : function(el){
+    el.onclick = function(ev){
+      var ev = ev ? ev : window.event;
+      // remember current actions state for 'cancel sorting' case
+//       gBeforeReorderFragment = document.getElementById('tabslist').innerHTML;
+      var lis = $A($('tabslist').getElementsByTagName('LI'));
+      gBeforeReorderData = [];
+      lis.each(function(el, idx) {
+          gBeforeReorderData.push(grepInfo(el));
+      });
+
+      shiftClassNames('app', 'viewing', 'sorting');
+      Sortable.create('tabslist', {handle: 'drag-handle'});
+      removeEdition('tabslist');
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app #save' : function(el){
+    el.onclick = function(ev){
+      var ev = ev?ev:window.event, params = 'category='+category;
+      $A($('tabslist').getElementsByTagName('INPUT')).findAll(function(h){return h.type=='hidden';}).each(function(f,idx){params += '&idxs='+f.value;});
+      new Ajax.Request('qpt_reorder',
+        {method: 'post',
+         parameters: params,
+         onSuccess: function(request){
+           var lis = $A($('tabslist').getElementsByTagName('LI')),
+               inputs = function(li){return $A(li.getElementsByTagName('INPUT'))};
+           lis.each(function(el,idx){
+             inputs(el).each(function(inpt){
+               inpt.type == 'hidden' ? inpt.value = idx : inpt.name = inpt.name.replace(/i\d+_/, 'i'+idx+'_');
+             });
+           });
+           shiftClassNames('app', 'sorting', 'viewing');
+           Sortable.destroy('tabslist');
+           new Effect.Highlight('tabslist',{});
+         },
+         onComplete: function(request){Behaviour.apply();}
+        }
+      );
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app #cancel' : function(el){
+    el.onclick = function(ev){
+      var ev = ev ? ev : window.event;
+      Sortable.destroy('tabslist');
+      shiftClassNames('app', 'sorting', 'viewing');
+
+      // update action to before sorting state
+//      Element.update('tabslist', gBeforeReorderFragment);
+      var tabslist = $('tabslist');
+      tabslist.innerHTML = "";
+      for (var i = 0, li; li = gBeforeReorderData[i]; i++) {
+          tabslist.appendChild(recoverAction(li));
+      }
+
+      el.attachEvent ? ieHover() : '';
+      Behaviour.apply();
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app .csshover li' : function(el){
+    if (Element.hasClassName(el, 'onHover')) {Element.removeClassName(el, 'onHover');};
+  },
+  '#app .visibility' : function(el){
+    el.onclick = function(ev) {
+      var ev = ev?ev:window.event, li = Event.findElement(ev, 'LI'),
+          num = $A($('tabslist').getElementsByTagName('LI')).indexOf(li),
+          params = {
+            onSuccess: function(request){
+              if (Element.hasClassName(li, 'invisible')) {
+                Element.removeClassName(li, 'invisible');
+              }
+              else {
+                Element.addClassName(li, 'invisible');
+              };
+            },
+            onFailure: function(request){
+              var message = (/Error Value\s*<\/dt>\s*<dd>(.*?)<\/dd>/i).exec(request.responseText);
+              window.alert(message[1]);
+            }
+          };
+      if (Event.findElement(ev, 'UL').id == 'roottabs'){
+        params['parameters'] = 'id='+el.id+'&visibility='+el.checked;
+        new Ajax.Request('qpt_setvisibility', params);
+      }
+      else {
+        params['parameters'] = 'category='+category+'&num='+num+'&visibility='+el.checked+'&'+Form.serialize(li.getElementsByTagName('FORM')[0]);
+        new Ajax.Request('qpt_edit', params);
+      };
+      if (ev.stopPropagation) {ev.stopPropagation();}
+      else {ev.cancelBubble = true;};
+      return true;
+    }
+  },
+  '#app .delete' : function(el){
+    el.onclick = function(ev){
+      var ev = ev?ev:window.event, item = el.parentNode,
+          num = $A($('tabslist').getElementsByTagName('LI')).indexOf(item);
+      new Ajax.Request('qpt_delete',
+        {parameters:'category='+category+'&idx='+num+'&id=' + item.id.replace('tabslist_', ''),
+         onComplete: function(request) {
+           new Effect.Fade(item, {duration: 0.3, afterFinish: function(){
+             Element.remove(item);
+             var lis = $A($('tabslist').getElementsByTagName('LI')),
+                 inputs = function(li){return $A(li.getElementsByTagName('INPUT'))};
+             if (lis.length > 0) {
+               lis.each(function(el,idx){
+                 inputs(el).each(function(inpt){
+                   inpt.type=='hidden'?inpt.value=idx:inpt.name=inpt.name.replace(/i\d+_/, 'i'+idx+'_');
+                 });
+               });
+             } else {
+                 Element.addClassName('reorder', 'noitems');
+             };
+           }});
+         }
+        }
+      );
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app .headerAdvanced' : function(el){
+    el.onclick =  function(ev){
+      var ev = ev?ev:window.event, dl = Event.findElement(ev, 'DL'),
+          dd = dl.getElementsByTagName('DD')[0];
+      if (!Element.visible(dd)) {
+        shiftClassNames(dl, 'collapsedBlock', 'expandedBlock');
+        Effect.BlindDown(dd, {duration:0.1});
+      }
+      else {
+        shiftClassNames(dl, 'expandedBlock', 'collapsedBlock');
+        Effect.BlindUp(dd, {duration:0.1});
+      };
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app #tabslist li' : function(el){
+    el.onclick = function(ev){
+      if (!el.sel) el.sel = true;
+      else {return;};
+      var ev = ev?ev:window.event,
+          inputs = $A(el.getElementsByTagName('FORM')[0].getElementsByTagName('INPUT')), tmp = [];
+      inputs.each(function(e,idx){(0<idx && idx<5)?tmp.push(e.value):''});
+      gBeforeEditData[el.id] = tmp;
+      Element.addClassName(el, 'editing');
+      inputs[1].focus();
+      return true;
+    };
+  },
+  '#app #tabslist input.editsave' : function(el){
+    el.onclick = function(ev){
+      var ev = ev?ev:window.event, v = validateField,
+          li = Event.findElement(ev, 'LI'), tds = Event.findElement(ev, 'FORM').getElementsByTagName('INPUT'), title = $F(tds[1]);
+      if (v('actname',title) && v('actid',$F(tds[3]))) {
+         var dl = li.getElementsByTagName('DL')[1], dd = dl.getElementsByTagName('DD')[0];
+         var num = $A($('tabslist').getElementsByTagName('LI')).indexOf(li);
+         new Ajax.Request('qpt_edit',
+           {parameters:'category='+category+'&num='+num+'&'+Form.serialize(Event.findElement(ev, 'FORM')),
+            onSuccess: function(request){
+              new Effect.BlindUp(dd, {
+                duration : 0.1,
+                afterFinish : function(){
+                  shiftClassNames(dl, 'expandedBlock', 'collapsedBlock');
+                  Element.removeClassName(li, 'editing');
+                  li.id = 'tabslist_'+title;
+                  var spans = li.getElementsByTagName('SPAN');
+                  Element.update(spans[1], title);
+                  Element.update(spans[0], $F(tds[2]));
+                  Behaviour.apply();
+                }
+              })
+            },
+            onFailure: function(request){
+              var message = (/Error Value\s*<\/dt>\s*<dd>(.*?)<\/dd>/i).exec(request.responseText);
+              window.alert(message[1]);
+            },
+            onComplete: function(request){if (li.sel) {li.sel = null;};}
+           }
+         );
+      };
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app #tabslist input.editcancel' : function(el){
+    el.onclick = function(ev){
+      var ev = ev?ev:window.event, li = Event.findElement(ev, 'LI'),
+          inputs = $A(li.getElementsByTagName('FORM')[0].getElementsByTagName('INPUT')),
+          values = $A(gBeforeEditData[li.id]),
+          dl = li.getElementsByTagName('DL')[1], dd = dl.getElementsByTagName('DD')[0];
+      values.each(function(el,idx){inputs[idx+1].value = el});
+      if (Element.visible(dd)) {
+        Effect.BlindUp(dd, {
+          duration : 0.1,
+          afterFinish : function(){
+              shiftClassNames(dl, 'expandedBlock', 'collapsedBlock');
+              Element.removeClassName(li, 'editing');
+          }
+        });
+      }
+      else {Element.removeClassName(li, 'editing');};
+      if (li.sel) {li.sel = null;};
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app #addaction' : function(el){
+    el.onsubmit = function(ev){
+        document.getElementById('actname').blur();
+        return false;
+    };
+  },
+  '#app #actname' : function(el){
+    var re = new RegExp('[^a-zA-Z0-9-_~,.\\$\\(\\)# ]','g'), initialVal = $F(el);
+    el.onfocus = function(){Element.addClassName('addaction', 'adding');};
+    el.onkeyup = function() {
+      var name = $F(el), id = $F('actid');
+      if (id == initialVal.replace(re,'') || id == name.replace(re,'')) {
+        $('actid').value = name.replace(re,'');
+        initialVal = name;
+      };
+    };
+  },
+  '#app #buttonadd' : function(el){
+    el.onclick = function(ev){
+      var ev = ev?ev:window.event, v = validateField;
+      if (v('actname',$F('actname')) && v('actid',$F('actid'))) {
+        var idx = $('tabslist').getElementsByTagName('LI').length;
+        new Ajax.Request('qpt_add',
+          {parameters:'category='+category+'&idx='+idx+'&'+Form.serialize('addaction'),
+           onSuccess: function(request){
+             var dl = $('addaction').getElementsByTagName('DL')[3], dd = dl.getElementsByTagName('DD')[0];
+             Effect.BlindUp(dd, {
+               duration : 0.1,
+               afterFinish : function(){
+                 shiftClassNames(dl, 'expandedBlock', 'collapsedBlock');
+                 new Insertion.Bottom('tabslist', request.responseText);
+                 Form.reset('addaction');
+                 Element.removeClassName('addaction', 'adding');
+                 var dls = $A($('tabslist').getElementsByTagName('DL')).findAll(
+                   function(dl){return Element.hasClassName(dl, 'collapseAdvanced');}
+                 );
+                 collapseLi(dls[dls.length-1]);
+                 if ($A($('tabslist').getElementsByTagName('LI').length > 0)) {
+                     Element.removeClassName('reorder', 'noitems');
+                 };
+                 ieHover();
+                 Behaviour.apply();
+               }
+             });
+           },
+           onFailure: function(request){
+             var message = (/Error Value\s*<\/dt>\s*<dd>(.*?)<\/dd>/i).exec(request.responseText);
+             window.alert(message[1]);
+           }
+          }
+        );
+      };
+      return true;
+    }
+  },
+  '#app #buttoncancel' : function(el){
+    el.onclick = function(ev){
+      var ev = ev ? ev : window.event, dl = $('addaction').getElementsByTagName('DL')[3], dd = dl.getElementsByTagName('DD')[0];
+      Effect.BlindUp(dd, {
+        duration : 0.1,
+        afterFinish : function(){
+          shiftClassNames(dl, 'expandedBlock', 'collapsedBlock');
+          Form.reset('addaction');
+          Element.removeClassName('addaction', 'adding');
+          Behaviour.apply();
+       }
+      });
+      Event.stop(ev);
+      return false;
+    }
+  },
+  '#app #generated_tabs' : function(el){
+    el.onclick = function(ev){
+      new Ajax.Request('qpt_setproperty', {
+        parameters : 'generated_tabs='+el.checked,
+        onSuccess : function(){
+          new Ajax.Updater({success:'roottabs'}, 'qpt_getroottabs', {
+            method : 'get',
+            onComplete : function(){
+              el.attachEvent?ieHover():'';
+              Behaviour.apply();
+            }
+          });
+       }
+      });
+    }
+  }
+};
+
+/* Registerin previous rules with Behaviour.js */
+
+Behaviour.register(myrules);
+
+/* Registering global handlers for Ajax requests through Prototype */
+
+var globalHandlers = {
+  onCreate: function(){Element.addClassName('app', 'working');},
+  onComplete: function() {if(Ajax.activeRequestCount == 0) Element.removeClassName('app', 'working');}
+};
+
+Ajax.Responders.register(globalHandlers);
+
+/* Adding event listeners for hovering in IE & collapsing Advanced sections on loading */
+
+Event.observe(window, 'load', collapseAdvanced);
+Event.observe(window, 'load', function() {
+    var category_input = document.getElementById('actions_category');
+    if (category_input && typeof(category_input.value) != 'undefined') {
+        category_input.value ? category = category_input.value : {};
+    };
+});
+Event.observe(window, 'unload', function() {
+    // cleanup memory to prevent memory lack
+    delete gBeforeReorderFragment;
+    delete gBeforeEditData;
+    delete gBeforeReorderData
+});
+
+if (window.attachEvent) {Event.observe(window, 'load', ieHover, false);}
+
+/* Utility functions */
+
+function ieHover(){
+  $A($('app').getElementsByTagName('LI')).each(function(el){
+    if (el.attachEvent){
+      if (!el.hovers) el.hovers = {};
+      if (el.hovers['hover']) return;
+      el.hovers['hover'] = true;
+      el.attachEvent('onmouseover', function(){
+        if (!Element.hasClassName(el, 'onHover')) {Element.addClassName(el, 'onHover');};
+      });
+      el.attachEvent('onmouseout',  function(){Element.removeClassName(el, 'onHover');});
+    }
+  });
+};
+
+function collapseLi(dl) {
+  var dd = dl.getElementsByTagName('DD')[0];
+  if (!Element.hasClassName(dl, 'collapsedBlock') && Element.visible(dd)) {
+    shiftClassNames(dl, 'expandedBlock', 'collapsedBlock');
+  };
+  Element.hide(dd);
+};
+
+function collapseAdvanced() {
+  var dls = $A($('app').getElementsByTagName('DL')).findAll(
+    function(dl){return Element.hasClassName(dl, 'collapseAdvanced');}
+  );
+  $A(dls).each(collapseLi);
+};
+
+function shiftClassNames(el, from, to) {
+  Element.removeClassName(el, from);
+  Element.addClassName(el, to);
+};
+
+function validateField(id, val) {
+  var re = new RegExp('[^a-zA-Z0-9-_~,.\$\(\)# ]','g');
+  if (!val) {
+    window.alert(id.replace(/act/,'') + ' field is required!');
+    document.getElementById(id).focus();
+    return false;
+  }
+  else {
+    er = id != 'actid' ? true : val.search(re) == -1 ? true : false;
+    if (!er) {
+      window.alert(val+' is not a legal name.\n The following characters are invalid:' + val.match(re).toString());
+      document.getElementById(id).focus();
+    }
+  }
+  return er;
+};
+
+function removeEdition(el) {
+  var el = el ? el : 'tabslist';
+  $A($(el).getElementsByTagName('LI')).each(function(li,idx){if(li != el) li.onclick=function(event){return false;};});
+};
+
+
+//**********************************************
+
+//    Fixed bug: innerHTML return not actual data, now used DOM
+
+
+function grepInfo(li) {
+    
+    // grep li state
+    var info = {"li.id" : li.id, "li.title": li.title, "li.className" : li.className};
+    
+    // grep visibility checkbox state
+    var vis_box = li.getElementsByTagName("INPUT")[0];
+    info["vis_box.checked"] = vis_box.checked;
+    info["vis_box.name"] = vis_box.name;
+    
+    var spans = li.getElementsByTagName("SPAN");
+    
+    // grep url helper state
+    info["url_helper.text"] = spans[0].innerHTML;
+    
+    // grep tab title state
+    info["tab_title.text"] = spans[1].innerHTML;
+    
+    // grep edit form state
+    var edit_form = li.getElementsByTagName("FORM")[0];
+    info["edit_form.name"] = edit_form.name;
+    info["edit_form.action"] = edit_form.action;
+    
+    var inputs = edit_form.getElementsByTagName("INPUT");
+    
+    // grep hidden input index value
+    info["input_idx.value"] = inputs[0].value;
+    
+    // grep action name input state
+    info["input_name.name"] = inputs[1].name;
+    info["input_name.value"] = inputs[1].value;
+    
+    // grep dl advanced section class name
+    info["dl_advanced.className"] = edit_form.getElementsByTagName("DL")[1].className;
+    info["dd_advanced.style.display"] = edit_form.getElementsByTagName("DD")[1].style.display;
+    
+    // grep url input state
+    info["input_url.name"] = inputs[2].name;
+    info["input_url.value"] = inputs[2].value;
+    
+    // grep id input state
+    info["input_id.name"] = inputs[3].name;
+    info["input_id.value"] = inputs[3].value;
+    
+    // grep condition input state
+    info["input_condition.name"] = inputs[4].name;
+    info["input_condition.value"] = inputs[4].value;
+    
+    // grep buttons state
+    info["save_button.value"] = inputs[5].value;
+    info["cancel_button.value"] = inputs[6].value;
+    
+    return info;
+}
+
+function recoverAction(info) {
+    
+    // create list item and assign corresponding attributes
+    var li = document.createElement("LI");
+    li.id = info["li.id"];
+    li.title = info["li.title"];
+    li.className = info["li.className"];
+    
+    // create drag handle image and assign corresponding attributes
+    var drag_handle = document.createElement("IMG");
+    drag_handle.className = "drag-handle";
+    drag_handle.src = "drag.gif";
+    drag_handle.alt = "";
+    drag_handle.height = "11";
+    drag_handle.width = "25";
+    
+    li.appendChild(drag_handle);
+    
+    // create bridge div element for visibility checkbox
+    var bridge = document.createElement("DIV");
+    bridge.className = "bridge";
+    
+    // create checkbox for visibility control
+    var vis_box = document.createElement("INPUT");
+    vis_box.type = "checkbox";
+    vis_box.className = "visibility";
+    vis_box.value = "1";
+    vis_box.title = "visibility";
+    vis_box.name = info["vis_box.name"];
+    
+    bridge.appendChild(vis_box);
+
+    // buggy IE
+    if (info["vis_box.checked"]) {
+        vis_box.setAttribute("checked", "checked");
+        vis_box.defaultChecked = true;
+    }
+
+    li.appendChild(bridge);
+    
+    // create Delete link
+    var del_link = document.createElement("A");
+    del_link.className = "delete";
+    del_link.href = "#";
+    del_link.appendChild(document.createTextNode("Delete"));
+    
+    li.appendChild(del_link);
+    
+    // create url-helper element
+    var url_helper = document.createElement("SPAN");
+    url_helper.className = "url-helper";
+    url_helper.appendChild(document.createTextNode(info["url_helper.text"]));
+    
+    li.appendChild(url_helper);
+    
+    // create tab title element
+    var tab_title = document.createElement("SPAN");
+    tab_title.className = "tab-title";
+    tab_title.appendChild(document.createTextNode(info["tab_title.text"]));
+    
+    li.appendChild(tab_title);
+    
+    // create edit form
+    var edit_form = document.createElement("FORM");
+    edit_form.className = "editform";
+    edit_form.method = "post";
+    edit_form.name = info["edit_form.name"];
+    edit_form.action = info["edit_form.action"];
+    
+    // create hidden input with index value
+    var input_idx = document.createElement("INPUT");
+    input_idx.type = "hidden";
+    input_idx.name = "idx";
+    input_idx.value = info["input_idx.value"];
+    
+    edit_form.appendChild(input_idx);
+    
+    // create dl element for name input section
+    var dl_name = document.createElement("DL");
+    var dt_name = document.createElement("DT");
+    var name_label = document.createElement("LABEL");
+    name_label.appendChild(document.createTextNode("Name"));
+    
+    dt_name.appendChild(name_label);
+    dl_name.appendChild(dt_name);
+    
+    // crete input for action name
+    var dd_name = document.createElement("DD");
+    var input_name = document.createElement("INPUT");
+    input_name.type = "text";
+    input_name.size = "30";
+    input_name.name = info["input_name.name"];
+    input_name.value = info["input_name.value"];
+    
+    dd_name.appendChild(input_name);
+    dl_name.appendChild(dd_name);
+    edit_form.appendChild(dl_name);
+    
+    // create dl element for advanced inputs
+    var dl_advanced = document.createElement("DL");
+    dl_advanced.className = info["dl_advanced.className"];
+    
+    // create collapsible header
+    var dt_advanced = document.createElement("DT");
+    dt_advanced.className = "headerAdvanced";
+    dt_advanced.appendChild(document.createTextNode("Advanced"));
+    
+    dl_advanced.appendChild(dt_advanced);
+    
+    // create collapsible content
+    var dd_advanced = document.createElement("DD");
+    dd_advanced.className = "contentAdvanced";
+    dd_advanced.style.display = info["dd_advanced.style.display"];
+    
+    // add URL section
+    // add url header
+    var dl_url = document.createElement("DL");
+    var dt_url = document.createElement("DT");
+    var label_url = document.createElement("LABEL");
+    label_url.appendChild(document.createTextNode("URL (Expression)"));
+    
+    dt_url.appendChild(label_url);
+    dl_url.appendChild(dt_url);
+    
+    //add url value
+    var dd_url = document.createElement("DD");
+    var input_url = document.createElement("INPUT");
+    input_url.type = "text";
+    input_url.size = "30";
+    input_url.name = info["input_url.name"];
+    input_url.value = info["input_url.value"];
+    
+    dd_url.appendChild(input_url);
+    dl_url.appendChild(dd_url);
+    dd_advanced.appendChild(dl_url);
+    
+    // add ID section
+    // add id header
+    var dl_id = document.createElement("DL");
+    var dt_id = document.createElement("DT");
+    var label_id = document.createElement("LABEL");
+    label_id.appendChild(document.createTextNode("Id"));
+    
+    dt_id.appendChild(label_id);
+    dl_id.appendChild(dt_id);
+    
+    //add id value
+    var dd_id = document.createElement("DD");
+    var input_id = document.createElement("INPUT");
+    input_id.type = "text";
+    input_id.name = info["input_id.name"];
+    input_id.value = info["input_id.value"];
+    
+    dd_id.appendChild(input_id);
+    dl_id.appendChild(dd_id);
+    dd_advanced.appendChild(dl_id);
+    
+    // add Condition section
+    // add condition header
+    var dl_condition = document.createElement("DL");
+    var dt_condition = document.createElement("DT");
+    var label_condition = document.createElement("LABEL");
+    label_condition.appendChild(document.createTextNode("Condition (Expression)"));
+    
+    dt_condition.appendChild(label_condition);
+    dl_condition.appendChild(dt_condition);
+    
+    //add condition value
+    var dd_condition = document.createElement("DD");
+    var input_condition = document.createElement("INPUT");
+    input_condition.type = "text";
+    input_condition.size = "30";
+    input_condition.name = info["input_condition.name"];
+    input_condition.value = info["input_condition.value"];
+    
+    dd_condition.appendChild(input_condition);
+    dl_condition.appendChild(dd_condition);
+    dd_advanced.appendChild(dl_condition);
+    
+    // add visual clear div to advanced dd element
+    var visual_clear = document.createElement("DIV");
+    visual_clear.className = "visualClear";
+    visual_clear.appendChild = document.createTextNode("<!-- -->");
+    
+    dd_advanced.appendChild(visual_clear);
+    dl_advanced.appendChild(dd_advanced);
+    edit_form.appendChild(dl_advanced);
+    
+    // create div element for form controls
+    var form_controls = document.createElement("DIV");
+    
+    // add save button
+    var save_button = document.createElement("INPUT");
+    save_button.type = "submit";
+    save_button.className = "editsave";
+    save_button.value = info["save_button.value"];
+    form_controls.appendChild(save_button);
+    
+    // add cancel button
+    var cancel_button = document.createElement("INPUT");
+    cancel_button.type = "submit";
+    cancel_button.className = "editcancel";
+    cancel_button.value = info["cancel_button.value"];
+    form_controls.appendChild(cancel_button);
+    
+    edit_form.appendChild(form_controls);
+    li.appendChild(edit_form);
+
+    return li;
+}
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/scriptaculous.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/scriptaculous.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/scriptaculous.js	(revision 32)
@@ -0,0 +1,48 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var Scriptaculous = {
+  Version: '1.5_rc5',
+  require: function(libraryName) {
+    // inserting via DOM fails in Safari 2.0, so brute force approach
+    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
+  },
+  load: function() {
+    if((typeof Prototype=='undefined') ||
+      parseFloat(Prototype.Version.split(".")[0] + "." +
+                 Prototype.Version.split(".")[1]) < 1.4)
+      throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0");
+    var scriptTags = document.getElementsByTagName("script");
+    for(var i=0;i<scriptTags.length;i++) {
+      if(scriptTags[i].src && scriptTags[i].src.match(/scriptaculous\.js(\?.*)?$/)) {
+        var path = scriptTags[i].src.replace(/scriptaculous\.js(\?.*)?$/,'');
+        this.require(path + 'builder.js');
+        this.require(path + 'effects.js');
+        this.require(path + 'dragdrop.js');
+        this.require(path + 'controls.js');
+        this.require(path + 'slider.js');
+        break;
+      }
+    }
+  }
+}
+
+Scriptaculous.load();
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/slider.js
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/slider.js	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/javascripts/slider.js	(revision 32)
@@ -0,0 +1,275 @@
+// Copyright (c) 2005 Marty Haught, Thomas Fuchs 
+//
+// See http://script.aculo.us for more info
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+if(!Control) var Control = {};
+Control.Slider = Class.create();
+
+// options:
+//  axis: 'vertical', or 'horizontal' (default)
+//
+// callbacks:
+//  onChange(value)
+//  onSlide(value)
+Control.Slider.prototype = {
+  initialize: function(handle, track, options) {
+    var slider = this;
+    
+    if(handle instanceof Array) {
+      this.handles = handle.collect( function(e) { return $(e) });
+    } else {
+      this.handles = [$(handle)];
+    }
+    
+    this.track   = $(track);
+    this.options = options || {};
+
+    this.axis      = this.options.axis || 'horizontal';
+    this.increment = this.options.increment || 1;
+    this.step      = parseInt(this.options.step || '1');
+    this.range     = this.options.range || $R(0,1);
+    
+    this.value     = 0; // assure backwards compat
+    this.values    = this.handles.map( function() { return 0 });
+    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
+    this.options.startSpan = $(this.options.startSpan || null);
+    this.options.endSpan   = $(this.options.endSpan || null);
+
+    this.restricted = this.options.restricted || false;
+
+    this.maximum   = this.options.maximum || this.range.end;
+    this.minimum   = this.options.minimum || this.range.start;
+
+    // Will be used to align the handle onto the track, if necessary
+    this.alignX = parseInt(this.options.alignX || '0');
+    this.alignY = parseInt(this.options.alignY || '0');
+    
+    this.trackLength = this.maximumOffset() - this.minimumOffset();
+
+    this.active   = false;
+    this.dragging = false;
+    this.disabled = false;
+
+    if(this.options.disabled) this.setDisabled();
+
+    // Allowed values array
+    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
+    if(this.allowedValues) {
+      this.minimum = this.allowedValues.min();
+      this.maximum = this.allowedValues.max();
+    }
+
+    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+    this.eventMouseMove = this.update.bindAsEventListener(this);
+
+    // Initialize handles in reverse (make sure first handle is active)
+    this.handles.each( function(h,i) {
+      i = slider.handles.length-1-i;
+      slider.setValue(parseFloat(
+        (slider.options.sliderValue instanceof Array ? 
+          slider.options.sliderValue[i] : slider.options.sliderValue) || 
+         slider.range.start), i);
+      Element.makePositioned(h); // fix IE
+      Event.observe(h, "mousedown", slider.eventMouseDown);
+    });
+    
+    Event.observe(this.track, "mousedown", this.eventMouseDown);
+    Event.observe(document, "mouseup", this.eventMouseUp);
+    Event.observe(document, "mousemove", this.eventMouseMove);
+    
+    this.initialized = true;
+  },
+  dispose: function() {
+    var slider = this;    
+    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
+    Event.stopObserving(document, "mouseup", this.eventMouseUp);
+    Event.stopObserving(document, "mousemove", this.eventMouseMove);
+    this.handles.each( function(h) {
+      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
+    });
+  },
+  setDisabled: function(){
+    this.disabled = true;
+  },
+  setEnabled: function(){
+    this.disabled = false;
+  },  
+  getNearestValue: function(value){
+    if(this.allowedValues){
+      if(value >= this.allowedValues.max()) return(this.allowedValues.max());
+      if(value <= this.allowedValues.min()) return(this.allowedValues.min());
+      
+      var offset = Math.abs(this.allowedValues[0] - value);
+      var newValue = this.allowedValues[0];
+      this.allowedValues.each( function(v) {
+        var currentOffset = Math.abs(v - value);
+        if(currentOffset <= offset){
+          newValue = v;
+          offset = currentOffset;
+        } 
+      });
+      return newValue;
+    }
+    if(value > this.range.end) return this.range.end;
+    if(value < this.range.start) return this.range.start;
+    return value;
+  },
+  setValue: function(sliderValue, handleIdx){
+    if(!this.active) {
+      this.activeHandle    = this.handles[handleIdx];
+      this.activeHandleIdx = handleIdx;
+      this.updateStyles();
+    }
+    handleIdx = handleIdx || this.activeHandleIdx || 0;
+    if(this.initialized && this.restricted) {
+      if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
+        sliderValue = this.values[handleIdx-1];
+      if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
+        sliderValue = this.values[handleIdx+1];
+    }
+    sliderValue = this.getNearestValue(sliderValue);
+    this.values[handleIdx] = sliderValue;
+    this.value = this.values[0]; // assure backwards compat
+    
+    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = 
+      this.translateToPx(sliderValue);
+    
+    this.drawSpans();
+    if(!this.dragging || !this.event) this.updateFinished();
+  },
+  setValueBy: function(delta, handleIdx) {
+    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
+      handleIdx || this.activeHandleIdx || 0);
+  },
+  translateToPx: function(value) {
+    return Math.round((this.trackLength / (this.range.end - this.range.start)) * (value - this.range.start)) + "px";
+  },
+  translateToValue: function(offset) {
+    return ((offset/this.trackLength) * (this.range.end - this.range.start)) + this.range.start;
+  },
+  getRange: function(range) {
+    var v = this.values.sortBy(Prototype.K); 
+    range = range || 0;
+    return $R(v[range],v[range+1]);
+  },
+  minimumOffset: function(){
+    return(this.isVertical() ? this.alignY : this.alignX);
+  },
+  maximumOffset: function(){
+    return(this.isVertical() ?
+      this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX);
+  },  
+  isVertical:  function(){
+    return (this.axis == 'vertical');
+  },
+  drawSpans: function() {
+    var slider = this;
+    if(this.spans)
+      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
+    if(this.options.startSpan)
+      this.setSpan(this.options.startSpan,
+        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
+    if(this.options.endSpan)
+      this.setSpan(this.options.endSpan, 
+        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
+  },
+  setSpan: function(span, range) {
+    if(this.isVertical()) {
+      span.style.top = this.translateToPx(range.start);
+      span.style.height = this.translateToPx(range.end - range.start);
+    } else {
+      span.style.left = this.translateToPx(range.start);
+      span.style.width = this.translateToPx(range.end - range.start);
+    }
+  },
+  updateStyles: function() {
+    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
+    Element.addClassName(this.activeHandle, 'selected');
+  },
+  startDrag: function(event) {
+    if(Event.isLeftClick(event)) {
+      if(!this.disabled){
+        this.active = true;
+        
+        var handle = Event.element(event);
+        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
+        if(handle==this.track) {
+          var offsets  = Position.cumulativeOffset(this.track); 
+          this.event = event;         
+          this.setValue(this.translateToValue( 
+            this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0]
+          ));
+        } else {
+          // find the handle (prevents issues with Safari)
+          while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
+            handle = handle.parentNode;
+        
+          this.activeHandle    = handle;
+          this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
+          this.updateStyles();
+        
+          var offsets  = Position.cumulativeOffset(this.activeHandle);
+          this.offsetX = (pointer[0] - offsets[0]);
+          this.offsetY = (pointer[1] - offsets[1]);
+        }
+      }
+      Event.stop(event);
+    }
+  },
+  update: function(event) {
+   if(this.active) {
+      if(!this.dragging) this.dragging = true;
+      this.draw(event);
+      // fix AppleWebKit rendering
+      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+      Event.stop(event);
+   }
+  },
+  draw: function(event) {
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    var offsets = Position.cumulativeOffset(this.track);
+    pointer[0] -= this.offsetX + offsets[0];
+    pointer[1] -= this.offsetY + offsets[1];
+    this.event = event;
+    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
+    if(this.initialized && this.options.onSlide) this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
+  },
+  endDrag: function(event) {
+    if(this.active && this.dragging) {
+      this.finishDrag(event, true);
+      Event.stop(event);
+    }
+    this.active = false;
+    this.dragging = false;
+  },  
+  finishDrag: function(event, success) {
+    this.active = false;
+    this.dragging = false;
+    this.updateFinished();
+  },
+  updateFinished: function() {
+    if(this.initialized && this.options.onChange) 
+      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
+    this.event = null;
+  }
+}
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/prefs_tabs_form.cpt
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/prefs_tabs_form.cpt	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/prefs_tabs_form.cpt	(revision 32)
@@ -0,0 +1,106 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      lang="en-US"
+      metal:use-macro="here/prefs_main_template/macros/master"
+      i18n:domain="qPloneTabs">
+
+  <metal:block fill-slot="top_slot"
+             tal:define="dummy python:request.set('disable_border',1)" />
+<head>
+  <metal:javascript_head_slot fill-slot="javascript_head_slot">
+        <script type="text/javascript" src="javascripts/prototype.js"></script>
+        <script type="text/javascript" src="javascripts/scriptaculous.js"></script>
+        <script type="text/javascript" src="javascripts/behaviour.js"></script>
+        <script type="text/javascript" src="javascripts/qplonetabs.js"></script>
+  </metal:javascript_head_slot>
+  <metal:cssslot fill-slot="css_slot">
+    <style type="text/css"  tal:content="string:@import url($portal_url/qplonetabs.css);" />
+  </metal:cssslot>
+</head>
+
+<body>
+  <div metal:fill-slot="prefs_configlet_main"
+       tal:define="errors python:request.get('errors', {});
+                   category here/REQUEST/category|nothing;
+                   category python:test(category and category!='', category, 'portal_tabs')">
+    <div id="app" class="viewing">
+      <h1 i18n:translate="heading_plonetabs">
+        <span tal:content="python:context.getPageTitle(category)" /><span id="busy"><img src='busy_icon.gif' alt="" /></span></h1>
+      <a href=""
+       class="link-parent"
+       tal:attributes="href string:${portal_url}/plone_control_panel"
+       i18n:translate="label_up_to_plone_setup">
+          Up to Site Setup
+      </a>
+      <input type="hidden" name="category" value="portal_tabs" id="actions_category"
+             tal:attributes="value category" />
+      <div class="reorder-controls"
+           tal:define="notempty python:modules['Products.qPloneTabs'].getPortalActions(here, category)">
+        <a href="#" id="reorder" class=""
+           tal:attributes="class python:test(notempty, '', 'noitems')">Reorder</a>
+      </div>
+      <div class="sort-controls">
+        <a href="#" id="save">Save</a> |
+        <a href="#" id="cancel">Cancel</a>
+      </div>
+      <ul class="csshover"
+          id="tabslist" tal:content="structure python:here.qpt_gettabslist(category=category)" />
+      <form id="addaction"
+            method="post"
+            action="addPTAction"
+            tal:attributes="action string:${portal_url}/prefs_tabs_form;">
+        <dl class="field-visible bridge"><dt><label>Visible</label></dt>
+            <dd><input id="actvisible" type="checkbox" value="1" checked="checked" title="visibility"
+                       name="visible" /></dd></dl>
+        <dl class="field-name"><dt><label>Name</label></dt>
+            <dd><input id="actname" type="text" value=""
+                        name="name" /></dd></dl>
+        <dl class="field-action"><dt><label>URL (Expression)</label></dt>
+            <dd><input id="actaction" type="text" value="" size="30"
+                        name="action" /></dd></dl>
+        <dl class="advanced collapseAdvanced expandedBlock">
+          <dt class="headerAdvanced">Advanced</dt>
+          <dd class="contentAdvanced">
+            <dl class="field-id"><dt><label>Id</label></dt>
+              <dd><input id="actid"        type="text" value=""
+                          name="id" /></dd></dl>
+            <dl class="field-condition"><dt><label>Condition (Expression)</label></dt>
+              <dd><input id="actcondition" type="text" value="" size="30"
+                          name="condition" /></dd></dl>
+            <div class="visualClear"><!-- --></div>
+          </dd>
+        </dl>
+        <div class="add-controls">
+            <input type="submit" id="buttonadd"    i18n:attributes="value" value="Add" />
+            <input type="submit" id="buttoncancel" i18n:attributes="value" value="Cancel" />
+        </div>
+      </form>
+      <div class="field"
+           tal:define="generated_tabs python: not site_properties.disable_folder_sections;
+                       generated_tabs request/generated_tabs | generated_tabs;
+                       error errors/generated_tabs | nothing;"
+           tal:attributes="class python:test(error, 'field error', 'field')"
+           tal:on-error="string:"
+           tal:condition="python:category == 'portal_tabs'">
+        <input type="checkbox"
+               id="generated_tabs"
+               name="generated_tabs"
+               value="1"
+               tal:attributes="checked python:test(generated_tabs, 'checked', None)" />
+        <label for="generated_tabs" i18n:translate="label_generated_tabs_enable">Automatically generate tabs</label>
+        <div class="formHelp" i18n:translate="help_generated_tabs">
+          By default, all folders created at the root level will have global section
+          navigation generated. You can turn this off if you prefer manually constructing
+          this part of the navigation.
+        </div>
+      </div>
+      <ul class="csshover" id="roottabs"
+          tal:on-error="string:"
+          tal:content="structure here/qpt_getroottabs"
+          tal:condition="python:category == 'portal_tabs'" />
+    </div>
+
+  </div>
+</body>
+</html>
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/prefs_tabs_form.cpt.metadata
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/prefs_tabs_form.cpt.metadata	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/prefs_tabs_form.cpt.metadata	(revision 32)
@@ -0,0 +1,8 @@
+[security]
+View=0:Authenticated
+
+[default]
+title=Edit Plone Tabs
+
+[actions]
+action.success=traverse_to:string:prefs_tabs_form
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qplonetabs.css.dtml
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qplonetabs.css.dtml	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qplonetabs.css.dtml	(revision 32)
@@ -0,0 +1,352 @@
+/* <dtml-with base_properties> (do not remove this :) */
+/* <dtml-call "REQUEST.set('portal_url', portal_url())"> (not this either :) */
+
+/*
+***************************************************************************************************
+*                                                                                                 *
+*   There are two global self-excluding classes: 'viewing' and 'sorting',                         *
+*   one local class 'editing' for menu items which are expanded for editing,                      *
+*   and one more local class 'adding' for adding form when it's in open state.                    *
+*                                                                                                 *
+*   Reorder action are always available and it TEMPORARY stop other actions on the form:          *
+*       editing items                                                                             *
+*       deleting items                                                                            *
+*       toggling item visibility                                                                  *
+*       adding item                                                                               *
+*                                                                                                 *
+*   After returning to 'viewing' class from 'sorting' all items become to the same state they     *
+*   were before sorting (in editing state or not).                                                *
+*                                                                                                 *
+*   And the same behaviour for adding form: form state ('adding' or not) become the same it was   *
+*   before sorting.                                                                               *
+*                                                                                                 *
+*   All actions except reordering could be done at any time in relationship to each other.        *
+*                                                                                                 *
+***************************************************************************************************
+*/
+
+#app.viewing                        .reorder-controls,
+#app.sorting                        .sort-controls,
+#app.viewing li.editing             .editform,
+#app #addaction.adding              .field-visible,
+#app #addaction.adding              .field-name dt,
+#app #addaction.adding              .field-action,
+#app #addaction.adding              .field-action label,
+#app #addaction.adding              .advanced,
+#app #addaction.adding              .field-id,
+#app #addaction.adding              .field-condition,
+#app #addaction.adding              .add-controls {
+    display: block;
+}
+
+#app                                #busy,
+#app.viewing                        .noitems,
+#app                                .reorder-controls,
+#app                                .sort-controls,
+#app.viewing li.editing             .url-helper,
+#app.viewing li.editing             .tab-title,
+#app                                .drag-handle,
+#app                                .delete,
+#app                                .visibility,
+#app                                .editform,
+#app.sorting                        #addaction,
+#app                                .field-visible,
+#app                                .field-name dt,
+#app                                .field-action,
+#app                                .advanced,
+#app                                .field-id,
+#app                                .field-condition,
+#app                                .add-controls {
+    display: none;
+}
+
+#app.working #busy,
+#app.viewing ul.csshover li:hover    .delete,
+#app.viewing ul.csshover li.onHover  .delete,
+#app.viewing ul.csshover li:hover    .visibility,
+#app.viewing ul.csshover li.onHover  .visibility,
+#app ul#roottabs.csshover li:hover   .visibility,
+#app ul#roottabs.csshover li.onHover .visibility,
+#app.sorting                         .drag-handle {
+    display: inline;
+}
+
+/**********************************************************/
+
+#app.sorting #tabslist img.drag-handle {
+    cursor: move;
+}
+
+#app .invisible span.tab-title {
+    color:#aaa;
+}
+
+/*common***************************************************/
+
+div.reorder-controls,
+div.sort-controls {
+    float:left;
+}
+
+.drag-handle {
+    float:left;
+    margin:0.5em 0.5em 0 0;
+}
+
+#app ul {
+    list-style-type: none;
+    list-style-image: none;
+    margin:2.5em 0 0 1.7em;
+}
+
+#app .delete {
+    background: url(&dtml-portal_url;/trash.gif) 12px 50% no-repeat;
+    padding: 0 0 0 25px;
+    line-height:1.8em;
+    font-size:80%;
+    border:0;
+    position:absolute;
+    right:0;
+    margin-right:-5.7em;
+    color: &dtml-linkColor; !important;
+}
+
+* html #app .delete {
+    margin-right:-4.7em;
+}
+
+#app ul.csshover li {
+    border: 1px &dtml-borderStyle; transparent;
+    border-right:0;
+    padding: 0 0.5em;
+    margin: 0.1em 0;
+    width:35em;
+    margin-right:6.5em;
+    line-height:1.5em;
+    clear:left;
+    position:relative;
+}
+
+* html #app ul.csshover li {
+    border-color:white;
+}
+
+#app ul.csshover li span {
+    display:block;
+}
+
+#app ul.csshover li span.url-helper {
+    position:absolute;
+    right:0;
+    margin-left:20%;
+    padding-right:0.5em;
+    overflow:hidden;
+    font-size:80%;
+    color:#aaa;
+    text-align: right;
+    width: 80%;
+
+}
+
+#app ul.csshover li.editing {
+    padding:0;
+    margin: 0;
+    width:36em;
+}
+
+#app.sorting ul.csshover li.editing {
+    padding: 0 0.5em;
+}
+
+#app li.editing form {
+    margin:-0.1em;
+}
+
+* html #app li.editing form {
+    margin: -0.2em 0 0 0;
+}
+
+#app dl {
+    margin:0;
+}
+
+#app dl label {
+    display:none;
+}
+
+#app dl dl label {
+    display:block;
+} 
+
+#app dd {
+    margin:0;
+}
+
+#app dd dl {
+    margin:0.5em 0;
+}
+
+#app input[type="text"] {
+    width: 99.5%;
+}
+
+#app .bridge {
+    position:absolute;
+    left:-1.5em;
+    top:0.2em;
+    width:3em;
+}
+
+* html #app div.bridge {
+    top:0;
+    left:-2em;
+}
+
+#app input:focus {
+    border:  1px inset threedface !important;
+}
+
+* html #app input {
+    width:97.3%;
+    padding: 0.1em 0.4em;
+}
+
+#app form div {
+    line-height:1.4em;
+    margin-top:0.1em;
+    min-height:1.6em;
+}
+
+* html #app form div {
+    margin-top:-0.2em;
+    position:relative;
+    height:1%;
+}
+
+#app div input {
+    width:auto;
+    padding: 0.05em 0.5em;
+    float:left;
+    margin:0 0.1em 0 0;
+}
+
+* html #app div input {
+    padding: 0 0.3em;
+}
+
+div.field { margin-top:2em; }
+
+/*hover***************************************************/
+
+#app.viewing ul#tabslist.csshover li:hover,
+#app.viewing ul#tabslist.csshover li.onHover {
+    background-color: #FFFFDD;
+    border: 1px inset threedface;
+    cursor: text;
+}
+
+#app.viewing ul#tabslist.csshover li.editing {
+    background-color: &dtml-backgroundColor;;
+    border: 1px &dtml-borderStyle; white;
+    cursor: auto;
+}
+
+#app ul#roottabs.csshover li:hover,
+#app ul#roottabs.csshover li.onHover {
+    background-color: #FFFFDD;
+/*    border:  1px inset threedface;*/
+    cursor: default;
+}
+
+/*edit*****************************************************/
+
+#app dl.collapseAdvanced {
+    margin-top:0.15em;
+    position:relative;
+    padding:0;
+    border:0 !important;
+    height:auto;
+    line-height:1em;
+}
+
+#app dl.collapseAdvanced dt.headerAdvanced {
+    cursor: pointer;
+    font-size: 90%;
+    font-weight:normal;
+    position:absolute;
+    padding-right:0.5em;
+    display:block;
+    margin:0;
+    top:0;
+    right:0;
+    color: &dtml-globalFontColor;;
+    line-height:2em;
+    background-color:transparent;
+}
+
+#app dl.collapseAdvanced dd.contentAdvanced {
+    padding:1em 1.5em 0 1.5em;
+    border:0;
+    background-color:&dtml-globalBackgroundColor;;
+}
+
+dl.expandedBlock dt.headerAdvanced {
+    padding: 0 6px 0 22px;
+    background: url(treeExpanded.gif) no-repeat 6px 50%;
+}
+
+dl.collapsedBlock dt.headerAdvanced {
+    padding: 0 0 0 22px;
+    background: url(treeCollapsed.gif) no-repeat 6px 50%;
+ }
+
+#app dl.advanced { 
+    clear: both;
+}
+
+/****************************************************************/
+
+#app #addaction {
+    margin-left:1.7em;
+    width:36.2em;
+    position:relative;
+}
+
+* html #app #addaction {
+    width:36.15em;
+}
+
+#app #addaction.adding .field-action label {
+    position:absolute;
+    line-height:2.1em;
+    font-size:80%;
+    color: &dtml-globalBorderColor;;
+    right:0.5em;
+    font-weight:normal;
+    background-color:transparent;
+}
+
+#app #addaction.adding .field-action input {
+    margin-top:0.1em; 
+    padding-right:9em;
+    width:27em;
+}
+
+* html #app #addaction.adding .field-action input {
+    padding-right:0.5em;
+    width:96.8%;
+    margin-top:-0.05em;
+}
+
+#app #addaction .bridge {
+    left:-1.8em;
+    top:0em;
+}
+
+* html #app #addaction .bridge {
+    width:1.7em;
+    left:-2em;
+}
+
+
+/* </dtml-with> (do not remove this either :) */
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_add.py
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_add.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_add.py	(revision 32)
@@ -0,0 +1,65 @@
+## Script (Python) "qpt_add"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= idx, name, action='', id, category='portal_tabs', condition='', visible=False
+##title=
+##
+
+from Products.CMFCore.utils import getToolByName
+
+if action:
+    from Products.qPloneTabs.utils import processUrl
+    action = processUrl(context, str(action))
+if visible:
+    checked = """checked="checked" """
+    liClass = ""
+    visible = True
+else:
+    checked = ""
+    liClass = "invisible"
+    visible = False
+params = {'id':id, 'name':name, 'action':action, 'condition':condition,
+          'permission':'View', 'category':category, 'visible':visible}
+act_tool = getToolByName(context, 'portal_actions')
+act_tool.addAction(**params)
+params.update({'idx':idx, 'abs_url':context.portal_url(), 'checked':checked, 'class':liClass})
+return """
+<li id="tabslist_%(id)s" class="%(class)s">
+  <img class="drag-handle" src="drag.gif" alt="" height="11" width="25">
+  <div class="bridge"><input class="visibility" value="1" name="i%(idx)s_visibility" %(checked)s type="checkbox" title="visibility"></div>
+  <a class="delete" href="#">Delete</a>
+  <span class="url-helper">%(action)s</span>
+  <span class="tab-title">%(name)s</span>
+  <form class="editform" method="post" action="%(abs_url)s/prefs_tabs_form" name="f%(idx)s">
+    <input type="hidden" name="idx" value="%(idx)s" />
+      <dl>
+        <dt><label>Name</label></dt>
+        <dd><input type="text"     value="%(name)s"      name="i%(idx)s_name"                /></dd>
+      </dl>
+      <dl class="collapseAdvanced collapsedBlock">
+        <dt class="headerAdvanced">Advanced</dt>
+        <dd class="contentAdvanced">
+          <dl>
+            <dt><label>URL (Expression)</label></dt>
+            <dd><input type="text" value="%(action)s"    name="i%(idx)s_action"    size="30" /></dd>
+          </dl>
+          <dl>
+            <dt><label>Id</label></dt>
+            <dd><input type="text" value="%(id)s"        name="i%(idx)s_id"                  /></dd>
+          </dl>
+          <dl>
+            <dt><label>Condition (Expression)</label></dt>
+            <dd><input type="text" value="%(condition)s" name="i%(idx)s_condition" size="30" /></dd>
+          </dl>
+          <div class="visualClear"><!-- --></div>
+        </dd>
+      </dl>
+    <div>
+      <input type="submit" class="editsave"   value="Save"   />
+      <input type="submit" class="editcancel" value="Cancel" />
+    </div>
+  </form>
+</li>\n""" % params
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_delete.py
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_delete.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_delete.py	(revision 32)
@@ -0,0 +1,13 @@
+## Script (Python) "qpt_delete"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= idx, id, category='portal_tabs'
+##title=
+##
+
+from Products.qPloneTabs.utils import deleteAction
+
+return deleteAction(context, idx, id, category)
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_edit.py
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_edit.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_edit.py	(revision 32)
@@ -0,0 +1,19 @@
+## Script (Python) "qpt_edit"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= idx, num, visibility=[], category='portal_tabs'
+##title=
+##
+
+req = context.REQUEST
+params = {'self':context,'num':num, 'category':category}
+fields = ['name', 'action', 'id', 'condition']
+if visibility != []: params['visibility'] = visibility
+for item in fields:
+    params[item] = req.get('i'+idx+'_'+item, None)
+
+from Products.qPloneTabs.utils import editAction
+return editAction(**params)
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_getroottabs.pt
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_getroottabs.pt	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_getroottabs.pt	(revision 32)
@@ -0,0 +1,15 @@
+<tal:tabs tal:define="dummy python:request.RESPONSE.setHeader('Expires', 'Mon, 26 Jul 1996 05:00:00 GMT')"
+          tal:repeat="tab python:modules['Products.qPloneTabs'].getRootTabs(here)">
+<li tal:define="id tab/id;
+                visible python:not tab['exclude_from_nav']"
+    tal:attributes="id string:tabslist_${id};
+                    title tab/description|nothing;
+                    class python:test(visible, '', 'invisible')">
+  <div class="bridge"><input type="checkbox" class="visibility" value="1" id="id" name="rootvis"  title="visibility"
+         tal:attributes="name string:i${repeat/tab/index}_${attrs/name};
+                         id id;
+                         checked python:test(visible, 'checked', None)" /></div>
+  <span class="url-helper" tal:content="python:tab['url']">Tab Action</span>
+  <span class="tab-title"  tal:content="tab/name">Tab Name</span>
+</li>
+</tal:tabs>
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_gettabslist.pt
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_gettabslist.pt	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_gettabslist.pt	(revision 32)
@@ -0,0 +1,59 @@
+<tal:tabs tal:define="dummy python:request.RESPONSE.setHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT');
+                      category python:options.get('category', None) or 'portal_tabs'"
+          tal:repeat="tab python:modules['Products.qPloneTabs'].getPortalActions(here, category)">
+<li tal:define="id tab/id;
+                visible tab/visible|nothing;
+                index repeat/tab/index"
+    tal:attributes="id string:tabslist_${id};
+                    title tab/description|nothing;
+                    class python:test(visible, '', 'invisible')">
+  <img class="drag-handle" src="drag.gif" alt="" height="11" width="25" />
+  <div class="bridge"><input type="checkbox" class="visibility" value="1" name="visibility" title="visibility"
+                             tal:attributes="checked python:test(visible, 'checked', None);
+                                             name string:i${index}_${attrs/name}" /></div>
+  <a class="delete" href="#">Delete</a>
+  <span class="url-helper" tal:content="tab/getActionExpression|nothing">Tab Action</span>
+  <span class="tab-title" tal:content="tab/title">Tab Name</span>
+  <form class="editform"
+        method="post"
+        action="editPTAction"
+        tal:attributes="action string:${here/portal_url}/prefs_tabs_form;
+                        name string:f$index">
+      <input type="hidden" name="idx" tal:attributes="value index" />
+      <dl>
+        <dt><label>Name</label></dt>
+        <dd><input type="text" value="" name="name"
+                   tal:attributes="value tab/title;
+                                   name string:i${index}_${attrs/name}"      /></dd>
+      </dl>
+      <dl class="collapseAdvanced expandedBlock">
+        <dt class="headerAdvanced">Advanced</dt>
+        <dd class="contentAdvanced">
+          <dl>
+            <dt><label>URL (Expression)</label></dt>
+            <dd><input type="text" value="" name="action"     size="30"
+                       tal:attributes="value tab/getActionExpression|nothing;
+                                       name string:i${index}_${attrs/name}" /></dd>
+          </dl>
+          <dl>
+            <dt><label>Id</label></dt>
+            <dd><input type="text" value="" name="id"
+                       tal:attributes="value tab/id|nothing;
+                                       name string:i${index}_${attrs/name}" /></dd>
+          </dl>
+          <dl>
+            <dt><label>Condition (Expression)</label></dt>
+            <dd><input type="text" value="" name="condition"  size="30"
+                       tal:attributes="value tab/getCondition|nothing;
+                                       name string:i${index}_${attrs/name}" /></dd>
+          </dl>
+          <div class="visualClear"><!-- --></div>
+        </dd>
+      </dl>
+      <div>
+        <input type="submit" class="editsave"   i18n:attributes="value" value="Save"   />
+        <input type="submit" class="editcancel" i18n:attributes="value" value="Cancel" />
+      </div>
+  </form>
+</li>
+</tal:tabs>
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_reorder.py
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_reorder.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_reorder.py	(revision 32)
@@ -0,0 +1,15 @@
+## Script (Python) "reorder"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= idxs=[], category='portal_tabs'
+##title=
+##
+
+if not idxs: return 'No items'
+
+from Products.qPloneTabs.utils import reorderActions
+
+return reorderActions(context, idxs, category)
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_setproperty.py
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_setproperty.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_setproperty.py	(revision 32)
@@ -0,0 +1,18 @@
+## Script (Python) "qpt_setproperty"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= generated_tabs=False
+##title=
+##
+
+from Products.CMFCore.utils import getToolByName
+changeProperties = getToolByName(context, 'portal_properties').site_properties.manage_changeProperties
+if generated_tabs == 'true':
+  changeProperties(disable_folder_sections=False)
+else:
+  changeProperties(disable_folder_sections=True)
+return True
+
Index: qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_setvisibility.py
===================================================================
--- qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_setvisibility.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/skins/qPloneTabs/qpt_setvisibility.py	(revision 32)
@@ -0,0 +1,18 @@
+## Script (Python) "qpt_setvisibility"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= id, visibility
+##title=
+##
+
+from Products.CMFCore.utils import getToolByName
+obj = getToolByName(context, id, None)
+if obj:
+    if visibility == 'true': visibility = False
+    else: visibility = True
+    obj.update(excludeFromNav = visibility)
+
+return "Visibility set to %s" % visibility
Index: qPloneTabs/tags/0.3.1/utils.py
===================================================================
--- qPloneTabs/tags/0.3.1/utils.py	(revision 32)
+++ qPloneTabs/tags/0.3.1/utils.py	(revision 32)
@@ -0,0 +1,90 @@
+"""
+   Utility functions for portal_tab actions modifications.
+"""
+from Products.CMFCore.Expression import Expression
+from Products.CMFCore.utils import getToolByName
+
+def getPortalActions(self, category='portal_tabs'):
+    """ Return all portal actions with 'portal_tabs' category """
+    return filter(lambda a: a.category == category, getToolByName(self, 'portal_actions')._cloneActions())
+
+def editAction(self, num, name, id, action='', condition='', visibility=[], category='portal_tabs'):
+    """ Function for editing given action """
+    actions = getToolByName(self, 'portal_actions')._actions
+    tabs = filter(lambda a: a.category == category, actions)
+    tab = tabs[int(num)]
+    if visibility != []:
+        if visibility == 'true': visibility = True
+        else: visibility = False
+        tab.visible = visibility
+        return 'Changed visibility'
+    if id: tab.id = id
+    if name: tab.title = name
+    if isinstance(condition, basestring): tab.condition = Expression(condition)
+    if isinstance(action, basestring): tab.setActionExpression(Expression(action))
+    return True
+
+def reorderActions(self, idxs, category='portal_tabs'):
+    """ Reorder portal_tabs actions in given order """
+    idxs = list(map(int,idxs))
+    portal_actions = getToolByName(self, 'portal_actions')
+    actions = portal_actions._cloneActions()
+    tabs = [[action, actions.index(action)] for action in actions if action.category == category]
+    for idx in range(len(idxs)):
+        actions[tabs[idx][1]] = tabs[idxs[idx]][0]
+    portal_actions._actions = tuple(actions)
+    return idxs
+
+def deleteAction(self, idx, id, category='portal_tabs'):
+    """ Delete portal_tabs action with given index """
+    portal_actions = getToolByName(self, 'portal_actions')
+    actions = portal_actions._cloneActions()
+    tabs = filter(lambda a: a.category == category, actions)
+    if tabs[int(idx)].id == id:
+        portal_actions.deleteActions([actions.index(tabs[int(idx)]),])
+    return id
+
+def processUrl(self, url):
+    """ Return url in a right format """
+    import re
+    if url.find('/') == 0: return 'string:${portal_url}' + url
+    elif re.compile('^(ht|f)tps?\:', re.I).search(url): return 'string:' + url
+    elif re.compile('^(python:|string:|not:|exists:|nocall:|path:)', re.I).search(url): return url
+    else: return 'string:${object_url}/' + url
+
+def getRootTabs(self):
+    """ Return all portal root elements which are displayed in poral globalnav """
+    stp = getToolByName(self, 'portal_properties').site_properties
+    if stp.getProperty('disable_folder_sections', True):
+        return []
+    result = []
+    query = {}
+    portal_path = getToolByName(self, 'portal_url').getPortalPath()
+    query['path'] = {'query':portal_path, 'navtree':1}
+    utils = getToolByName(self, 'plone_utils')
+    ct = getToolByName(self, 'portal_catalog')
+    ntp = getToolByName(self, 'portal_properties').navtree_properties
+    views = stp.getProperty('typesUseViewActionInListings', ())
+    query['portal_type'] = utils.typesToList()
+    if ntp.getProperty('sortAttribute', False):
+        query['sort_on'] = ntp.sortAttribute
+    if (ntp.getProperty('sortAttribute', False) and ntp.getProperty('sortOrder', False)):
+        query['sort_order'] = ntp.sortOrder
+    if ntp.getProperty('enable_wf_state_filtering', False):
+        query['review_state'] = ntp.wf_states_to_show
+    query['is_default_page'] = False
+    query['is_folderish'] = True
+    excluded_ids = {}
+    for exc_id in ntp.getProperty('idsNotToList', ()):
+        excluded_ids[exc_id] = 1
+    rawresult = ct(**query)
+    for item in rawresult:
+        if not excluded_ids.has_key(item.getId):
+            item_url = (item.portal_type in views and item.getURL() + '/view') or item.getURL()
+            data = {'name' : utils.pretty_title_or_id(item),
+                    'id' : item.getId,
+                    'url' : item_url,
+                    'description' : item.Description,
+                    'exclude_from_nav' : item.exclude_from_nav}
+            result.append(data)
+    return result
Index: qPloneTabs/tags/0.3.1/version.txt
===================================================================
--- qPloneTabs/tags/0.3.1/version.txt	(revision 32)
+++ qPloneTabs/tags/0.3.1/version.txt	(revision 32)
@@ -0,0 +1,1 @@
+0.3.1
