fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2012-05-01 09:58 am

[dw-free] move the 6A libraries into a folder in the local repo (to keep them grouped with their LIC

[commit: http://hg.dwscoalition.org/dw-free/rev/4b328d722eab]

move the 6A libraries into a folder in the local repo (to keep them grouped
with their LICENSE file). Update various includes

Files modified:
  • cgi-bin/LJ/CProd.pm
  • cgi-bin/LJ/S2.pm
  • cgi-bin/LJ/Talk.pm
  • cgi-bin/LJ/Web.pm
  • cgi-bin/LJ/Widget/InboxFolder.pm
  • cgi-bin/LJ/Widget/InboxFolderNav.pm
  • cgi-bin/LJ/Widget/ThemeNav.pm
  • cgi-bin/ljlib.pl
  • cvs/multicvs.conf
  • htdocs/editicons.bml
  • htdocs/editjournal.bml
  • htdocs/inbox/index.bml
  • htdocs/js/6alib/LICENSE.txt
  • htdocs/js/6alib/checkallbutton.js
  • htdocs/js/6alib/core.js
  • htdocs/js/6alib/datasource.js
  • htdocs/js/6alib/devel.js
  • htdocs/js/6alib/dom.js
  • htdocs/js/6alib/hourglass.js
  • htdocs/js/6alib/httpreq.js
  • htdocs/js/6alib/image-region-select.js
  • htdocs/js/6alib/inputcomplete.js
  • htdocs/js/6alib/ippu.js
  • htdocs/js/6alib/json.js
  • htdocs/js/6alib/perlbal-uploadtrack.js
  • htdocs/js/6alib/progressbar.js
  • htdocs/js/6alib/selectable_table.js
  • htdocs/js/6alib/template.js
  • htdocs/js/6alib/timer.js
  • htdocs/js/6alib/view.js
  • htdocs/tools/userpicfactory.bml
  • htdocs/update.bml
  • views/dev/tests/commentmanage.js
  • views/dev/tests/libfunctions.js
  • views/dev/tests/login.js
  • views/dev/tests/quickreply.js
--------------------------------------------------------------------------------
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/LJ/CProd.pm
--- a/cgi-bin/LJ/CProd.pm	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/LJ/CProd.pm	Tue May 01 17:55:19 2012 +0800
@@ -413,10 +413,10 @@
     my ($class, $content, %opts) = @_;
 
     # include js libraries
-    LJ::need_res("js/core.js");
-    LJ::need_res("js/dom.js");
-    LJ::need_res("js/httpreq.js");
-    LJ::need_res("js/hourglass.js");
+    LJ::need_res("js/6alib/core.js");
+    LJ::need_res("js/6alib/dom.js");
+    LJ::need_res("js/6alib/httpreq.js");
+    LJ::need_res("js/6alib/hourglass.js");
     LJ::need_res("js/cprod.js");
     LJ::need_res("stc/cprod.css");
     my $e_class = LJ::ehtml($class);
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/LJ/S2.pm
--- a/cgi-bin/LJ/S2.pm	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/LJ/S2.pm	Tue May 01 17:55:19 2012 +0800
@@ -183,9 +183,9 @@
 
         # used if we're using our old library
         LJ::need_res(qw(
-                    js/core.js
-                    js/dom.js
-                    js/httpreq.js
+                    js/6alib/core.js
+                    js/6alib/dom.js
+                    js/6alib/httpreq.js
                     js/livejournal.js
                     js/md5.js
                     js/login.js
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/LJ/Talk.pm
--- a/cgi-bin/LJ/Talk.pm	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/LJ/Talk.pm	Tue May 01 17:55:19 2012 +0800
@@ -2173,24 +2173,24 @@
         @additional,
     ) : (
         # base libraries
-        'js/core.js',
-        'js/dom.js',
-        'js/json.js',
+        'js/6alib/core.js',
+        'js/6alib/dom.js',
+        'js/6alib/json.js',
         # for the formatting of the icon selector popup
-        'js/template.js',
-        'js/ippu.js',
+        'js/6alib/template.js',
+        'js/6alib/ippu.js',
         'js/lj_ippu.js',
         # logic for the icon selector
         'js/userpicselect.js',
         # fetching the userpic information
-        'js/httpreq.js',
-        'js/hourglass.js',
+        'js/6alib/httpreq.js',
+        'js/6alib/hourglass.js',
         # autocomplete
-        'js/inputcomplete.js',
+        'js/6alib/inputcomplete.js',
         'stc/ups.css',
         # selecting an icon by clicking on a row
-        'js/datasource.js',
-        'js/selectable_table.js',
+        'js/6alib/datasource.js',
+        'js/6alib/selectable_table.js',
         # additional files from arguments
         @additional,
     );
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/LJ/Web.pm
--- a/cgi-bin/LJ/Web.pm	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/LJ/Web.pm	Tue May 01 17:55:19 2012 +0800
@@ -1473,9 +1473,9 @@
 
     ### Insert Object Toolbar:
     LJ::need_res(qw(
-                    js/core.js
-                    js/dom.js
-                    js/ippu.js
+                    js/6alib/core.js
+                    js/6alib/dom.js
+                    js/6alib/ippu.js
                     js/lj_ippu.js
                     ));
     $out .= "<div id='htmltools' class='pkg'>\n";
@@ -3303,9 +3303,9 @@
     croak "Invalid options passed to subscribe_interface" if (scalar keys %opts);
 
     LJ::need_res('stc/esn.css');
-    LJ::need_res('js/core.js');
-    LJ::need_res('js/dom.js');
-    LJ::need_res('js/checkallbutton.js');
+    LJ::need_res('js/6alib/core.js');
+    LJ::need_res('js/6alib/dom.js');
+    LJ::need_res('js/6alib/checkallbutton.js');
     LJ::need_res('js/esn.js');
 
     my @categories = $catref ? @$catref : ();
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/LJ/Widget/InboxFolder.pm
--- a/cgi-bin/LJ/Widget/InboxFolder.pm	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/LJ/Widget/InboxFolder.pm	Tue May 01 17:55:19 2012 +0800
@@ -25,14 +25,14 @@
 
 sub need_res {
     return qw(
-            js/core.js
-            js/dom.js
-            js/view.js
-            js/datasource.js
-            js/checkallbutton.js
-            js/selectable_table.js
-            js/httpreq.js
-            js/hourglass.js
+            js/6alib/core.js
+            js/6alib/dom.js
+            js/6alib/view.js
+            js/6alib/datasource.js
+            js/6alib/checkallbutton.js
+            js/6alib/selectable_table.js
+            js/6alib/httpreq.js
+            js/6alib/hourglass.js
             js/esn_inbox.js
             stc/esn.css
             stc/lj_base.css
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/LJ/Widget/InboxFolderNav.pm
--- a/cgi-bin/LJ/Widget/InboxFolderNav.pm	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/LJ/Widget/InboxFolderNav.pm	Tue May 01 17:55:19 2012 +0800
@@ -19,9 +19,9 @@
 
 sub need_res {
     return qw(
-            js/core.js
-            js/dom.js
-            js/hourglass.js
+            js/6alib/core.js
+            js/6alib/dom.js
+            js/6alib/hourglass.js
             stc/esn.css
             stc/lj_base.css
             );
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/LJ/Widget/ThemeNav.pm
--- a/cgi-bin/LJ/Widget/ThemeNav.pm	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/LJ/Widget/ThemeNav.pm	Tue May 01 17:55:19 2012 +0800
@@ -21,7 +21,7 @@
 sub ajax { 1 }
 sub can_fake_ajax_post { 1 }
 sub authas { 1 }
-sub need_res { qw( stc/widgets/themenav.css js/inputcomplete.js ) }
+sub need_res { qw( stc/widgets/themenav.css js/6alib/inputcomplete.js ) }
 
 sub render_body {
     my $class = shift;
diff -r cb15f9c272f5 -r 4b328d722eab cgi-bin/ljlib.pl
--- a/cgi-bin/ljlib.pl	Tue May 01 16:18:41 2012 +0800
+++ b/cgi-bin/ljlib.pl	Tue May 01 17:55:19 2012 +0800
@@ -1072,9 +1072,9 @@
 
         # standard site-wide JS and CSS
         LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY }, qw(
-                        js/core.js
-                        js/dom.js
-                        js/httpreq.js
+                        js/6alib/core.js
+                        js/6alib/dom.js
+                        js/6alib/httpreq.js
                         js/livejournal.js
                         stc/lj_base.css
                         ));
@@ -1089,9 +1089,9 @@
         # contextual popup JS
         if ( $LJ::CTX_POPUP ) {
             LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY }, qw(
-                            js/ippu.js
+                            js/6alib/ippu.js
                             js/lj_ippu.js
-                            js/hourglass.js
+                            js/6alib/hourglass.js
                             js/contextualhover.js
                             stc/contextualhover.css
                             ));
@@ -1113,7 +1113,7 @@
 
         # development JS
         LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY }, qw(
-                        js/devel.js
+                        js/6alib/devel.js
                         js/livejournal-devel.js
                         ))
             if $LJ::IS_DEV_SERVER;
diff -r cb15f9c272f5 -r 4b328d722eab cvs/multicvs.conf
--- a/cvs/multicvs.conf	Tue May 01 16:18:41 2012 +0800
+++ b/cvs/multicvs.conf	Tue May 01 17:55:19 2012 +0800
@@ -12,7 +12,6 @@
 
 # DreamWidth repositories
 HG(dw-free)               = http://hg.dwscoalition.org/dw-free @stable
-HG(js)                    = http://hg.dwscoalition.org/js
 
 # stock/unchanged repositories pulled from external sources
 SVN(gearman)              = http://code.livejournal.org/svn/gearman/trunk/
@@ -30,9 +29,6 @@
 TheSchwartz-Worker-SendEmail/lib              cgi-bin/
 TheSchwartz/bin/schwartzmon                   bin/schwartzmon
 
-js/                                           htdocs/js
-js/ImageRegionSelect/image-region-select.js   htdocs/js/image-region-select.js
-
 memcached/server                              src/memcached
 memcached/api/perl/lib/                       cgi-bin
 
@@ -63,12 +59,12 @@
 dw-free/htdocs/manage/circle/invite.bml   ssldocs/manage/circle/invite.bml
 dw-free/htdocs/shop/                      ssldocs/shop/
 
-js/core.js                                    ssldocs/js/core.js
-js/devel.js                                   ssldocs/js/devel.js
-js/dom.js                                     ssldocs/js/dom.js
-js/httpreq.js                                 ssldocs/js/httpreq.js
-js/ippu.js                                    ssldocs/js/ippu.js
-js/hourglass.js                               ssldocs/js/hourglass.js
+dw-free/htdocs/js/6alib/core.js           ssldocs/js/6alib/core.js
+dw-free/htdocs/js/6alib/devel.js          ssldocs/js/6alib/devel.js
+dw-free/htdocs/js/6alib/dom.js            ssldocs/js/6alib/dom.js
+dw-free/htdocs/js/6alib/httpreq.js        ssldocs/js/6alib/httpreq.js
+dw-free/htdocs/js/6alib/ippu.js           ssldocs/js/6alib/ippu.js
+dw-free/htdocs/js/6alib/hourglass.js      ssldocs/js/6alib/hourglass.js
 
 dw-free/htdocs/_config.bml                    ssldocs/_config.bml
 dw-free/htdocs/img/                           ssldocs/img/
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/editicons.bml
--- a/htdocs/editicons.bml	Tue May 01 16:18:41 2012 +0800
+++ b/htdocs/editicons.bml	Tue May 01 17:55:19 2012 +0800
@@ -48,9 +48,9 @@
 
     LJ::need_res(qw(
                     stc/editicons.css
-                    js/progressbar.js
+                    js/6alib/progressbar.js
                     js/ljprogressbar.js
-                    js/perlbal-uploadtrack.js
+                    js/6alib/perlbal-uploadtrack.js
                     js/editicons.js
                     ));
 
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/editjournal.bml
--- a/htdocs/editjournal.bml	Tue May 01 16:18:41 2012 +0800
+++ b/htdocs/editjournal.bml	Tue May 01 17:55:19 2012 +0800
@@ -33,7 +33,7 @@
     if ($GET{'itemid'} || $POST{'itemid'}) { $mode = "edit"; }
 
     LJ::need_res( { priority => $LJ::OLD_RES_PRIORITY }, 'stc/entry.css' );
-    LJ::need_res( 'js/inputcomplete.js' );
+    LJ::need_res( 'js/6alib/inputcomplete.js' );
     
     # are they asking to be authed as someone else?
     my $authas = $GET{'authas'} || $remote->{'user'};
@@ -639,9 +639,9 @@
     my $ret;
 
     LJ::need_res(qw(
-                    js/core.js
-                    js/dom.js
-                    js/httpreq.js
+                    js/6alib/core.js
+                    js/6alib/dom.js
+                    js/6alib/httpreq.js
                     js/livejournal.js
                     js/entry.js
                     js/poll.js
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/inbox/index.bml
--- a/htdocs/inbox/index.bml	Tue May 01 16:18:41 2012 +0800
+++ b/htdocs/inbox/index.bml	Tue May 01 17:55:19 2012 +0800
@@ -30,14 +30,14 @@
     return $ML{'.error.not_ready'} unless $remote->can_use_esn;
 
     LJ::need_res(qw(
-                  js/core.js
-                  js/dom.js
-                  js/view.js
-                  js/datasource.js
-                  js/checkallbutton.js
-                  js/selectable_table.js
-                  js/httpreq.js
-                  js/hourglass.js
+                  js/6alib/core.js
+                  js/6alib/dom.js
+                  js/6alib/view.js
+                  js/6alib/datasource.js
+                  js/6alib/checkallbutton.js
+                  js/6alib/selectable_table.js
+                  js/6alib/httpreq.js
+                  js/6alib/hourglass.js
                   js/esn_inbox.js
                   stc/esn.css
                   stc/lj_base.css
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/LICENSE.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/LICENSE.txt	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,30 @@
+Copyright (c) 2005, 2006, 2007 Six Apart, Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+    * Neither the name of "Six Apart" nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/checkallbutton.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/checkallbutton.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,70 @@
+/*
+  This is a class which you can attach to a checkbox element.
+  When that element is clicked, it will toggle every checkbox
+  with a specified classname to be the same as the checkbox that was
+  clicked.
+
+  $Id: checkallbutton.js 69 2006-07-14 22:38:26Z mischa $
+*/
+
+CheckallButton = new Class(Object, {
+
+  // opts:
+  //  class => what class all of the checkboxes have
+  //  button => the "check all" button element
+  //  parent => [optional] only check boxes that are children of this element
+  init: function (opts) {
+    if ( CheckallButton.superClass.init ) {
+        CheckallButton.superClass.init.apply(arguments);
+    }
+
+    this.button = opts["button"];
+    this.className = opts["class"];
+    this.parent = opts["parent"];
+    this.attachEvents();
+  },
+
+  attachEvents: function () {
+    if (!this.button || !this.className)
+      return;
+
+    DOM.addEventListener(this.button, "click", this.buttonClicked.bindEventListener(this));
+  },
+
+  buttonClicked: function (e) {
+    if (!this.button || !this.className)
+      return;
+
+    var parent = this.parent;
+    if (!parent)
+      parent = document;
+
+    var viewObjects = parent.getElementsByTagName("*");
+    var boxes = DOM.filterElementsByClassName(viewObjects, this.className) || [];
+
+    var checkallBox = this.button;
+
+    for (var i = 0; i < boxes.length; i++) {
+      var box = boxes[i];
+
+      if (!box)
+        continue;
+
+      if (box.checked == checkallBox.checked) continue;
+
+      // send a "clicked" event to the checkbox
+      try {
+          // w3c
+          var evt = document.createEvent("MouseEvents");
+          evt.initMouseEvent("click", true, false, window,
+                             0, 0, 0, 0, 0, false, false, false, false, 0, null);
+          box.dispatchEvent(evt);
+      } catch (e) {
+          try {
+              // ie
+              box.click();
+          } catch (e2) { }
+      }
+    }
+  }
+});
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/core.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/core.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,843 @@
+/*
+Core JavaScript Library
+$Id: core.js 232 2007-10-01 20:32:42Z whitaker $
+
+Copyright (c) 2005, Six Apart, Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+    * Neither the name of "Six Apart" nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+/* stubs */
+
+log = function() {};
+log.error = log.warn = log.debug = log;
+
+
+/* utility functions */
+
+defined = function( x ) {
+    return x === undefined ? false : true;
+}
+
+
+/**
+ * Utility method.
+ * @param x <code>any</code> Any JavaScript value, including <code>undefined</code>.
+ * @return boolean <code>true</code> if the value is not <code>null</code> and is not <code>undefined</code>.
+ */
+exists = function( x ) {
+   return (x === undefined || x === null) ? false : true;
+}
+
+
+finite = function( x ) {
+    return isFinite( x ) ? x : 0;
+}
+
+
+finiteInt = function( x, base ) {
+    return finite( parseInt( x, base ) );
+}
+
+
+finiteFloat = function( x ) {
+    return finite( parseFloat( x ) );
+}
+
+
+max = function() {
+    var a = arguments;
+    var n = a[ 0 ];
+    for( var i = 1; i < a.length; i++ )
+        if( a[ i ] > n )
+            n = a[ i ];
+    return n;
+}
+
+
+min = function() {
+    var a = arguments;
+    var n = a[ 0 ];
+    for( var i = 1; i < a.length; i++ )
+        if( a[ i ] < n )
+            n = a[ i ];
+    return n;
+}
+
+
+/* try block */  
+ 
+Try = {
+    these: function() {
+        for( var i = 0; i < arguments.length; i++ ) {
+            try {
+                return arguments[ i ]();
+            } catch( e ) {}
+        }
+        return undefined;
+    }
+}
+
+
+/* unique id generator */
+
+Unique = {
+    length: 0,
+    
+    id: function() {
+        return ++this.length;
+    }
+}
+
+
+/* event methods */
+
+if( !defined( window.Event ) )
+    Event = {};
+
+
+Event.stop = function( event ) {
+    event = event || this;
+    if( event === Event )
+        event = window.event;
+
+    // w3c
+    if( event.preventDefault )
+        event.preventDefault();
+    if( event.stopPropagation )
+        event.stopPropagation();
+
+    // ie
+    try {
+        event.cancelBubble = true;
+        event.returnValue = false;
+    } catch( e ) {}
+
+    return false;
+}
+
+
+Event.prep = function( event ) {
+    event = event || window.event;
+    if( !defined( event.stop ) )
+        event.stop = this.stop;
+    if( !defined( event.target ) )
+        event.target = event.srcElement;
+    if( !defined( event.relatedTarget ) ) 
+        event.relatedTarget = event.toElement;
+    return event;
+}
+
+
+try { Event.prototype.stop = Event.stop; }
+catch( e ) {}
+
+
+/* object extensions */
+
+Function.stub = function() {};
+
+
+if( !Object.prototype.hasOwnProperty ) {
+    Object.prototype.hasOwnProperty = function( p ) {
+        if( !(p in this) )
+            return false;
+        try {
+            var pr = this.constructor.prototype;
+            while( pr ) {
+                if( pr[ p ] === this[ p ] )
+                    return false;
+                if( pr === pr.constructor.prototype )
+                    break;
+                pr = pr.constructor.prototype;
+            }
+        } catch( e ) {}
+        return true;
+    }
+}
+
+
+if ( ! defined ( window.OBJ ) ) 
+    OBJ = {};
+
+OBJ.extend = function(obj_this) {
+    var a = arguments;
+    for( var i = 0; i < a.length; i++ ) {
+        var o = a[ i ];
+        for( var p in o ) {
+            try {
+                if( !obj_this[ p ] &&
+                    (!o.hasOwnProperty || o.hasOwnProperty( p )) )
+                    obj_this[ p ] = o[ p ];
+            } catch( e ) {}
+        }
+    }
+    return obj_this;
+}
+
+
+OBJ.override = function(obj_this) {
+    var a = arguments;
+    for( var i = 0; i < a.length; i++ ) {
+        var o = a[ i ];
+        for( var p in o ) {
+            try {
+                if( !o.hasOwnProperty || o.hasOwnProperty( p ) )
+                    obj_this[ p ] = o[ p ];
+            } catch( e ) {}
+        }
+    }
+    return obj_this;
+}
+
+
+/* function extensions */
+
+OBJ.extend( Function.prototype, {
+    bind: function( object ) {
+        var method = this;
+        return function() {
+            return method.apply( object, arguments );
+        };
+    },
+    
+    
+    bindEventListener: function( object ) {
+        var method = this; // Use double closure to work around IE 6 memory leak.
+        return function( event ) {
+            try {
+                event = Event.prep( event );
+            } catch( e ) {}
+            return method.call( object, event );
+        };
+    }
+} );
+
+
+/* class helpers */
+
+indirectObjects = [];
+
+
+Class = function( superClass ) {
+
+    // Set the constructor:
+    var constructor = function() {
+        if( arguments.length && this.init )
+            this.init.apply( this, arguments );
+    };    
+    //   -- Accomplish static-inheritance:
+    OBJ.override( constructor,Class );  // inherit static methods from Class
+    superClass = superClass || Object; 
+    OBJ.override(constructor, superClass ); // inherit static methods from the superClass 
+    constructor.superClass = superClass.prototype;
+    
+    // Set the constructor's prototype (accomplish object-inheritance):
+    constructor.prototype = new superClass();
+    constructor.prototype.constructor = constructor; // rev. 0.7    
+    //   -- extend prototype with Class instance methods
+    OBJ.extend(constructor.prototype, Class.prototype );    
+    //   -- override prototype with interface methods
+    for( var i = 1; i < arguments.length; i++ )
+        OBJ.override(constructor.prototype, arguments[ i ] );
+    
+    return constructor;
+}
+
+
+OBJ.extend( Class, {
+    initSingleton: function() {
+        if( this.singleton )
+            return this.singleton;
+        this.singleton = this.singletonConstructor
+            ? new this.singletonConstructor()
+            : new this();
+        if ( this.singleton.init )
+            this.singleton.init.apply( this.singleton, arguments );
+        return this.singleton;
+    }
+} );
+
+
+Class.prototype = {
+    destroy: function() {
+        try {
+            if( this.indirectIndex )
+                indirectObjects[ this.indirectIndex ] = undefined;
+            delete this.indirectIndex;
+        } catch( e ) {}
+        
+        for( var property in this ) {
+            try {
+                if( this.hasOwnProperty( property ) )
+                    delete this[ property ];
+            } catch( e ) {}
+        }
+    },
+    
+    
+    getBoundMethod: function( methodName ) {
+        return this[ name ].bind( this );
+    },
+    
+    
+    getEventListener: function( methodName ) {
+        return this[ methodName ].bindEventListener( this );
+    },
+    
+    
+    getIndirectIndex: function() {
+        if( !defined( this.indirectIndex ) ) {
+            this.indirectIndex = indirectObjects.length;
+            indirectObjects.push( this );
+        }
+        return this.indirectIndex;
+    },
+    
+    
+    getIndirectMethod: function( methodName ) {
+        if( !this.indirectMethods )
+            this.indirectMethods = {};
+        var method = this[ methodName ];
+        if( typeof method != "function" )
+            return undefined;
+        var indirectIndex = this.getIndirectIndex();
+        if( !this.indirectMethods[ methodName ] ) {
+            this.indirectMethods[ methodName ] = new Function(
+                "var o = indirectObjects[" + indirectIndex + "];" +
+                "return o." + methodName + ".apply( o, arguments );"
+            );
+        }
+        return this.indirectMethods[ methodName ];
+    },
+    
+    
+    getIndirectEventListener: function( methodName ) {
+        if( !this.indirectEventListeners )
+            this.indirectEventListeners = {};
+        var method = this[ methodName ];
+        if( typeof method != "function" )
+            return undefined;
+        var indirectIndex = this.getIndirectIndex();
+        if( !this.indirectEventListeners[ methodName ] ) {
+            this.indirectEventListeners[ methodName ] = new Function( "event",
+                "try { event = Event.prep( event ); } catch( e ) {}" +
+                "var o = indirectObjects[" + indirectIndex + "];" +
+                "return o." + methodName + ".call( o, event );"
+            );
+        }
+        return this.indirectEventListeners[ methodName ];
+    }
+}
+
+
+/* string extensions */
+
+OBJ.extend( String, {
+    escapeJSChar: function( c ) {
+        // try simple escaping
+        switch( c ) {
+            case "\\": return "\\\\";
+            case "\"": return "\\\"";
+            case "'":  return "\\'";
+            case "\b": return "\\b";
+            case "\f": return "\\f";
+            case "\n": return "\\n";
+            case "\r": return "\\r";
+            case "\t": return "\\t";
+        }
+        
+        // return raw bytes now ... should be UTF-8
+        if( c >= " " )
+            return c;
+        
+        // try \uXXXX escaping, but shouldn't make it for case 1, 2
+        c = c.charCodeAt( 0 ).toString( 16 );
+        switch( c.length ) {
+            case 1: return "\\u000" + c;
+            case 2: return "\\u00" + c;
+            case 3: return "\\u0" + c;
+            case 4: return "\\u" + c;
+        }
+        
+        // should never make it here
+        return "";
+    },
+    
+    
+    encodeEntity: function( c ) {
+        switch( c ) {
+            case "<": return "&lt;";
+            case ">": return "&gt;";
+            case "&": return "&amp;";
+            case '"': return "&quot;";
+            case "'": return "&apos;";
+        }
+        return c;
+    },
+
+
+    decodeEntity: function( c ) {
+        switch( c ) {
+            case "amp": return "&";
+            case "quot": return '"';
+            case "gt": return ">";
+            case "lt": return "<";
+        }
+        var m = c.match( /^#(\d+)$/ );
+        if( m && defined( m[ 1 ] ) )
+            return String.fromCharCode( m[ 1 ] );
+        m = c.match( /^#x([0-9a-f]+)$/i );
+        if(  m && defined( m[ 1 ] ) )
+            return String.fromCharCode( parseInt( hex, m[ 1 ] ) );
+        return c;
+    }
+} );
+
+
+OBJ.extend( String.prototype, {
+    escapeJS: function() {
+        return this.replace( /([^ -!#-\[\]-~])/g, function( m, c ) { return String.escapeJSChar( c ); } )
+    },
+    
+    
+    escapeJS2: function() {
+        return this.replace( /([\u0000-\u0031'"\\])/g, function( m, c ) { return String.escapeJSChar( c ); } )
+    },
+    
+    
+    escapeJS3: function() {
+        return this.replace( /[\u0000-\u0031'"\\]/g, function( m ) { return String.escapeJSChar( m ); } )
+    },
+    
+    
+    escapeJS4: function() {
+        return this.replace( /./g, function( m ) { return String.escapeJSChar( m ); } )
+    },
+    
+    
+    encodeHTML: function() {
+        return this.replace( /([<>&"])/g, function( m, c ) { return String.encodeEntity( c ) } );
+    },
+
+
+    decodeHTML: function() {
+        return this.replace( /&(.*?);/g, function( m, c ) { return String.decodeEntity( c ) } );
+    },
+    
+    
+    cssToJS: function() {
+        return this.replace( /-([a-z])/g, function( m, c ) { return c.toUpperCase() } );
+    },
+    
+    
+    jsToCSS: function() {
+        return this.replace( /([A-Z])/g, function( m, c ) { return "-" + c.toLowerCase() } );
+    },
+    
+    
+    firstToLowerCase: function() {
+        return this.replace( /^(.)/, function( m, c ) { return c.toLowerCase() } );
+    },
+    
+        
+    rgbToHex: function() {
+        var c = this.match( /(\d+)\D+(\d+)\D+(\d+)/ );
+        if( !c )
+            return undefined;
+        return "#" +
+            finiteInt( c[ 1 ] ).toString( 16 ).pad( 2, "0" ) +
+            finiteInt( c[ 2 ] ).toString( 16 ).pad( 2, "0" ) +
+            finiteInt( c[ 3 ] ).toString( 16 ).pad( 2, "0" );
+    },
+    
+    
+    pad: function( length, padChar ) {
+        var padding = length - this.length;
+        if( padding <= 0 )
+            return this;
+        if( !defined( padChar ) )
+            padChar = " ";
+        var out = [];
+        for( var i = 0; i < padding; i++ )
+            out.push( padChar );
+        out.push( this );
+        return out.join( "" );
+    },
+
+
+    trim: function() {
+        return this.replace( /^\s+|\s+$/g, "" );
+    }
+
+} );
+
+
+/* extend array object */
+
+OBJ.extend( Array, { 
+    fromPseudo: function ( args ) {
+        var out = [];
+        for ( var i = 0; i < args.length; i++ )
+            out.push( args[ i ] );
+        return out;
+    }
+});
+
+
+/* extend array object */
+
+OBJ.extend(Array.prototype, {
+    copy: function() {
+        var out = [];
+        for( var i = 0; i < this.length; i++ )
+            out[ i ] = this[ i ];
+        return out;
+    },
+
+
+    first: function( callback, object ) {
+        var length = this.length;
+        for( var i = 0; i < length; i++ ) {
+            var result = object
+                ? callback.call( object, this[ i ], i, this )
+                : callback( this[ i ], i, this );
+            if( result )
+                return this[ i ];
+        }
+        return null;
+    },
+
+
+    fitIndex: function( fromIndex, defaultIndex ) {
+        if( !defined( fromIndex ) || fromIndex == null )
+            fromIndex = defaultIndex;
+        else if( fromIndex < 0 ) {
+            fromIndex = this.length + fromIndex;
+            if( fromIndex < 0 )
+                fromIndex = 0;
+        } else if( fromIndex >= this.length )
+            fromIndex = this.length - 1;
+        return fromIndex;
+    },
+
+
+    scramble: function() {
+        for( var i = 0; i < this.length; i++ ) {
+            var j = Math.floor( Math.random() * this.length );
+            var temp = this[ i ];
+            this[ i ] = this[ j ];
+            this[ j ] = temp;
+        }
+    },
+    
+    
+    add: function() {
+        var a = arguments;
+        for( var i = 0; i < a.length; i++ ) {
+            var index = this.indexOf( a[ i ] );
+            if( index < 0 ) 
+                this.push( arguments[ i ] );
+        }
+        return this.length;
+    },
+        
+    
+    remove: function() {
+        var a = arguments;
+        for( var i = 0; i < a.length; i++ ) {
+            var j = this.indexOf( a[ i ] );
+            if( j >= 0 )
+                this.splice( j, 1 );
+        }
+        return this.length;
+    },
+
+
+    /* javascript 1.5 array methods */
+    /* http://developer-test.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array#Methods */
+
+    every: function( callback, object ) {
+        var length = this.length;
+        for( var i = 0; i < length; i++ ) {
+            var result = object
+                ? callback.call( object, this[ i ], i, this )
+                : callback( this[ i ], i, this );
+            if( !result )
+                return false;
+        }
+        return true;
+    },
+
+
+    filter: function( callback, object ) {
+        var out = [];
+        var length = this.length;
+        for( var i = 0; i < length; i++ ) {
+            var result = object
+                ? callback.call( object, this[ i ], i, this )
+                : callback( this[ i ], i, this );
+            if( result )
+                out.push( this[ i ] );
+        }
+        return out;
+    },
+    
+    
+    forEach: function( callback, object ) {
+        var length = this.length;
+        for( var i = 0; i < length; i++ ) {
+            object
+                ? callback.call( object, this[ i ], i, this )
+                : callback( this[ i ], i, this );
+        }
+    },
+    
+    
+    indexOf: function( value, fromIndex ) {
+        fromIndex = this.fitIndex( fromIndex, 0 );
+        for( var i = 0; i < this.length; i++ ) {
+            if( this[ i ] === value )
+                return i; 
+        }
+        return -1;
+    },
+
+
+    lastIndexOf: function( value, fromIndex ) {
+        fromIndex = this.fitIndex( fromIndex, this.length - 1 );
+        for( var i = fromIndex; i >= 0; i-- ) {
+            if( this[ i ] == value )
+                return i;
+        }
+        return -1;
+    },
+
+
+    some: function( callback, object ) {
+        var length = this.length;
+        for( var i = 0; i < length; i++ ) {
+            var result = object
+                ? callback.call( object, this[ i ], i, this )
+                : callback( this[ i ], i, this );
+            if( result )
+                return true;
+        }
+        return false;
+    },
+
+
+    /* javascript 1.2 array methods */
+
+    concat: function() {
+        var a = arguments;
+        var out = this.copy();
+        for( i = 0; i < a.length; i++ ) {
+            var b = a[ i ];
+            for( j = 0; j < b.length; j++ )
+                out.push( b[ j ] );
+        }
+        return out;
+    },
+    
+
+    push: function() {
+        var a = arguments;
+        for( var i = 0; i < a.length; i++ )
+            this[ this.length ] = a[ i ];
+        return this.length;     
+    },
+
+
+    pop: function() {
+        if( this.length == 0 )
+            return undefined;
+        var out = this[ this.length - 1 ];
+        this.length--;
+        return out;
+    },
+    
+    
+    unshift: function() {
+        var a = arguments;
+        for( var i = 0; i < a.length; i++ ) {
+            this[ i + a.length ] = this[ i ];
+            this[ i ] = a[ i ];
+        }
+        return this.length;     
+    },
+    
+    
+    shift: function() {
+        if( this.length == 0 )
+            return undefined;
+        var out = this[ 0 ];
+        for( var i = 1; i < this.length; i++ )
+            this[ i - 1 ] = this[ i ];
+        this.length--;
+        return out;
+    }
+} );
+
+
+/* date extensions */
+
+OBJ.extend(Date, {
+    /*  iso 8601 date format parser
+        this was fun to write...
+        thanks to: http://www.cl.cam.ac.uk/~mgk25/iso-time.html */
+
+    matchISOString: new RegExp(
+        "^([0-9]{4})" +                                                     // year
+        "(?:-(?=0[1-9]|1[0-2])|$)(..)?" +                                   // month
+        "(?:-(?=0[1-9]|[12][0-9]|3[01])|$)([0-9]{2})?" +                    // day of the month
+        "(?:T(?=[01][0-9]|2[0-4])|$)T?([0-9]{2})?" +                        // hours
+        "(?::(?=[0-5][0-9])|\\+|-|Z|$)([0-9]{2})?" +                        // minutes
+        "(?::(?=[0-5][0-9]|60$|60[+|-|Z]|60.0+)|\\+|-|Z|$):?([0-9]{2})?" +  // seconds
+        "(\.[0-9]+)?" +                                                     // fractional seconds
+        "(Z|\\+[01][0-9]|\\+2[0-4]|-[01][0-9]|-2[0-4])?" +                  // timezone hours
+        ":?([0-5][0-9]|60)?$"                                               // timezone minutes
+    ),
+    
+    
+    fromISOString: function( string ) {
+        var t = this.matchISOString.exec( string );
+        if( !t )
+            return undefined;
+
+        var year = finiteInt( t[ 1 ], 10 );
+        var month = finiteInt( t[ 2 ], 10 ) - 1;
+        var day = finiteInt( t[ 3 ], 10 );
+        var hours = finiteInt( t[ 4 ], 10 );
+        var minutes = finiteInt( t[ 5 ], 10 );
+        var seconds = finiteInt( t[ 6 ], 10 );
+        var milliseconds = finiteInt( Math.round( parseFloat( t[ 7 ] ) * 1000 ) );
+        var tzHours = finiteInt( t[ 8 ], 10 );
+        var tzMinutes = finiteInt( t[ 9 ], 10 );
+
+        var date = new this( 0 );
+        if( defined( t[ 8 ] ) ) {
+            date.setUTCFullYear( year, month, day );
+            date.setUTCHours( hours, minutes, seconds, milliseconds );
+            var offset = (tzHours * 60 + tzMinutes) * 60000;
+            if( offset )
+                date = new this( date - offset );
+        } else {
+            date.setFullYear( year, month, day );
+            date.setHours( hours, minutes, seconds, milliseconds );
+        }
+
+        return date;
+    }
+} );
+
+
+OBJ.extend(Date.prototype, {
+    getISOTimezoneOffset: function() {
+        var offset = -this.getTimezoneOffset();
+        var negative = false;
+        if( offset < 0 ) {
+            negative = true;
+            offset *= -1;
+        }
+        var offsetHours = Math.floor( offset / 60 ).toString().pad( 2, "0" );
+        var offsetMinutes = Math.floor( offset % 60 ).toString().pad( 2, "0" );
+        return (negative ? "-" : "+") + offsetHours + ":" + offsetMinutes;
+    },
+
+
+    toISODateString: function() {
+        var year = this.getFullYear();
+        var month = (this.getMonth() + 1).toString().pad( 2, "0" );
+        var day = this.getDate().toString().pad( 2, "0" );
+        return year + "-" + month + "-" + day;
+    },
+
+
+    toUTCISODateString: function() {
+        var year = this.getUTCFullYear();
+        var month = (this.getUTCMonth() + 1).toString().pad( 2, "0" );
+        var day = this.getUTCDate().toString().pad( 2, "0" );
+        return year + "-" + month + "-" + day;
+    },
+
+
+    toISOTimeString: function() {
+        var hours = this.getHours().toString().pad( 2, "0" );
+        var minutes = this.getMinutes().toString().pad( 2, "0" );
+        var seconds = this.getSeconds().toString().pad( 2, "0" );
+        var milliseconds = this.getMilliseconds().toString().pad( 3, "0" );
+        var timezone = this.getISOTimezoneOffset();
+        return hours + ":" + minutes + ":" + seconds + "." + milliseconds + timezone;
+    },
+
+
+    toUTCISOTimeString: function() {
+        var hours = this.getUTCHours().toString().pad( 2, "0" );
+        var minutes = this.getUTCMinutes().toString().pad( 2, "0" );
+        var seconds = this.getUTCSeconds().toString().pad( 2, "0" );
+        var milliseconds = this.getUTCMilliseconds().toString().pad( 3, "0" );
+        return hours + ":" + minutes + ":" + seconds + "." + milliseconds + "Z";
+    },
+
+
+    toISOString: function() {
+        return this.toISODateString() + "T" + this.toISOTimeString();
+    },
+
+
+    toUTCISOString: function() {
+        return this.toUTCISODateString() + "T" + this.toUTCISOTimeString();
+    }
+} );
+
+
+/* ajax */
+
+if( !defined( window.XMLHttpRequest ) ) {
+    window.XMLHttpRequest = function() {
+        var types = [
+            "Microsoft.XMLHTTP",
+            "MSXML2.XMLHTTP.5.0",
+            "MSXML2.XMLHTTP.4.0",
+            "MSXML2.XMLHTTP.3.0",
+            "MSXML2.XMLHTTP"
+        ];
+        
+        for( var i = 0; i < types.length; i++ ) {
+            try {
+                return new ActiveXObject( types[ i ] );
+            } catch( e ) {}
+        }
+        
+        return undefined;
+    }
+}
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/datasource.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/datasource.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,148 @@
+// datasource base class, the "M" in MVC
+// subclass this and override theData to provide your data
+
+DataSource = new Class(Object, {
+
+  init: function (initialData) {
+    if ( DataSource.superClass.init )
+        DataSource.superClass.init.apply(this, arguments);
+    this.watchers = [];
+    this.theData = defined(initialData) ? initialData : [];
+    this.sortField = "";
+    this.sortType = "";
+    this.sortDesc = false;
+  },
+
+  addWatcher: function (callback) {
+    this.watchers.add(callback);
+  },
+
+  removeWatcher: function (callback) {
+    this.watchers.remove(callback);
+  },
+
+  // call this if updating data and not using _setData
+  _updated: function () {
+    this.callWatchers();
+  },
+
+  callWatchers: function () {
+    for (var i = 0; i < this.watchers.length; i++)
+      this.watchers[i].apply(this, [this.data()]);
+  },
+
+  setData: function (theData) {
+    this.theData = theData;
+
+    if (this.sortField)
+      this.sortDataBy(this.sortField, this.sortType, this.sortDesc);
+
+    this._setData(theData);
+  },
+
+  _setData: function (theData) {
+    this.theData = theData;
+    this.callWatchers();
+    return theData;
+  },
+
+  data: function () {
+    return this.theData;
+  },
+
+  sortBy: function () {
+    return this.sortField;
+  },
+
+  sortInverted: function () {
+    return this.sortDesc;
+  },
+
+  // mimic some array functionality
+  push: function (data) {
+    this.theData.push(data);
+    this.callWatchers();
+  },
+
+  pop: function () {
+    var val = this.theData.pop();
+    this.callWatchers();
+    return val;
+  },
+
+  indexOf: function (value) {
+    return this.theData.indexOf(value);
+  },
+
+  remove: function (value) {
+    this.theData.remove(value);
+    this.callWatchers();
+  },
+
+  empty: function () {
+    this.theData = [];
+    this.callWatchers();
+  },
+
+  length: function () {
+    return this.theData.length;
+  },
+
+  totalLength: function () {
+    return this.allData().length;
+  },
+
+  allData: function () {
+    var theData = this.theData;
+
+    if (this.dataField && theData)
+      theData = theData[this.dataField];
+
+    return theData;
+  },
+
+  sortDataBy: function (field, type, invert) {
+    this.sortField = field;
+    this.sortDesc = invert;
+    this.sortType = type;
+
+    if (!field || !this.theData || !this.theData.sort)
+      return;
+
+    var sorted = this.theData.sort(function (a, b) {
+      var ad = a[""+field], bd = b[""+field];
+      ad = ad ? ad : "";
+      bd = bd ? bd : "";
+
+      switch(type) {
+
+      case "string":
+        var aname = ad.toUpperCase(), bname = bd.toUpperCase();
+
+        if (aname < bname)
+          return -1;
+        else if (aname > bname)
+          return 1;
+        else
+          return 0;
+
+      case "isodate":
+        var datA = Date.fromISOString(ad) || new Date(0);
+        var datB = Date.fromISOString(bd) || new Date(0);
+
+        return ((datA - datB) || 0);
+
+      default:
+      case "numeric":
+        return ad - bd;
+
+      }
+    });
+
+    if (invert)
+      sorted.reverse();
+
+    this._setData(sorted);
+  }
+
+});
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/devel.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/devel.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,178 @@
+/*
+Development Library
+$Id$
+
+Copyright (c) 2006, Six Apart, Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+    * Neither the name of "Six Apart" nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+/* benchmarking */
+
+benchmark = function( callback, iterations ) {
+    var start = new Date();
+    for( var i = 0; i < iterations; i++ )
+        callback();
+    var end = new Date();
+    return (end.getSeconds() - start.getSeconds()) +
+        (end.getMilliseconds() - start.getMilliseconds()) / 1000;
+}
+
+
+inspect = function( object, allProperties, noBreaks ) {
+    var out = "";
+    for( var property in object ) {
+        try {
+            if( !allProperties && !object.hasOwnProperty( property ) )
+                continue;
+            out += property + ": " + object[ property ] +
+                (noBreaks ? "\n" : "<br />");
+        } catch( e ) {}
+    }
+    return out;
+}
+
+
+/* logging, alert override */
+
+Logger = new Class( Object, {
+    width: 320,
+    height: 240,
+    windowName: "log",
+    
+    
+    log: function() {
+        try {
+            // concat arguments
+            var args = [];
+            for( var i = 0; i < arguments.length; i++ )
+                args[ i ] = arguments[ i ];
+            var msg = args.join( "" );
+
+            // create window
+            this.createWindow();
+
+            // check for no window
+            if( !this.window ) {
+                confirm( "Logger popup window blocked. Using confirm() instead.\n\n" + msg );
+                return true;
+            }
+
+            // create div
+            var div = this.window.document.createElement( "div" );
+            div.style.backgroundColor = (this.count % 2) ? "#eee" : "#fff";
+            div.style.width = "auto";
+            div.style.padding = "3px";
+            div.innerHTML = msg;
+
+            // append to window
+            this.window.document.body.appendChild( div );
+            this.window.scroll( 0, this.window.document.body.scrollHeight );
+            this.count++;
+            return true;
+        } catch( e ) {}
+    },
+    
+    
+    createWindow: function() {
+        if( this.window && this.window.document )
+            return;
+        
+        // create window
+        var x = "auto";
+        var y = "auto";
+        var attr = "resizable=yes, menubar=no, location=no, directories=no, scrollbars=yes, status=no, " +
+            "width=" + this.width + ", height=" + this.height + 
+            "screenX=" + x + ", screenY=" + y + ", " +
+            "left=" + x + ", top=" + y + ", ";
+        this.window = window.open( "", this.windowName, attr );
+        
+        // check for blocked popup
+        if( !this.window )
+            return;
+        
+        var instance;
+        try {
+            instance = this.window.__Logger;
+        }
+        
+        catch( e ) {
+            this.window.location.replace( "about:blank" );
+        }
+        
+        // check for pre-existing instance
+        if( instance ) {
+            // create divider div
+            var div = this.window.document.createElement( "div" );
+            div.style.backgroundColor = "#f00";
+            div.style.width = "auto";
+            div.style.height = "2px";
+            div.style.fontSize = "0.1px";
+            div.style.lineHeight = "0.1px";
+            this.window.document.body.appendChild( div );
+        }
+        else {
+            // write body
+            this.window.document.open( "text/html", "replace" );
+            this.window.document.write( "<html><head><title>JavaScript Loggers</title></head><body></body></html>" );
+            this.window.document.close();
+            
+            // setup style
+            this.window.title = "JavaScript Loggers";
+            this.window.document.body.style.margin = "0";
+            this.window.document.body.style.padding = "0";
+            this.window.document.body.style.fontFamily = "verdana, 'lucida grande', geneva, arial, helvetica, sans-serif";
+            this.window.document.body.style.fontSize = "10px";
+        }
+        
+        // get previous instance and attach new instance
+        this.prev = instance;
+        this.window.__Logger = this;
+        
+        // dereference previous previous
+        if( this.prev )
+            this.prev.prev = null;
+        
+        // copy message count
+        this.count = this.prev ? this.prev.count : 0;
+    }
+} );
+
+
+log =
+Logger.log = function() {
+    if( !Logger.singleton )
+        Logger.singleton = new Logger();
+    if( Logger.singleton )
+        return Logger.singleton.log.apply( Logger.singleton, arguments );
+    
+    return true;
+}
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/dom.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/dom.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,784 @@
+/*
+DOM Library - Copyright 2005 Six Apart
+$Id: dom.js 261 2008-02-26 23:40:50Z janine $
+
+Copyright (c) 2005, Six Apart, Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+    * Neither the name of "Six Apart" nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+
+/* Node class */
+
+if( !defined( window.Node ) )
+    Node = {};
+
+try {
+    OBJ.extend( Node, {
+        ELEMENT_NODE: 1,
+        ATTRIBUTE_NODE: 2,
+        TEXT_NODE: 3,
+        CDATA_SECTION_NODE: 4,  
+        COMMENT_NODE: 8,    
+        DOCUMENT_NODE: 9,
+        DOCUMENT_FRAGMENT_NODE: 11
+    } );
+} catch( e ) {}
+
+
+/* DOM class */
+
+if( !defined( window.DOM ) )
+    DOM = {};
+
+
+OBJ.extend( DOM, {
+    getElement: function( e ) {
+        return (typeof e == "string" || typeof e == "number") ? document.getElementById( e ) : e;
+    },
+
+
+    addEventListener: function( e, eventName, func, useCapture ) {
+        try {
+            if( e.addEventListener )
+                e.addEventListener( eventName, func, useCapture );
+            else if( e.attachEvent )
+                e.attachEvent( "on" + eventName, func );
+            else
+                e[ "on" + eventName ] = func;
+        } catch( e ) {}
+    },
+
+
+    removeEventListener: function( e, eventName, func, useCapture ) {
+        try {
+            if( e.removeEventListener )
+                e.removeEventListener( eventName, func, useCapture );
+            else if( e.detachEvent )
+                e.detachEvent( "on" + eventName, func );
+            else
+                e[ "on" + eventName ] = undefined;
+        } catch( e ) {}
+    },
+    
+    
+    focus: function( e ) {
+        try {
+            e = DOM.getElement( e );
+            e.focus();
+        } catch( e ) {}
+    },
+
+
+    blur: function( e ) {
+        try {
+            e = DOM.getElement( e );
+            e.blur();
+        } catch( e ) {}
+    },
+    
+
+    /* style */
+    
+    getComputedStyle: function( e ) {
+        if( e.currentStyle )
+            return e.currentStyle;
+        var style = {};
+        var owner = DOM.getOwnerDocument( e );
+        if( owner && owner.defaultView && owner.defaultView.getComputedStyle ) {            
+            try {
+                style = owner.defaultView.getComputedStyle( e, null );
+            } catch( e ) {}
+        }
+        return style;
+    },
+
+
+    getStyle: function( e, p ) {
+        var s = DOM.getComputedStyle( e );
+        return s[ p ];
+    },
+
+
+    // given a window (or defaulting to current window), returns
+    // object with .x and .y of client's usable area
+    getClientDimensions: function( w ) {
+        if( !w )
+            w = window;
+
+        var d = {};
+
+        // most browsers
+        if( w.innerHeight ) {
+            d.x = w.innerWidth;
+            d.y = w.innerHeight;
+            return d;
+        }
+
+        // IE6, strict
+        var de = w.document.documentElement;
+        if( de && de.clientHeight ) {
+            d.x = de.clientWidth;
+            d.y = de.clientHeight;
+            return d;
+        }
+
+        // IE, misc
+        if( document.body ) {
+            d.x = document.body.clientWidth;
+            d.y = document.body.clientHeight;
+            return d;
+        }
+        
+        return undefined;
+    },
+
+
+    getDimensions: function( e ) {
+        if( !e )
+            return undefined;
+
+        var style = DOM.getComputedStyle( e );
+
+        return {
+            offsetLeft: e.offsetLeft,
+            offsetTop: e.offsetTop,
+            offsetWidth: e.offsetWidth,
+            offsetHeight: e.offsetHeight,
+            clientWidth: e.clientWidth,
+            clientHeight: e.clientHeight,
+            
+            offsetRight: e.offsetLeft + e.offsetWidth,
+            offsetBottom: e.offsetTop + e.offsetHeight,
+            clientLeft: finiteInt( style.borderLeftWidth ) + finiteInt( style.paddingLeft ),
+            clientTop: finiteInt( style.borderTopWidth ) + finiteInt( style.paddingTop ),
+            clientRight: e.clientLeft + e.clientWidth,
+            clientBottom: e.clientTop + e.clientHeight
+        };
+    },
+
+
+    getAbsoluteDimensions: function( e ) {
+        var d = DOM.getDimensions( e );
+        if( !d )
+            return d;
+        d.absoluteLeft = d.offsetLeft;
+        d.absoluteTop = d.offsetTop;
+        d.absoluteRight = d.offsetRight;
+        d.absoluteBottom = d.offsetBottom;
+        var bork = 0;
+        while( e ) {
+            try { // IE 6 sometimes gives an unwarranted error ("htmlfile: Unspecified error").
+                e = e.offsetParent;
+            } catch ( err ) {
+                log( "In DOM.getAbsoluteDimensions: " + err.message ); 
+                if ( ++bork > 25 )
+                    return null;
+            }
+            if( !e )
+                return d;
+            d.absoluteLeft += e.offsetLeft;
+            d.absoluteTop += e.offsetTop;
+            d.absoluteRight += e.offsetLeft;
+            d.absoluteBottom += e.offsetTop;
+        }
+        return d;
+    },
+    
+    
+    getIframeAbsoluteDimensions: function( e ) {
+        var d = DOM.getAbsoluteDimensions( e );
+        if( !d )
+            return d;
+        var iframe = DOM.getOwnerIframe( e );
+        if( !defined( iframe ) )
+            return d;
+        
+        var d2 = DOM.getIframeAbsoluteDimensions( iframe );
+        var scroll = DOM.getWindowScroll( iframe.contentWindow );
+        var left = d2.absoluteLeft - scroll.left;
+        var top = d2.absoluteTop - scroll.top;
+        
+        d.absoluteLeft += left;
+        d.absoluteTop += top;
+        d.absoluteRight += left;
+        d.absoluteBottom += top;
+        
+        return d;
+    },
+    
+    
+    setLeft: function( e, v ) { e.style.left = finiteInt( v ) + "px"; },
+    setTop: function( e, v ) { e.style.top = finiteInt( v ) + "px"; },
+    setRight: function( e, v ) { e.style.right = finiteInt( v ) + "px"; },
+    setBottom: function( e, v ) { e.style.bottom = finiteInt( v ) + "px"; },
+    setWidth: function( e, v ) { e.style.width = max( 0, finiteInt( v ) ) + "px"; },
+    setHeight: function( e, v ) { e.style.height = max( 0, finiteInt( v ) ) + "px"; },
+    setZIndex: function( e, v ) { e.style.zIndex = finiteInt( v ); },
+
+
+    getWindowScroll: function( w ) {
+        var s = {
+            left: 0,
+            top: 0
+        };
+
+        if (!w) w = window;
+        var d = w.document;
+        var de = d.documentElement;
+
+        // most browsers
+        if ( defined( w.pageXOffset ) ) {
+            s.left = w.pageXOffset;
+            s.top = w.pageYOffset;
+        }
+
+        // ie
+        else if( de && defined( de.scrollLeft ) ) {
+            s.left = de.scrollLeft;
+            s.top = de.scrollTop;
+        }
+
+        // safari
+        else if( defined( w.scrollX ) ) {
+            s.left = w.scrollX;
+            s.top = w.scrollY;
+        }
+
+        // opera
+        else if( d.body && defined( d.body.scrollLeft ) ) {
+            s.left = d.body.scrollLeft;
+            s.top = d.body.scrollTop;
+        }
+
+
+        return s;
+    },
+
+
+    getAbsoluteCursorPosition: function( event ) {
+        event = event || window.event;
+        var s = DOM.getWindowScroll( window );
+        return {
+            x: s.left + event.clientX,
+            y: s.top + event.clientY
+        };
+    },
+    
+    
+    invisibleStyle: {
+        display: "block",
+        position: "absolute",
+        left: 0,
+        top: 0,
+        width: 0,
+        height: 0,
+        margin: 0,
+        border: 0,
+        padding: 0,
+        fontSize: "0.1px",
+        lineHeight: 0,
+        opacity: 0,
+        MozOpacity: 0,
+        filter: "alpha(opacity=0)"
+    },
+    
+    
+    makeInvisible: function( e ) {
+        for( var p in this.invisibleStyle ) {
+            if( this.invisibleStyle.hasOwnProperty( p ) )
+                e.style[ p ] = this.invisibleStyle[ p ];
+        }
+    },
+
+
+    /* text and selection related methods */
+
+    mergeTextNodes: function( n ) {
+        var c = 0;
+        while( n ) {
+            if( n.nodeType == Node.TEXT_NODE && n.nextSibling && n.nextSibling.nodeType == Node.TEXT_NODE ) {
+                n.nodeValue += n.nextSibling.nodeValue;
+                n.parentNode.removeChild( n.nextSibling );
+                c++;
+            } else {
+                if( n.firstChild )
+                    c += DOM.mergeTextNodes( n.firstChild );
+                n = n.nextSibling;
+            }
+        }
+        return c;
+    },
+    
+    
+    selectElement: function( e ) {  
+        var d = e.ownerDocument;  
+        
+        // internet explorer  
+        if( d.body.createControlRange ) {  
+            var r = d.body.createControlRange();  
+            r.addElement( e );  
+            r.select();  
+        }  
+    }, 
+    
+    
+    /* dom methods */
+    
+    isImmutable: function( n ) {
+        try {
+            if( n.getAttribute( "contenteditable" ) == "false" )
+                return true;
+        } catch( e ) {}
+        return false;
+    },
+    
+    
+    getImmutable: function( n ) {
+        var immutable = null;
+        while( n ) {
+            if( DOM.isImmutable( n ) )
+                immutable = n;
+            n = n.parentNode;
+        }
+        return immutable;
+    },
+
+
+    getOwnerDocument: function( n ) {
+        if( !n )
+            return document;
+        if( n.ownerDocument )
+            return n.ownerDocument;
+        if( n.getElementById )
+            return n;
+        return document;
+    },
+
+
+    getOwnerWindow: function( n ) {
+        if( !n )
+            return window;
+        if( n.parentWindow )
+            return n.parentWindow;
+        var doc = DOM.getOwnerDocument( n );
+        if( doc && doc.defaultView )
+            return doc.defaultView;
+        return window;
+    },
+    
+    
+    getOwnerIframe: function( n ) {
+        if( !n )
+            return undefined;
+        var nw = DOM.getOwnerWindow( n );
+        var nd = DOM.getOwnerDocument( n );
+        var pw = nw.parent || nw.parentWindow;
+        if( !pw )
+            return undefined;
+        var parentDocument = pw.document;
+        var es = parentDocument.getElementsByTagName( "iframe" );
+        for( var i = 0; i < es.length; i++ ) {
+            var e = es[ i ];
+            try {
+                var d = e.contentDocument || e.contentWindow.document;
+                if( d === nd )
+                    return e;
+            }catch(err) {};
+        }
+        return undefined;
+    },
+
+
+    filterElementsByClassName: function( es, className ) {
+        var filtered = [];
+        for( var i = 0; i < es.length; i++ ) {
+            var e = es[ i ];
+            if( DOM.hasClassName( e, className ) )
+                filtered[ filtered.length ] = e;
+        }
+        return filtered;
+    },
+    
+    
+    filterElementsByAttribute: function( es, attr ) {
+        if( !es )
+            return [];
+        if( !defined( attr ) || attr == null || attr == "" )
+            return es;
+        var filtered = [];
+        for( var i = 0; i < es.length; i++ ) {
+            var element = es[ i ];
+            if( !element )
+                continue;
+            if( element.getAttribute && ( element.getAttribute( attr ) ) )
+                filtered[ filtered.length ] = element;
+        }
+        return filtered;
+    },
+
+
+    filterElementsByTagName: function( es, tagName ) {
+        if( tagName == "*" )
+            return es;
+        var filtered = [];
+        tagName = tagName.toLowerCase();
+        for( var i = 0; i < es.length; i++ ) {
+            var e = es[ i ];
+            if( e.tagName && e.tagName.toLowerCase() == tagName )
+                filtered[ filtered.length ] = e;
+        }
+        return filtered;
+    },
+
+
+    getElementsByTagAndAttribute: function( root, tagName, attr ) {
+        if( !root )
+            root = document;
+        var es = root.getElementsByTagName( tagName );
+        return DOM.filterElementsByAttribute( es, attr );
+    },
+    
+    
+    getElementsByAttribute: function( root, attr ) {
+        return DOM.getElementsByTagAndAttribute( root, "*", attr );
+    },
+
+
+    getElementsByAttributeAndValue: function( root, attr, value ) {
+        var es = DOM.getElementsByTagAndAttribute( root, "*", attr );
+        var filtered = [];
+        for ( var i = 0; i < es.length; i++ )
+            if ( es[ i ].getAttribute( attr ) == value )
+                filtered.push( es[ i ] );
+        return filtered;
+    },
+    
+
+    getElementsByTagAndClassName: function( root, tagName, className ) {
+        if( !root )
+            root = document;
+        var elements = root.getElementsByTagName( tagName );
+        return DOM.filterElementsByClassName( elements, className );
+    },
+
+
+    getElementsByClassName: function( root, className ) {
+        return DOM.getElementsByTagAndClassName( root, "*", className );
+    },
+
+
+    getAncestors: function( n, includeSelf ) {
+        if( !n )
+            return [];
+        var as = includeSelf ? [ n ] : [];
+        n = n.parentNode;
+        while( n ) {
+            as.push( n );
+            n = n.parentNode;
+        }
+        return as;
+    },
+    
+    
+    getAncestorsByTagName: function( n, tagName, includeSelf ) {
+        var es = DOM.getAncestors( n, includeSelf );
+        return DOM.filterElementsByTagName( es, tagName );
+    },
+    
+    
+    getFirstAncestorByTagName: function( n, tagName, includeSelf ) {
+        return DOM.getAncestorsByTagName( n, tagName, includeSelf )[ 0 ];
+    },
+
+
+    getAncestorsByClassName: function( n, className, includeSelf ) {
+        var es = DOM.getAncestors( n, includeSelf );
+        return DOM.filterElementsByClassName( es, className );
+    },
+
+
+    getFirstAncestorByClassName: function( n, className, includeSelf ) {
+        return DOM.getAncestorsByClassName( n, className, includeSelf )[ 0 ];
+    },
+
+
+    getAncestorsByTagAndClassName: function( n, tagName, className, includeSelf ) {
+        var es = DOM.getAncestorsByTagName( n, tagName, includeSelf );
+        return DOM.filterElementsByClassName( es, className );
+    },
+
+
+    getFirstAncestorByTagAndClassName: function( n, tagName, className, includeSelf ) {
+        return DOM.getAncestorsByTagAndClassName( n, tagName, className, includeSelf )[ 0 ];
+    },
+
+
+    getPreviousElement: function( n ) {
+        n = n.previousSibling;
+        while( n ) {
+            if( n.nodeType == Node.ELEMENT_NODE )
+                return n;
+            n = n.previousSibling;
+        }
+        return null;
+    },
+
+
+    getNextElement: function( n ) {
+        n = n.nextSibling;
+        while( n ) {
+            if( n.nodeType == Node.ELEMENT_NODE )
+                return n;
+            n = n.nextSibling;
+        }
+        return null;
+    },
+
+
+    isInlineNode: function( n ) {
+        // text nodes are inline
+        if( n.nodeType == Node.TEXT_NODE )
+            return n;
+
+        // document nodes are non-inline
+        if( n.nodeType == Node.DOCUMENT_NODE )
+            return false;
+
+        // all nonelement nodes are inline
+        if( n.nodeType != Node.ELEMENT_NODE )
+            return n;
+
+        // br elements are not inline
+        if( n.tagName && n.tagName.toLowerCase() == "br" )
+            return false;
+
+        // examine the style property of the inline n
+        var display = DOM.getStyle( n, "display" ); 
+        if( display && display.indexOf( "inline" ) >= 0 ) 
+            return n;
+    },
+    
+    
+    isTextNode: function( n ) {
+        if( n.nodeType == Node.TEXT_NODE )
+            return n;
+    },
+    
+    
+    isInlineTextNode: function( n ) {
+        if( n.nodeType == Node.TEXT_NODE )
+            return n;
+        if( !DOM.isInlineNode( n ) )
+            return null;
+    },
+
+
+    /* this and the following classname functions honor w3c case-sensitive classnames */
+
+    getClassNames: function( e ) {
+        if( !e || !e.className )
+            return [];
+        return e.className.split( /\s+/g );
+    },
+
+
+    hasClassName: function( e, className ) {
+        if( !e || !e.className )
+            return false;
+        var cs = DOM.getClassNames( e );
+        for( var i = 0; i < cs.length; i++ ) {
+            if( cs[ i ] == className )
+                return true;
+        }
+        return false;
+    },
+
+
+    addClassName: function( e, className ) {
+        if( !e || !className )
+            return false;
+        var cs = DOM.getClassNames( e );
+        for( var i = 0; i < cs.length; i++ ) {
+            if( cs[ i ] == className )
+                return true;
+        }
+        cs.push( className );
+        e.className = cs.join( " " );
+        return false;
+    },
+
+
+    removeClassName: function( e, className ) {
+        var r = false;
+        if( !e || !e.className || !className )
+            return r;
+        var cs = (e.className && e.className.length)
+            ? e.className.split( /\s+/g )
+            : [];
+        var ncs = [];
+        for( var i = 0; i < cs.length; i++ ) {
+            if( cs[ i ] == className ) {
+                r = true;
+                continue;
+            }
+            ncs.push( cs[ i ] );
+        }
+        if( r )
+            e.className = ncs.join( " " );
+        return r;
+    },
+    
+    
+    /* tree manipulation methods */
+    
+    replaceWithChildNodes: function( n ) {
+        var firstChild = n.firstChild;
+        var parentNode = n.parentNode;
+        while( n.firstChild )
+            parentNode.insertBefore( n.removeChild( n.firstChild ), n );
+        parentNode.removeChild( n );
+        return firstChild;
+    },
+    
+    
+    /* factory methods */
+    
+    createInvisibleInput: function( d ) {
+        if( !d )
+            d = window.document;
+        var e = document.createElement( "input" );
+        e.setAttribute( "autocomplete", "off" );
+        e.autocomplete = "off";
+        DOM.makeInvisible( e );
+        return e;
+    },
+
+
+    getMouseEventAttribute: function( event, a ) {
+        if( !a )
+            return;
+        var es = DOM.getAncestors( event.target, true );
+        for( var i = 0; i < es.length; i++ ) {
+            try {
+                var e = es[ i ]
+                var v = e.getAttribute ? e.getAttribute( a ) : null;
+                if( v ) {
+                    event.attributeElement = e;
+                    event.attribute = v;
+                    return v;
+                }
+            } catch( e ) {}
+        }
+    },
+    
+
+    setElementAttribute: function( e, a, v ) {
+        /* safari workaround
+         * safari's setAttribute assumes you want to use a namespace
+         * when you have a colon in your attribute
+         */
+        if ( navigator.userAgent.toLowerCase().match(/webkit/) ) {
+            var at = e.attributes;
+            for ( var i = 0; i < at.length; i++ )
+                if ( at[ i ].name == a )
+                    return at[ i ].nodeValue = v;
+        } else
+            e.setAttribute( a, v );
+    },
+
+
+    swapAttributes: function( e, tg, at ) {
+        var ar = e.getAttribute( tg );
+        if( !ar )
+            return false;
+        
+        /* clone the node with all children */
+        if ( e.tagName.toLowerCase() == 'script' ) {
+            /* only clone and replace script tags */
+            var cl = e.cloneNode( true );
+            if ( !cl )
+                return false;
+
+            DOM.setElementAttribute( cl, at, ar );
+            cl.removeAttribute( tg );
+        
+            /* replace new, old */
+            return e.parentNode.replaceChild( cl, e );
+        } else {
+            DOM.setElementAttribute( e, at, ar );
+            e.removeAttribute( tg );
+        }
+    },
+
+
+    findPosX: function( e ) {
+        var curleft = 0;
+
+        if (e.offsetParent) {
+            while (1) {
+                curleft += e.offsetLeft;
+                if (!e.offsetParent) {
+                    break;
+                }
+                e = e.offsetParent;
+            }
+        } else if (e.x) {
+            curleft += e.x;
+        }
+
+        return curleft;
+    },
+
+
+    findPosY: function( e ) {
+        var curtop = 0;
+
+        if (e.offsetParent) {
+            while (1) {
+                curtop += e.offsetTop;
+                if (!e.offsetParent) {
+                    break;
+                }
+                e = e.offsetParent;
+            }
+        } else if (e.y) {
+            curtop += e.y;
+        }
+
+        return curtop;
+    }
+    
+    
+} );
+
+
+$ = DOM.getElement;
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/hourglass.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/hourglass.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,59 @@
+// LiveJournal javascript standard interface routines
+
+// create a little animated hourglass at (x,y) with a unique-ish ID
+// returns the element created
+Hourglass = new Class( Object, {
+  init: function(widget, classname) {
+    this.ele = document.createElement("img");
+    if (!this.ele) return;
+
+    var imgprefix = Site ? Site.imgprefix : '';
+
+    this.ele.src = imgprefix ? imgprefix + "/hourglass.gif" : "/img/hourglass.gif";
+    this.ele.style.position = "absolute";
+
+    DOM.addClassName(this.ele, classname);
+
+    if (widget)
+      this.hourglass_at_widget(widget);
+  },
+
+  hourglass_at: function (x, y) {
+    this.ele.width = 17;
+    this.ele.height = 17;
+    this.ele.style.top = (y - 8) + "px";
+    this.ele.style.left = (x - 8) + "px";
+
+    // unique ID
+    this.ele.id = "lj_hourglass" + x + "." + y;
+
+    document.body.appendChild(this.ele);
+  },
+
+  add_class_name: function (classname) {
+      if (this.ele)
+      DOM.addClassName(this.ele, classname);
+  },
+
+  hourglass_at_widget: function (widget) {
+    var dim = DOM.getAbsoluteDimensions(widget);
+    var x = dim.absoluteLeft;
+    var y = dim.absoluteTop;
+    var w = dim.absoluteRight - x;
+    var h = dim.absoluteBottom - y;
+    if (w && h) {
+      x += w/2;
+      y += h/2;
+    }
+    this.hourglass_at(x, y);
+  },
+
+  hide: function () {
+    if (this.ele) {
+      try {
+        document.body.removeChild(this.ele);
+      } catch (e) {}
+    }
+  }
+
+} );
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/httpreq.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/httpreq.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,113 @@
+var HTTPReq = new Object;
+
+HTTPReq.create = function () {
+    var xtr;
+    var ex;
+
+    if (typeof(XMLHttpRequest) != "undefined") {
+        xtr = new XMLHttpRequest();
+    } else {
+        try {
+            xtr = new ActiveXObject("Msxml2.XMLHTTP.4.0");
+        } catch (ex) {
+            try {
+                xtr = new ActiveXObject("Msxml2.XMLHTTP");
+            } catch (ex) {
+            }
+        }
+    }
+
+    // let me explain this.  Opera 8 does XMLHttpRequest, but not setRequestHeader.
+    // no problem, we thought:  we'll test for setRequestHeader and if it's not present
+    // then fall back to the old behavior (treat it as not working).  BUT --- IE6 won't
+    // let you even test for setRequestHeader without throwing an exception (you need
+    // to call .open on the .xtr first or something)
+    try {
+        if (xtr && ! xtr.setRequestHeader)
+            xtr = null;
+    } catch (ex) { }
+
+    return xtr;
+};
+
+// opts:
+// url, onError, onData, method (GET or POST), data
+// url: where to get/post to
+// onError: callback on error
+// onData: callback on data received
+// method: HTTP method, GET by default
+// data: what to send to the server (urlencoded)
+HTTPReq.getJSON = function (opts) {
+    var req = HTTPReq.create();
+    if (! req) {
+        if (opts.onError) opts.onError("noxmlhttprequest");
+        return;
+    }
+
+    var state_callback = function () {
+        if (req.readyState != 4) return;
+
+        if (req.status != 200) {
+            if (opts.onError) opts.onError(req.status ? "status: " + req.status : "no data");
+            return;
+        }
+
+        var resObj;
+        var e;
+        try {
+            eval("resObj = " + req.responseText + ";");
+        } catch (e) {
+        }
+
+        if (e || ! resObj) {
+            if (opts.onError)
+                opts.onError("Error parsing response: \"" + req.responseText + "\"");
+
+            return;
+        }
+
+        if (opts.onData)
+            opts.onData(resObj);
+    };
+
+    req.onreadystatechange = state_callback;
+
+    var method = opts.method || "GET";
+    var data = opts.data || null;
+
+    var url = opts.url;
+    if (opts.method == "GET" && opts.data) {
+        url += url.match(/\?/) ? "&" : "?";
+        url += opts.data
+    }
+
+    url += url.match(/\?/) ? "&" : "?";
+    url += "_rand=" + Math.random();
+
+    req.open(method, url, true);
+
+    // we should send null unless we're in a POST
+    var to_send = null;
+
+    if (method.toUpperCase() == "POST") {
+        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        to_send = data;
+    }
+
+    req.send(to_send);
+};
+
+HTTPReq.formEncoded = function (vars) {
+    var enc = [];
+    var e;
+    for (var key in vars) {
+        try {
+            if (!vars.hasOwnProperty(key))
+                continue;
+            enc.push(encodeURIComponent(key) + "=" + encodeURIComponent(vars[key]));
+        } catch( e ) {}
+    }
+    return enc.join("&");
+
+};
+
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/image-region-select.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/image-region-select.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,337 @@
+/*
+  Copyright (c) 2006, Six Apart, Ltd.
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are
+  met:
+
+  * Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the following disclaimer
+  in the documentation and/or other materials provided with the
+  distribution.
+
+  * Neither the name of "Six Apart" nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+/* ***************************************************************************
+
+  Class: ImageRegionSelect
+
+  About:
+
+  Constructor:
+
+*************************************************************************** */
+
+var ImageRegionSelect = new Class( Object, {
+    init: function () {
+        var self = this;
+
+	if (arguments.length != 1) {
+            alert("Bogus args");
+            return;
+        }
+
+	var opts = arguments[0];
+        ["onRegionChange",
+         "onRegionChanged",
+         "onClick",
+         "onDebug",
+         ].forEach(function (v) {
+            self[v] = opts[v];
+        });
+
+        if (opts.src && typeof opts.src == 'object') {
+            this.imgEle = opts.src;
+        } else {
+            this.imgEle = $(opts.src);
+        }
+
+	var imgEle = this.imgEle;
+
+        if (!imgEle) {
+            this.error = "no image element";
+            return;
+        }
+
+        var w = imgEle.width;
+        var h = imgEle.height;
+        this.width = w;
+        this.height = h;
+
+        this.keepSquareAspect = false;
+
+        var containerDiv = this.containerDiv = document.createElement("div");
+        //containerDiv.style.border = "2px solid red";
+        containerDiv.style.width = w + "px";
+        containerDiv.style.height = h + "px";
+        containerDiv.style.background = "yellow";
+        containerDiv.style.position = "relative";
+        containerDiv.style.cursor = "crosshair";
+
+        imgEle.parentNode.replaceChild(containerDiv, imgEle);
+        containerDiv.appendChild(imgEle);
+        imgEle.style.position = "absolute";
+        imgEle.style.top = "0px";
+        imgEle.style.left = "0px";
+
+        // IE-specific attribute to disable the on-image toolbar:
+        imgEle.setAttribute("galleryimg", "no");
+
+        // make the black and white dotted ants
+        var makeAnts = function (color) {
+            var ele = document.createElement("div");
+            ele.style.position = "absolute";
+            ele.style.top  = "0px";
+            ele.style.left = "0px";
+            ele.style.width = w + "px";
+            ele.style.height = h + "px";
+            ele.style.border = "2px dashed " + color;
+            containerDiv.appendChild(ele);
+            return ele;
+        };
+        this.ants1 = makeAnts("white");
+        this.ants2 = makeAnts("black");
+
+        var coverNode = document.createElement("div");
+        coverNode.style.width = w + "px";
+        coverNode.style.height = h + "px";
+        //coverNode.style.border = "2px solid green";
+        coverNode.style.position = "absolute";
+        coverNode.style.top = "0px";
+        coverNode.style.left = "0px";
+        containerDiv.appendChild(coverNode);
+
+        // setup event handlers
+        var eatEvent = function (e) {
+            e = Event.prep(e);
+            return e.stop();
+        };
+        imgEle.onmousemove       = eatEvent;
+        containerDiv.onmousemove = eatEvent;
+        containerDiv.onmousedown = ImageRegionSelect.containerMouseDown.bindEventListener(this);
+
+        this.setEnabled(true);
+        this.reset();
+    },
+
+    fireOnRegionChanged: function () {
+        if (!this.onRegionChanged) return;
+        this.onRegionChanged(this.getSelectedRegion());
+    },
+
+    getSelectedRegion: function () {
+        return {
+            x1: this.tlx,
+            y1: this.tly,
+            x2: this.brx,
+            y2: this.bry
+        };
+    },
+
+    setEnabled: function (onoff) {
+        this.enabled = onoff;
+        [this.ants1, this.ants2].forEach(function (ele) { ele.style.display = onoff ? "block" : "none"; });
+    },
+
+    dbg: function (msg) {
+	if (this.onDebug)
+           this.onDebug(msg);
+    },
+
+    reset: function () {
+        this.tlx = 0;
+        this.brx = this.width;
+        this.tly = 0;
+        this.bry = this.height;
+        this.adjustAnts();
+        this.fireOnRegionChanged();
+    },
+
+    handleClick: function (e) {
+        if (! this.enabled) return;
+
+        e = Event.prep(e);
+        var pos = this.relPos(e);
+
+        if (this.onClick) {
+            this.onClick(pos);  // contains .x and .y
+        }
+    },
+
+    relPos: function (e) {
+        e = Event.prep(e);
+        var loc    = DOM.getAbsoluteCursorPosition(e);
+        var ctrDim = DOM.getAbsoluteDimensions(this.containerDiv);
+        return {x: loc.x - ctrDim.absoluteLeft,
+                       y: loc.y - ctrDim.absoluteTop };
+    },
+
+    sortPoints: function () {
+        var t;
+        if (this.tlx > this.brx) {
+            t = this.tlx;
+            this.tlx = this.brx;
+            this.brx = t;
+        }
+
+        if (this.tly > this.bry) {
+            t = this.tly;
+            this.tly = this.bry;
+            this.bry = t;
+        }
+    },
+
+    setTopLeft: function (x, y) {
+        if (! this.enabled) return;
+
+        x = max(0, min(x, this.width));
+        y = max(0, min(y, this.height));
+        this.tlx = x;
+        this.tly = y;
+        this.adjustAnts();
+    },
+
+    setBottomRight: function (x, y, isShift) {
+        if (! this.enabled) return;
+
+        x = max(0, min(x, this.width));
+        y = max(0, min(y, this.height));
+
+        if (isShift) {
+            var dx = Math.abs(this.tlx - x);
+            var dy = Math.abs(this.tly - y);
+            var d = max(dx, dy);
+
+            this.brx = min(this.tlx + ((x > this.tlx) ? 1 : -1) * d, this.width);
+            this.bry = min(this.tly + ((y > this.tly) ? 1 : -1) * d, this.height);
+            if (this.brx < 0) this.brx = 0;
+            if (this.bry < 0) this.bry = 0;
+        } else {
+            this.brx = x;
+            this.bry = y;
+        }
+        this.adjustAnts();
+    },
+
+    adjustAnts: function () {
+        if (! this.enabled) return;
+
+        var minx = min(this.tlx, this.brx);
+        var miny = min(this.tly, this.bry);
+        var width = max(4, Math.abs(this.brx - this.tlx));
+        var height = max(4, Math.abs(this.bry - this.tly));
+
+        if (this.onRegionChange) {
+            this.onRegionChange({
+                x1: minx,
+                y1: miny,
+                x2: minx + width,
+                y2: miny + height
+            });
+        }
+
+        this.ants1.style.left = minx + "px";
+        this.ants1.style.top = miny + "px";
+        this.ants1.style.width = width + "px";
+        this.ants1.style.height = height + "px";
+
+        this.ants2.style.left = (minx + 2) + "px";
+        this.ants2.style.top = (miny + 2) + "px";
+        this.ants2.style.width = (width - 4) + "px";
+        this.ants2.style.height = (height - 4) + "px";
+    },
+
+    keepSquare: function (keepSquareAspect) {
+      this.keepSquareAspect = keepSquareAspect;
+    },
+
+    dummy: 1
+});
+
+ImageRegionSelect.containerMouseDown = function (e) {
+    //this.dbg("onKeyDown, code="+code+", shift="+e.shiftKey);
+
+    e = Event.prep(e);
+    var dpos = this.relPos(e);
+    var self = this;
+
+    var ctrDiv = this.containerDiv;
+    var did_topleft = false;
+    var imgEle = this.imgEle;
+
+    var isClose = function (pos) {
+        // this looks stupid, but it's the only way that'll work:  I tried
+        // to just return the expression in the if block, but it wasn't working.
+        // not sure what I'm not understanding here.  --brad
+        if (Math.abs(pos.x - dpos.x) < 10 &&
+            Math.abs(pos.y - dpos.y) < 10) {
+                return true;
+            } else {
+                return false;
+            }
+    };
+
+
+    // IE's onmousemove event comes in from a different place than the mousedown/up.
+    // whatever.
+    imgEle.onmousemove = ctrDiv.onmousemove = function (emove) {
+        emove = Event.prep(emove);
+        var mpos = self.relPos(emove);
+
+        var isShift = emove.shiftKey || self.keepSquareAspect;
+        //log(trackerPlane + ":  mouse move");
+
+        if (did_topleft) {
+            self.setBottomRight(mpos.x, mpos.y, isShift);
+        } else if (! isClose(mpos)) {
+            did_topleft = true;
+            self.setTopLeft(dpos.x, dpos.y);
+            self.setBottomRight(mpos.x, mpos.y, isShift);
+        }
+
+        return emove.stop();
+    };
+
+    ctrDiv.onmouseup = function (eup) {
+        eup = Event.prep(eup);
+        var upos = self.relPos(eup);
+
+        ctrDiv.onmousemove = null;
+        ctrDiv.onmouseup   = null;
+        imgEle.onmousemove = null;
+
+        if (!did_topleft) {
+            self.handleClick(eup);
+            return eup.stop();
+        }
+
+        self.sortPoints();
+
+        self.fireOnRegionChanged();
+
+        return eup.stop();
+
+    };
+
+    return e.stop();
+};
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/inputcomplete.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/inputcomplete.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,426 @@
+/* input completion library */
+
+/* TODO:
+    -- test on non-US keyboard layouts (too much use of KeyCode)
+    -- lazy data model (xmlhttprequest, or generic callbacks)
+    -- drop-down menu?
+    -- option to disable comma-separated mode (or explicitly ask for it)
+*/
+
+/*
+  Copyright (c) 2005, Six Apart, Ltd.
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are
+  met:
+
+  * Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the following disclaimer
+  in the documentation and/or other materials provided with the
+  distribution.
+
+  * Neither the name of "Six Apart" nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+
+/* ***************************************************************************
+
+  Class: InputCompleteData
+
+  About: An InputComplete object needs a data source to auto-complete
+          from.  This is that model.  You can create one from an
+          array, or create a lazy version that gets its data over the
+          network, on demand.  You will probably not use this class'
+          methods directly, as they're called by the InputComplete
+          object.
+
+          The closer a word is to the beginning of the array, the more
+          likely it will be recommended as the word the user is typing.
+
+          If you pass the string "ignorecase" as the second argument in
+          the constructor, then the case of both the user's input and
+          the data in the array will be ignored when looking for a match.
+
+  Constructor:
+
+    var model = new InputCompleteData ([ "foo", "bar", "alpha" ]);
+
+*************************************************************************** */
+
+var InputCompleteData = new Class ( Object, {
+    init: function () {
+        if (arguments[0] instanceof Array) {
+            this.source = [];
+
+            // copy the user-provided array (which is sorted most
+            // likely to least likely) into our internal form, which
+            // is opposite, with most likely at the end.
+            var arg = arguments[0];
+            for (var i=arg.length-1; i>=0; i--) {
+                this.source.length++;
+                this.source[this.source.length-1] = arg[i];
+            }
+        }
+
+        this.ignoreCase = 0;
+        if (arguments[1] == "ignorecase") {
+            this.ignoreCase = 1;
+        }
+    },
+
+    // method: given prefix, returns best suffix, or null if no answer
+    bestFinish: function (pre) {
+        if (! pre || pre.length == 0)
+            return null;
+
+        if (! this.source)
+            return null;
+
+        var i;
+        for (i=this.source.length-1; i>=0; i--) {
+            var item = this.source[i];
+
+            var itemToCompare = item;
+            var preToCompare = pre;
+            if (this.ignoreCase) {
+                item += '';
+                pre += '';
+                itemToCompare = item.toLowerCase();
+                preToCompare = pre.toLowerCase();
+            }
+
+            if (itemToCompare.substring(0, pre.length) == preToCompare) {
+                var suff = item.substring(pre.length, item.length);
+                return suff;
+            }
+        }
+
+        return null;
+    },
+
+    // method: given a piece of data, learn it, and prioritize it for future completions
+    learn: function (word) {
+        if (!word) return false;
+        if (!this.source) return false;
+        this.source[this.source.length++] = word;
+
+        if (this.onModelChange)
+            this.onModelChange();
+    },
+
+    getItems: function () {
+        if (!this.source) return [];
+
+        // return only unique items to caller
+        var uniq = [];
+        var seen = {};
+        for (i=this.source.length-1; i>=0; i--) {
+            var item = this.source[i];
+            if (! seen[item]) {
+                seen[item] = 1;
+                uniq.length++;
+                uniq[uniq.length - 1] = item;
+            }
+        }
+
+        return uniq;
+    },
+
+    dummy: 1
+});
+
+/* ***************************************************************************
+
+  Class: InputComplete
+
+  About:
+
+  Constructor:
+
+*************************************************************************** */
+
+var InputComplete = new Class( Object, {
+    init: function () {
+        var opts = arguments[0];
+        var ele;
+        var model;
+        var debug;
+
+        if (arguments.length == 1) {
+            ele = opts["target"];
+            model = opts["model"];
+            debug = opts["debug"];
+        } else {
+            ele = arguments[0];
+            model = arguments[1];
+            debug = arguments[2];
+        }
+
+        this.ele   = ele;
+        this.model = model;
+        this.debug = debug;
+
+        // no model?  don't setup object.
+        if (! ele) {
+            this.disabled = true;
+            return;
+        }
+
+        // return false if auto-complete won't work anyway
+        if (! (("selectionStart" in ele) || (document.selection && document.selection.createRange)) ) {
+            this.disabled = true;
+            return false;
+        }
+
+        DOM.addEventListener(ele, "focus",   InputComplete.onFocus.bindEventListener(this));
+        DOM.addEventListener(ele, "keydown", InputComplete.onKeyDown.bindEventListener(this));
+        DOM.addEventListener(ele, "keyup",   InputComplete.onKeyUp.bindEventListener(this));
+        DOM.addEventListener(ele, "blur",    InputComplete.onBlur.bindEventListener(this));
+    },
+
+    dbg: function (msg) {
+        if (this.debug) {
+            this.debug(msg);
+        }
+    },
+
+    // returns the word currently being typed, or null
+    wordInProgress: function () {
+        var sel = this.getSelectedRange();
+        if (!sel) return null;
+
+        var cidx = sel.selectionStart; // current indx
+        var sidx = cidx;  // start of word index
+        while (sidx > 0 && this.ele.value.charAt(sidx) != ',') {
+            sidx--;
+        }
+        var skipStartForward = function (chr) { return (chr == "," || chr == " "); }
+
+        while (skipStartForward(this.ele.value.charAt(sidx))) {
+            sidx++;
+        }
+
+        return this.ele.value.substring(sidx, this.ele.value.length);
+    },
+
+    // appends some selected text after the care
+    addSelectedText: function (chars) {
+        var sel = this.getSelectedRange();
+        this.ele.value = this.ele.value + chars;
+        this.setSelectedRange(sel.selectionStart, this.ele.value.length);
+    },
+
+    moveCaretToEnd: function () {
+        var len = this.ele.value.length;
+        this.setSelectedRange(len, len);
+    },
+
+    getSelectedRange: function () {
+        var ret = {};
+        var ele = this.ele;
+
+        if ("selectionStart" in ele) {
+            ret.selectionStart = ele.selectionStart;
+            ret.selectionEnd   = ele.selectionEnd;
+            return ret;
+        }
+
+        if (document.selection && document.selection.createRange) {
+            var range = document.selection.createRange();
+            ret.selectionStart = InputComplete.IEOffset(range, "StartToStart");
+            ret.selectionEnd   = InputComplete.IEOffset(range, "EndToEnd");
+            return ret;
+        }
+
+        return null;
+    },
+
+    setSelectedRange: function (sidx, eidx) {
+        var ele = this.ele;
+
+        // preferred to setting selectionStart and end
+        if (ele.setSelectionRange) {
+            ele.focus();
+            ele.setSelectionRange(sidx, eidx);
+            return true;
+        }
+
+        // IE
+        if (document.selection && document.selection.createRange) {
+            ele.focus();
+            var sel = document.selection.createRange ();
+            sel.moveStart('character', -ele.value.length);
+            sel.moveStart('character', sidx);
+            sel.moveEnd('character', eidx - sidx);
+            sel.select();
+            return true;
+        }
+
+        // mozilla
+        if ("selectionStart" in ele) {
+            ele.selectionStart = sidx;
+            ele.selectionEnd   = eidx;
+            return true;
+        }
+
+        return false;
+    },
+
+    // returns true if caret is at end of line, or everything to the right
+    // of us is selected
+    caretAtEndOfNotSelected: function (sel) {
+        sel = sel || this.getSelectedRange();
+        var len = this.ele.value.length;
+        return sel.selectionEnd == len;
+    },
+
+    disable: function () {
+        this.disabled = true;
+    },
+
+    dummy: 1
+});
+
+InputComplete.onKeyDown = function (e) {
+    if (this.disabled) return;
+
+    var code = e.keyCode || e.which;
+
+    this.dbg("onKeyDown, code="+code+", shift="+e.shiftKey);
+    
+    // if comma, but not with a shift which would be "<".  (FIXME: what about other keyboards layouts?)
+    //FIXME: may be there is a stable cross-browser way to detect so-called other keyboard layouts - but i don't know anything easier than ... (see onKeyUp changes in tis revision)
+    /*if ((code == 188 || code == 44) && ! e.shiftKey && this.caretAtEndOfNotSelected()) {
+        this.moveCaretToEnd();
+        return Event.stop(e);
+    }*/
+
+    return true;
+};
+
+InputComplete.onKeyUp = function (e) {
+    if (this.disabled) return;
+
+    var val = this.ele.value;
+
+    var code = e.keyCode || e.which;
+    this.dbg("keyUp = " + code);
+    
+    
+    // ignore tab, backspace, left, right, delete, and enter
+    if (code == 9 || code == 8 || code == 37 || code == 39 || code == 46 || code == 13)
+       return false;
+
+    var sel = this.getSelectedRange();
+
+    var ss = sel.selectionStart;
+    var se = sel.selectionEnd;
+
+    this.dbg("keyUp, got ss="+ss +  ", se="+se+", val.length="+val.length);
+
+    // only auto-complete if we're at the end of the line
+    if (se != val.length) return false;
+
+    var chr = String.fromCharCode(code);
+
+    this.dbg("keyUp, got chr="+chr);
+    //if (code == 188 || chr == ",") {
+    if(/,$/.test(val)){	
+        if (! this.caretAtEndOfNotSelected(sel)) {
+            return false;
+        }
+
+        this.dbg("hit comma! .. value = " + this.ele.value);
+
+        this.ele.value = this.ele.value.replace(/[\s,]+$/, "") + ", ";
+        this.moveCaretToEnd();
+
+        return Event.stop(e);
+    }
+
+
+    var inProg = this.wordInProgress();
+    if (!inProg) return true;
+
+    var rest = this.model.bestFinish(inProg);
+
+    if (rest && rest.length > 0) {
+        this.addSelectedText(rest);
+    }
+};
+
+InputComplete.onBlur = function (e) {
+    if (this.disabled) return;
+
+    var tg = e.target;
+    var list = tg.value;
+
+    var noendjunk = list.replace(/[\s,]+$/, "");
+    if (noendjunk != list) {
+        tg.value = list = noendjunk;
+    }
+
+    var tags = list.split(",");
+    for (var i =0; i<tags.length; i++) {
+        var tag = tags[i].replace(/^\s+/,"").replace(/\s+$/,"");
+        if (tag.length) {
+            this.model.learn(tag);
+        }
+    }
+};
+
+InputComplete.onFocus = function (e) {
+    if (this.disabled) return;
+};
+
+
+InputComplete.IEOffset = function ( range, compareType ) {
+    if (this.disabled) return;
+
+    var range2 = range.duplicate();
+    range2.collapse( true );
+    var parent = range2.parentElement();
+    var length = range2.text.length;
+    range2.move("character", -parent.value.length);
+
+    var delta = max( 1, finiteInt( length * 0.5 ) );
+    range2.collapse( true );
+    var offset = 0;
+    var steps = 0;
+
+    // bail after 10k iterations in case of borkage
+    while( (test = range2.compareEndPoints( compareType, range )) != 0 ) {
+        if( test < 0 ) {
+            range2.move( "character", delta );
+            offset += delta;
+        } else {
+            range2.move( "character", -delta );
+            offset -= delta;
+        }
+        delta = max( 1, finiteInt( delta * 0.5 ) );
+        steps++;
+        if( steps > 1000 )
+            throw "unable to find textrange endpoint in " + steps + " steps";
+    }
+
+    return offset;
+};
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/ippu.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/ippu.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,732 @@
+/*
+  IPPU methods:
+     init([innerHTML]) -- takes innerHTML as optional argument
+     show() -- shows the popup
+     hide() -- hides popup
+     cancel() -- hides and calls cancel callback
+
+  Content setters:
+     setContent(innerHTML) -- set innerHTML
+     setContentElement(element) -- adds element as a child of the popup
+
+   Accessors:
+     getElement() -- returns popup DIV element
+     visible() -- returns whether the popup is visible or not
+
+   Titlebar:
+     setTitlebar(show) -- true: show titlebar / false: no titlebar
+     setTitle(title) -- sets the titlebar text
+     getTitlebarElement() -- returns the titlebar element
+     setTitlebarClass(className) -- set the class of the titlebar
+
+   Styling:
+     setOverflow(overflow) -- sets ele.style.overflow to overflow
+     addClass(className) -- adds class to popup
+     removeClass(className) -- removes class to popup
+
+   Browser Hacks:
+     setAutoHideSelects(autohide) -- when the popup is shown should it find all the selects
+                                on the page and hide them (and show them again) (for IE)
+
+   Positioning/Sizing:
+     setLocation(left, top) -- set popup location: will be pixels if units not specified
+     setLeft(left) -- set left location
+     setTop(top)   -- set top location
+     setDimensions(width, height) -- set popup dimensions: will be pixels if units not specified
+     setAutoCenter(x, y) -- what dimensions to auto-center
+     center() -- centers popup on screen
+     centerX() -- centers popup horizontally
+     centerY() -- centers popup vertically
+     setFixedPosition(fixed) -- should the popup stay fixed on the page when it scrolls?
+     centerOnWidget(widget) -- center popup on this widget
+     setAutoCenterCallback(callback) -- calls callback with this IPPU instance as a parameter
+                                        for auto-centering. Some common built-in class methods
+                                        you can use as callbacks are:
+                                        IPPU.center
+                                        IPPU.centerX
+                                        IPPU.centerY
+
+     moveForward(amount) -- increases the zIndex by one or amount if specified
+     moveBackward(amount) -- decreases the zIndex by one or amount if specified
+
+   Modality:
+     setClickToClose(clickToClose) -- if clickToClose is true, clicking outside of the popup
+                                      will close it
+     setModal(modality) -- If modality is true, then popup will capture all mouse events
+                     and optionally gray out the rest of the page. (overrides clickToClose)
+     setOverlayVisible(visible) -- If visible is true, when this popup is on the page it
+                                   will gray out the rest of the page if this is modal
+
+   Callbacks:
+     setCancelledCallback(callback) -- call this when the dialog is closed through clicking
+                                       outside, titlebar close button, etc...
+     setHiddenCallback(callback) -- called when the dialog is closed in any fashion
+
+   Fading:
+     setFadeIn(fadeIn) -- set whether or not to automatically fade the ippu in
+     setFadeOut(fadeOut) -- set whether or not to automatically fade the ippu out
+     setFadeSpeed(secs) -- sets fade speed
+
+  Class Methods:
+   Handy callbacks:
+     IPPU.center
+     IPPU.centerX
+     IPPU.centerY
+   Browser testing:
+     IPPU.isIE() -- is the browser internet exploder?
+     IPPU.ieSafari() -- is this safari?
+
+////////////////////
+
+
+ippu.setModalDenialCallback(IPPU.cssBorderFlash);
+
+
+   private:
+    Properties:
+     ele -- DOM node of div
+     shown -- boolean; if element is in DOM
+     autoCenterX -- boolean; auto-center horiz
+     autoCenterY -- boolean; auto-center vertical
+     fixedPosition -- boolean; stay in fixed position when browser scrolls?
+     titlebar -- titlebar element
+     title -- string; text to go in titlebar
+     showTitlebar -- boolean; whether or not to show titlebar
+     content -- DIV containing user's specified content
+     clickToClose -- boolean; clicking outside popup will close it
+     clickHandlerSetup -- boolean; have we set up the click handlers?
+     docOverlay -- DIV that overlays the document for capturing clicks
+     modal -- boolean; capture all events and prevent user from doing anything
+                       until dialog is dismissed
+     visibleOverlay -- boolean; make overlay slightly opaque
+     clickHandlerFunc -- function; function to handle document clicks
+     resizeHandlerFunc -- function; function to handle document resizing
+     autoCenterCallback -- function; what callback to call for auto-centering
+     cancelledCallback -- function; called when dialog is cancelled
+     setAutoHideSelects -- boolean; autohide all SELECT elements on the page when popup is visible
+     hiddenSelects -- array; SELECT elements that have been hidden
+     hiddenCallback -- funciton; called when dialog is hidden
+     fadeIn, fadeOut, fadeSpeed -- fading settings
+     fadeMode -- current fading mode (in, out) if there is fading going on
+
+    Methods:
+     updateTitlebar() -- create titlebar if it doesn't exist,
+                         hide it if titlebar == false,
+                         update titlebar text
+     updateContent() -- makes sure all currently specified properties are applied
+     setupClickCapture() -- if modal, create document-sized div overlay to capture click events
+                            otherwise install document onclick handler
+     removeClickHandlers() -- remove overlay, event handlers
+     clickHandler() -- event handler for clicks
+     updateOverlay() -- if we have an overlay, make sure it's where it should be and (in)visible
+                        if it should be
+     autoCenter() -- centers popup on screen according to autoCenterX and autoCenterY
+     hideSelects() -- hide all select element on page
+     showSelects() -- show all selects
+     _hide () -- actually hides everything, called by hide(), which does fading if necessary
+*/
+
+// this belongs somewhere else:
+function changeOpac(id, opacity) {
+    var e =  $(id);
+    if (e && e.style) {
+        var object = e.style;
+        if (object) {
+            //reduce flicker
+            if (IPPU.isSafari() && opacity >= 100)
+                opacity = 99.99;
+
+            // IE
+            if (object.filters)
+                object.filters.alpha.opacity = opacity * 100;
+
+            object.opacity = opacity;
+        }
+    }
+}
+
+IPPU = new Class( Object, {
+  setFixedPosition: function (fixed) {
+    // no fixed position for IE
+    if (IPPU.isIE())
+      return;
+
+    this.fixedPosition = fixed;
+    this.updateContent();
+  },
+
+  clickHandler : function (evt) {
+    if (!this.clickToClose) return;
+    if (!this.visible()) return;
+
+    evt = Event.prep(evt);
+    var target = evt.target;
+    // don't do anything if inside the popup
+    if (DOM.getAncestorsByClassName(target, "ippu", true).length > 0) return;
+    this.cancel();
+  },
+
+  setCancelledCallback : function (callback) {
+    this.cancelledCallback = callback;
+  },
+
+  cancel : function () {
+    if (this.cancelledCallback)
+      this.cancelledCallback();
+    this.hide();
+  },
+
+  setHiddenCallback: function (callback) {
+    this.hiddenCallback = callback;
+  },
+
+  setupClickCapture : function () {
+    if (!this.visible() || this.clickHandlerSetup){return;}
+    if (!this.clickToClose && !this.modal) {return;}
+
+    this.clickHandlerFunc = this.clickHandler.bindEventListener(this);
+
+    if (this.modal) {
+      // create document-sized div to capture events
+      if (this.overlay) return; // wtf? shouldn't exist yet
+      this.overlay = document.createElement("div");
+      this.overlay.style.left = "0px";
+      this.overlay.style.top = "0px";
+      this.overlay.style.margin = "0px";
+      this.overlay.style.padding = "0px";
+      
+      this.overlay.style.backgroundColor = "#000000";
+      this.overlay.style.zIndex="900";
+      if (IPPU.isIE()){
+      		this.overlay.style.filter="progid:DXImageTransform.Microsoft.Alpha(opacity=50)";
+		this.overlay.style.position="absolute";
+		this.overlay.style.width=document.body.scrollWidth;
+		this.overlay.style.height=document.body.scrollHeight;
+      }
+      else{
+      	this.overlay.style.position = "fixed";
+      }
+
+      this.ele.parentNode.insertBefore(this.overlay, this.ele);
+      this.updateOverlay();
+
+      DOM.addEventListener(this.overlay, "click", this.clickHandlerFunc);
+    } else {
+      // simple document onclick handler
+      DOM.addEventListener(document, "click", this.clickHandlerFunc);
+    }
+
+    this.clickHandlerSetup = true;
+  },
+
+  updateOverlay : function () {
+    if (this.overlay) {
+      var cd = DOM.getClientDimensions();
+      this.overlay.style.width = (cd.x - 1) + "px";
+      if(!IPPU.isIE()){
+      	this.overlay.style.height = (cd.y - 1) + "px";
+      }	
+      if (this.visibleOverlay) {
+        this.overlay.backgroundColor = "#000000";
+        changeOpac(this.overlay, 0.50);
+      } else {
+        this.overlay.backgroundColor = "#FFFFFF";
+        changeOpac(this.overlay, 0.0);
+      }
+    }
+  },
+
+  resizeHandler : function (evt) {
+    this.updateContent();
+  },
+
+  removeClickHandlers : function () {
+    if (!this.clickHandlerSetup) return;
+
+    var myself = this;
+    var handlerFunc = function (evt) {
+      myself.clickHandler(evt);
+    };
+
+    DOM.removeEventListener(document, "click", this.clickHandlerFunc, false);
+
+    if (this.overlay) {
+      DOM.removeEventListener(this.overlay, "click", this.clickHandlerFunc, true);
+      this.overlay.parentNode.removeChild(this.overlay);
+      this.overlay = undefined;
+    }
+
+    this.clickHandlerFunc = undefined;
+    this.clickHandlerSetup = false;
+  },
+
+  setClickToClose : function (clickToClose) {
+    this.clickToClose = clickToClose;
+
+    if (!this.clickHandlerSetup && clickToClose && this.visible()) {
+      // popup is already visible, need to set up click handler
+      var setupClickCaptureCallback = this.setupClickCapture.bind(this);
+      window.setTimeout(setupClickCaptureCallback, 100);
+    } else if (!clickToClose && this.clickHandlerSetup) {
+      this.removeClickHandlers();
+    }
+
+    this.updateContent();
+  },
+
+  setModal : function (modal) {
+    var changed = (this.modal == modal);
+
+    // if it's modal, we don't want click-to-close
+    if (modal)
+      this.setClickToClose(false);
+
+    this.modal = modal;
+    if (changed) {
+      this.removeClickHandlers();
+      this.updateContent();
+    }
+  },
+
+  setOverlayVisible : function (vis) {
+    this.visibleOverlay = vis;
+    this.updateContent();
+  },
+
+  updateContent : function () {
+    this.autoCenter();
+    this.updateTitlebar();
+    this.updateOverlay();
+    if (this.titlebar)
+      this.setTitlebarClass(this.titlebar.className);
+
+    var setupClickCaptureCallback = this.setupClickCapture.bind(this);
+    window.setTimeout(setupClickCaptureCallback, 100);
+
+    if (this.fixedPosition && this.ele.style.position != "fixed")
+      this.ele.style.position = "fixed";
+    else if (!this.fixedPosition && this.ele.style.position == "fixed")
+      this.ele.style.position = "absolute";
+  },
+
+  getTitlebarElement : function () {
+    return this.titlebar;
+  },
+
+  setTitlebarClass : function (className) {
+    if (this.titlebar)
+      this.titlebar.className = className;
+  },
+
+  setOverflow : function (overflow) {
+    if (this.ele)
+      this.ele.style.overflow = overflow;
+  },
+
+  visible : function () {
+    return this.shown;
+  },
+
+  setTitlebar : function (show) {
+    this.showTitlebar = show;
+
+    if (show) {
+      if (!this.titlebar) {
+        // titlebar hasn't been created. Create it.
+        var tbar = document.createElement("div");
+        if (!tbar) return;
+        tbar.style.width = "100%";
+
+        if (this.title) tbar.innerHTML = this.title;
+        this.ele.insertBefore(tbar, this.content);
+        this.titlebar = tbar;
+
+      }
+    } else if (this.titlebar) {
+      this.ele.removeChild(this.titlebar);
+      this.titlebar = false;
+    }
+  },
+
+  setTitle : function (title) {
+    this.title = title;
+    this.updateTitlebar();
+  },
+
+  updateTitlebar : function() {
+    if (this.showTitlebar && this.titlebar && this.title != this.titlebar.innerHTML) {
+      this.titlebar.innerHTML = this.title;
+    }
+  },
+
+  addClass : function (className) {
+    DOM.addClassName(this.ele, className);
+  },
+
+  removeClass : function (className) {
+    DOM.removeClassName(this.ele, className);
+  },
+
+  setAutoCenterCallback : function (callback) {
+    this.autoCenterCallback = callback;
+  },
+
+  autoCenter : function () {
+    if (!this.visible || !this.visible()) return;
+
+    if (this.autoCenterCallback) {
+      this.autoCenterCallback(this);
+      return;
+    }
+
+    if (this.autoCenterX)
+      this.centerX();
+
+    if (this.autoCenterY)
+      this.centerY();
+  },
+
+  center : function () {
+    this.centerX();
+    this.centerY();
+  },
+
+  centerOnWidget : function (widget, offsetTop, offsetLeft) {
+        offsetTop = offsetTop || 0;
+        offsetLeft = offsetLeft || 0;
+        this.setAutoCenter(false, false);
+        this.setAutoCenterCallback(null);
+  var wd = DOM.getAbsoluteDimensions(widget);
+    var ed = DOM.getAbsoluteDimensions(this.ele);
+    var newleft = (wd.absoluteRight - wd.offsetWidth / 2 - ed.offsetWidth / 2) + offsetLeft;
+    var newtop = (wd.absoluteBottom - wd.offsetHeight / 2 - ed.offsetHeight / 2) + offsetTop;
+
+        newleft = newleft < 0 ? 0 : newleft;
+        newtop  = newtop  < 0 ? 0 : newtop;
+    DOM.setLeft(this.ele, newleft);
+    DOM.setTop(this.ele, newtop);
+  },
+
+  centerX : function () {
+    if (!this.visible || !this.visible()) return;
+
+    var cd = DOM.getClientDimensions();
+    var newleft = cd.x / 2 - DOM.getDimensions(this.ele).offsetWidth / 2;
+
+    // If not fixed position, center relative to the left of the page
+    if (!this.fixedPosition) {
+        var wd = DOM.getWindowScroll();
+        newleft += wd.left;
+    }
+
+   DOM.setLeft(this.ele, newleft);
+  },
+
+  centerY : function () {
+    if (!this.visible || !this.visible()) return;
+
+    var cd = DOM.getClientDimensions();
+    var newtop = cd.y / 2 - DOM.getDimensions(this.ele).offsetHeight / 2;
+
+    // If not fixed position, center relative to the top of the page
+    if (!this.fixedPosition) {
+        var wd = DOM.getWindowScroll();
+        newtop += wd.top;
+    }
+
+    DOM.setTop(this.ele, newtop);
+  },
+
+  setAutoCenter : function (autoCenterX, autoCenterY) {
+    this.autoCenterX = autoCenterX || false;
+    this.autoCenterY = autoCenterY || false;
+
+    if (!autoCenterX && !autoCenterY) {
+        this.setAutoCenterCallback(null);
+        return;
+    }
+
+    this.autoCenter();
+  },
+
+  setDimensions : function (width, height) {
+    width = width + "";
+    height = height + "";
+    if (width.match(/^\d+$/)) width += "px";
+    if (height.match(/^\d+$/)) height += "px";
+
+    this.ele.style.width  = width;
+    this.ele.style.height = height;
+  },
+
+  moveForward : function (howMuch) {
+      if (!howMuch) howMuch = 1;
+      if (! this.ele) return;
+
+      this.ele.style.zIndex += howMuch;
+  },
+
+  moveBackward : function (howMuch) {
+      if (!howMuch) howMuch = 1;
+      if (! this.ele) return;
+
+      this.ele.style.zIndex -= howMuch;
+  },
+
+  setLocation : function (left, top) {
+      this.setLeft(left);
+      this.setTop(top);
+  },
+
+  setTop : function (top) {
+    top = top + "";
+    if (top.match(/^\d+$/)) top += "px";
+    this.ele.style.top = top;
+  },
+
+  setLeft : function (left) {
+    left = left + "";
+    if (left.match(/^\d+$/)) left += "px";
+    this.ele.style.left = left;
+  },
+
+  getElement : function () {
+    return this.ele;
+  },
+
+  setContent : function (html) {
+    this.content.innerHTML = html;
+  },
+
+  setContentElement : function (element) {
+      // remove child nodes
+      while (this.content.firstChild) {
+          this.content.removeChild(this.content.firstChild);
+      };
+
+    this.content.appendChild(element);
+  },
+
+  setFadeIn : function (fadeIn) {
+      this.fadeIn = fadeIn;
+  },
+
+  setFadeOut : function (fadeOut) {
+      this.fadeOut = fadeOut;
+  },
+
+  setFadeSpeed : function (fadeSpeed) {
+      this.fadeSpeed = fadeSpeed;
+  },
+
+  show : function () {
+    this.shown = true;
+
+    if (this.fadeIn) {
+        var opp = 0.01;
+
+        changeOpac(this.ele, opp);
+    }
+
+    document.body.appendChild(this.ele);
+    this.ele.style.position = "absolute";
+    if (this.autoCenterX || this.autoCenterY) this.center();
+
+    this.updateContent();
+
+    if (!this.resizeHandlerFunc) {
+      this.resizeHandlerFunc = this.resizeHandler.bindEventListener(this);
+      DOM.addEventListener(window, "resize", this.resizeHandlerFunc, false);
+    }
+
+    if (this.fadeIn)
+        this.fade("in");
+
+    this.hideSelects();
+  },
+
+  fade : function (mode, callback) {
+      var opp;
+      var delta;
+
+      var steps = 10.0;
+
+      if (mode == "in") {
+          delta = 1 / steps;
+          opp = 0.1;
+      } else {
+          if (this.ele.style.opacity)
+          opp = finiteFloat(this.ele.style.opacity);
+          else
+          opp = 0.99;
+
+          delta = -1 / steps;
+      }
+
+      var fadeSpeed = this.fadeSpeed;
+      if (!fadeSpeed) fadeSpeed = 1;
+
+      var fadeInterval = steps / fadeSpeed * 5;
+
+      this.fadeMode = mode;
+
+      var self = this;
+      var fade = function () {
+          opp += delta;
+
+          // did someone start a fade in the other direction? if so,
+          // cancel this fade
+          if (self.fadeMode && self.fadeMode != mode) {
+              if (callback)
+                  callback.call(self, []);
+
+              return;
+          }
+
+          if (opp <= 0.1) {
+              if (callback)
+                  callback.call(self, []);
+
+              self.fadeMode = null;
+
+              return;
+          } else if (opp >= 1.0) {
+              if (callback)
+                  callback.call(self, []);
+
+              self.fadeMode = null;
+
+              return;
+          } else {
+              changeOpac(self.ele, opp);
+              window.setTimeout(fade, fadeInterval);
+          }
+      };
+
+      fade();
+  },
+
+  hide : function () {
+    if (! this.visible()) return;
+
+    if (this.fadeOut && this.ele) {
+        this.fade("out", this._hide.bind(this));
+    } else {
+        this._hide();
+    }
+  },
+
+  _hide : function () {
+    if (this.hiddenCallback)
+      this.hiddenCallback();
+
+    this.shown = false;
+    this.removeClickHandlers();
+
+    if (this.ele)
+    document.body.removeChild(this.ele);
+
+    if (this.resizeHandlerFunc)
+      DOM.removeEventListener(window, "resize", this.resizeHandlerFunc);
+
+    this.showSelects();
+  },
+
+  // you probably want this for IE being dumb
+  // (IE thinks select elements are cool and puts them in front of every element on the page)
+  setAutoHideSelects : function (autohide) {
+    this.autoHideSelects = autohide;
+    this.updateContent();
+  },
+
+  hideSelects : function () {
+    if (!this.autoHideSelects || !IPPU.isIE()) return;
+    var sels = document.getElementsByTagName("select");
+    var ele;
+    for (var i = 0; i < sels.length; i++) {
+      ele = sels[i];
+      if (!ele) continue;
+
+      // if this element is inside the ippu, skip it
+      if (DOM.getAncestorsByClassName(ele, "ippu", true).length > 0) continue;
+
+      if (ele.style.visibility != 'hidden') {
+        ele.style.visibility = 'hidden';
+        this.hiddenSelects.push(ele);
+      }
+    }
+  },
+
+  showSelects : function () {
+    if (! this.autoHideSelects) return;
+    var ele;
+    while (ele = this.hiddenSelects.pop())
+      ele.style.visibility = '';
+  },
+
+  init: function (html) {
+    var ele = document.createElement("div");
+    this.ele = ele;
+    this.shown = false;
+    this.autoCenterX = false;
+    this.autoCenterY = false;
+    this.titlebar = null;
+    this.title = "";
+    this.showTitlebar = false;
+    this.clickToClose = false;
+    this.modal = false;
+    this.clickHandlerSetup = false;
+    this.docOverlay = false;
+    this.visibleOverlay = false;
+    this.clickHandlerFunc = false;
+    this.resizeHandlerFunc = false;
+    this.fixedPosition = false;
+    this.autoCenterCallback = null;
+    this.cancelledCallback = null;
+    this.autoHideSelects = false;
+    this.hiddenCallback = null;
+    this.fadeOut = false;
+    this.fadeIn = false;
+    this.hiddenSelects = [];
+    this.fadeMode = null;
+
+    ele.style.position = "absolute";
+    ele.style.zIndex   = "1000";
+
+    // plz don't remove thx
+    DOM.addClassName(ele, "ippu");
+
+    // create DIV to hold user's content
+    this.content = document.createElement("div");
+
+    this.content.innerHTML = html;
+
+    this.ele.appendChild(this.content);
+  }
+});
+
+// class methods
+IPPU.center = function (obj) {
+  obj.centerX();
+  obj.centerY();
+};
+
+IPPU.centerX = function (obj) {
+  obj.centerX();
+};
+
+IPPU.centerY = function (obj) {
+  obj.centerY();
+};
+
+IPPU.isIE = function () {
+    var UA = navigator.userAgent.toLowerCase();
+    if (UA.indexOf('msie') != -1) return true;
+    return false;
+};
+
+IPPU.isSafari = function () {
+    var UA = navigator.userAgent.toLowerCase();
+    if (UA.indexOf('safari') != -1) return true;
+    return false;
+};
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/json.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/json.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,132 @@
+/*
+JavaSript Object Notation (JSON)
+$Id$
+
+Copyright (c) 2005-2006, Six Apart, Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+    * Neither the name of "Six Apart" nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+
+JSON = {
+    parse: function( string ) {
+        string = string.replace( /=/g, "\\u3D" );
+        string = string.replace( /\(/g, "\\u28" );
+        try {
+            return eval( "(" + string + ")" );
+        } catch( e ) {}
+        return undefined;
+    }
+}
+
+
+Boolean.prototype.toJSON = function() {
+    return this.toString();
+}
+
+
+Number.prototype.toJSON = function() {
+    return isFinite( this ) ? this.toString() : "0";
+}
+
+
+Date.prototype.toJSON = function() {
+    return this.toUTCISOString
+        ? this.toUTCISOString().toJSON()
+        : this.toString().toJSON();
+}
+
+
+String.prototype.toJSON = function() {
+    return '"' + this.escapeJS() + '"';
+}
+
+
+RegExp.prototype.toJSON = function() {
+    return this.toString().toJSON();
+}
+
+
+Function.prototype.toJSON = function() {
+    return this.toString().toJSON();
+}
+
+
+Array.prototype.toJSON = function( root ) {
+    // crude recursion detection
+    if( !root )
+        root = this;
+    else if( root == this )
+        return "[]";
+    
+    var out = [ "[" ];
+    for( var i = 0; i < this.length; i++ ) {
+        if( out.length > 1 )
+            out.push( "," );
+        if( typeof this[ i ] == "undefined" || this[ i ] == null )
+            out.push( "null" );
+        else if( !this[ i ].toJSON )
+            out.push( "{}" );
+        else
+            out.push( this[ i ].toJSON( root ) );
+    }
+    out.push( "]" );
+    return out.join( "" );
+}
+
+
+OBJ.toJSON = function( obj_this, root ) {
+    // crude recursion detection
+    if( !root )
+        root = obj_this;
+    else if( root == obj_this )
+        return "{}";
+    
+    var out = [ "{" ];
+    for( var i in obj_this ) {
+        if( typeof obj_this[ i ] == "undefined" ||
+            (obj_this.hasOwnProperty && !obj_this.hasOwnProperty( i )) )
+            continue;
+        if( out.length > 1 )
+            out.push( "," );
+        out.push( OBJ.toJSON(i) );
+        if( this[ i ] == null )
+            out.push( ":null" );
+        else if( typeof obj_this[ i ] == "function" )
+            continue;
+        else if( !OBJ.toJSON(obj_this[ i ]) )
+            out.push( ":{}" );
+        else
+            out.push( ":", OBJ.toJSON(this[ i ], root) );
+    }
+    out.push( "}" );
+    return out.join( "" );
+}
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/perlbal-uploadtrack.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/perlbal-uploadtrack.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,88 @@
+////// public interface:
+
+function UploadTracker (formele, cb) {
+    this.form      = formele;
+    this.callback = cb;
+    this.session  = UploadTracker._generateSession();
+    this.stopped  = false;
+
+    var action = this.form.action;
+    if (action.match(/\bclient_up_sess=(\w+)/)) {
+        action = action.replace(/\bclient_up_sess=(\w+)/, "client_up_sess=" + this.session);
+    } else {
+        action += (action.match(/\?/) ? "&" : "?");
+        action += "client_up_sess=" + this.session;
+    }
+    this.form.action = action;
+
+    this._startCheckStatus();
+}
+
+
+// method to stop tracking a form's upload status
+UploadTracker.prototype.stopTracking = function () {
+    this.stopped = true;
+};
+
+
+// private implementation details:
+UploadTracker._XTR = function () {
+    var xtr;
+    var ex;
+
+    if (typeof(XMLHttpRequest) != "undefined") {
+        xtr = new XMLHttpRequest();
+    } else {
+        try {
+            xtr = new ActiveXObject("Msxml2.XMLHTTP.4.0");
+        } catch (ex) {
+            try {
+                xtr = new ActiveXObject("Msxml2.XMLHTTP");
+            } catch (ex) {
+            }
+        }
+    }
+
+    // let me explain this.  Opera 8 does XMLHttpRequest, but not setRequestHeader.
+    // no problem, we thought:  we'll test for setRequestHeader and if it's not present
+    // then fall back to the old behavior (treat it as not working).  BUT --- IE6 won't
+    // let you even test for setRequestHeader without throwing an exception (you need
+    // to call .open on the .xtr first or something)
+    try {
+        if (xtr && ! xtr.setRequestHeader)
+            xtr = null;
+    } catch (ex) { }
+
+    return xtr;
+};
+
+
+UploadTracker._generateSession = function () {
+    var str = Math.random() + "";
+    return curSession = str.replace(/[^\d]/, "");
+};
+
+
+UploadTracker.prototype._startCheckStatus = function () {
+    var uptrack = this;
+    if (uptrack.stopped) return true;
+
+    var xtr = UploadTracker._XTR();
+    if (!xtr) return;
+
+    var callback = function () {
+        if (xtr.readyState != 4) return;
+        if (uptrack.stopped)     return;
+
+        if (xtr.status == 200) {
+            var val;
+            eval("val = " + xtr.responseText + ";");
+            uptrack.callback(val);
+        }
+        setTimeout(function () { uptrack._startCheckStatus(); }, 1000);
+    };
+
+    xtr.onreadystatechange = callback;
+    xtr.open("GET", "/__upload_status?client_up_sess=" + uptrack.session + "&rand=" + Math.random());
+    xtr.send(null);
+}
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/progressbar.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/progressbar.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,140 @@
+/*
+  Javascript progress bar class
+
+  To use: create a div you wish to be the progress bar, and
+  instantiate a ProgressBar with the div element
+
+  To style it you need to define several classes, a container class which
+  defines the progress bar container, an indefinite class which has your
+  "barber shop" background tile, and an overlay class which defines the look
+  of the progress bar.
+
+  requires core.js, dom.js
+*/
+
+ProgressBar = new Class ( Object, {
+  init: function (pbar) {
+    this.max = 0;
+    this.value = 0;
+    this.pbar = pbar; // container
+    this.containerClassName = "";
+    this.indefiniteClassName = "";
+    this.overlayClassName = "";
+    this.indefinite = 1;
+    this.overlay = null;
+    this.update();
+  },
+
+  setContainerClassName: function (classname) {
+    if (!this.pbar)
+      return;
+
+    DOM.removeClassName(this.pbar, classname);
+    this.containerClassName = classname;
+    this.update();
+  },
+
+  setIndefiniteClassName: function (classname) {
+    this.indefiniteClassName = classname;
+    this.update();
+  },
+
+  setOverlayClassName: function (classname) {
+    this.overlayClassName = classname;
+    this.update();
+  },
+
+  setWidth: function (w) {
+    if (!this.pbar)
+      return;
+
+    if (w+0 == w)
+      DOM.setWidth(this.pbar, w);
+    else
+      this.pbar.style.width = w;
+  },
+
+  setMax: function (max) {
+    this.max = max;
+    this.update();
+  },
+
+  setValue: function (value) {
+    this.value = value;
+    this.indefinite = false;
+    this.update();
+  },
+
+  setIndefinite: function (indef) {
+    this.indefinite = indef;
+    this.update();
+  },
+
+  max: function () {
+    return this.max;
+  },
+
+  value: function () {
+    return this.value;
+  },
+
+  hide: function () {
+    if (!this.pbar)
+      return;
+
+    this.pbar.style.display = "none";
+  },
+
+  show: function () {
+    if (!this.pbar)
+      return;
+
+    this.pbar.style.display = "";
+  },
+
+  update: function () {
+    if (!this.pbar)
+      return;
+
+    DOM.addClassName(this.pbar, this.containerClassName);
+
+    // definite or indefinite bar?
+    if (this.indefinite || this.value < 0) {
+      // barber shop
+      // is there an overlay? if so, kill it
+      if (this.overlay) {
+        this.overlay.parentNode.removeChild(this.overlay);
+        this.overlay = null;
+      }
+
+      // set the indefinite class
+      DOM.addClassName(this.pbar, this.indefiniteClassName);
+      return;
+    }
+
+    DOM.removeClassName(this.pbar, this.indefiniteClassName);
+
+    var overlay = this.overlay;
+
+    // does the progress bar container have the overlay?
+    if (!this.overlay) {
+      overlay = document.createElement("div");
+
+      if (!overlay)
+        return;
+
+      this.pbar.appendChild(overlay);
+    }
+
+    DOM.addClassName(overlay, this.overlayClassName);
+
+    var dim = DOM.getAbsoluteDimensions(this.pbar);
+    var pct = this.value/this.max;
+    var oldWidth = dim.absoluteRight - dim.absoluteLeft;
+    var newWidth = oldWidth * pct;
+    DOM.setWidth(overlay, newWidth);
+    DOM.setHeight(overlay, dim.absoluteBottom - dim.absoluteTop);
+
+    this.overlay = overlay;
+  }
+});
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/selectable_table.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/selectable_table.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,179 @@
+/*
+  This is a datasource you can attach to a table. It will enable
+  the selection of rows or cells in the table.
+
+  The data in the datasource is elements that are selected.
+
+  $id:$
+*/
+
+SelectableTable = new Class(DataSource, {
+
+    // options:
+    //   table: what table element to attach to
+    //   selectableClass: if you only want elements with a certain class to be selectable,
+    //       specifiy this class with selectableClass
+    //   multiple: can more than one thing be selected at once? default is true
+    //   selectedClass: class to apply to selected elements
+    //   checkboxClass: since there are frequently checkboxes associated with selectable elements,
+    //       you can specify the class of your checkboxes to make them stay in sync
+    //   selectableItem: What type of elements can be selected. Values are "cell" or "row"
+    init: function (opts) {
+        if ( SelectableTable.superClass.init )
+            SelectableTable.superClass.init.apply(this, []);
+
+        var table = opts.table;
+        var selectableClass = opts.selectableClass;
+        var multiple = opts.multiple;
+        var selectedClass = opts.selectedClass
+        var checkboxClass = opts.checkboxClass
+        var selectableItem = opts.selectableItem;
+
+        selectableItem = selectableItem == "cell" ? "cell" : "row";
+
+        if (!defined(multiple)) multiple = true;
+
+        this.table = table;
+        this.selectableClass = selectableClass;
+        this.multiple = multiple;
+        this.selectedClass = opts.selectedClass;
+        this.checkboxClass = opts.checkboxClass;
+
+        this.selectedElements = [];
+
+        // if it's not a table, die
+        if (!table || !table.tagName || table.tagName.toLowerCase() != "table") return null;
+
+        // get selectable items
+        var tableElements = table.getElementsByTagName("*");
+
+        var selectableElements;
+
+        if (selectableItem == "cell") {
+            selectableElements = DOM.filterElementsByTagName(tableElements, "td");
+        } else {
+            selectableElements = DOM.filterElementsByTagName(tableElements, "tr");
+        }
+
+        var self = this;
+        selectableElements.forEach(function(ele) {
+            // if selectableClass is defined and this element doesn't have the class, skip it
+            if (selectableClass && !DOM.hasClassName(ele, selectableClass)) return;
+
+            // attach click handler to every element inside the element
+            var itemElements = ele.getElementsByTagName("*");
+            for (var i = 0; i < itemElements.length; i++) {
+                self.attachClickHandler(itemElements[i], ele);
+            }
+
+            // attach click handler to the element itself
+            self.attachClickHandler(ele, ele);
+        });
+    },
+
+    // stop our handling of this event
+    stopHandlingEvent: function (evt) {
+        if (!evt) return;
+
+        // w3c
+        if (evt.stopPropagation)
+        evt.stopPropagation();
+
+        // ie
+        try {
+            event.cancelBubble = true;
+        } catch(e) {}
+    },
+
+    // attach a click handler to this element
+    attachClickHandler: function (ele, parent) {
+        if (!ele) return;
+
+        var self = this;
+
+        var rowClicked = function (evt) {
+            // if it was a control-click or a command-click
+            // they're probably trying to open a new tab or something.
+            // let's not handle it
+            if (evt && (evt.ctrlKey || evt.metaKey)) return false;
+
+            var tagName = ele.tagName.toLowerCase();
+
+            // if this is a link or has an onclick handler,
+            // return true and tell other events to return true
+            if ((ele.href && tagName != "img") || ele.onclick) {
+                self.stopHandlingEvent(evt);
+                return true;
+            }
+
+            // if this is the child of a link, propagate the event up
+            var ancestors = DOM.getAncestors(ele, true);
+            for (var i = 0; i < ancestors.length; i++) {
+                var ancestor = ancestors[i];
+                if (ancestor.href && ancestor.tagName.toLowerCase() != "img") {
+                    return true;
+                }
+            }
+
+            // if this is an input or select element, skip it
+            if ((tagName == "select" || tagName == "input") && parent.checkbox != ele) {
+                self.stopHandlingEvent(evt);
+                return true;
+            }
+
+            // toggle selection of this parent element
+            if (self.selectedElements.indexOf(parent) != -1) {
+                if (self.selectedClass) DOM.removeClassName(parent, self.selectedClass);
+
+                self.selectedElements.remove(parent);
+            } else {
+                if (self.selectedClass) DOM.addClassName(parent, self.selectedClass);
+
+                if (self.multiple) {
+                    self.selectedElements.push(parent);
+                } else {
+                    if (self.selectedClass && self.selectedElements.length > 0) {
+                        var oldParent = self.selectedElements[0];
+                        if (oldParent) {
+                            DOM.removeClassName(oldParent, self.selectedClass);
+                            if (oldParent.checkbox) oldParent.checkbox.checked = "";
+                        }
+                    }
+
+                    self.selectedElements = [parent];
+                }
+            }
+
+            // update our data
+            self.setData(self.selectedElements);
+
+            // if there's a checkbox associated with this parent, set it's value
+            // to the parent selected value
+            if (parent.checkbox) parent.checkbox.checked = (self.selectedElements.indexOf(parent) != -1) ? "on" : '';
+            if (parent.checkbox == ele) { self.stopHandlingEvent(evt); return true; }
+
+            // always? not sure
+            if (evt)
+                Event.stop(evt);
+        }
+
+        // if this is a checkbox we need to keep in sync, set up its event handler
+        if (this.checkboxClass && ele.tagName.toLowerCase() == "input"
+            && ele.type == "checkbox" && DOM.hasClassName(ele, this.checkboxClass)) {
+
+            parent.checkbox = ele;
+
+            // override default event handler for the checkbox
+            DOM.addEventListener(ele, "click", function (evt) {
+                return true;
+            });
+        }
+
+        // attach a method to the row so other people can programatically
+        // select it.
+        ele.rowClicked = rowClicked;
+
+        DOM.addEventListener(ele, "click", rowClicked);
+    }
+
+});
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/template.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/template.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,225 @@
+/*
+Template - Copyright 2005 Six Apart
+$Id: template.js 35 2006-02-18 00:46:55Z mischa $
+
+Copyright (c) 2005, Six Apart, Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+    * Neither the name of "Six Apart" nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+
+/* core template object */
+
+Template = new Class( Object, {
+    beginToken: "[#",
+    endToken: "#]",
+    
+    
+    init: function( source ) {
+        if( source )
+            this.compile( source );
+    },
+    
+    
+    compile: function( source ) {
+        var statements = [
+            "context.open();",
+            "with( context.vars ) {"
+        ];
+        
+        var start = 0, end = -this.endToken.length;
+        while( start < source.length ) {
+            end += this.endToken.length;
+            
+            // plaintext
+            start = source.indexOf( this.beginToken, end );
+            if( start < 0 )
+                start = source.length;
+            if( start > end )
+                statements.push( "context.write( ", source.substring( end, start ).toJSON(), " );" );
+            start += this.beginToken.length;
+            
+            // code
+            if( start >= source.length )
+                break;
+            end = source.indexOf( this.endToken, start );
+            if( end < 0 )
+                throw "Template parsing error: Unable to find matching end token (" + this.endToken + ").";
+            var length = (end - start);
+            
+            // empty tag
+            if( length <= 0 )
+                continue;
+            
+            // comment
+            else if( length >= 4 &&
+                source.charAt( start ) == "-" && source.charAt( start + 1 ) == "-" &&
+                source.charAt( end - 1 ) == "-" && source.charAt( end - 2 ) == "-" )
+                continue;
+            
+            // write
+            else if( source.charAt( start ) == "=" )
+                statements.push( "context.write( ", source.substring( start + 1, end ), " );" );
+            
+            // filters
+            else if( source.charAt( start ) == "|" ) {
+                start += 1;
+
+                // find the first whitespace
+                var afterfilters = source.substring(start,end).search(/\s/);
+                
+                var filters;
+                if (afterfilters > 0) {
+                    // allow pipes or commas to seperate filters
+                    // split the string, reverse and rejoin to reverse it
+                    filters = source.substring(start,start + afterfilters).replace(/,|\|/g,"").split('');
+                    afterfilters += 1; // data starts after whitespace and filter list
+                } else {
+                    // default to escapeHTML
+                    filters = ["h"];
+                }
+                // we have to do them in reverse order
+                filters = filters.reverse();
+               
+                // start with our original filter number
+                var numfilters = filters.length;
+                
+                // add the text between [#|  #]
+                filters.push(source.substring( start + afterfilters, end ));
+                
+                // adjust each filter into a function call
+                // eg. u ( h ( H ( blah ) ) )
+                for (i=0; i<numfilters; i++) {
+                    filters[i] = "context.f."+filters[i]+"( ";
+                    filters.push(" )");
+                }
+                
+                statements.push( "context.write( " + filters.join('') + " );");
+            }
+            
+            // evaluate
+            else
+                statements.push( source.substring( start, end ) );
+        }
+        
+        statements.push( "} return context.close();" );
+
+        this.exec = new Function( "context", statements.join( "\n" ) );
+    },
+    
+    
+    exec: function( context ) {
+        return "";
+    }
+} );
+
+
+/* static members */
+
+Template.templates = {};
+
+
+/* context object */
+
+Template.Context = new Class( Object, {
+    init: function( vars, templates ) {
+        this.vars = vars || {};
+        this.templates = templates || Template.templates;
+        this.stack = [];
+        this.out = [];
+        this.f = Template.Filter;
+    },
+    
+    
+    include: function( name ) {
+        return this.templates[ name ].exec( this );
+    },
+
+
+    write: function() {
+        this.out.push.apply( this.out, arguments );
+    },
+
+
+    writeln: function() {
+        this.write.apply( this, arguments );
+        this.write( "\n" );
+    },
+
+    
+    clear: function() {
+        this.out.length = 0;
+    },
+
+
+    getOutput: function() {
+        return this.out.join( "" );
+    },
+    
+    
+    open: function() {
+        this.stack.push( this.out );
+        this.out = [];
+    },
+    
+    
+    close: function() {
+        var result = this.getOutput();
+        this.out = this.stack.pop() || [];
+        return result;
+    }
+   
+} );
+
+/* filters */
+
+Template.Filter = {
+
+    // escapeHTML
+    h: function(obj) {
+        var div = document.createElement('div');
+        var textNode = document.createTextNode(obj);
+        div.appendChild(textNode);
+        return (div.innerHTML);
+    },
+
+    // unescapeHTML
+    H: function(obj) {
+        return (unescape(obj));
+    },
+
+    // encodeURL
+    u: function(obj) {
+        return (escape(obj).replace(/\//g,"%2F"));
+    }
+
+};
+
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/timer.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/timer.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,121 @@
+/*
+Timer Library
+$Id$
+
+Copyright (c) 2005, Six Apart, Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+    * Neither the name of "Six Apart" nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+
+Timer = new Class( Object, {
+    init: function( callback, delay, count ) {
+        this.callback = callback;
+        this.delay = max( delay, 1 );
+        this.nextDelay = this.delay;
+        this.startTime = 0;
+        this.count = count;
+        this.execCount = 0;
+        this.state = "new";
+        this.timeout = null;
+        this.id = Unique.id();
+        this.start();
+    },
+    
+    
+    destroy: function() {
+        this.stop();
+        this.callback = null;
+    },
+    
+    
+    exec: function() {
+        this.execCount++;
+        this.callback( this );
+        if ( this.count && this.execCount >= this.count ) {
+            return this.destroy();
+        }
+        if( this.state == "started" )
+            this.run();
+    },
+    
+    
+    run: function() {
+        this.state = "started";
+        this.timeout = window.setTimeout( this.exec.bind( this ), this.nextDelay );
+        this.nextDelay = this.delay;
+        var date = new Date();
+        this.startTime = date.UTC;
+    },
+    
+    
+    start: function() {
+        switch( this.state ) {
+            case "new":
+            case "paused":
+            case "stopped":
+                this.run();
+                break;
+        }
+    },
+    
+    
+    stop: function() {
+        this.state = "stopped";
+        try {
+            window.clearTimeout( this.timeout );
+        } catch( e ) {}
+        this.timeout = null;
+    },
+    
+    
+    pause: function() {
+        if( this.state != "started" )
+            return;
+        this.stop();
+        this.state = "paused";
+        var date = new Date();
+        this.nextDelay = max( this.delay - (date.UTC - this.startTime), 1 );
+    },
+
+
+    reset: function( delay ) {
+        if( this.state != "started" )
+            return;
+        if( defined( delay ) )
+            this.delay = this.nextDelay = max( delay, 1 );
+        try {
+            window.clearTimeout( this.timeout );
+        } catch( e ) {}
+        this.timeout = window.setTimeout( this.exec.bind( this ), this.nextDelay );
+    }
+    
+} );
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/js/6alib/view.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/6alib/view.js	Tue May 01 17:55:19 2012 +0800
@@ -0,0 +1,99 @@
+// the V in MVC
+// renders content into "view" element, calls data() on "datasource"
+
+View = new Class(Object, {
+
+  init: function (opts) {
+    if ( View.superClass.init )
+        View.superClass.init.apply(this, arguments);
+    this.watchers = [];
+    this.datasource = opts.datasource;
+    this.view = opts.view;
+    this.controller = opts.controller;
+
+    this.rendered = false;
+
+    if (opts.showHourglass) {
+      this.hourglass = new Hourglass();
+      this.hourglass.init(view);
+    }
+
+    if (opts.datasource)
+      opts.datasource.addWatcher(this.dataUpdated.bind(this));
+  },
+
+  addWatcher: function (callback) {
+    if (!this.watchers)
+      return;
+
+    this.watchers.add(callback);
+  },
+
+  removeWatcher: function (callback) {
+    if (!this.watchers)
+      return;
+
+    this.watchers.remove(callback);
+  },
+
+  callWatchers: function () {
+    if (!this.watchers)
+      return;
+
+    for (var i = 0; i < this.watchers.length; i++)
+      this.watchers[i].apply(this);
+  },
+
+  getView: function () {
+    return this.view;
+  },
+
+  dataUpdated: function () {
+    this._render();
+
+    if (this.hourglass) {
+      this.hourglass.hide();
+      this.hourglass = null;
+    }
+  },
+
+  setContent: function (content) {
+      if (this.view) {
+          var children = this.view.childNodes;
+          for (var i = 0; i < children.length; i++) {
+              this.view.removeChild(children[i]);
+          }
+      }
+
+      this.view.appendChild(content);
+  },
+
+  _render: function () {
+    var ds = this.datasource;
+    var view = this.view;
+
+    if (!view)
+      return;
+
+    var data;
+
+    if (ds) {
+      data = ds.data();
+      if (!this.rendered && this.unRender)
+        this.unRender(data, ds);
+    }
+
+    if (this.render) {
+      this.render(data, ds);
+      this.rendered = true;
+    }
+
+    this.callWatchers();
+  },
+
+  destroy: function () {
+    if (this.rendered)
+      this.unRender();
+  }
+
+});
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/tools/userpicfactory.bml
--- a/htdocs/tools/userpicfactory.bml	Tue May 01 16:18:41 2012 +0800
+++ b/htdocs/tools/userpicfactory.bml	Tue May 01 17:55:19 2012 +0800
@@ -26,9 +26,9 @@
     }
 
     LJ::need_res(qw(
-        js/core.js
-        js/dom.js
-        js/image-region-select.js
+        js/6alib/core.js
+        js/6alib/dom.js
+        js/6alib/image-region-select.js
     ));
 
     # optional suffix, for beta testing.  can be removed soon after 2006-03-29
diff -r cb15f9c272f5 -r 4b328d722eab htdocs/update.bml
--- a/htdocs/update.bml	Tue May 01 16:18:41 2012 +0800
+++ b/htdocs/update.bml	Tue May 01 17:55:19 2012 +0800
@@ -82,7 +82,7 @@
     LJ::Hooks::run_hooks("transform_update_$POST{transform}", \%GET, \%POST) if $POST{transform};
 
     LJ::need_res( { priority => $LJ::OLD_RES_PRIORITY }, 'stc/entry.css' );
-    LJ::need_res('stc/display_none.css', 'stc/lj_base.css', 'js/inputcomplete.js');
+    LJ::need_res('stc/display_none.css', 'stc/lj_base.css', 'js/6alib/inputcomplete.js');
 
     ## figure out times
     my $now = DateTime->now;
@@ -525,9 +525,9 @@
     my $ret = $_[1]->{'head'};
 
     LJ::need_res(qw(
-                    js/core.js
-                    js/dom.js
-                    js/httpreq.js
+                    js/6alib/core.js
+                    js/6alib/dom.js
+                    js/6alib/httpreq.js
                     js/livejournal.js
                     js/entry.js
                     js/poll.js
diff -r cb15f9c272f5 -r 4b328d722eab views/dev/tests/commentmanage.js
--- a/views/dev/tests/commentmanage.js	Tue May 01 16:18:41 2012 +0800
+++ b/views/dev/tests/commentmanage.js	Tue May 01 17:55:19 2012 +0800
@@ -1,6 +1,6 @@
 /* INCLUDE:
 
-old: js/commentmanage.js
+old: js/6alib/commentmanage.js
 jquery: js/jquery/jquery.ui.widget.js
 jquery: js/jquery.ajaxtip.js
 jquery: js/jquery.commentmanage.js
diff -r cb15f9c272f5 -r 4b328d722eab views/dev/tests/libfunctions.js
--- a/views/dev/tests/libfunctions.js	Tue May 01 16:18:41 2012 +0800
+++ b/views/dev/tests/libfunctions.js	Tue May 01 17:55:19 2012 +0800
@@ -1,33 +1,33 @@
 /* INCLUDE:
-old: js/core.js
-old: js/dom.js
-old: js/json.js
-old: js/httpreq.js
-old: js/hourglass.js
-old: js/inputcomplete.js
-old: js/datasource.js
-old: js/selectable_table.js
+old: js/6alib/core.js
+old: js/6alib/dom.js
+old: js/6alib/json.js
+old: js/6alib/httpreq.js
+old: js/6alib/hourglass.js
+old: js/6alib/inputcomplete.js
+old: js/6alib/datasource.js
+old: js/6alib/selectable_table.js
 
-old: js/checkallbutton.js
+old: js/6alib/checkallbutton.js
 
-old: js/progressbar.js
-old: js/ljprogressbar.js
+old: js/6alib/progressbar.js
+old: js/6alib/ljprogressbar.js
 
-old: js/ippu.js
-old: js/lj_ippu.js
-old: js/template.js
-old: js/userpicselect.js
+old: js/6alib/ippu.js
+old: js/6alib/lj_ippu.js
+old: js/6alib/template.js
+old: js/6alib/userpicselect.js
 
-old: js/view.js
-old: js/directorysearch.js
-old: js/directorysearchconstraints.js
-old: js/directorysearchresults.js
+old: js/6alib/view.js
+old: js/6alib/directorysearch.js
+old: js/6alib/directorysearchconstraints.js
+old: js/6alib/directorysearchresults.js
 
-old: js/esnmanager.js
+old: js/6alib/esnmanager.js
 
-old: js/ljwidget.js
-old: js/ljwidget_ippu.js
-old: js/widget_ippu/settingprod.js
+old: js/6alib/ljwidget.js
+old: js/6alib/ljwidget_ippu.js
+old: js/6alib/widget_ippu/settingprod.js
 */
 
 module( "old" );
diff -r cb15f9c272f5 -r 4b328d722eab views/dev/tests/login.js
--- a/views/dev/tests/login.js	Tue May 01 16:18:41 2012 +0800
+++ b/views/dev/tests/login.js	Tue May 01 17:55:19 2012 +0800
@@ -1,6 +1,6 @@
 /* INCLUDE:
 js/md5.js
-old: js/login.js
+old: js/6alib/login.js
 jquery: js/login-jquery.js
 */
 
diff -r cb15f9c272f5 -r 4b328d722eab views/dev/tests/quickreply.js
--- a/views/dev/tests/quickreply.js	Tue May 01 16:18:41 2012 +0800
+++ b/views/dev/tests/quickreply.js	Tue May 01 17:55:19 2012 +0800
@@ -1,6 +1,6 @@
 /* INCLUDE:
-old: js/x_core.js
-old: js/quickreply.js
+old: js/6alib/x_core.js
+old: js/6alib/quickreply.js
 jquery: js/jquery/jquery.ui.widget.js
 jquery: js/jquery.quickreply.js
 */
--------------------------------------------------------------------------------