* Checked in a copy of the "script.aculo.us" library by Thomas Fuchs.
authorArt Cancro <ajc@citadel.org>
Fri, 9 Sep 2005 04:43:31 +0000 (04:43 +0000)
committerArt Cancro <ajc@citadel.org>
Fri, 9 Sep 2005 04:43:31 +0000 (04:43 +0000)
  NOTE: I had to search-and-replace "Effect" to "ScriptaculousEffect" in
  all of their files, to avoid a conflict with the name "Effect" in Rico.
* Implemented recipient autocompletion when composing mail, using the
  script.aculo.us drop-down box and an ajax fetch.  Cool!!

webcit/po/Makefile.in [deleted file]
webcit/static/controls.js [new file with mode: 0644]
webcit/static/dragdrop.js [new file with mode: 0644]
webcit/static/effects.js [new file with mode: 0644]
webcit/static/head.html
webcit/static/prototype.js
webcit/static/rico.js
webcit/static/scriptaculous.js [new file with mode: 0644]
webcit/static/unittest.js [new file with mode: 0644]
webcit/static/util.js [new file with mode: 0644]
webcit/static/wclib.js

diff --git a/webcit/po/Makefile.in b/webcit/po/Makefile.in
deleted file mode 100644 (file)
index 84f09b2..0000000
+++ /dev/null
@@ -1,366 +0,0 @@
-# Makefile for PO directory in any package using GNU gettext.
-# Copyright (C) 1995-1997, 2000-2004 by Ulrich Drepper <drepper@gnu.ai.mit.edu>
-#
-# This file can be copied and used freely without restrictions.  It can
-# be used in projects which are not available under the GNU General Public
-# License but which still want to provide support for the GNU gettext
-# functionality.
-# Please note that the actual code of GNU gettext is covered by the GNU
-# General Public License and is *not* in the public domain.
-#
-# Origin: gettext-0.14
-
-PACKAGE = webcit
-VERSION = 6.21
-
-SHELL = /bin/sh
-
-
-srcdir = .
-top_srcdir = ..
-
-
-prefix = /appl/citadel
-exec_prefix = ${prefix}
-datadir = ${prefix}/share
-localedir = $(datadir)/locale
-gettextsrcdir = $(datadir)/gettext/po
-
-INSTALL = /usr/bin/install -c
-INSTALL_DATA = ${INSTALL} -m 644
-MKINSTALLDIRS = $(top_builddir)/./mkinstalldirs
-mkinstalldirs = $(SHELL) $(MKINSTALLDIRS)
-
-GMSGFMT = /usr/bin/msgfmt
-MSGFMT = /usr/bin/msgfmt
-XGETTEXT = /usr/bin/xgettext
-MSGMERGE = msgmerge
-MSGMERGE_UPDATE = /usr/bin/msgmerge --update
-MSGINIT = msginit
-MSGCONV = msgconv
-MSGFILTER = msgfilter
-
-POFILES = @POFILES@
-GMOFILES = @GMOFILES@
-UPDATEPOFILES = @UPDATEPOFILES@
-DUMMYPOFILES = @DUMMYPOFILES@
-DISTFILES.common = Makefile.in.in remove-potcdate.sin \
-$(DISTFILES.common.extra1) $(DISTFILES.common.extra2) $(DISTFILES.common.extra3)
-DISTFILES = $(DISTFILES.common) Makevars POTFILES.in $(DOMAIN).pot stamp-po \
-$(POFILES) $(GMOFILES) \
-$(DISTFILES.extra1) $(DISTFILES.extra2) $(DISTFILES.extra3)
-
-POTFILES = \
-
-CATALOGS = @CATALOGS@
-
-# Makevars gets inserted here. (Don't remove this line!)
-
-.SUFFIXES:
-.SUFFIXES: .po .gmo .mo .sed .sin .nop .po-create .po-update
-
-.po.mo:
-       @echo "$(MSGFMT) -c -o $@ $<"; \
-       $(MSGFMT) -c -o t-$@ $< && mv t-$@ $@
-
-.po.gmo:
-       @lang=`echo $* | sed -e 's,.*/,,'`; \
-       test "$(srcdir)" = . && cdcmd="" || cdcmd="cd $(srcdir) && "; \
-       echo "$${cdcmd}rm -f $${lang}.gmo && $(GMSGFMT) -c --statistics -o $${lang}.gmo $${lang}.po"; \
-       cd $(srcdir) && rm -f $${lang}.gmo && $(GMSGFMT) -c --statistics -o t-$${lang}.gmo $${lang}.po && mv t-$${lang}.gmo $${lang}.gmo
-
-.sin.sed:
-       sed -e '/^#/d' $< > t-$@
-       mv t-$@ $@
-
-
-all: all-yes
-
-all-yes: stamp-po
-all-no:
-
-# stamp-po is a timestamp denoting the last time at which the CATALOGS have
-# been loosely updated. Its purpose is that when a developer or translator
-# checks out the package via CVS, and the $(DOMAIN).pot file is not in CVS,
-# "make" will update the $(DOMAIN).pot and the $(CATALOGS), but subsequent
-# invocations of "make" will do nothing. This timestamp would not be necessary
-# if updating the $(CATALOGS) would always touch them; however, the rule for
-# $(POFILES) has been designed to not touch files that don't need to be
-# changed.
-stamp-po: $(srcdir)/$(DOMAIN).pot
-       test -z "$(GMOFILES)" || $(MAKE) $(GMOFILES)
-       @echo "touch stamp-po"
-       @echo timestamp > stamp-poT
-       @mv stamp-poT stamp-po
-
-# Note: Target 'all' must not depend on target '$(DOMAIN).pot-update',
-# otherwise packages like GCC can not be built if only parts of the source
-# have been downloaded.
-
-# This target rebuilds $(DOMAIN).pot; it is an expensive operation.
-# Note that $(DOMAIN).pot is not touched if it doesn't need to be changed.
-$(DOMAIN).pot-update: $(POTFILES) $(srcdir)/POTFILES.in remove-potcdate.sed
-       $(XGETTEXT) --default-domain=$(DOMAIN) --directory=$(top_srcdir) \
-         --add-comments=TRANSLATORS: $(XGETTEXT_OPTIONS) \
-         --files-from=$(srcdir)/POTFILES.in \
-         --copyright-holder='$(COPYRIGHT_HOLDER)' \
-         --msgid-bugs-address='$(MSGID_BUGS_ADDRESS)'
-       test ! -f $(DOMAIN).po || { \
-         if test -f $(srcdir)/$(DOMAIN).pot; then \
-           sed -f remove-potcdate.sed < $(srcdir)/$(DOMAIN).pot > $(DOMAIN).1po && \
-           sed -f remove-potcdate.sed < $(DOMAIN).po > $(DOMAIN).2po && \
-           if cmp $(DOMAIN).1po $(DOMAIN).2po >/dev/null 2>&1; then \
-             rm -f $(DOMAIN).1po $(DOMAIN).2po $(DOMAIN).po; \
-           else \
-             rm -f $(DOMAIN).1po $(DOMAIN).2po $(srcdir)/$(DOMAIN).pot && \
-             mv $(DOMAIN).po $(srcdir)/$(DOMAIN).pot; \
-           fi; \
-         else \
-           mv $(DOMAIN).po $(srcdir)/$(DOMAIN).pot; \
-         fi; \
-       }
-
-# This rule has no dependencies: we don't need to update $(DOMAIN).pot at
-# every "make" invocation, only create it when it is missing.
-# Only "make $(DOMAIN).pot-update" or "make dist" will force an update.
-$(srcdir)/$(DOMAIN).pot:
-       $(MAKE) $(DOMAIN).pot-update
-
-# This target rebuilds a PO file if $(DOMAIN).pot has changed.
-# Note that a PO file is not touched if it doesn't need to be changed.
-$(POFILES): $(srcdir)/$(DOMAIN).pot
-       @lang=`echo $@ | sed -e 's,.*/,,' -e 's/\.po$$//'`; \
-       if test -f "$(srcdir)/$${lang}.po"; then \
-         test "$(srcdir)" = . && cdcmd="" || cdcmd="cd $(srcdir) && "; \
-         echo "$${cdcmd}$(MSGMERGE_UPDATE) $${lang}.po $(DOMAIN).pot"; \
-         cd $(srcdir) && $(MSGMERGE_UPDATE) $${lang}.po $(DOMAIN).pot; \
-       else \
-         $(MAKE) $${lang}.po-create; \
-       fi
-
-
-install: install-exec install-data
-install-exec:
-install-data: install-data-yes
-       if test "$(PACKAGE)" = "gettext-tools"; then \
-         $(mkinstalldirs) $(DESTDIR)$(gettextsrcdir); \
-         for file in $(DISTFILES.common) Makevars.template; do \
-           $(INSTALL_DATA) $(srcdir)/$$file \
-                           $(DESTDIR)$(gettextsrcdir)/$$file; \
-         done; \
-         for file in Makevars; do \
-           rm -f $(DESTDIR)$(gettextsrcdir)/$$file; \
-         done; \
-       else \
-         : ; \
-       fi
-install-data-no: all
-install-data-yes: all
-       $(mkinstalldirs) $(DESTDIR)$(datadir)
-       @catalogs='$(CATALOGS)'; \
-       for cat in $$catalogs; do \
-         cat=`basename $$cat`; \
-         lang=`echo $$cat | sed -e 's/\.gmo$$//'`; \
-         dir=$(localedir)/$$lang/LC_MESSAGES; \
-         $(mkinstalldirs) $(DESTDIR)$$dir; \
-         if test -r $$cat; then realcat=$$cat; else realcat=$(srcdir)/$$cat; fi; \
-         $(INSTALL_DATA) $$realcat $(DESTDIR)$$dir/$(DOMAIN).mo; \
-         echo "installing $$realcat as $(DESTDIR)$$dir/$(DOMAIN).mo"; \
-         for lc in '' $(EXTRA_LOCALE_CATEGORIES); do \
-           if test -n "$$lc"; then \
-             if (cd $(DESTDIR)$(localedir)/$$lang && LC_ALL=C ls -l -d $$lc 2>/dev/null) | grep ' -> ' >/dev/null; then \
-               link=`cd $(DESTDIR)$(localedir)/$$lang && LC_ALL=C ls -l -d $$lc | sed -e 's/^.* -> //'`; \
-               mv $(DESTDIR)$(localedir)/$$lang/$$lc $(DESTDIR)$(localedir)/$$lang/$$lc.old; \
-               mkdir $(DESTDIR)$(localedir)/$$lang/$$lc; \
-               (cd $(DESTDIR)$(localedir)/$$lang/$$lc.old && \
-                for file in *; do \
-                  if test -f $$file; then \
-                    ln -s ../$$link/$$file $(DESTDIR)$(localedir)/$$lang/$$lc/$$file; \
-                  fi; \
-                done); \
-               rm -f $(DESTDIR)$(localedir)/$$lang/$$lc.old; \
-             else \
-               if test -d $(DESTDIR)$(localedir)/$$lang/$$lc; then \
-                 :; \
-               else \
-                 rm -f $(DESTDIR)$(localedir)/$$lang/$$lc; \
-                 mkdir $(DESTDIR)$(localedir)/$$lang/$$lc; \
-               fi; \
-             fi; \
-             rm -f $(DESTDIR)$(localedir)/$$lang/$$lc/$(DOMAIN).mo; \
-             ln -s ../LC_MESSAGES/$(DOMAIN).mo $(DESTDIR)$(localedir)/$$lang/$$lc/$(DOMAIN).mo 2>/dev/null || \
-             ln $(DESTDIR)$(localedir)/$$lang/LC_MESSAGES/$(DOMAIN).mo $(DESTDIR)$(localedir)/$$lang/$$lc/$(DOMAIN).mo 2>/dev/null || \
-             cp -p $(DESTDIR)$(localedir)/$$lang/LC_MESSAGES/$(DOMAIN).mo $(DESTDIR)$(localedir)/$$lang/$$lc/$(DOMAIN).mo; \
-             echo "installing $$realcat link as $(DESTDIR)$(localedir)/$$lang/$$lc/$(DOMAIN).mo"; \
-           fi; \
-         done; \
-       done
-
-install-strip: install
-
-installdirs: installdirs-exec installdirs-data
-installdirs-exec:
-installdirs-data: installdirs-data-yes
-       if test "$(PACKAGE)" = "gettext-tools"; then \
-         $(mkinstalldirs) $(DESTDIR)$(gettextsrcdir); \
-       else \
-         : ; \
-       fi
-installdirs-data-no:
-installdirs-data-yes:
-       $(mkinstalldirs) $(DESTDIR)$(datadir)
-       @catalogs='$(CATALOGS)'; \
-       for cat in $$catalogs; do \
-         cat=`basename $$cat`; \
-         lang=`echo $$cat | sed -e 's/\.gmo$$//'`; \
-         dir=$(localedir)/$$lang/LC_MESSAGES; \
-         $(mkinstalldirs) $(DESTDIR)$$dir; \
-         for lc in '' $(EXTRA_LOCALE_CATEGORIES); do \
-           if test -n "$$lc"; then \
-             if (cd $(DESTDIR)$(localedir)/$$lang && LC_ALL=C ls -l -d $$lc 2>/dev/null) | grep ' -> ' >/dev/null; then \
-               link=`cd $(DESTDIR)$(localedir)/$$lang && LC_ALL=C ls -l -d $$lc | sed -e 's/^.* -> //'`; \
-               mv $(DESTDIR)$(localedir)/$$lang/$$lc $(DESTDIR)$(localedir)/$$lang/$$lc.old; \
-               mkdir $(DESTDIR)$(localedir)/$$lang/$$lc; \
-               (cd $(DESTDIR)$(localedir)/$$lang/$$lc.old && \
-                for file in *; do \
-                  if test -f $$file; then \
-                    ln -s ../$$link/$$file $(DESTDIR)$(localedir)/$$lang/$$lc/$$file; \
-                  fi; \
-                done); \
-               rm -f $(DESTDIR)$(localedir)/$$lang/$$lc.old; \
-             else \
-               if test -d $(DESTDIR)$(localedir)/$$lang/$$lc; then \
-                 :; \
-               else \
-                 rm -f $(DESTDIR)$(localedir)/$$lang/$$lc; \
-                 mkdir $(DESTDIR)$(localedir)/$$lang/$$lc; \
-               fi; \
-             fi; \
-           fi; \
-         done; \
-       done
-
-# Define this as empty until I found a useful application.
-installcheck:
-
-uninstall: uninstall-exec uninstall-data
-uninstall-exec:
-uninstall-data: uninstall-data-yes
-       if test "$(PACKAGE)" = "gettext-tools"; then \
-         for file in $(DISTFILES.common) Makevars.template; do \
-           rm -f $(DESTDIR)$(gettextsrcdir)/$$file; \
-         done; \
-       else \
-         : ; \
-       fi
-uninstall-data-no:
-uninstall-data-yes:
-       catalogs='$(CATALOGS)'; \
-       for cat in $$catalogs; do \
-         cat=`basename $$cat`; \
-         lang=`echo $$cat | sed -e 's/\.gmo$$//'`; \
-         for lc in LC_MESSAGES $(EXTRA_LOCALE_CATEGORIES); do \
-           rm -f $(DESTDIR)$(localedir)/$$lang/$$lc/$(DOMAIN).mo; \
-         done; \
-       done
-
-check: all
-
-info dvi ps pdf html tags TAGS ctags CTAGS ID:
-
-mostlyclean:
-       rm -f remove-potcdate.sed
-       rm -f stamp-poT
-       rm -f core core.* $(DOMAIN).po $(DOMAIN).1po $(DOMAIN).2po *.new.po
-       rm -fr *.o
-
-clean: mostlyclean
-
-distclean: clean
-       rm -f Makefile Makefile.in POTFILES *.mo
-
-maintainer-clean: distclean
-       @echo "This command is intended for maintainers to use;"
-       @echo "it deletes files that may require special tools to rebuild."
-       rm -f stamp-po $(GMOFILES)
-
-distdir = $(top_builddir)/$(PACKAGE)-$(VERSION)/$(subdir)
-dist distdir:
-       $(MAKE) update-po
-       @$(MAKE) dist2
-# This is a separate target because 'update-po' must be executed before.
-dist2: $(DISTFILES)
-       dists="$(DISTFILES)"; \
-       if test "$(PACKAGE)" = "gettext-tools"; then \
-         dists="$$dists Makevars.template"; \
-       fi; \
-       if test -f $(srcdir)/ChangeLog; then \
-         dists="$$dists ChangeLog"; \
-       fi; \
-       for i in 0 1 2 3 4 5 6 7 8 9; do \
-         if test -f $(srcdir)/ChangeLog.$$i; then \
-           dists="$$dists ChangeLog.$$i"; \
-         fi; \
-       done; \
-       if test -f $(srcdir)/LINGUAS; then dists="$$dists LINGUAS"; fi; \
-       for file in $$dists; do \
-         if test -f $$file; then \
-           cp -p $$file $(distdir); \
-         else \
-           cp -p $(srcdir)/$$file $(distdir); \
-         fi; \
-       done
-
-update-po: Makefile
-       $(MAKE) $(DOMAIN).pot-update
-       test -z "$(UPDATEPOFILES)" || $(MAKE) $(UPDATEPOFILES)
-       $(MAKE) update-gmo
-
-# General rule for creating PO files.
-
-.nop.po-create:
-       @lang=`echo $@ | sed -e 's/\.po-create$$//'`; \
-       echo "File $$lang.po does not exist. If you are a translator, you can create it through 'msginit'." 1>&2; \
-       exit 1
-
-# General rule for updating PO files.
-
-.nop.po-update:
-       @lang=`echo $@ | sed -e 's/\.po-update$$//'`; \
-       if test "$(PACKAGE)" = "gettext-tools"; then PATH=`pwd`/../src:$$PATH; fi; \
-       tmpdir=`pwd`; \
-       echo "$$lang:"; \
-       test "$(srcdir)" = . && cdcmd="" || cdcmd="cd $(srcdir) && "; \
-       echo "$${cdcmd}$(MSGMERGE) $$lang.po $(DOMAIN).pot -o $$lang.new.po"; \
-       cd $(srcdir); \
-       if $(MSGMERGE) $$lang.po $(DOMAIN).pot -o $$tmpdir/$$lang.new.po; then \
-         if cmp $$lang.po $$tmpdir/$$lang.new.po >/dev/null 2>&1; then \
-           rm -f $$tmpdir/$$lang.new.po; \
-         else \
-           if mv -f $$tmpdir/$$lang.new.po $$lang.po; then \
-             :; \
-           else \
-             echo "msgmerge for $$lang.po failed: cannot move $$tmpdir/$$lang.new.po to $$lang.po" 1>&2; \
-             exit 1; \
-           fi; \
-         fi; \
-       else \
-         echo "msgmerge for $$lang.po failed!" 1>&2; \
-         rm -f $$tmpdir/$$lang.new.po; \
-       fi
-
-$(DUMMYPOFILES):
-
-update-gmo: Makefile $(GMOFILES)
-       @:
-
-Makefile: Makefile.in.in $(top_builddir)/config.status @POMAKEFILEDEPS@
-       cd $(top_builddir) \
-         && CONFIG_FILES=$(subdir)/$@.in CONFIG_HEADERS= \
-              $(SHELL) ./config.status
-
-force:
-
-# Tell versions [3.59,3.63) of GNU make not to export all variables.
-# Otherwise a system limit (for SysV at least) may be exceeded.
-.NOEXPORT:
diff --git a/webcit/static/controls.js b/webcit/static/controls.js
new file mode 100644 (file)
index 0000000..d8c9cf5
--- /dev/null
@@ -0,0 +1,699 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// Autocompleter.Base handles all the autocompletion functionality 
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least, 
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method 
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most 
+// useful when one of the tokens is \n (a newline), as it 
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+  baseInitialize: function(element, update, options) {
+    this.element     = $(element); 
+    this.update      = $(update);  
+    this.hasFocus    = false; 
+    this.changed     = false; 
+    this.active      = false; 
+    this.index       = 0;     
+    this.entryCount  = 0;
+
+    if (this.setOptions)
+      this.setOptions(options);
+    else
+      this.options = options || {};
+
+    this.options.paramName    = this.options.paramName || this.element.name;
+    this.options.tokens       = this.options.tokens || [];
+    this.options.frequency    = this.options.frequency || 0.4;
+    this.options.minChars     = this.options.minChars || 1;
+    this.options.onShow       = this.options.onShow || 
+    function(element, update){ 
+      if(!update.style.position || update.style.position=='absolute') {
+        update.style.position = 'absolute';
+        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+      }
+      new ScriptaculousEffect.Appear(update,{duration:0.15});
+    };
+    this.options.onHide = this.options.onHide || 
+    function(element, update){ new ScriptaculousEffect.Fade(update,{duration:0.15}) };
+
+    if (typeof(this.options.tokens) == 'string') 
+      this.options.tokens = new Array(this.options.tokens);
+
+    this.observer = null;
+    
+    this.element.setAttribute('autocomplete','off');
+
+    Element.hide(this.update);
+
+    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+  },
+
+  show: function() {
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
+       '<iframe id="' + this.update.id + '_iefix" '+
+       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+      this.iefix = $(this.update.id+'_iefix');
+    }
+    if(this.iefix) {
+      Position.clone(this.update, this.iefix);
+      this.iefix.style.zIndex = 1;
+      this.update.style.zIndex = 2;
+      Element.show(this.iefix);
+    }
+  },
+
+  hide: function() {
+    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+    if(this.iefix) Element.hide(this.iefix);
+  },
+
+  startIndicator: function() {
+    if(this.options.indicator) Element.show(this.options.indicator);
+  },
+
+  stopIndicator: function() {
+    if(this.options.indicator) Element.hide(this.options.indicator);
+  },
+
+  onKeyPress: function(event) {
+    if(this.active)
+      switch(event.keyCode) {
+       case Event.KEY_TAB:
+       case Event.KEY_RETURN:
+         this.selectEntry();
+         Event.stop(event);
+       case Event.KEY_ESC:
+         this.hide();
+         this.active = false;
+         Event.stop(event);
+         return;
+       case Event.KEY_LEFT:
+       case Event.KEY_RIGHT:
+         return;
+       case Event.KEY_UP:
+         this.markPrevious();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+       case Event.KEY_DOWN:
+         this.markNext();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+      }
+     else 
+      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
+        return;
+
+    this.changed = true;
+    this.hasFocus = true;
+
+    if(this.observer) clearTimeout(this.observer);
+      this.observer = 
+        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+  },
+
+  onHover: function(event) {
+    var element = Event.findElement(event, 'LI');
+    if(this.index != element.autocompleteIndex) 
+    {
+        this.index = element.autocompleteIndex;
+        this.render();
+    }
+    Event.stop(event);
+  },
+  
+  onClick: function(event) {
+    var element = Event.findElement(event, 'LI');
+    this.index = element.autocompleteIndex;
+    this.selectEntry();
+    this.hide();
+  },
+  
+  onBlur: function(event) {
+    // needed to make click events working
+    setTimeout(this.hide.bind(this), 250);
+    this.hasFocus = false;
+    this.active = false;     
+  }, 
+  
+  render: function() {
+    if(this.entryCount > 0) {
+      for (var i = 0; i < this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),"selected") : 
+          Element.removeClassName(this.getEntry(i),"selected");
+        
+      if(this.hasFocus) { 
+        this.show();
+        this.active = true;
+      }
+    } else this.hide();
+  },
+  
+  markPrevious: function() {
+    if(this.index > 0) this.index--
+      else this.index = this.entryCcount-1;
+  },
+  
+  markNext: function() {
+    if(this.index < this.entryCount-1) this.index++
+      else this.index = 0;
+  },
+  
+  getEntry: function(index) {
+    return this.update.firstChild.childNodes[index];
+  },
+  
+  getCurrentEntry: function() {
+    return this.getEntry(this.index);
+  },
+  
+  selectEntry: function() {
+    this.active = false;
+    this.updateElement(this.getCurrentEntry());
+  },
+
+  updateElement: function(selectedElement) {
+    if (this.options.updateElement) {
+      this.options.updateElement(selectedElement);
+      return;
+    }
+
+    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    var lastTokenPos = this.findLastToken();
+    if (lastTokenPos != -1) {
+      var newValue = this.element.value.substr(0, lastTokenPos + 1);
+      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+      if (whitespace)
+        newValue += whitespace[0];
+      this.element.value = newValue + value;
+    } else {
+      this.element.value = value;
+    }
+    this.element.focus(); 
+  },
+
+  updateChoices: function(choices) {
+    if(!this.changed && this.hasFocus) {
+      this.update.innerHTML = choices;
+      Element.cleanWhitespace(this.update);
+      Element.cleanWhitespace(this.update.firstChild);
+
+      if(this.update.firstChild && this.update.firstChild.childNodes) {
+        this.entryCount = 
+          this.update.firstChild.childNodes.length;
+        for (var i = 0; i < this.entryCount; i++) {
+          var entry = this.getEntry(i);
+          entry.autocompleteIndex = i;
+          this.addObservers(entry);
+        }
+      } else { 
+        this.entryCount = 0;
+      }
+
+      this.stopIndicator();
+
+      this.index = 0;
+      this.render();
+    }
+  },
+
+  addObservers: function(element) {
+    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+  },
+
+  onObserverEvent: function() {
+    this.changed = false;   
+    if(this.getToken().length>=this.options.minChars) {
+      this.startIndicator();
+      this.getUpdatedChoices();
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+
+  getToken: function() {
+    var tokenPos = this.findLastToken();
+    if (tokenPos != -1)
+      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+    else
+      var ret = this.element.value;
+
+    return /\n/.test(ret) ? '' : ret;
+  },
+
+  findLastToken: function() {
+    var lastTokenPos = -1;
+
+    for (var i=0; i<this.options.tokens.length; i++) {
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos > lastTokenPos)
+        lastTokenPos = thisTokenPos;
+    }
+    return lastTokenPos;
+  }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+  initialize: function(element, update, url, options) {
+         this.baseInitialize(element, update, options);
+    this.options.asynchronous  = true;
+    this.options.onComplete    = this.onComplete.bind(this);
+    this.options.defaultParams = this.options.parameters || null;
+    this.url                   = url;
+  },
+
+  getUpdatedChoices: function() {
+    entry = encodeURIComponent(this.options.paramName) + '=' + 
+      encodeURIComponent(this.getToken());
+
+    this.options.parameters = this.options.callback ?
+      this.options.callback(this.element, entry) : entry;
+
+    if(this.options.defaultParams) 
+      this.options.parameters += '&' + this.options.defaultParams;
+
+    new Ajax.Request(this.url, this.options);
+  },
+
+  onComplete: function(request) {
+    this.updateChoices(request.responseText);
+  }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+//                    text only at the beginning of strings in the 
+//                    autocomplete array. Defaults to true, which will
+//                    match text at the beginning of any *word* in the
+//                    strings in the autocomplete array. If you want to
+//                    search anywhere in the string, additionally set
+//                    the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+//                   a partial match (unlike minChars, which defines
+//                   how many characters are required to do any match
+//                   at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+//                 Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector' 
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+  initialize: function(element, update, array, options) {
+    this.baseInitialize(element, update, options);
+    this.options.array = array;
+  },
+
+  getUpdatedChoices: function() {
+    this.updateChoices(this.options.selector(this));
+  },
+
+  setOptions: function(options) {
+    this.options = Object.extend({
+      choices: 10,
+      partialSearch: true,
+      partialChars: 2,
+      ignoreCase: true,
+      fullSearch: false,
+      selector: function(instance) {
+        var ret       = []; // Beginning matches
+        var partial   = []; // Inside matches
+        var entry     = instance.getToken();
+        var count     = 0;
+
+        for (var i = 0; i < instance.options.array.length &&  
+          ret.length < instance.options.choices ; i++) { 
+
+          var elem = instance.options.array[i];
+          var foundPos = instance.options.ignoreCase ? 
+            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
+            elem.indexOf(entry);
+
+          while (foundPos != -1) {
+            if (foundPos == 0 && elem.length != entry.length) { 
+              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
+                elem.substr(entry.length) + "</li>");
+              break;
+            } else if (entry.length >= instance.options.partialChars && 
+              instance.options.partialSearch && foundPos != -1) {
+              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+                  foundPos + entry.length) + "</li>");
+                break;
+              }
+            }
+
+            foundPos = instance.options.ignoreCase ? 
+              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
+              elem.indexOf(entry, foundPos + 1);
+
+          }
+        }
+        if (partial.length)
+          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+        return "<ul>" + ret.join('') + "</ul>";
+      }
+    }, options || {});
+  }
+});
+
+// AJAX in-place editor
+//
+// The constructor takes three parameters. The first is the element
+// that should support in-place editing. The second is the url to submit
+// the changed value to. The server should respond with the updated
+// value (the server might have post-processed it or validation might
+// have prevented it from changing). The third is a hash of options.
+//
+// Supported options are (all are optional and have sensible defaults):
+// - okText - The text of the submit button that submits the changed value
+//            to the server (default: "ok")
+// - cancelText - The text of the link that cancels editing (default: "cancel")
+// - savingText - The text being displayed as the AJAX engine communicates
+//                with the server (default: "Saving...")
+// - formId - The id given to the <form> element
+//            (default: the id of the element to edit plus '-inplaceeditor')
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+  initialize: function(element, url, options) {
+    this.url = url;
+    this.element = $(element);
+
+    this.options = Object.extend({
+      okText: "ok",
+      cancelText: "cancel",
+      savingText: "Saving...",
+      clickToEditText: "Click to edit",
+      okText: "ok",
+      rows: 1,
+      onComplete: function(transport, element) {
+        new ScriptaculousEffect.Highlight(element, {startcolor: this.options.highlightcolor});
+      },
+      onFailure: function(transport) {
+        alert("Error communicating with the server: " + transport.responseText.stripTags());
+      },
+      callback: function(form) {
+        return Form.serialize(form);
+      },
+      loadingText: 'Loading...',
+      savingClassName: 'inplaceeditor-saving',
+      formClassName: 'inplaceeditor-form',
+      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+      highlightendcolor: "#FFFFFF",
+      externalControl: null,
+      ajaxOptions: {}
+    }, options || {});
+
+    if(!this.options.formId && this.element.id) {
+      this.options.formId = this.element.id + "-inplaceeditor";
+      if ($(this.options.formId)) {
+        // there's already a form with that name, don't specify an id
+        this.options.formId = null;
+      }
+    }
+    
+    if (this.options.externalControl) {
+      this.options.externalControl = $(this.options.externalControl);
+    }
+    
+    this.originalBackground = Element.getStyle(this.element, 'background-color');
+    if (!this.originalBackground) {
+      this.originalBackground = "transparent";
+    }
+    
+    this.element.title = this.options.clickToEditText;
+    
+    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+    Event.observe(this.element, 'click', this.onclickListener);
+    Event.observe(this.element, 'mouseover', this.mouseoverListener);
+    Event.observe(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.observe(this.options.externalControl, 'click', this.onclickListener);
+      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  },
+  enterEditMode: function() {
+    if (this.saving) return;
+    if (this.editing) return;
+    this.editing = true;
+    this.onEnterEditMode();
+    if (this.options.externalControl) {
+      Element.hide(this.options.externalControl);
+    }
+    Element.hide(this.element);
+    this.form = this.getForm();
+    this.element.parentNode.insertBefore(this.form, this.element);
+  },
+  getForm: function() {
+    form = document.createElement("form");
+    form.id = this.options.formId;
+    Element.addClassName(form, this.options.formClassName)
+    form.onsubmit = this.onSubmit.bind(this);
+
+    this.createEditField(form);
+
+    if (this.options.textarea) {
+      var br = document.createElement("br");
+      form.appendChild(br);
+    }
+
+    okButton = document.createElement("input");
+    okButton.type = "submit";
+    okButton.value = this.options.okText;
+    form.appendChild(okButton);
+
+    cancelLink = document.createElement("a");
+    cancelLink.href = "#";
+    cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+    cancelLink.onclick = this.onclickCancel.bind(this);
+    form.appendChild(cancelLink);
+    return form;
+  },
+  createEditField: function(form) {
+    if (this.options.rows == 1) {
+      this.options.textarea = false;
+      var textField = document.createElement("input");
+      textField.type = "text";
+      textField.name = "value";
+      textField.value = this.getText();
+      textField.style.backgroundColor = this.options.highlightcolor;
+      var size = this.options.size || this.options.cols || 0;
+      if (size != 0)
+        textField.size = size;
+      form.appendChild(textField);
+      this.editField = textField;
+    } else {
+      this.options.textarea = true;
+      var textArea = document.createElement("textarea");
+      textArea.name = "value";
+      textArea.value = this.getText();
+      textArea.rows = this.options.rows;
+      textArea.cols = this.options.cols || 40;
+      form.appendChild(textArea);
+      this.editField = textArea;
+    }
+  },
+  getText: function() {
+    if (this.options.loadTextURL) {
+      this.loadExternalText();
+      return this.options.loadingText;
+    } else {
+      return this.element.innerHTML;
+    }
+  },
+  loadExternalText: function() {
+    new Ajax.Request(
+      this.options.loadTextURL,
+      {
+        asynchronous: true,
+        onComplete: this.onLoadedExternalText.bind(this)
+      }
+    );
+  },
+  onLoadedExternalText: function(transport) {
+    this.form.value.value = transport.responseText.stripTags();
+  },
+  onclickCancel: function() {
+    this.onComplete();
+    this.leaveEditMode();
+    return false;
+  },
+  onFailure: function(transport) {
+    this.options.onFailure(transport);
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+      this.oldInnerHTML = null;
+    }
+    return false;
+  },
+  onSubmit: function() {
+    this.saving = true;
+    new Ajax.Updater(
+      { 
+        success: this.element,
+         // don't update on failure (this could be an option)
+        failure: null
+      },
+      this.url,
+      Object.extend({
+        parameters: this.options.callback(this.form, this.editField.value),
+        onComplete: this.onComplete.bind(this),
+        onFailure: this.onFailure.bind(this)
+      }, this.options.ajaxOptions)
+    );
+    this.onLoading();
+    return false;
+  },
+  onLoading: function() {
+    this.saving = true;
+    this.removeForm();
+    this.leaveHover();
+    this.showSaving();
+  },
+  showSaving: function() {
+    this.oldInnerHTML = this.element.innerHTML;
+    this.element.innerHTML = this.options.savingText;
+    Element.addClassName(this.element, this.options.savingClassName);
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+  },
+  removeForm: function() {
+    if(this.form) {
+      Element.remove(this.form);
+      this.form = null;
+    }
+  },
+  enterHover: function() {
+    if (this.saving) return;
+    this.element.style.backgroundColor = this.options.highlightcolor;
+    if (this.effect) {
+      this.effect.cancel();
+    }
+    Element.addClassName(this.element, this.options.hoverClassName)
+  },
+  leaveHover: function() {
+    if (this.options.backgroundColor) {
+      this.element.style.backgroundColor = this.oldBackground;
+    }
+    Element.removeClassName(this.element, this.options.hoverClassName)
+    if (this.saving) return;
+    this.effect = new ScriptaculousEffect.Highlight(this.element, {
+      startcolor: this.options.highlightcolor,
+      endcolor: this.options.highlightendcolor,
+      restorecolor: this.originalBackground
+    });
+  },
+  leaveEditMode: function() {
+    Element.removeClassName(this.element, this.options.savingClassName);
+    this.removeForm();
+    this.leaveHover();
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+    if (this.options.externalControl) {
+      Element.show(this.options.externalControl);
+    }
+    this.editing = false;
+    this.saving = false;
+    this.oldInnerHTML = null;
+    this.onLeaveEditMode();
+  },
+  onComplete: function(transport) {
+    this.leaveEditMode();
+    this.options.onComplete.bind(this)(transport, this.element);
+  },
+  onEnterEditMode: function() {},
+  onLeaveEditMode: function() {},
+  dispose: function() {
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+    }
+    this.leaveEditMode();
+    Event.stopObserving(this.element, 'click', this.onclickListener);
+    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  }
+};
\ No newline at end of file
diff --git a/webcit/static/dragdrop.js b/webcit/static/dragdrop.js
new file mode 100644 (file)
index 0000000..9113f3d
--- /dev/null
@@ -0,0 +1,545 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// Element.Class part Copyright (c) 2005 by Rick Olson
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+  drops: false,
+
+  remove: function(element) {
+    for(var i = 0; i < this.drops.length; i++)
+      if(this.drops[i].element == element)
+        this.drops.splice(i,1);
+  },
+
+  add: function(element) {
+    element = $(element);
+    var options = Object.extend({
+      greedy:     true,
+      hoverclass: null  
+    }, arguments[1] || {});
+
+    // cache containers
+    if(options.containment) {
+      options._containers = new Array();
+      var containment = options.containment;
+      if((typeof containment == 'object') && 
+        (containment.constructor == Array)) {
+        for(var i=0; i<containment.length; i++)
+          options._containers.push($(containment[i]));
+      } else {
+        options._containers.push($(containment));
+      }
+      options._containers_length = 
+        options._containers.length-1;
+    }
+
+    Element.makePositioned(element); // fix IE
+
+    options.element = element;
+
+    // activate the droppable    
+    if(!this.drops) this.drops = [];
+    this.drops.push(options);
+  },
+
+  isContained: function(element, drop) {
+    var containers = drop._containers;
+    var parentNode = element.parentNode;
+    var i = drop._containers_length;
+    do { if(parentNode==containers[i]) return true; } while (i--);
+    return false;
+  },
+
+  isAffected: function(pX, pY, element, drop) {
+    return (
+      (drop.element!=element) &&
+      ((!drop._containers) ||
+        this.isContained(element, drop)) &&
+      ((!drop.accept) ||
+        (Element.Class.has_any(element, drop.accept))) &&
+      Position.within(drop.element, pX, pY) );
+  },
+
+  deactivate: function(drop) {
+    Element.Class.remove(drop.element, drop.hoverclass);
+    this.last_active = null;
+  },
+
+  activate: function(drop) {
+    if(this.last_active) this.deactivate(this.last_active);
+    if(drop.hoverclass)
+      Element.Class.add(drop.element, drop.hoverclass);
+    this.last_active = drop;
+  },
+
+  show: function(event, element) {
+    if(!this.drops) return;
+    var pX = Event.pointerX(event);
+    var pY = Event.pointerY(event);
+    Position.prepare();
+
+    var i = this.drops.length-1; do {
+      var drop = this.drops[i];
+      if(this.isAffected(pX, pY, element, drop)) {
+        if(drop.onHover)
+           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+        if(drop.greedy) { 
+          this.activate(drop);
+          return;
+        }
+      }
+    } while (i--);
+  },
+
+  fire: function(event, element) {
+    if(!this.last_active) return;
+    Position.prepare();
+
+    if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
+      if (this.last_active.onDrop) 
+        this.last_active.onDrop(element, this.last_active.element);
+
+  },
+
+  reset: function() {
+    if(this.last_active)
+      this.deactivate(this.last_active);
+  }
+}
+
+var Draggables = {
+  observers: new Array(),
+  addObserver: function(observer) {
+    this.observers.push(observer);    
+  },
+  removeObserver: function(element) {  // element instead of obsever fixes mem leaks
+    for(var i = 0; i < this.observers.length; i++)
+      if(this.observers[i].element && (this.observers[i].element == element))
+        this.observers.splice(i,1);
+  },
+  notify: function(eventName, draggable) {  // 'onStart', 'onEnd'
+    for(var i = 0; i < this.observers.length; i++)
+      this.observers[i][eventName](draggable);
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable.prototype = {
+  initialize: function(element) {
+    var options = Object.extend({
+      handle: false,
+      starteffect: function(element) { 
+        new ScriptaculousEffect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
+      },
+      reverteffect: function(element, top_offset, left_offset) {
+        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+        new ScriptaculousEffect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
+      },
+      endeffect: function(element) { 
+         new ScriptaculousEffect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
+      },
+      zindex: 1000,
+      revert: false
+    }, arguments[1] || {});
+
+    this.element      = $(element);
+    this.handle       = options.handle ? $(options.handle) : this.element;
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.offsetX      = 0;
+    this.offsetY      = 0;
+    this.originalLeft = this.currentLeft();
+    this.originalTop  = this.currentTop();
+    this.originalX    = this.element.offsetLeft;
+    this.originalY    = this.element.offsetTop;
+    this.originalZ    = parseInt(this.element.style.zIndex || "0");
+
+    this.options      = options;
+
+    this.active       = false;
+    this.dragging     = false;   
+
+    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+    this.eventMouseMove = this.update.bindAsEventListener(this);
+    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+
+    Event.observe(this.handle, "mousedown", this.eventMouseDown);
+  },
+  destroy: function() {
+    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+    this.unregisterEvents();
+  },
+  registerEvents: function() {
+    if(this.active) return;
+    Event.observe(document, "mouseup", this.eventMouseUp);
+    Event.observe(document, "mousemove", this.eventMouseMove);
+    Event.observe(document, "keypress", this.eventKeypress);
+  },
+  unregisterEvents: function() {
+    if(!this.active) return;
+    Event.stopObserving(document, "mouseup", this.eventMouseUp);
+    Event.stopObserving(document, "mousemove", this.eventMouseMove);
+    Event.stopObserving(document, "keypress", this.eventKeypress);
+  },
+  currentLeft: function() {
+    return parseInt(this.element.style.left || '0');
+  },
+  currentTop: function() {
+    return parseInt(this.element.style.top || '0')
+  },
+  startDrag: function(event) {
+    if(Event.isLeftClick(event)) {
+      this.registerEvents();
+      this.active = true;
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var offsets = Position.cumulativeOffset(this.element);
+      this.offsetX =  (pointer[0] - offsets[0]);
+      this.offsetY =  (pointer[1] - offsets[1]);
+      Event.stop(event);
+    }
+  },
+  finishDrag: function(event, success) {
+    this.unregisterEvents();
+
+    this.active = false;
+    this.dragging = false;
+
+    if(this.options.ghosting) {
+      Position.relativize(this.element);
+      Element.remove(this._clone);
+      this._clone = null;
+    }
+
+    if(success) Droppables.fire(event, this.element);
+    Draggables.notify('onEnd', this);
+
+    var revert = this.options.revert;
+    if(revert && typeof revert == 'function') revert = revert(this.element);
+
+    if(revert && this.options.reverteffect) {
+      this.options.reverteffect(this.element, 
+      this.currentTop()-this.originalTop,
+      this.currentLeft()-this.originalLeft);
+    } else {
+      this.originalLeft = this.currentLeft();
+      this.originalTop  = this.currentTop();
+    }
+
+    this.element.style.zIndex = this.originalZ;
+
+    if(this.options.endeffect) 
+      this.options.endeffect(this.element);
+
+
+    Droppables.reset();
+  },
+  keyPress: function(event) {
+    if(this.active) {
+      if(event.keyCode==Event.KEY_ESC) {
+        this.finishDrag(event, false);
+        Event.stop(event);
+      }
+    }
+  },
+  endDrag: function(event) {
+    if(this.active && this.dragging) {
+      this.finishDrag(event, true);
+      Event.stop(event);
+    }
+    this.active = false;
+    this.dragging = false;
+  },
+  draw: function(event) {
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    var offsets = Position.cumulativeOffset(this.element);
+    offsets[0] -= this.currentLeft();
+    offsets[1] -= this.currentTop();
+    var style = this.element.style;
+    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+      style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
+    if((!this.options.constraint) || (this.options.constraint=='vertical'))
+      style.top  = (pointer[1] - offsets[1] - this.offsetY) + "px";
+    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+  },
+  update: function(event) {
+   if(this.active) {
+      if(!this.dragging) {
+        var style = this.element.style;
+        this.dragging = true;
+        if(style.position=="") style.position = "relative";
+        style.zIndex = this.options.zindex;
+
+        if(this.options.ghosting) {
+          this._clone = this.element.cloneNode(true);
+          Position.absolutize(this.element);
+          this.element.parentNode.insertBefore(this._clone, this.element);
+        }
+
+        Draggables.notify('onStart', this);
+        if(this.options.starteffect) this.options.starteffect(this.element);
+      }
+
+      Droppables.show(event, this.element);
+      this.draw(event);
+      if(this.options.change) this.options.change(this);
+
+      // fix AppleWebKit rendering
+      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 
+
+      Event.stop(event);
+   }
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+  initialize: function(element, observer) {
+    this.element   = $(element);
+    this.observer  = observer;
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  onStart: function() {
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  onEnd: function() {
+    Sortable.unmark();
+    if(this.lastValue != Sortable.serialize(this.element))
+      this.observer(this.element)
+  }
+}
+
+var Sortable = {
+  sortables: new Array(),
+  options: function(element){
+    element = $(element);
+    for(var i=0;i<this.sortables.length;i++)
+      if(this.sortables[i].element == element)
+        return this.sortables[i];
+    return null;        
+  },
+  destroy: function(element){
+    element = $(element);
+    for(var i=0;i<this.sortables.length;i++) {
+      if(this.sortables[i].element == element) {
+        var s = this.sortables[i];
+        Draggables.removeObserver(s.element);
+        for(var j=0;j<s.droppables.length;j++)
+          Droppables.remove(s.droppables[j]);
+        for(j=0;j<s.draggables.length;j++)
+          s.draggables[j].destroy();
+        this.sortables.splice(i,1);
+      }
+    }
+  },
+  create: function(element) {
+    element = $(element);
+    var options = Object.extend({ 
+      element:     element,
+      tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,      // fixme: unimplemented
+      overlap:     'vertical', // one of 'vertical', 'horizontal'
+      constraint:  'vertical', // one of 'vertical', 'horizontal', false
+      containment: element,    // also takes array of elements (or id's); or false
+      handle:      false,      // or a CSS class
+      only:        false,
+      hoverclass:  null,
+      ghosting:    false,
+      onChange:    function() {},
+      onUpdate:    function() {}
+    }, arguments[1] || {});
+
+    // clear any old sortable with same element
+    this.destroy(element);
+
+    // build options for the draggables
+    var options_for_draggable = {
+      revert:      true,
+      ghosting:    options.ghosting,
+      constraint:  options.constraint,
+      handle:      handle };
+
+    if(options.starteffect)
+      options_for_draggable.starteffect = options.starteffect;
+
+    if(options.reverteffect)
+      options_for_draggable.reverteffect = options.reverteffect;
+    else
+      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+        element.style.top  = 0;
+        element.style.left = 0;
+      };
+
+    if(options.endeffect)
+      options_for_draggable.endeffect = options.endeffect;
+
+    if(options.zindex)
+      options_for_draggable.zindex = options.zindex;
+
+    // build options for the droppables  
+    var options_for_droppable = {
+      overlap:     options.overlap,
+      containment: options.containment,
+      hoverclass:  options.hoverclass,
+      onHover:     Sortable.onHover,
+      greedy:      !options.dropOnEmpty
+    }
+
+    // fix for gecko engine
+    Element.cleanWhitespace(element); 
+
+    options.draggables = [];
+    options.droppables = [];
+
+    // make it so
+
+    // drop on empty handling
+    if(options.dropOnEmpty) {
+      Droppables.add(element,
+        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
+      options.droppables.push(element);
+    }
+
+    var elements = this.findElements(element, options);
+    if(elements) {
+      for (var i = 0; i < elements.length; i++) {
+        // handles are per-draggable
+        var handle = options.handle ? 
+          Element.Class.childrenWith(elements[i], options.handle)[0] : elements[i];
+            options.draggables.push(new Draggable(elements[i], Object.extend(options_for_draggable, { handle: handle })));
+            Droppables.add(elements[i], options_for_droppable);
+
+        options.droppables.push(elements[i]);
+      }
+    }
+
+    // keep reference
+    this.sortables.push(options);
+
+    // for onupdate
+    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+  },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    if(!element.hasChildNodes()) return null;
+    var elements = [];
+    var children = element.childNodes;
+    for(var i = 0; i<children.length; i++) {
+      if(children[i].tagName && children[i].tagName==options.tag.toUpperCase() &&
+        (!options.only || (Element.Class.has(children[i], options.only))))
+          elements.push(children[i]);
+      if(options.tree) {
+        var grandchildren = this.findElements(children[i], options);
+        if(grandchildren) elements.push(grandchildren);
+      }
+    }
+
+    return (elements.length>0 ? elements.flatten() : null);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(overlap>0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, dropon);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    } else {
+      Sortable.mark(dropon, 'after');
+      var nextElement = dropon.nextSibling || null;
+      if(nextElement != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, nextElement);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    }
+  },
+
+  onEmptyHover: function(element, dropon) {
+    if(element.parentNode!=dropon) {
+      dropon.appendChild(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Element.hide(Sortable._marker);
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable && !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = $('dropmarker') || document.createElement('DIV');
+      Element.hide(Sortable._marker);
+      Element.Class.add(Sortable._marker, 'dropmarker');
+      Sortable._marker.style.position = 'absolute';
+      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.style.top  = offsets[1] + 'px';
+    if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+    Sortable._marker.style.left = offsets[0] + 'px';
+    Element.show(Sortable._marker);
+  },
+
+  serialize: function(element) {
+    element = $(element);
+    var sortableOptions = this.options(element);
+    var options = Object.extend({
+      tag:  sortableOptions.tag,
+      only: sortableOptions.only,
+      name: element.id
+    }, arguments[1] || {});
+
+    var items = $(element).childNodes;
+    var queryComponents = new Array();
+
+    for(var i=0; i<items.length; i++)
+      if(items[i].tagName && items[i].tagName==options.tag.toUpperCase() &&
+        (!options.only || (Element.Class.has(items[i], options.only))))
+        queryComponents.push(
+          encodeURIComponent(options.name) + "[]=" + 
+          encodeURIComponent(items[i].id.split("_")[1]));
+
+    return queryComponents.join("&");
+  }
+} 
\ No newline at end of file
diff --git a/webcit/static/effects.js b/webcit/static/effects.js
new file mode 100644 (file)
index 0000000..0b59d48
--- /dev/null
@@ -0,0 +1,707 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Parts (c) 2005 Justin Palmer (http://encytemedia.com/)
+// Parts (c) 2005 Mark Pilgrim (http://diveintomark.org/)
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var ScriptaculousEffect = {
+  tagifyText: function(element) {
+    var tagifyStyle = "position:relative";
+    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1"; 
+    element = $(element);
+    var children = element.childNodes;
+    for (var i = 0; i < children.length; i++)
+      if(children[i].nodeType==3) {
+        var child = children[i];
+        for (var j = 0; j < child.nodeValue.length; j++)
+          element.insertBefore(
+            Builder.node('span',{style: tagifyStyle},
+              child.nodeValue.substr(j,1) == " " ? String.fromCharCode(160) : 
+              child.nodeValue.substr(j,1)), child);
+        Element.remove(child);
+      }
+  },
+  multiple: function(element, effect) {
+    if(((typeof element == 'object') || 
+        (typeof element == 'function')) && 
+       (element.length))
+      var elements = element;
+    else
+      var elements = $(element).childNodes;
+      
+    var options = Object.extend({
+      speed: 0.1,
+      delay: 0.0
+    }, arguments[2] || {});
+    var speed = options.speed;
+    var delay = options.delay;
+
+    for(var i = 0; i < elements.length; i++)
+      new effect(elements[i], 
+        Object.extend(options, { delay: delay + i*speed }));
+  }
+};
+
+var ScriptaculousEffect2 = ScriptaculousEffect; // deprecated
+
+/* ------------- transitions ------------- */
+
+ScriptaculousEffect.Transitions = {}
+
+ScriptaculousEffect.Transitions.linear = function(pos) {
+  return pos;
+}
+ScriptaculousEffect.Transitions.sinoidal = function(pos) {
+  return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+ScriptaculousEffect.Transitions.reverse  = function(pos) {
+  return 1-pos;
+}
+ScriptaculousEffect.Transitions.flicker = function(pos) {
+  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random(0.25);
+}
+ScriptaculousEffect.Transitions.wobble = function(pos) {
+  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+ScriptaculousEffect.Transitions.pulse = function(pos) {
+  return (Math.floor(pos*10) % 2 == 0 ? 
+    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+ScriptaculousEffect.Transitions.none = function(pos) {
+  return 0;
+}
+ScriptaculousEffect.Transitions.full = function(pos) {
+  return 1;
+}
+
+/* ------------- core effects ------------- */
+
+ScriptaculousEffect.Queue = {
+  effects:  [],
+  interval: null,
+  findLast: function() {
+    var timestamp = false;
+    for(var i = 0; i < this.effects.length; i++)
+      if(!timestamp || (this.effects[i].finishOn>timestamp))
+        timestamp = this.effects[i].finishOn;
+    return timestamp;
+  },
+  add: function(effect) {
+    var timestamp = new Date().getTime();
+    
+    switch(effect.options.queue) {
+      case 'front':
+        // move unstarted effects after this effect  
+        for(var i = 0; i < this.effects.length; i++)
+          if(this.effects[i].state == 'idle') {
+            this.effects[i].startOn  += effect.finishOn;
+            this.effects[i].finishOn += effect.finishOn;
+          }
+        break;
+      case 'end':
+        // start effect after last queued effect has finished
+        timestamp = this.findLast() || timestamp;
+        break;
+    }
+    
+    effect.startOn  += timestamp;
+    effect.finishOn += timestamp;
+    
+    this.effects.push(effect);
+    
+    if(!this.interval) 
+      this.interval = setInterval(this.loop.bind(this), 40);
+  },
+  remove: function(effect) {
+    for(var i = 0; i < this.effects.length; i++)
+      if(this.effects[i]==effect) this.effects.splice(i,1);
+    if(this.effects.length == 0) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+  },
+  loop: function() {
+    var timePos = new Date().getTime();
+    for(var i = 0; i < this.effects.length; i++) {
+      this.effects[i].loop(timePos);
+    }
+  }
+}
+
+ScriptaculousEffect.Base = function() {};
+ScriptaculousEffect.Base.prototype = {
+  setOptions: function(options) {
+    this.options = Object.extend({
+      transition: ScriptaculousEffect.Transitions.sinoidal,
+      duration:   1.0,   // seconds
+      fps:        25.0,  // max. 25fps due to ScriptaculousEffect.Queue implementation
+      sync:       false, // true for combining
+      from:       0.0,
+      to:         1.0,
+      delay:      0.0,
+      queue:      'parallel'
+    }, options || {});
+  },
+  start: function(options) {
+    this.setOptions(options || {});
+    this.currentFrame = 0;
+    this.state        = 'idle';
+    this.startOn      = this.options.delay*1000;
+    this.finishOn     = this.startOn + (this.options.duration*1000);
+    if(this.options.beforeStart) this.options.beforeStart(this);
+    if(!this.options.sync) ScriptaculousEffect.Queue.add(this);
+  },
+  loop: function(timePos) {
+    if(timePos >= this.startOn) {
+      if(timePos >= this.finishOn) {
+        this.render(1.0);
+        this.cancel();
+        if(this.finish) this.finish(); 
+        if(this.options.afterFinish) this.options.afterFinish(this);
+        return;  
+      }
+      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
+      var frame = Math.round(pos * this.options.fps * this.options.duration);
+      if(frame > this.currentFrame) {
+        this.render(pos);
+        this.currentFrame = frame;
+      }
+    }
+  },
+  render: function(pos) {
+    if(this.state == 'idle') {
+      this.state = 'running';
+      if(this.setup) this.setup();
+    }
+    if(this.options.transition) pos = this.options.transition(pos);
+    pos *= (this.options.to-this.options.from);
+    pos += this.options.from; 
+    if(this.options.beforeUpdate) this.options.beforeUpdate(this);
+    if(this.update) this.update(pos);
+    if(this.options.afterUpdate) this.options.afterUpdate(this);  
+  },
+  cancel: function() {
+    if(!this.options.sync) ScriptaculousEffect.Queue.remove(this);
+    this.state = 'finished';
+  }
+}
+
+ScriptaculousEffect.Parallel = Class.create();
+Object.extend(Object.extend(ScriptaculousEffect.Parallel.prototype, ScriptaculousEffect.Base.prototype), {
+  initialize: function(effects) {
+    this.effects = effects || [];
+    this.start(arguments[1]);
+  },
+  update: function(position) {
+    for (var i = 0; i < this.effects.length; i++)
+      this.effects[i].render(position);
+  },
+  finish: function(position) {
+    for (var i = 0; i < this.effects.length; i++) {
+      this.effects[i].cancel();
+      if(this.effects[i].finish) this.effects[i].finish(position);
+    }
+  }
+});
+
+// Internet Explorer caveat: works only on elements that have
+// a 'layout', meaning having a given width or height. 
+// There is no way to safely set this automatically.
+ScriptaculousEffect.Opacity = Class.create();
+Object.extend(Object.extend(ScriptaculousEffect.Opacity.prototype, ScriptaculousEffect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    var options = Object.extend({
+      from: 0.0,
+      to:   1.0
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  update: function(position) {
+    this.setOpacity(position);
+  }, 
+  setOpacity: function(opacity) {
+    if(opacity<0.0001) opacity = 0; // fix errors with things like 6.152242992829571e-8
+    if(opacity==1.0) {
+      this.element.style.opacity = '0.999999';
+      this.element.style.filter  = null;
+    } else {
+      this.element.style.opacity = opacity;
+      this.element.style.filter  = "alpha(opacity:"+opacity*100+")";
+    }
+  }
+});
+
+ScriptaculousEffect.MoveBy = Class.create();
+Object.extend(Object.extend(ScriptaculousEffect.MoveBy.prototype, ScriptaculousEffect.Base.prototype), {
+  initialize: function(element, toTop, toLeft) {
+    this.element      = $(element);
+    this.toTop        = toTop;
+    this.toLeft       = toLeft;
+    this.start(arguments[3]);
+  },
+  setup: function() {
+    this.originalTop  = parseFloat(Element.getStyle(this.element,'top')  || '0');
+    this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
+    Element.makePositioned(this.element);
+  },
+  update: function(position) {
+    topd  = this.toTop  * position + this.originalTop;
+    leftd = this.toLeft * position + this.originalLeft;
+    this.setPosition(topd, leftd);
+  },
+  setPosition: function(topd, leftd) {
+    this.element.style.top  = topd  + "px";
+    this.element.style.left = leftd + "px";
+  }
+});
+
+ScriptaculousEffect.Scale = Class.create();
+Object.extend(Object.extend(ScriptaculousEffect.Scale.prototype, ScriptaculousEffect.Base.prototype), {
+  initialize: function(element, percent) {
+    this.element = $(element)
+    var options = Object.extend({
+      scaleX: true,
+      scaleY: true,
+      scaleContent: true,
+      scaleFromCenter: false,
+      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
+      scaleFrom: 100.0,
+      scaleTo:   percent
+    }, arguments[2] || {});
+    this.start(options);
+  },
+  setup: function() {
+    this.originalTop    = this.element.offsetTop;
+    this.originalLeft   = this.element.offsetLeft;
+    if(Element.getStyle(this.element,'font-size')=="") this.sizeEm = 1.0;
+    if(Element.getStyle(this.element,'font-size') && Element.getStyle(this.element,'font-size').indexOf("em")>0)
+      this.sizeEm = parseFloat(Element.getStyle(this.element,'font-size'));
+    this.factor = (this.options.scaleTo/100.0) - (this.options.scaleFrom/100.0);
+    if(this.options.scaleMode=='box') {
+      this.originalHeight = this.element.clientHeight;
+      this.originalWidth  = this.element.clientWidth; 
+    } else 
+    if(this.options.scaleMode=='contents') {
+      this.originalHeight = this.element.scrollHeight;
+      this.originalWidth  = this.element.scrollWidth;
+    } else {
+      this.originalHeight = this.options.scaleMode.originalHeight;
+      this.originalWidth  = this.options.scaleMode.originalWidth;
+    }
+  },
+  update: function(position) {
+    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+    if(this.options.scaleContent && this.sizeEm) 
+      this.element.style.fontSize = this.sizeEm*currentScale + "em";
+    this.setDimensions(
+      this.originalWidth * currentScale, 
+      this.originalHeight * currentScale);
+  },
+  setDimensions: function(width, height) {
+    if(this.options.scaleX) this.element.style.width = width + 'px';
+    if(this.options.scaleY) this.element.style.height = height + 'px';
+    if(this.options.scaleFromCenter) {
+      var topd  = (height - this.originalHeight)/2;
+      var leftd = (width  - this.originalWidth)/2;
+      if(Element.getStyle(this.element,'position')=='absolute') {
+        if(this.options.scaleY) this.element.style.top = this.originalTop-topd + "px";
+        if(this.options.scaleX) this.element.style.left = this.originalLeft-leftd + "px";
+      } else {
+        if(this.options.scaleY) this.element.style.top = -topd + "px";
+        if(this.options.scaleX) this.element.style.left = -leftd + "px";
+      }
+    }
+  }
+});
+
+ScriptaculousEffect.Highlight = Class.create();
+Object.extend(Object.extend(ScriptaculousEffect.Highlight.prototype, ScriptaculousEffect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    var options = Object.extend({
+      startcolor:   "#ffff99"
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // try to parse current background color as default for endcolor
+    // browser stores this as: "rgb(255, 255, 255)", convert to "#ffffff" format
+    if(!this.options.endcolor) {
+      var endcolor = "#ffffff";
+      var current = Element.getStyle(this.element, 'background-color');
+      if(current && current.slice(0,4) == "rgb(") {
+        endcolor = "#";
+        var cols = current.slice(4,current.length-1).split(',');
+        var i=0; do { endcolor += parseInt(cols[i]).toColorPart() } while (++i<3);
+      }
+      this.options.endcolor = endcolor;
+    }    
+    // init color calculations
+    this.colors_base = [
+      parseInt(this.options.startcolor.slice(1,3),16),
+      parseInt(this.options.startcolor.slice(3,5),16),
+      parseInt(this.options.startcolor.slice(5),16) ];
+    this.colors_delta = [
+      parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
+      parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
+      parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
+  },
+  update: function(position) {
+    var colors = [
+      Math.round(this.colors_base[0]+(this.colors_delta[0]*position)),
+      Math.round(this.colors_base[1]+(this.colors_delta[1]*position)),
+      Math.round(this.colors_base[2]+(this.colors_delta[2]*position)) ];
+    this.element.style.backgroundColor = "#" +
+      colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
+  },
+  finish: function() {
+    this.element.style.backgroundColor = this.options.restorecolor;
+  }
+});
+
+ScriptaculousEffect.ScrollTo = Class.create();
+Object.extend(Object.extend(ScriptaculousEffect.ScrollTo.prototype, ScriptaculousEffect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    this.start(arguments[1] || {});
+  },
+  setup: function() {
+    Position.prepare();
+    var offsets = Position.cumulativeOffset(this.element);
+    var max = window.innerHeight ? 
+      window.height - window.innerHeight :
+      document.body.scrollHeight - 
+        (document.documentElement.clientHeight ? 
+          document.documentElement.clientHeight : document.body.clientHeight);
+    this.scrollStart = Position.deltaY;
+    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+  },
+  update: function(position) {
+    Position.prepare();
+    window.scrollTo(Position.deltaX, 
+      this.scrollStart + (position*this.delta));
+  }
+});
+
+/* ------------- combination effects ------------- */
+
+ScriptaculousEffect.Fade = function(element) {
+  var options = Object.extend({
+  from: 1.0,
+  to:   0.0,
+  afterFinish: function(effect) 
+    { Element.hide(effect.element);
+      effect.setOpacity(1); } 
+  }, arguments[1] || {});
+  return new ScriptaculousEffect.Opacity(element,options);
+}
+
+ScriptaculousEffect.Appear = function(element) {
+  var options = Object.extend({
+  from: 0.0,
+  to:   1.0,
+  beforeStart: function(effect)  
+    { effect.setOpacity(0);
+      Element.show(effect.element); },
+  afterUpdate: function(effect)  
+    { Element.show(effect.element); }
+  }, arguments[1] || {});
+  return new ScriptaculousEffect.Opacity(element,options);
+}
+
+ScriptaculousEffect.Puff = function(element) {
+  return new ScriptaculousEffect.Parallel(
+   [ new ScriptaculousEffect.Scale(element, 200, { sync: true, scaleFromCenter: true }), 
+     new ScriptaculousEffect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ], 
+     Object.extend({ duration: 1.0, 
+      beforeUpdate: function(effect) 
+       { effect.effects[0].element.style.position = 'absolute'; },
+      afterFinish: function(effect)
+       { Element.hide(effect.effects[0].element); }
+     }, arguments[1] || {})
+   );
+}
+
+ScriptaculousEffect.BlindUp = function(element) {
+  element = $(element);
+  Element.makeClipping(element);
+  return new ScriptaculousEffect.Scale(element, 0, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      afterFinish: function(effect) 
+        { 
+          Element.hide(effect.element);
+          Element.undoClipping(effect.element);
+        } 
+    }, arguments[1] || {})
+  );
+}
+
+ScriptaculousEffect.BlindDown = function(element) {
+  element = $(element);
+  element.style.height = '0px';
+  Element.makeClipping(element);
+  Element.show(element);
+  return new ScriptaculousEffect.Scale(element, 100, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      scaleMode: 'contents',
+      scaleFrom: 0,
+      afterFinish: function(effect) {
+        Element.undoClipping(effect.element);
+      }
+    }, arguments[1] || {})
+  );
+}
+
+ScriptaculousEffect.SwitchOff = function(element) {
+  return new ScriptaculousEffect.Appear(element,
+    { duration: 0.4,
+     transition: ScriptaculousEffect.Transitions.flicker,
+     afterFinish: function(effect)
+      { effect.element.style.overflow = 'hidden';
+        new ScriptaculousEffect.Scale(effect.element, 1, 
+         { duration: 0.3, scaleFromCenter: true,
+          scaleX: false, scaleContent: false,
+          afterUpdate: function(effect) { 
+           if(effect.element.style.position=="")
+             effect.element.style.position = 'relative'; },
+          afterFinish: function(effect) { Element.hide(effect.element); }
+         } )
+      }
+    } );
+}
+
+ScriptaculousEffect.DropOut = function(element) {
+  return new ScriptaculousEffect.Parallel(
+    [ new ScriptaculousEffect.MoveBy(element, 100, 0, { sync: true }), 
+      new ScriptaculousEffect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ], 
+    Object.extend(
+      { duration: 0.5, 
+        afterFinish: function(effect)
+          { Element.hide(effect.effects[0].element); } 
+      }, arguments[1] || {}));
+}
+
+ScriptaculousEffect.Shake = function(element) {
+  return new ScriptaculousEffect.MoveBy(element, 0, 20, 
+    { duration: 0.05, afterFinish: function(effect) {
+  new ScriptaculousEffect.MoveBy(effect.element, 0, -40, 
+    { duration: 0.1, afterFinish: function(effect) { 
+  new ScriptaculousEffect.MoveBy(effect.element, 0, 40, 
+    { duration: 0.1, afterFinish: function(effect) {  
+  new ScriptaculousEffect.MoveBy(effect.element, 0, -40, 
+    { duration: 0.1, afterFinish: function(effect) {  
+  new ScriptaculousEffect.MoveBy(effect.element, 0, 40, 
+    { duration: 0.1, afterFinish: function(effect) {  
+  new ScriptaculousEffect.MoveBy(effect.element, 0, -20, 
+    { duration: 0.05, afterFinish: function(effect) {  
+  }}) }}) }}) }}) }}) }});
+}
+
+ScriptaculousEffect.SlideDown = function(element) {
+  element = $(element);
+  element.style.height   = '0px';
+  Element.makeClipping(element);
+  Element.cleanWhitespace(element);
+  Element.makePositioned(element.firstChild);
+  Element.show(element);
+  return new ScriptaculousEffect.Scale(element, 100, 
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleMode: 'contents',
+    scaleFrom: 0,
+    afterUpdate: function(effect) 
+      { effect.element.firstChild.style.bottom = 
+          (effect.originalHeight - effect.element.clientHeight) + 'px'; },
+    afterFinish: function(effect) 
+      {  Element.undoClipping(effect.element); }
+    }, arguments[1] || {})
+  );
+}
+  
+ScriptaculousEffect.SlideUp = function(element) {
+  element = $(element);
+  Element.makeClipping(element);
+  Element.cleanWhitespace(element);
+  Element.makePositioned(element.firstChild);
+  Element.show(element);
+  return new ScriptaculousEffect.Scale(element, 0, 
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    afterUpdate: function(effect) 
+      { effect.element.firstChild.style.bottom = 
+          (effect.originalHeight - effect.element.clientHeight) + 'px'; },
+    afterFinish: function(effect)
+      { 
+        Element.hide(effect.element);
+        Element.undoClipping(effect.element);
+      }
+   }, arguments[1] || {})
+  );
+}
+
+ScriptaculousEffect.Squish = function(element) {
+ return new ScriptaculousEffect.Scale(element, 0, 
+   { afterFinish: function(effect) { Element.hide(effect.element); } });
+}
+
+ScriptaculousEffect.Grow = function(element) {
+  element = $(element);
+  var options = arguments[1] || {};
+  
+  var originalWidth = element.clientWidth;
+  var originalHeight = element.clientHeight;
+  element.style.overflow = 'hidden';
+  Element.show(element);
+  
+  var direction = options.direction || 'center';
+  var moveTransition = options.moveTransition || ScriptaculousEffect.Transitions.sinoidal;
+  var scaleTransition = options.scaleTransition || ScriptaculousEffect.Transitions.sinoidal;
+  var opacityTransition = options.opacityTransition || ScriptaculousEffect.Transitions.full;
+  
+  var initialMoveX, initialMoveY;
+  var moveX, moveY;
+  
+  switch (direction) {
+    case 'top-left':
+      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      break;
+    case 'top-right':
+      initialMoveX = originalWidth;
+      initialMoveY = moveY = 0;
+      moveX = -originalWidth;
+      break;
+    case 'bottom-left':
+      initialMoveX = moveX = 0;
+      initialMoveY = originalHeight;
+      moveY = -originalHeight;
+      break;
+    case 'bottom-right':
+      initialMoveX = originalWidth;
+      initialMoveY = originalHeight;
+      moveX = -originalWidth;
+      moveY = -originalHeight;
+      break;
+    case 'center':
+      initialMoveX = originalWidth / 2;
+      initialMoveY = originalHeight / 2;
+      moveX = -originalWidth / 2;
+      moveY = -originalHeight / 2;
+      break;
+  }
+  
+  return new ScriptaculousEffect.MoveBy(element, initialMoveY, initialMoveX, { 
+    duration: 0.01, 
+    beforeUpdate: function(effect) { $(element).style.height = '0px'; },
+    afterFinish: function(effect) {
+      new ScriptaculousEffect.Parallel(
+        [ new ScriptaculousEffect.Opacity(element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
+          new ScriptaculousEffect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }),
+          new ScriptaculousEffect.Scale(element, 100, { 
+            scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, 
+            sync: true, scaleFrom: 0, scaleTo: 100, transition: scaleTransition })],
+        options); }
+    });
+}
+
+ScriptaculousEffect.Shrink = function(element) {
+  element = $(element);
+  var options = arguments[1] || {};
+  
+  var originalWidth = element.clientWidth;
+  var originalHeight = element.clientHeight;
+  element.style.overflow = 'hidden';
+  Element.show(element);
+
+  var direction = options.direction || 'center';
+  var moveTransition = options.moveTransition || ScriptaculousEffect.Transitions.sinoidal;
+  var scaleTransition = options.scaleTransition || ScriptaculousEffect.Transitions.sinoidal;
+  var opacityTransition = options.opacityTransition || ScriptaculousEffect.Transitions.none;
+  
+  var moveX, moveY;
+  
+  switch (direction) {
+    case 'top-left':
+      moveX = moveY = 0;
+      break;
+    case 'top-right':
+      moveX = originalWidth;
+      moveY = 0;
+      break;
+    case 'bottom-left':
+      moveX = 0;
+      moveY = originalHeight;
+      break;
+    case 'bottom-right':
+      moveX = originalWidth;
+      moveY = originalHeight;
+      break;
+    case 'center':  
+      moveX = originalWidth / 2;
+      moveY = originalHeight / 2;
+      break;
+  }
+  
+  return new ScriptaculousEffect.Parallel(
+    [ new ScriptaculousEffect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
+      new ScriptaculousEffect.Scale(element, 0, { sync: true, transition: moveTransition }),
+      new ScriptaculousEffect.MoveBy(element, moveY, moveX, { sync: true, transition: scaleTransition }) ],
+    options);
+}
+
+ScriptaculousEffect.Pulsate = function(element) {
+  element = $(element);
+  var options    = arguments[1] || {};
+  var transition = options.transition || ScriptaculousEffect.Transitions.sinoidal;
+  var reverser   = function(pos){ return transition(1-ScriptaculousEffect.Transitions.pulse(pos)) };
+  reverser.bind(transition);
+  return new ScriptaculousEffect.Opacity(element, 
+    Object.extend(Object.extend({  duration: 3.0,
+       afterFinish: function(effect) { Element.show(effect.element); }
+    }, options), {transition: reverser}));
+}
+
+ScriptaculousEffect.Fold = function(element) {
+ element = $(element);
+ element.style.overflow = 'hidden';
+ return new ScriptaculousEffect.Scale(element, 5, Object.extend({   
+   scaleContent: false,
+   scaleTo: 100,
+   scaleX: false,
+   afterFinish: function(effect) {
+   new ScriptaculousEffect.Scale(element, 1, { 
+     scaleContent: false, 
+     scaleTo: 0,
+     scaleY: false,
+     afterFinish: function(effect) { Element.hide(effect.element) } });
+ }}, arguments[1] || {}));
+}
+
+// old: new ScriptaculousEffect.ContentZoom(element, percent)
+// new: Element.setContentZoom(element, percent) 
+
+Element.setContentZoom = function(element, percent) {
+  element = $(element);
+  element.style.fontSize = (percent/100) + "em";  
+  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+}
index 03aa1db3bbfbba9ba4d07838b11678b150dce9b8..cfe9b9857483c6ab6f45e4957eef77ebbbfdb2ab 100644 (file)
@@ -11,5 +11,8 @@
 <script type="text/javascript" src="static/wclib.js"></script>
 <script type="text/javascript" src="static/prototype.js"></script>
 <script type="text/javascript" src="static/rico.js"></script>
+
+<script type="text/javascript" src="static/scriptaculous.js"></script>
+
 </head>
 <body>
index 006d60332005b248cb73c35a0aa7a4e428fab0f5..ed7d920cb5f21fde81089940afe6d3832642c117 100644 (file)
@@ -1,4 +1,4 @@
-/*  Prototype: an object-oriented Javascript library, version 1.2.1
+/*  Prototype JavaScript framework, version 1.4.0_pre4
  *  (c) 2005 Sam Stephenson <sam@conio.net>
  *
  *  THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
 /*--------------------------------------------------------------------------*/
 
 var Prototype = {
-  Version: '1.2.1'
+  Version: '1.4.0_pre4',
+  
+  emptyFunction: function() {},
+  K: function(x) {return x}
 }
 
 var Class = {
@@ -24,24 +27,24 @@ var Class = {
 
 var Abstract = new Object();
 
-Object.prototype.extend = function(object) {
-  for (property in object) {
-    this[property] = object[property];
+Object.extend = function(destination, source) {
+  for (property in source) {
+    destination[property] = source[property];
   }
-  return this;
+  return destination;
 }
 
 Function.prototype.bind = function(object) {
-  var method = this;
+  var __method = this;
   return function() {
-    method.apply(object, arguments);
+    return __method.apply(object, arguments);
   }
 }
 
 Function.prototype.bindAsEventListener = function(object) {
-  var method = this;
+  var __method = this;
   return function(event) {
-    method.call(object, event || window.event);
+    return __method.call(object, event || window.event);
   }
 }
 
@@ -54,7 +57,7 @@ Number.prototype.toColorPart = function() {
 var Try = {
   these: function() {
     var returnValue;
-    
+
     for (var i = 0; i < arguments.length; i++) {
       var lambda = arguments[i];
       try {
@@ -62,7 +65,7 @@ var Try = {
         break;
       } catch (e) {}
     }
-    
+
     return returnValue;
   }
 }
@@ -75,14 +78,14 @@ PeriodicalExecuter.prototype = {
     this.callback = callback;
     this.frequency = frequency;
     this.currentlyExecuting = false;
-    
+
     this.registerCallback();
   },
-  
+
   registerCallback: function() {
-    setTimeout(this.onTimerEvent.bind(this), this.frequency * 1000);
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
   },
-  
+
   onTimerEvent: function() {
     if (!this.currentlyExecuting) {
       try { 
@@ -92,8 +95,6 @@ PeriodicalExecuter.prototype = {
         this.currentlyExecuting = false;
       }
     }
-    
-    this.registerCallback();
   }
 }
 
@@ -101,7 +102,7 @@ PeriodicalExecuter.prototype = {
 
 function $() {
   var elements = new Array();
-  
+
   for (var i = 0; i < arguments.length; i++) {
     var element = arguments[i];
     if (typeof element == 'string')
@@ -109,15 +110,13 @@ function $() {
 
     if (arguments.length == 1) 
       return element;
-      
+
     elements.push(element);
   }
-  
+
   return elements;
 }
 
-/*--------------------------------------------------------------------------*/
-
 if (!Array.prototype.push) {
   Array.prototype.push = function() {
                var startLength = this.length;
@@ -135,18 +134,255 @@ if (!Function.prototype.apply) {
     if (!parameters) parameters = new Array();
     
     for (var i = 0; i < parameters.length; i++)
-      parameterStrings[i] = 'x[' + i + ']';
+      parameterStrings[i] = 'parameters[' + i + ']';
     
     object.__apply__ = this;
-    var result = eval('obj.__apply__(' + 
-      parameterStrings[i].join(', ') + ')');
+    var result = eval('object.__apply__(' + 
+      parameterStrings.join(', ') + ')');
     object.__apply__ = null;
     
     return result;
   }
 }
 
-/*--------------------------------------------------------------------------*/
+Object.extend(String.prototype, {
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  escapeHTML: function() {
+    var div = document.createElement('div');
+    var text = document.createTextNode(this);
+    div.appendChild(text);
+    return div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0].nodeValue;
+  },
+  
+  parseQuery: function() {
+    var str = this;
+    if (str.substring(0,1) == '?') {
+      str = this.substring(1);
+    }
+    var result = {};
+    var pairs = str.split('&');
+    for (var i = 0; i < pairs.length; i++) {
+      var pair = pairs[i].split('=');
+      result[pair[0]] = pair[1];
+    }
+    return result;
+  }
+});
+
+
+var _break    = new Object();
+var _continue = new Object();
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        try {
+          iterator(value, index++);
+        } catch (e) {
+          if (e != _continue) throw e;
+        }
+      });
+    } catch (e) {
+      if (e != _break) throw e;
+    }
+  },
+  
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      if (!(result &= (iterator || Prototype.K)(value, index))) 
+        throw _break;
+    });
+    return result;
+  },
+  
+  any: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      if (result &= (iterator || Prototype.K)(value, index)) 
+        throw _break;
+    });
+    return result;
+  },
+  
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(iterator(value, index));
+    });
+    return results;
+  },
+  
+  detect: function (iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw _break;
+      }
+    });
+    return result;
+  },
+  
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+  
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+  
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw _break;
+      }
+    });
+    return found;
+  },
+  
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+  
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.collect(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+  
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (value >= (result || value))
+        result = value;
+    });
+    return result;
+  },
+  
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (value <= (result || value))
+        result = value;
+    });
+    return result;
+  },
+  
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ? 
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+  
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+  
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+  
+  sortBy: function(iterator) {
+    return this.collect(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+  
+  toArray: function() {
+    return this.collect(Prototype.K);
+  },
+  
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      iterator(value = collections.pluck(index));
+      return value;
+    });
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+
+$A = Array.from = function(iterable) {
+  var results = [];
+  for (var i = 0; i < iterable.length; i++)
+    results.push(iterable[i]);
+  return results;
+}
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0; i < this.length; i++)
+      iterator(this[i]);
+  },
+  
+  first: function() {
+    return this[0];
+  },
+  
+  last: function() {
+    return this[this.length - 1];
+  }
+});
+
+Object.extend(Array.prototype, Enumerable);
+
 
 var Ajax = {
   getTransport: function() {
@@ -155,9 +391,7 @@ var Ajax = {
       function() {return new ActiveXObject('Microsoft.XMLHTTP')},
       function() {return new XMLHttpRequest()}
     ) || false;
-  },
-  
-  emptyFunction: function() {}
+  }
 }
 
 Ajax.Base = function() {};
@@ -167,7 +401,18 @@ Ajax.Base.prototype = {
       method:       'post',
       asynchronous: true,
       parameters:   ''
-    }.extend(options || {});
+    }
+    Object.extend(this.options, options || {});
+  },
+
+  responseIsSuccess: function() {
+    return this.transport.status == undefined
+        || this.transport.status == 0 
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  responseIsFailure: function() {
+    return !this.responseIsSuccess();
   }
 }
 
@@ -175,97 +420,358 @@ Ajax.Request = Class.create();
 Ajax.Request.Events = 
   ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
 
-Ajax.Request.prototype = (new Ajax.Base()).extend({
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
   initialize: function(url, options) {
     this.transport = Ajax.getTransport();
     this.setOptions(options);
-  
+    this.request(url);
+  },
+
+  request: function(url) {
+    var parameters = this.options.parameters || '';
+    if (parameters.length > 0) parameters += '&_=';
+
     try {
       if (this.options.method == 'get')
-        url += '?' + this.options.parameters + '&_=';
-    
+        url += '?' + parameters;
+
       this.transport.open(this.options.method, url,
         this.options.asynchronous);
-      
+
       if (this.options.asynchronous) {
         this.transport.onreadystatechange = this.onStateChange.bind(this);
         setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
       }
-              
-      this.transport.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
-      this.transport.setRequestHeader('X-Prototype-Version',
-        Prototype.Version);
-
-      if (this.options.method == 'post') {
-        this.transport.setRequestHeader('Connection', 'close');
-        this.transport.setRequestHeader('Content-type',
-          'application/x-www-form-urlencoded');
-      }
 
-      if (this.options.requestHeaders) {
-         for (var i=0; i< (this.options.requestHeaders.length-1);i+=2)
-            this.transport.setRequestHeader(this.options.requestHeaders[i],
-                                            this.options.requestHeaders[i+1]);
-      }
+      this.setRequestHeaders();
 
-      var sendData = this.options.postBody   ? this.options.postBody
-                   : this.options.parameters ? this.options.parameters + '&_'
-                   : null;
+      var body = this.options.postBody ? this.options.postBody : parameters;
+      this.transport.send(this.options.method == 'post' ? body : null);
 
-      this.transport.send(this.options.method == 'post' ? sendData : null );
-                      
     } catch (e) {
-    }    
+    }
   },
-      
+
+  setRequestHeaders: function() {
+    var requestHeaders = 
+      ['X-Requested-With', 'XMLHttpRequest',
+       'X-Prototype-Version', Prototype.Version];
+
+    if (this.options.method == 'post') {
+      requestHeaders.push('Content-type', 
+        'application/x-www-form-urlencoded');
+
+      /* Force "Connection: close" for Mozilla browsers to work around
+       * a bug where XMLHttpReqeuest sends an incorrect Content-length
+       * header. See Mozilla Bugzilla #246651. 
+       */
+      if (this.transport.overrideMimeType)
+        requestHeaders.push('Connection', 'close');
+    }
+
+    if (this.options.requestHeaders)
+      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
+
+    for (var i = 0; i < requestHeaders.length; i += 2)
+      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
+  },
+
   onStateChange: function() {
     var readyState = this.transport.readyState;
     if (readyState != 1)
       this.respondToReadyState(this.transport.readyState);
   },
-  
+
   respondToReadyState: function(readyState) {
     var event = Ajax.Request.Events[readyState];
-    (this.options['on' + event] || Ajax.emptyFunction)(this.transport);
+
+    if (event == 'Complete')
+      (this.options['on' + this.transport.status]
+       || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+       || Prototype.emptyFunction)(this.transport);
+
+    (this.options['on' + event] || Prototype.emptyFunction)(this.transport);
+
+    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
+    if (event == 'Complete')
+      this.transport.onreadystatechange = Prototype.emptyFunction;
   }
 });
 
 Ajax.Updater = Class.create();
-Ajax.Updater.prototype = (new Ajax.Base()).extend({
+Ajax.Updater.ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:<\/script>)';
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
   initialize: function(container, url, options) {
-    this.container = $(container);
-    this.setOptions(options);
-  
-    if (this.options.asynchronous) {
-      this.onComplete = this.options.onComplete;
-      this.options.onComplete = this.updateContent.bind(this);
+    this.containers = {
+      success: container.success ? $(container.success) : $(container),
+      failure: container.failure ? $(container.failure) :
+        (container.success ? null : $(container))
     }
 
-    this.request = new Ajax.Request(url, this.options);
-    
-    if (!this.options.asynchronous)
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function() {
       this.updateContent();
+      onComplete(this.transport);
+    }).bind(this);
+
+    this.request(url);
   },
-  
+
   updateContent: function() {
-    if (this.request.transport.status == 200) {
+    var receiver = this.responseIsSuccess() ?
+      this.containers.success : this.containers.failure;
+
+    var match    = new RegExp(Ajax.Updater.ScriptFragment, 'img');
+    var response = this.transport.responseText.replace(match, '');
+    var scripts  = this.transport.responseText.match(match);
+
+    if (receiver) {
       if (this.options.insertion) {
-        new this.options.insertion(this.container,
-        this.request.transport.responseText);
+        new this.options.insertion(receiver, response);
       } else {
-        this.container.innerHTML = this.request.transport.responseText;
+        receiver.innerHTML = response;
       }
-    }  
+    }
+
+    if (this.responseIsSuccess()) {
+      if (this.onComplete)
+        setTimeout((function() {this.onComplete(
+          this.transport)}).bind(this), 10);
+    }
+
+    if (this.options.evalScripts && scripts) {
+      match = new RegExp(Ajax.Updater.ScriptFragment, 'im');
+      setTimeout((function() {
+        for (var i = 0; i < scripts.length; i++)
+          eval(scripts[i].match(match)[1]);
+      }).bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = 1;
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Ajax.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ? 
+        this.decay * this.options.decay : 1);
 
-    if (this.onComplete) {
-      setTimeout((function() {this.onComplete(
-        this.request.transport)}).bind(this), 10);
+      this.lastText = request.responseText;
     }
+    this.timer = setTimeout(this.onTimerEvent.bind(this), 
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
   }
 });
 
+document.getElementsByClassName = function(className) {
+  var children = document.getElementsByTagName('*') || document.all;
+  var elements = new Array();
+  
+  for (var i = 0; i < children.length; i++) {
+    var child = children[i];
+    var classNames = child.className.split(' ');
+    for (var j = 0; j < classNames.length; j++) {
+      if (classNames[j] == className) {
+        elements.push(child);
+        break;
+      }
+    }
+  }
+  
+  return elements;
+}
+
 /*--------------------------------------------------------------------------*/
 
+if (!window.Element) {
+  var Element = new Object();
+}
+
+Object.extend(Element, {
+  toggle: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = 
+        (element.style.display == 'none' ? '' : 'none');
+    }
+  },
+
+  hide: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = 'none';
+    }
+  },
+
+  show: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = '';
+    }
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+  },
+   
+  getHeight: function(element) {
+    element = $(element);
+    return element.offsetHeight; 
+  },
+
+  hasClassName: function(element, className) {
+    element = $(element);
+    if (!element)
+      return;
+    var a = element.className.split(' ');
+    for (var i = 0; i < a.length; i++) {
+      if (a[i] == className)
+        return true;
+    }
+    return false;
+  },
+
+  addClassName: function(element, className) {
+    element = $(element);
+    Element.removeClassName(element, className);
+    element.className += ' ' + className;
+  },
+
+  removeClassName: function(element, className) {
+    element = $(element);
+    if (!element)
+      return;
+    var newClassName = '';
+    var a = element.className.split(' ');
+    for (var i = 0; i < a.length; i++) {
+      if (a[i] != className) {
+        if (i > 0)
+          newClassName += ' ';
+        newClassName += a[i];
+      }
+    }
+    element.className = newClassName;
+  },
+  
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    var element = $(element);
+    for (var i = 0; i < element.childNodes.length; i++) {
+      var node = element.childNodes[i];
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) 
+        Element.remove(node);
+    }
+  }
+});
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content;
+    
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      this.element.insertAdjacentHTML(this.adjacency, this.content);
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.fragment = this.range.createContextualFragment(this.content);
+      this.insertContent();
+    }
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+  
+  insertContent: function() {
+    this.element.parentNode.insertBefore(this.fragment, this.element);
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+  
+  insertContent: function() {  
+    this.element.insertBefore(this.fragment, this.element.firstChild);
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+  
+  insertContent: function() {
+    this.element.appendChild(this.fragment);
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+  
+  insertContent: function() {
+    this.element.parentNode.insertBefore(this.fragment, 
+      this.element.nextSibling);
+  }
+});
+
 var Field = {
   clear: function() {
     for (var i = 0; i < arguments.length; i++)
@@ -309,7 +815,7 @@ var Form = {
   },
   
   getElements: function(form) {
-    form = $(form);
+    var form = $(form);
     var elements = new Array();
 
     for (tagName in Form.Element.Serializers) {
@@ -320,17 +826,44 @@ var Form = {
     return elements;
   },
   
+  getInputs: function(form, typeName, name) {
+    var form = $(form);
+    var inputs = form.getElementsByTagName('input');
+    
+    if (!typeName && !name)
+      return inputs;
+      
+    var matchingInputs = new Array();
+    for (var i = 0; i < inputs.length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) ||
+          (name && input.name != name)) 
+        continue;
+      matchingInputs.push(input);
+    }
+
+    return matchingInputs;
+  },
+
   disable: function(form) {
     var elements = Form.getElements(form);
     for (var i = 0; i < elements.length; i++) {
       var element = elements[i];
       element.blur();
-      element.disable = 'true';
+      element.disabled = 'true';
+    }
+  },
+
+  enable: function(form) {
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.disabled = '';
     }
   },
 
   focusFirstElement: function(form) {
-    form = $(form);
+    var form = $(form);
     var elements = Form.getElements(form);
     for (var i = 0; i < elements.length; i++) {
       var element = elements[i];
@@ -348,7 +881,7 @@ var Form = {
 
 Form.Element = {
   serialize: function(element) {
-    element = $(element);
+    var element = $(element);
     var method = element.tagName.toLowerCase();
     var parameter = Form.Element.Serializers[method](element);
     
@@ -358,7 +891,7 @@ Form.Element = {
   },
   
   getValue: function(element) {
-    element = $(element);
+    var element = $(element);
     var method = element.tagName.toLowerCase();
     var parameter = Form.Element.Serializers[method](element);
     
@@ -370,6 +903,7 @@ Form.Element = {
 Form.Element.Serializers = {
   input: function(element) {
     switch (element.type.toLowerCase()) {
+      case 'submit':
       case 'hidden':
       case 'password':
       case 'text':
@@ -391,9 +925,20 @@ Form.Element.Serializers = {
   },
 
   select: function(element) {
-    var index = element.selectedIndex;
-    var value = element.options[index].value || element.options[index].text;
-    return [element.name, (index >= 0) ? value : ''];
+    var value = '';
+    if (element.type == 'select-one') {
+      var index = element.selectedIndex;
+      if (index >= 0)
+        value = element.options[index].value || element.options[index].text;
+    } else {
+      value = new Array();
+      for (var i = 0; i < element.length; i++) {
+        var opt = element.options[i];
+        if (opt.selected)
+          value.push(opt.value || opt.text);
+      }
+    }
+    return [element.name, value];
   }
 }
 
@@ -415,7 +960,7 @@ Abstract.TimedObserver.prototype = {
   },
   
   registerCallback: function() {
-    setTimeout(this.onTimerEvent.bind(this), this.frequency * 1000);
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
   },
   
   onTimerEvent: function() {
@@ -424,355 +969,289 @@ Abstract.TimedObserver.prototype = {
       this.callback(this.element, value);
       this.lastValue = value;
     }
-    
-    this.registerCallback();
   }
 }
 
 Form.Element.Observer = Class.create();
-Form.Element.Observer.prototype = (new Abstract.TimedObserver()).extend({
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
   getValue: function() {
     return Form.Element.getValue(this.element);
   }
 });
 
 Form.Observer = Class.create();
-Form.Observer.prototype = (new Abstract.TimedObserver()).extend({
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
   getValue: function() {
     return Form.serialize(this.element);
   }
 });
 
-
 /*--------------------------------------------------------------------------*/
 
-document.getElementsByClassName = function(className) {
-  var children = document.getElementsByTagName('*') || document.all;
-  var elements = new Array();
-  
-  for (var i = 0; i < children.length; i++) {
-    var child = children[i];
-    var classNames = child.className.split(' ');
-    for (var j = 0; j < classNames.length; j++) {
-      if (classNames[j] == className) {
-        elements.push(child);
-        break;
-      }
-    }
-  }
-  
-  return elements;
-}
-
-/*--------------------------------------------------------------------------*/
-
-var Element = {
-  toggle: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = 
-        (element.style.display == 'none' ? '' : 'none');
-    }
-  },
-
-  hide: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = 'none';
-    }
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+    
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
   },
-
-  show: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = '';
+  
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
     }
   },
-
-  remove: function(element) {
-    element = $(element);
-    element.parentNode.removeChild(element);
+  
+  registerFormCallbacks: function() {
+    var elements = Form.getElements(this.element);
+    for (var i = 0; i < elements.length; i++)
+      this.registerCallback(elements[i]);
   },
-   
-  getHeight: function(element) {
-    element = $(element);
-    return element.offsetHeight; 
+  
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':  
+        case 'radio':
+          element.target = this;
+          element.prev_onclick = element.onclick || Prototype.emptyFunction;
+          element.onclick = function() {
+            this.prev_onclick(); 
+            this.target.onElementEvent();
+          }
+          break;
+        case 'password':
+        case 'text':
+        case 'textarea':
+        case 'select-one':
+        case 'select-multiple':
+          element.target = this;
+          element.prev_onchange = element.onchange || Prototype.emptyFunction;
+          element.onchange = function() {
+            this.prev_onchange(); 
+            this.target.onElementEvent();
+          }
+          break;
+      }
+    }    
   }
 }
 
-var Toggle = new Object();
-Toggle.display = Element.toggle;
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
 
-/*--------------------------------------------------------------------------*/
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
 
-Abstract.Insertion = function(adjacency) {
-  this.adjacency = adjacency;
-}
 
-Abstract.Insertion.prototype = {
-  initialize: function(element, content) {
-    this.element = $(element);
-    this.content = content;
-    
-    if (this.adjacency && this.element.insertAdjacentHTML) {
-      this.element.insertAdjacentHTML(this.adjacency, this.content);
-    } else {
-      this.range = this.element.ownerDocument.createRange();
-      if (this.initializeRange) this.initializeRange();
-      this.fragment = this.range.createContextualFragment(this.content);
-      this.insertContent();
-    }
-  }
+if (!window.Event) {
+  var Event = new Object();
 }
 
-var Insertion = new Object();
-
-Insertion.Before = Class.create();
-Insertion.Before.prototype = (new Abstract.Insertion('beforeBegin')).extend({
-  initializeRange: function() {
-    this.range.setStartBefore(this.element);
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+
+  element: function(event) {
+    return event.target || event.srcElement;
   },
-  
-  insertContent: function() {
-    this.element.parentNode.insertBefore(this.fragment, this.element);
-  }
-});
 
-Insertion.Top = Class.create();
-Insertion.Top.prototype = (new Abstract.Insertion('afterBegin')).extend({
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(true);
+  isLeftClick: function(event) {
+    return (((event.which) && (event.which == 1)) ||
+            ((event.button) && (event.button == 1)));
   },
-  
-  insertContent: function() {  
-    this.element.insertBefore(this.fragment, this.element.firstChild);
-  }
-});
 
-Insertion.Bottom = Class.create();
-Insertion.Bottom.prototype = (new Abstract.Insertion('beforeEnd')).extend({
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(this.element);
+  pointerX: function(event) {
+    return event.pageX || (event.clientX + 
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
   },
-  
-  insertContent: function() {
-    this.element.appendChild(this.fragment);
-  }
-});
 
-Insertion.After = Class.create();
-Insertion.After.prototype = (new Abstract.Insertion('afterEnd')).extend({
-  initializeRange: function() {
-    this.range.setStartAfter(this.element);
+  pointerY: function(event) {
+    return event.pageY || (event.clientY + 
+      (document.documentElement.scrollTop || document.body.scrollTop));
   },
-  
-  insertContent: function() {
-    this.element.parentNode.insertBefore(this.fragment, 
-      this.element.nextSibling);
-  }
-});
-
-/*--------------------------------------------------------------------------*/
 
-var Effect = new Object();
+  stop: function(event) {
+    if (event.preventDefault) { 
+      event.preventDefault(); 
+      event.stopPropagation(); 
+    } else {
+      event.returnValue = false;
+    }
+  },
 
-Effect.Highlight = Class.create();
-Effect.Highlight.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-    this.start  = 153;
-    this.finish = 255;
-    this.current = this.start;
-    this.fade();
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode && (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
   },
+
+  observers: false,
   
-  fade: function() {
-    if (this.isFinished()) return;
-    if (this.timer) clearTimeout(this.timer);
-    this.highlight(this.element, this.current);
-    this.current += 17;
-    this.timer = setTimeout(this.fade.bind(this), 250);
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
   },
   
-  isFinished: function() {
-    return this.current > this.finish;
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0; i < Event.observers.length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
   },
-  
-  highlight: function(element, current) {
-    element.style.backgroundColor = "#ffff" + current.toColorPart();
-  }
-}
-
 
-Effect.Fade = Class.create();
-Effect.Fade.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-    this.start  = 100;
-    this.finish = 0;
-    this.current = this.start;
-    this.fade();
-  },
-  
-  fade: function() {
-    if (this.isFinished()) { this.element.style.display = 'none'; return; }
-    if (this.timer) clearTimeout(this.timer);
-    this.setOpacity(this.element, this.current);
-    this.current -= 10;
-    this.timer = setTimeout(this.fade.bind(this), 50);
-  },
-  
-  isFinished: function() {
-    return this.current <= this.finish;
+  observe: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+    
+    if (name == 'keypress' &&
+        ((/Konqueror|Safari|KHTML/.test(navigator.userAgent)) 
+        || element.attachEvent))
+      name = 'keydown';
+    
+    this._observeAndCache(element, name, observer, useCapture);
   },
-  
-  setOpacity: function(element, opacity) {
-    opacity = (opacity == 100) ? 99.999 : opacity;
-    element.style.filter = "alpha(opacity:"+opacity+")";
-    element.style.opacity = opacity/100 /*//*/;
-  }
-}
 
-Effect.Scale = Class.create();
-Effect.Scale.prototype = {
-  initialize: function(element, percent) {
-    this.element = $(element);
-    this.startScale    = 1.0;
-    this.startHeight   = this.element.offsetHeight;
-    this.startWidth    = this.element.offsetWidth;
-    this.currentHeight = this.startHeight;
-    this.currentWidth  = this.startWidth;
-    this.finishScale   = (percent/100) /*//*/;
-    if (this.element.style.fontSize=="") this.sizeEm = 1.0;
-    if (this.element.style.fontSize.indexOf("em")>0)
-       this.sizeEm      = parseFloat(this.element.style.fontSize);
-    if(this.element.effect_scale) {
-      clearTimeout(this.element.effect_scale.timer);
-      this.startScale  = this.element.effect_scale.currentScale;
-      this.startHeight = this.element.effect_scale.startHeight;
-      this.startWidth  = this.element.effect_scale.startWidth;
-      if(this.element.effect_scale.sizeEm) 
-        this.sizeEm    = this.element.effect_scale.sizeEm;      
-    }
-    this.element.effect_scale = this;
-    this.currentScale  = this.startScale;
-    this.factor        = this.finishScale - this.startScale;
-    this.options       = arguments[2] || {}; 
-    this.scale();
-  },
-  
-  scale: function() {
-    if (this.isFinished()) { 
-      this.setDimensions(this.element, this.startWidth*this.finishScale, this.startHeight*this.finishScale);
-      if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.finishScale + "em";
-      if(this.options.complete) this.options.complete(this);
-      return; 
-    }
-    if (this.timer) clearTimeout(this.timer);
-    if (this.options.step) this.options.step(this);
-    this.setDimensions(this.element, this.currentWidth, this.currentHeight);
-    if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.currentScale + "em";
-    this.currentScale += (this.factor/10) /*//*/;
-    this.currentWidth = this.startWidth * this.currentScale;
-    this.currentHeight = this.startHeight * this.currentScale;
-    this.timer = setTimeout(this.scale.bind(this), 50);
-  },
-  
-  isFinished: function() {
-    return (this.factor < 0) ? 
-      this.currentScale <= this.finishScale : this.currentScale >= this.finishScale;
-  },
-  
-  setDimensions: function(element, width, height) {
-    element.style.width = width + 'px';
-    element.style.height = height + 'px';
+  stopObserving: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+    
+    if (name == 'keypress' &&
+        ((/Konqueror|Safari|KHTML/.test(navigator.userAgent)) 
+        || element.detachEvent))
+      name = 'keydown';
+    
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      element.detachEvent('on' + name, observer);
+    }
   }
-}
+});
 
-Effect.Squish = Class.create();
-Effect.Squish.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-    new Effect.Scale(this.element, 1, { complete: this.hide.bind(this) } );
+/* prevent memory leaks in IE */
+Event.observe(window, 'unload', Event.unloadCache, false);
+
+var Position = {
+
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false, 
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset 
+                || document.documentElement.scrollLeft 
+                || document.body.scrollLeft 
+                || 0;
+    this.deltaY =  window.pageYOffset 
+                || document.documentElement.scrollTop 
+                || document.body.scrollTop 
+                || 0;
   },
-  hide: function() {
-    this.element.style.display = 'none';
-  } 
-}
 
-Effect.Puff = Class.create();
-Effect.Puff.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-    this.opacity = 100;
-    this.startTop  = this.element.top || this.element.offsetTop;
-    this.startLeft = this.element.left || this.element.offsetLeft;
-    new Effect.Scale(this.element, 200, { step: this.fade.bind(this), complete: this.hide.bind(this) } );
-  },
-  fade: function(effect) {
-    topd    = (((effect.currentScale)*effect.startHeight) - effect.startHeight)/2;
-    leftd   = (((effect.currentScale)*effect.startWidth) - effect.startWidth)/2;
-    this.element.style.position='absolute';
-    this.element.style.top = this.startTop-topd + "px";
-    this.element.style.left = this.startLeft-leftd + "px";
-    this.opacity -= 10;
-    this.setOpacity(this.element, this.opacity); 
-    if(navigator.appVersion.indexOf('AppleWebKit')>0) this.element.innerHTML += ''; //force redraw on safari
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0; 
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
   },
-  hide: function() {
-    this.element.style.display = 'none';
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
   },
-  setOpacity: function(element, opacity) {
-    opacity = (opacity == 100) ? 99.999 : opacity;
-    element.style.filter = "alpha(opacity:"+opacity+")";
-    element.style.opacity = opacity/100 /*//*/;
-  }
-}
 
-Effect.Appear = Class.create();
-Effect.Appear.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-    this.start  = 0;
-    this.finish = 100;
-    this.current = this.start;
-    this.fade();
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] && 
+            x <  this.offset[0] + element.offsetWidth);
   },
-  
-  fade: function() {
-    if (this.isFinished()) return;
-    if (this.timer) clearTimeout(this.timer);
-    this.setOpacity(this.element, this.current);
-    this.current += 10;
-    this.timer = setTimeout(this.fade.bind(this), 50);
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = this.realOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = this.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] && 
+            this.xcomp <  this.offset[0] + element.offsetWidth);
   },
-  
-  isFinished: function() {
-    return this.current > this.finish;
+
+  // within must be called directly before
+  overlap: function(mode, element) {  
+    if (!mode) return 0;  
+    if (mode == 'vertical') 
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) / 
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) / 
+        element.offsetWidth;
   },
-  
-  setOpacity: function(element, opacity) {
-    opacity = (opacity == 100) ? 99.999 : opacity;
-    element.style.filter = "alpha(opacity:"+opacity+")";
-    element.style.opacity = opacity/100 /*//*/;
-    element.style.display = '';
-  }
-}
 
-Effect.ContentZoom = Class.create();
-Effect.ContentZoom.prototype = {
-  initialize: function(element, percent) {
-    this.element = $(element);
-    if (this.element.style.fontSize=="") this.sizeEm = 1.0;
-    if (this.element.style.fontSize.indexOf("em")>0)
-       this.sizeEm = parseFloat(this.element.style.fontSize);
-    if(this.element.effect_contentzoom) {
-      this.sizeEm = this.element.effect_contentzoom.sizeEm;
-    }
-    this.element.effect_contentzoom = this;
-    this.element.style.fontSize = this.sizeEm*(percent/100) + "em" /*//*/;
-    if(navigator.appVersion.indexOf('AppleWebKit')>0) { this.element.scrollTop -= 1; };
+  clone: function(source, target) {
+    source = $(source);
+    target = $(target);
+    target.style.position = 'absolute';
+    var offsets = this.cumulativeOffset(source);
+    target.style.top    = offsets[1] + 'px';
+    target.style.left   = offsets[0] + 'px';
+    target.style.width  = source.offsetWidth + 'px';
+    target.style.height = source.offsetHeight + 'px';
   }
 }
index d17acb1e1caf88adc3585a75d8270438609e0ad9..ed07dad9ea066a6c6a40f6cd008743cf32011be8 100644 (file)
@@ -1,5 +1,3 @@
-/* openrico.org rico.js */
 /**
   *
   *  Copyright 2005 Sabre Airline Solutions
   *  and limitations under the License.
   **/
 
-// rico.js --------------------
 
+//-------------------- rico.js
 var Rico = {
-  Version: '1.1-beta'
+  Version: '1.1-beta2'
 }
 
 Rico.ArrayExtensions = new Array();
@@ -122,10 +120,9 @@ document.getElementsByTagAndClassName = function(tagName, className) {
 
   return elements;
 }
-// ricoAccordion.js --------------------
+
+
+//-------------------- ricoAccordion.js
 
 Rico.Accordion = Class.create();
 
@@ -305,10 +302,9 @@ Rico.Accordion.Tab.prototype = {
    }
 
 };
-// ricoAjaxEngine.js --------------------
+
+
+//-------------------- ricoAjaxEngine.js
 
 Rico.AjaxEngine = Class.create();
 
@@ -469,34 +465,15 @@ Rico.AjaxEngine.prototype = {
    },
 
    _processAjaxElementUpdate: function( ajaxElement, responseElement ) {
-      if ( responseElement.xml != undefined )
-         this._processAjaxElementUpdateIE( ajaxElement, responseElement );
-      else
-         this._processAjaxElementUpdateMozilla( ajaxElement, responseElement );
-   },
-
-   _processAjaxElementUpdateIE: function( ajaxElement, responseElement ) {
-      var newHTML = "";
-      for ( var i = 0 ; i < responseElement.childNodes.length ; i++ )
-         newHTML += responseElement.childNodes[i].xml;
+      ajaxElement.innerHTML = RicoUtil.getContentAsString(responseElement);
+   }
 
-      ajaxElement.innerHTML = newHTML;
-   },
+}
 
-   _processAjaxElementUpdateMozilla: function( ajaxElement, responseElement ) {
-      var xmlSerializer = new XMLSerializer();
-      var newHTML = "";
-      for ( var i = 0 ; i < responseElement.childNodes.length ; i++ )
-         newHTML += xmlSerializer.serializeToString(responseElement.childNodes[i]);
+var ajaxEngine = new Rico.AjaxEngine();
 
-      ajaxElement.innerHTML = newHTML;
-   }
-}
 
-var ajaxEngine = new Rico.AjaxEngine(); 
-// ricoColor.js --------------------
+//-------------------- ricoColor.js
 Rico.Color = Class.create();
 
 Rico.Color.prototype = {
@@ -725,10 +702,10 @@ Rico.Color.RGBtoHSB = function(r, g, b) {
    }
 
    return { h : hue, s : saturation, b : brightness };
-} 
-// ricoCorner.js --------------------
+}
+
+
+//-------------------- ricoCorner.js
 
 Rico.Corner = {
 
@@ -951,10 +928,10 @@ Rico.Corner = {
    _isTopRounded: function() { return this._hasString(this.options.corners, "all", "top", "tl", "tr"); },
    _isBottomRounded: function() { return this._hasString(this.options.corners, "all", "bottom", "bl", "br"); },
    _hasSingleTextChild: function(el) { return el.childNodes.length == 1 && el.childNodes[0].nodeType == 3; }
-} 
-// ricoDragAndDrop.js --------------------
+}
+
+
+//-------------------- ricoDragAndDrop.js
 Rico.DragAndDrop = Class.create();
 
 Rico.DragAndDrop.prototype = {
@@ -1042,8 +1019,17 @@ Rico.DragAndDrop.prototype = {
       if ( (nsEvent && e.which != 1) || (!nsEvent && e.button != 1))
          return;
 
-      var eventTarget     = e.target ? e.target : e.srcElement;
-      var draggableObject = eventTarget.draggable;
+      var eventTarget      = e.target ? e.target : e.srcElement;
+      var draggableObject  = eventTarget.draggable;
+
+      var candidate = eventTarget;
+      while (draggableObject == null && candidate.parentNode) {
+         candidate = candidate.parentNode;
+         draggableObject = candidate.draggable;
+      }
+   
+      if ( draggableObject == null )
+         return;
 
       this.updateSelection( draggableObject, e.ctrlKey );
 
@@ -1308,10 +1294,10 @@ Rico.DragAndDrop.prototype = {
 }
 
 var dndMgr = new Rico.DragAndDrop();
-dndMgr.initializeEventHandlers(); 
-// ricoDraggable.js --------------------
+dndMgr.initializeEventHandlers();
+
+
+//-------------------- ricoDraggable.js
 Rico.Draggable = Class.create();
 
 Rico.Draggable.prototype = {
@@ -1387,10 +1373,10 @@ Rico.Draggable.prototype = {
       return this.type + ":" + this.htmlElement + ":";
    }
 
-} 
-// ricoDropzone.js --------------------
+}
+
+
+//-------------------- ricoDropzone.js
 Rico.Dropzone = Class.create();
 
 Rico.Dropzone.prototype = {
@@ -1500,10 +1486,18 @@ Rico.Dropzone.prototype = {
          htmlElement.appendChild(theGUI);
       }
    }
-} 
-// ricoEffects.js --------------------
+}
+
+
+//-------------------- ricoEffects.js
+
+/**
+  *  Use the Effect namespace for effects.  If using scriptaculous effects
+  *  this will already be defined, otherwise we'll just create an empty
+  *  object for it...
+ **/
+if ( window.Effect == undefined )
+   Effect = {};
 
 Effect.SizeAndPosition = Class.create();
 Effect.SizeAndPosition.prototype = {
@@ -1736,10 +1730,9 @@ Effect.AccordionSize.prototype = {
    }
 
 };
-// ricoLiveGrid.js --------------------
+
+
+//-------------------- ricoLiveGrid.js
 
 // Rico.LiveGridMetaData -----------------------------------------------------
 
@@ -1747,17 +1740,17 @@ Rico.LiveGridMetaData = Class.create();
 
 Rico.LiveGridMetaData.prototype = {
 
-   initialize: function( pageSize, totalRows, options ) {
+   initialize: function( pageSize, totalRows, columnCount, options ) {
       this.pageSize  = pageSize;
       this.totalRows = totalRows;
       this.setOptions(options);
       this.scrollArrowHeight = 16;
+      this.columnCount = columnCount;
    },
 
    setOptions: function(options) {
       this.options = {
          largeBufferSize    : 7.0,   // 7 pages
-         smallBufferSize    : 1.0,   // 1 page
          nearLimitFactor    : 0.2    // 20% of buffer
       }.extend(options || {});
    },
@@ -1778,17 +1771,9 @@ Rico.LiveGridMetaData.prototype = {
       return parseInt(this.options.largeBufferSize * this.pageSize);
    },
 
-   getSmallBufferSize: function() {
-      return parseInt(this.options.smallBufferSize * this.pageSize);
-   },
-
    getLimitTolerance: function() {
       return parseInt(this.getLargeBufferSize() * this.options.nearLimitFactor);
-   },
-
-       getBufferSize: function(isFull) {
-               return isFull ? this.getLargeBufferSize() : this.getSmallBufferSize();
-       }
+   }
 };
 
 // Rico.LiveGridScroller -----------------------------------------------------
@@ -1797,14 +1782,15 @@ Rico.LiveGridScroller = Class.create();
 
 Rico.LiveGridScroller.prototype = {
 
-   initialize: function(liveGrid) {
+   initialize: function(liveGrid, viewPort) {
       this.isIE = navigator.userAgent.toLowerCase().indexOf("msie") >= 0;
       this.liveGrid = liveGrid;
       this.metaData = liveGrid.metaData;
       this.createScrollBar();
       this.scrollTimeout = null;
-      //this.sizeIEHeaderHack();
       this.lastScrollPos = 0;
+      this.viewPort = viewPort;
+      this.rows = new Array();
    },
 
    isUnPlugged: function() {
@@ -1828,9 +1814,7 @@ Rico.LiveGridScroller.prototype = {
    },
 
    createScrollBar: function() {
-      var table = this.liveGrid.table;
-      var visibleHeight = table.offsetHeight;
-
+      var visibleHeight = this.liveGrid.viewPort.visibleHeight();
       // create the outer div...
       this.scrollerDiv  = document.createElement("div");
       var scrollerStyle = this.scrollerDiv.style;
@@ -1844,45 +1828,43 @@ Rico.LiveGridScroller.prototype = {
       // create the inner div...
       this.heightDiv = document.createElement("div");
       this.heightDiv.style.width  = "1px";
+
       this.heightDiv.style.height = parseInt(visibleHeight *
                         this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px" ;
-      this.lineHeight =  visibleHeight/this.metaData.getPageSize();
-
       this.scrollerDiv.appendChild(this.heightDiv);
       this.scrollerDiv.onscroll = this.handleScroll.bindAsEventListener(this);
-      table.parentNode.insertBefore( this.scrollerDiv, table.nextSibling );
+
+     var table = this.liveGrid.table;
+     table.parentNode.parentNode.insertBefore( this.scrollerDiv, table.parentNode.nextSibling );
    },
 
    updateSize: function() {
       var table = this.liveGrid.table;
-      var visibleHeight = table.offsetHeight;
+      var visibleHeight = this.viewPort.visibleHeight();
       this.heightDiv.style.height = parseInt(visibleHeight *
                                   this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px";
    },
 
-   adjustScrollTop: function() {
-       this.unplug();
-       var rem = this.scrollerDiv.scrollTop % this.lineHeight;
-       if (rem != 0) {
-         if (this.lastScrollPos < this.scrollerDiv.scrollTop)
-            this.scrollerDiv.scrollTop = this.scrollerDiv.scrollTop + this.lineHeight -rem;
-         else
-            this.scrollerDiv.scrollTop = this.scrollerDiv.scrollTop - rem;
-      }
-      this.lastScrollPos = this.scrollerDiv.scrollTop;
-       this.plugin();
+   rowToPixel: function(rowOffset) {
+      return (rowOffset / this.metaData.getTotalRows()) * this.heightDiv.offsetHeight
+   },
+   
+   moveScroll: function(rowOffset) {
+      this.scrollerDiv.scrollTop = this.rowToPixel(rowOffset);
+      if ( this.metaData.options.onscroll )
+         this.metaData.options.onscroll( this.liveGrid, rowOffset );    
    },
 
    handleScroll: function() {
-      if ( this.scrollTimeout )
+     if ( this.scrollTimeout )
          clearTimeout( this.scrollTimeout );
 
-          //this.adjustScrollTop();
-          var contentOffset = parseInt(this.scrollerDiv.scrollTop *
-                                   this.metaData.getTotalRows() / this.heightDiv.offsetHeight);
+      var contentOffset = parseInt(this.scrollerDiv.scrollTop / this.viewPort.rowHeight);
       this.liveGrid.requestContentRefresh(contentOffset);
+      this.viewPort.scrollTo(this.scrollerDiv.scrollTop);
+      
       if ( this.metaData.options.onscroll )
-         this.metaData.options.onscroll( contentOffset, this.metaData );
+         this.metaData.options.onscroll( this.liveGrid, contentOffset );
 
       this.scrollTimeout = setTimeout( this.scrollIdle.bind(this), 1200 );
    },
@@ -1899,60 +1881,105 @@ Rico.LiveGridBuffer = Class.create();
 
 Rico.LiveGridBuffer.prototype = {
 
-   initialize: function(metaData) {
+   initialize: function(metaData, viewPort) {
       this.startPos = 0;
       this.size     = 0;
       this.metaData = metaData;
       this.rows     = new Array();
       this.updateInProgress = false;
-   },
-
-   update: function(ajaxResponse,start) {
-
-      this.startPos = start;
-      this.rows = new Array();
-
+      this.viewPort = viewPort;
+      this.maxBufferSize = metaData.getLargeBufferSize() * 2;
+      this.maxFetchSize = metaData.getLargeBufferSize();
+      this.lastOffset = 0;
+   },
+
+   getBlankRow: function() {
+      if (!this.blankRow ) {
+         this.blankRow = new Array();
+         for ( var i=0; i < this.metaData.columnCount ; i++ ) 
+            this.blankRow[i] = "&nbsp;";
+     }
+     return this.blankRow;
+   },
+   
+   loadRows: function(ajaxResponse) {
       var rowsElement = ajaxResponse.getElementsByTagName('rows')[0];
       this.updateUI = rowsElement.getAttribute("update_ui") == "true"
+      var newRows = new Array()
       var trs = rowsElement.getElementsByTagName("tr");
       for ( var i=0 ; i < trs.length; i++ ) {
-         var row = this.rows[i] = new Array();
+         var row = newRows[i] = new Array(); 
          var cells = trs[i].getElementsByTagName("td");
          for ( var j=0; j < cells.length ; j++ ) {
             var cell = cells[j];
             var convertSpaces = cell.getAttribute("convert_spaces") == "true";
-            var cellContent = cell.text != undefined ? cell.text : cell.textContent;
+            var cellContent = RicoUtil.getContentAsString(cell);
             row[j] = convertSpaces ? this.convertSpaces(cellContent) : cellContent;
+            if (!row[j]) 
+               row[j] = '&nbsp;';
          }
       }
-      this.size = trs.length;
+      return newRows;
+   },
+      
+   update: function(ajaxResponse, start) {
+     var newRows = this.loadRows(ajaxResponse);
+      if (this.rows.length == 0) { // initial load
+         this.rows = newRows;
+         this.size = this.rows.length;
+         this.startPos = start;
+         return;
+      }
+      if (start > this.startPos) { //appending
+         if (this.startPos + this.rows.length < start) {
+            this.rows =  newRows;
+            this.startPos = start;//
+         } else {
+              this.rows = this.rows.concat( newRows.slice(0, newRows.length));
+            if (this.rows.length > this.maxBufferSize) {
+               var fullSize = this.rows.length;
+               this.rows = this.rows.slice(this.rows.length - this.maxBufferSize, this.rows.length)
+               this.startPos = this.startPos +  (fullSize - this.rows.length);
+            }
+         }
+      } else { //prepending
+         if (start + newRows.length < this.startPos) {
+            this.rows =  newRows;
+         } else {
+            this.rows = newRows.slice(0, this.startPos).concat(this.rows);
+            if (this.rows.length > this.maxBufferSize) 
+               this.rows = this.rows.slice(0, this.maxBufferSize)
+         }
+         this.startPos =  start;
+      }
+      this.size = this.rows.length;
+   },
+   
+   clear: function() {
+      this.rows = new Array();
+      this.startPos = 0;
+      this.size = 0;
    },
 
-       isFullP: function() {
-          return this.metaData.pageSize != this.size;
-       },
-
-       isClose: function(start) {
-               return (start < this.startPos + this.size + (this.size/2)) &&
-             (start + this.size + (this.size/2) > this.startPos)
-       },
-
-   isInRange: function(start, count) {
-      return (start < this.startPos + this.size) && (start + count > this.startPos)
+   isOverlapping: function(start, size) {
+      return ((start < this.endPos()) && (this.startPos < start + size)) || (this.endPos() == 0)
    },
 
-   isFullyInRange: function(position) {
-      return (position >= this.startPos) && (position+this.metaData.getPageSize()) <= (this.startPos + this.size)
+   isInRange: function(position) {
+      return (position >= this.startPos) && (position + this.metaData.getPageSize() <= this.endPos()); 
+             //&& this.size()  != 0;
    },
 
    isNearingTopLimit: function(position) {
       return position - this.startPos < this.metaData.getLimitTolerance();
    },
 
+   endPos: function() {
+      return this.startPos + this.rows.length;
+   },
+   
    isNearingBottomLimit: function(position) {
-      var myEnd     = position + this.metaData.getPageSize();
-      var bufferEnd = this.startPos + this.size;
-      return bufferEnd - myEnd < this.metaData.getLimitTolerance();
+      return this.endPos() - (position + this.metaData.getPageSize()) < this.metaData.getLimitTolerance();
    },
 
    isAtTop: function() {
@@ -1960,7 +1987,7 @@ Rico.LiveGridBuffer.prototype = {
    },
 
    isAtBottom: function() {
-      return this.startPos + this.size == this.metaData.getTotalRows();
+      return this.endPos() == this.metaData.getTotalRows();
    },
 
    isNearingLimit: function(position) {
@@ -1968,6 +1995,37 @@ Rico.LiveGridBuffer.prototype = {
              ( !this.isAtBottom() && this.isNearingBottomLimit(position) )
    },
 
+   getFetchSize: function(offset) {
+      var adjustedOffset = this.getFetchOffset(offset);
+      var adjustedSize = 0;
+      if (adjustedOffset >= this.startPos) { //apending
+         var endFetchOffset = this.maxFetchSize  + adjustedOffset;
+         if (endFetchOffset > this.metaData.totalRows)
+            endFetchOffset = this.metaData.totalRows;
+         adjustedSize = endFetchOffset - adjustedOffset;   
+      } else {//prepending
+         var adjustedSize = this.startPos - adjustedOffset;
+         if (adjustedSize > this.maxFetchSize)
+            adjustedSize = this.maxFetchSize;
+      }
+      return adjustedSize;
+   }, 
+
+   getFetchOffset: function(offset) {
+      var adjustedOffset = offset;
+      if (offset > this.startPos)  //apending
+         adjustedOffset = (offset > this.endPos()) ? offset :  this.endPos(); 
+      else { //prepending
+         if (offset + this.maxFetchSize >= this.startPos) {
+            var adjustedOffset = this.startPos - this.maxFetchSize;
+            if (adjustedOffset < 0)
+               adjustedOffset = 0;
+         }
+      }
+      this.lastOffset = adjustedOffset;
+      return adjustedOffset;
+   },
+
    getRows: function(start, count) {
       var begPos = start - this.startPos
       var endPos = begPos + count
@@ -1990,10 +2048,105 @@ Rico.LiveGridBuffer.prototype = {
 
 };
 
+
+//Rico.GridViewPort --------------------------------------------------
+Rico.GridViewPort = Class.create();
+
+Rico.GridViewPort.prototype = {
+
+   initialize: function(table, rowHeight, visibleRows, buffer, liveGrid) {
+      this.lastDisplayedStartPos = 0;
+      this.div = table.parentNode;
+      this.table = table
+      this.rowHeight = rowHeight;
+      this.div.style.height = this.rowHeight * visibleRows;
+      this.div.style.overflow = "hidden";
+      this.buffer = buffer;
+      this.liveGrid = liveGrid;
+      this.visibleRows = visibleRows + 1;
+      this.lastPixelOffset = 0;
+      this.startPos = 0;
+   },
+
+   populateRow: function(htmlRow, row) {
+      for (var j=0; j < row.length; j++) {
+         htmlRow.cells[j].innerHTML = row[j]
+      }
+   },
+   
+   bufferChanged: function() {
+      this.refreshContents( parseInt(this.lastPixelOffset / this.rowHeight));
+   },
+   
+   clearRows: function() {
+      if (!this.isBlank) {
+         for (var i=0; i < this.visibleRows; i++)
+            this.populateRow(this.table.rows[i], this.buffer.getBlankRow());
+         this.isBlank = true;
+      }
+   },
+   
+   clearContents: function() {   
+      this.clearRows();
+      this.scrollTo(0);
+      this.startPos = 0;
+      this.lastStartPos = -1;   
+   },
+   
+   refreshContents: function(startPos) {
+      if (startPos == this.lastRowPos && !this.isPartialBlank && !this.isBlank) {
+         return;
+      }
+      if ((startPos + this.visibleRows < this.buffer.startPos)  
+          || (this.buffer.startPos + this.buffer.size < startPos) 
+          || (this.buffer.size == 0)) {
+         this.clearRows();
+         return;
+      }
+      this.isBlank = false;
+      var viewPrecedesBuffer = this.buffer.startPos > startPos
+      var contentStartPos = viewPrecedesBuffer ? this.buffer.startPos: startPos;
+   
+      var contentEndPos = (this.buffer.startPos + this.buffer.size < startPos + this.visibleRows) 
+                                 ? this.buffer.startPos + this.buffer.size
+                                 : startPos + this.visibleRows;       
+      var rowSize = contentEndPos - contentStartPos;
+      var rows = this.buffer.getRows(contentStartPos, rowSize ); 
+      var blankSize = this.visibleRows - rowSize;
+      var blankOffset = viewPrecedesBuffer ? 0: rowSize;
+      var contentOffset = viewPrecedesBuffer ? blankSize: 0;
+
+      for (var i=0; i < rows.length; i++) {//initialize what we have
+        this.populateRow(this.table.rows[i + contentOffset], rows[i]);
+      }       
+      for (var i=0; i < blankSize; i++) {// blank out the rest 
+        this.populateRow(this.table.rows[i + blankOffset], this.buffer.getBlankRow());
+      }
+      this.isPartialBlank = blankSize > 0;
+      this.lastRowPos = startPos;   
+   },
+
+   scrollTo: function(pixelOffset) {      
+      if (this.lastPixelOffset == pixelOffset)
+         return;
+
+      this.refreshContents(parseInt(pixelOffset / this.rowHeight))
+      this.div.scrollTop = pixelOffset % this.rowHeight        
+      
+      this.lastPixelOffset = pixelOffset;
+   },
+   
+   visibleHeight: function() {
+      return parseInt(this.div.style.height);
+   }
+   
+};
+
+
 Rico.LiveGridRequest = Class.create();
 Rico.LiveGridRequest.prototype = {
    initialize: function( requestOffset, options ) {
-               this.requestOffset = requestOffset;
+      this.requestOffset = requestOffset;
    }
 };
 
@@ -2004,28 +2157,62 @@ Rico.LiveGrid = Class.create();
 Rico.LiveGrid.prototype = {
 
    initialize: function( tableId, visibleRows, totalRows, url, options ) {
-
       if ( options == null )
          options = {};
 
-      this.tableId     = tableId;
+      this.tableId     = tableId; 
       this.table       = $(tableId);
-      this.metaData    = new Rico.LiveGridMetaData(visibleRows, totalRows, options);
+      var columnCount  = this.table.rows[0].cells.length
+      this.metaData    = new Rico.LiveGridMetaData(visibleRows, totalRows, columnCount, options);
       this.buffer      = new Rico.LiveGridBuffer(this.metaData);
-      this.scroller    = new Rico.LiveGridScroller(this);
 
-      this.lastDisplayedStartPos = 0;
-      this.timeoutHander         = null;
+      var rowCount = this.table.rows.length;
+      this.viewPort =  new Rico.GridViewPort(this.table, 
+                                            this.table.offsetHeight/rowCount,
+                                            visibleRows,
+                                            this.buffer, this);
+      this.scroller    = new Rico.LiveGridScroller(this,this.viewPort);
+      
       this.additionalParms       = options.requestParameters || [];
+      
+      options.sortHandler = this.sortHandler.bind(this);
+
+      if ( $(tableId + '_header') )
+         this.sort = new Rico.LiveGridSort(tableId + '_header', options)
 
       this.processingRequest = null;
       this.unprocessedRequest = null;
 
       this.initAjax(url);
-      if ( options.prefetchBuffer )
-         this.fetchBuffer(0, true);
+      if ( options.prefetchBuffer || options.prefetchOffset > 0) {
+         var offset = 0;
+         if (options.offset ) {
+            offset = options.offset;            
+            this.scroller.moveScroll(offset);
+            this.viewPort.scrollTo(this.scroller.rowToPixel(offset));            
+         }
+         if (options.sortCol) {
+             this.sortCol = options.sortCol;
+             this.sortDir = options.sortDir;
+         }
+         this.requestContentRefresh(offset);
+      }
    },
 
+   resetContents: function() {
+      this.scroller.moveScroll(0);
+      this.buffer.clear();
+      this.viewPort.clearContents();
+   },
+   
+   sortHandler: function(column) {
+      this.sortCol = column.name;
+      this.sortDir = column.currentSort;
+
+      this.resetContents();
+      this.requestContentRefresh(0) 
+   },
+   
    setRequestParams: function() {
       this.additionalParms = [];
       for ( var i=0 ; i < arguments.length ; i++ )
@@ -2033,6 +2220,7 @@ Rico.LiveGrid.prototype = {
    },
 
    setTotalRows: function( newTotalRows ) {
+      this.resetContents();
       this.metaData.setTotalRows(newTotalRows);
       this.scroller.updateSize();
    },
@@ -2045,95 +2233,265 @@ Rico.LiveGrid.prototype = {
    invokeAjax: function() {
    },
 
-   largeBufferWindowStart: function(offset) {
-      val = offset - ( (.5 * this.metaData.getLargeBufferSize()) - (.5 * this.metaData.getPageSize()) )
-      return Math.max(parseInt(val), 0);
-   },
-
    handleTimedOut: function() {
       //server did not respond in 4 seconds... assume that there could have been
       //an error or something, and allow requests to be processed again...
       this.processingRequest = null;
+      this.processQueuedRequest();
    },
 
-   fetchBuffer: function(offset, fullBufferp) {
+   fetchBuffer: function(offset) {
+      if ( this.buffer.isInRange(offset) &&
+         !this.buffer.isNearingLimit(offset)) {
+         return;
+      }
       if (this.processingRequest) {
-         this.unprocessedRequest = new Rico.LiveGridRequest(offset);
-           return;
+          this.unprocessedRequest = new Rico.LiveGridRequest(offset);
+         return;
       }
-
-      var fetchSize = this.metaData.getBufferSize(fullBufferp);
-      bufferStartPos = Math.max(0,fullBufferp ? this.largeBufferWindowStart(offset) : offset);
-
+      var bufferStartPos = this.buffer.getFetchOffset(offset);
       this.processingRequest = new Rico.LiveGridRequest(offset);
-      this.processingRequest.bufferOffset = bufferStartPos;
-
-      var callParms = [];
+      this.processingRequest.bufferOffset = bufferStartPos;   
+      var fetchSize = this.buffer.getFetchSize(offset);
+      var partialLoaded = false;
+      var callParms = []; 
       callParms.push(this.tableId + '_request');
       callParms.push('id='        + this.tableId);
       callParms.push('page_size=' + fetchSize);
       callParms.push('offset='    + bufferStartPos);
-
+      if ( this.sortCol) {
+         callParms.push('sort_col='    + this.sortCol);
+         callParms.push('sort_dir='    + this.sortDir);
+      }
+      
       for( var i=0 ; i < this.additionalParms.length ; i++ )
          callParms.push(this.additionalParms[i]);
-
       ajaxEngine.sendRequest.apply( ajaxEngine, callParms );
-      this.timeoutHandler = setTimeout( this.handleTimedOut.bind(this), 4000 );
+        
+      this.timeoutHandler = setTimeout( this.handleTimedOut.bind(this), 20000 ); //todo: make as option
    },
 
    requestContentRefresh: function(contentOffset) {
-      if ( this.buffer && this.buffer.isFullyInRange(contentOffset) ) {
-         this.updateContent(contentOffset);
-         if (this.buffer.isNearingLimit(contentOffset))
-            this.fetchBuffer(contentOffset, true);
-      }
-      else if (this.buffer && this.buffer.isClose(contentOffset))
-         this.fetchBuffer(contentOffset, true);
-      else
-             this.fetchBuffer(contentOffset, false);
+      this.fetchBuffer(contentOffset);
    },
 
    ajaxUpdate: function(ajaxResponse) {
       try {
          clearTimeout( this.timeoutHandler );
-         this.buffer = new Rico.LiveGridBuffer(this.metaData);
          this.buffer.update(ajaxResponse,this.processingRequest.bufferOffset);
-         if (this.unprocessedRequest == null) {
-            offset = this.processingRequest.requestOffset;
-            this.updateContent (offset);
-         }
-         this.processingRequest = null;
-         if (this.unprocessedRequest != null) {
-            this.requestContentRefresh(this.unprocessedRequest.requestOffset);
-            this.unprocessedRequest = null
+         this.viewPort.bufferChanged();
+      }
+      catch(err) {}
+      finally {this.processingRequest = null; }
+      this.processQueuedRequest();
+   },
+
+   processQueuedRequest: function() {
+      if (this.unprocessedRequest != null) {
+         this.requestContentRefresh(this.unprocessedRequest.requestOffset);
+         this.unprocessedRequest = null
+      }  
+   }
+};
+
+
+//-------------------- ricoLiveGridSort.js
+Rico.LiveGridSort = Class.create();
+
+Rico.LiveGridSort.prototype = {
+
+   initialize: function(headerTableId, options) {
+      this.headerTableId = headerTableId;
+      this.headerTable   = $(headerTableId);
+      this.setOptions(options);
+      this.applySortBehavior();
+
+      if ( this.options.sortCol ) {
+         this.setSortUI( this.options.sortCol, this.options.sortDir );
+      }
+   },
+
+   setSortUI: function( columnName, sortDirection ) {
+      var cols = this.options.columns;
+      for ( var i = 0 ; i < cols.length ; i++ ) {
+         if ( cols[i].name == columnName ) {
+            this.setColumnSort(i, sortDirection);
+            break;
          }
       }
-      catch(err) {
+   },
+
+   setOptions: function(options) {
+      this.options = {
+         sortAscendImg:    'images/sort_asc.gif',
+         sortDescendImg:   'images/sort_desc.gif',
+         imageWidth:       9,
+         imageHeight:      5,
+         ajaxSortURLParms: []
+      }.extend(options);
+
+      // preload the images...
+      new Image().src = this.options.sortAscendImg;
+      new Image().src = this.options.sortDescendImg;
+
+      this.sort = options.sortHandler;
+      if ( !this.options.columns )
+         this.options.columns = this.introspectForColumnInfo();
+      else {
+         // allow client to pass { columns: [ ["a", true], ["b", false] ] }
+         // and convert to an array of Rico.TableColumn objs...
+         this.options.columns = this.convertToTableColumns(this.options.columns);
       }
    },
 
-   updateContent: function( offset ) {
-      this.replaceCellContents(this.buffer, offset);
+   applySortBehavior: function() {
+      var headerRow   = this.headerTable.rows[0];
+      var headerCells = headerRow.cells;
+      for ( var i = 0 ; i < headerCells.length ; i++ ) {
+         this.addSortBehaviorToColumn( i, headerCells[i] );
+      }
    },
 
-   replaceCellContents: function(buffer, startPos) {
-      if (startPos == this.lastDisplayedStartPos){
-         return;
+   addSortBehaviorToColumn: function( n, cell ) {
+      if ( this.options.columns[n].isSortable() ) {
+         cell.id            = this.headerTableId + '_' + n;
+         cell.style.cursor  = 'pointer';
+         cell.onclick       = this.headerCellClicked.bindAsEventListener(this);
+         cell.innerHTML     = cell.innerHTML + '<span id="' + this.headerTableId + '_img_' + n + '">'
+                           + '&nbsp;&nbsp;&nbsp;</span>';
       }
+   },
 
-      this.lastDisplayedStartPos = startPos
-      var rows = buffer.getRows(startPos, this.metaData.getPageSize());
-      for (var i=0; i < rows.length; i++) {
-         var row = rows[i];
-         for (var j=0; j < row.length; j++) {
-            this.table.rows[i].cells[j].innerHTML = rows[i][j]
+   // event handler....
+   headerCellClicked: function(evt) {
+      var eventTarget = evt.target ? evt.target : evt.srcElement;
+      var cellId = eventTarget.id;
+      var columnNumber = parseInt(cellId.substring( cellId.lastIndexOf('_') + 1 ));
+      var sortedColumnIndex = this.getSortedColumnIndex();
+      if ( sortedColumnIndex != -1 ) {
+         if ( sortedColumnIndex != columnNumber ) {
+            this.removeColumnSort(sortedColumnIndex);
+            this.setColumnSort(columnNumber, Rico.TableColumn.SORT_ASC);
          }
+         else
+            this.toggleColumnSort(sortedColumnIndex);
+      }
+      else
+         this.setColumnSort(columnNumber, Rico.TableColumn.SORT_ASC);
+
+      if (this.options.sortHandler) {
+         this.options.sortHandler(this.options.columns[columnNumber]);
+      }
+   },
+
+   removeColumnSort: function(n) {
+      this.options.columns[n].setUnsorted();
+      this.setSortImage(n);
+   },
+
+   setColumnSort: function(n, direction) {
+      this.options.columns[n].setSorted(direction);
+      this.setSortImage(n);
+   },
+
+   toggleColumnSort: function(n) {
+      this.options.columns[n].toggleSort();
+      this.setSortImage(n);
+   },
+
+   setSortImage: function(n) {
+      var sortDirection = this.options.columns[n].getSortDirection();
+
+      var sortImageSpan = $( this.headerTableId + '_img_' + n );
+      if ( sortDirection == Rico.TableColumn.UNSORTED )
+         sortImageSpan.innerHTML = '&nbsp;&nbsp;';
+      else if ( sortDirection == Rico.TableColumn.SORT_ASC )
+         sortImageSpan.innerHTML = '&nbsp;&nbsp;<img width="'  + this.options.imageWidth    + '" ' +
+                                                     'height="'+ this.options.imageHeight   + '" ' +
+                                                     'src="'   + this.options.sortAscendImg + '"/>';
+      else if ( sortDirection == Rico.TableColumn.SORT_DESC )
+         sortImageSpan.innerHTML = '&nbsp;&nbsp;<img width="'  + this.options.imageWidth    + '" ' +
+                                                     'height="'+ this.options.imageHeight   + '" ' +
+                                                     'src="'   + this.options.sortDescendImg + '"/>';
+   },
+
+   getSortedColumnIndex: function() {
+      var cols = this.options.columns;
+      for ( var i = 0 ; i < cols.length ; i++ ) {
+         if ( cols[i].isSorted() )
+            return i;
       }
+
+      return -1;
+   },
+
+   introspectForColumnInfo: function() {
+      var columns = new Array();
+      var headerRow   = this.headerTable.rows[0];
+      var headerCells = headerRow.cells;
+      for ( var i = 0 ; i < headerCells.length ; i++ )
+         columns.push( new Rico.TableColumn( this.deriveColumnNameFromCell(headerCells[i],i), true ) );
+      return columns;
+   },
+
+   convertToTableColumns: function(cols) {
+      var columns = new Array();
+      for ( var i = 0 ; i < cols.length ; i++ )
+         columns.push( new Rico.TableColumn( cols[i][0], cols[i][1] ) );
+   },
+
+   deriveColumnNameFromCell: function(cell,columnNumber) {
+      var cellContent = cell.innerText != undefined ? cell.innerText : cell.textContent;
+      return cellContent ? cellContent.toLowerCase().split(' ').join('_') : "col_" + columnNumber;
    }
-}; 
-// ricoUtil.js --------------------
+};
+
+Rico.TableColumn = Class.create();
+
+Rico.TableColumn.UNSORTED  = 0;
+Rico.TableColumn.SORT_ASC  = "ASC";
+Rico.TableColumn.SORT_DESC = "DESC";
+
+Rico.TableColumn.prototype = {
+   initialize: function(name, sortable) {
+      this.name        = name;
+      this.sortable    = sortable;
+      this.currentSort = Rico.TableColumn.UNSORTED;
+   },
+
+   isSortable: function() {
+      return this.sortable;
+   },
+
+   isSorted: function() {
+      return this.currentSort != Rico.TableColumn.UNSORTED;
+   },
+
+   getSortDirection: function() {
+      return this.currentSort;
+   },
+
+   toggleSort: function() {
+      if ( this.currentSort == Rico.TableColumn.UNSORTED || this.currentSort == Rico.TableColumn.SORT_DESC )
+         this.currentSort = Rico.TableColumn.SORT_ASC;
+      else if ( this.currentSort == Rico.TableColumn.SORT_ASC )
+         this.currentSort = Rico.TableColumn.SORT_DESC;
+   },
+
+   setUnsorted: function(direction) {
+      this.setSorted(Rico.TableColumn.UNSORTED);
+   },
+
+   setSorted: function(direction) {
+      // direction must by one of Rico.TableColumn.UNSORTED, .SORT_ASC, or .SET_DESC...
+      this.currentSort = direction;
+   }
+
+};
+
+
+//-------------------- ricoUtil.js
 
 var RicoUtil = {
 
@@ -2175,6 +2533,27 @@ var RicoUtil = {
       return null;
    },
 
+   getContentAsString: function( parentNode ) {
+      return parentNode.xml != undefined ? 
+         this._getContentAsStringIE(parentNode) :
+         this._getContentAsStringMozilla(parentNode);
+   },
+
+   _getContentAsStringIE: function(parentNode) {
+      var contentStr = "";
+      for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
+         contentStr += parentNode.childNodes[i].xml;
+      return contentStr;
+   },
+
+   _getContentAsStringMozilla: function(parentNode) {
+      var xmlSerializer = new XMLSerializer();
+      var contentStr = "";
+      for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
+         contentStr += xmlSerializer.serializeToString(parentNode.childNodes[i]);
+      return contentStr;
+   },
+
    toViewportPosition: function(element) {
       return this._toAbsolute(element,true);
    },
@@ -2284,4 +2663,4 @@ var RicoUtil = {
          return 0;
    }
 
-};
\ No newline at end of file
+};
diff --git a/webcit/static/scriptaculous.js b/webcit/static/scriptaculous.js
new file mode 100644 (file)
index 0000000..ea1ad40
--- /dev/null
@@ -0,0 +1,26 @@
+var Scriptaculous = {
+  Version: '1.5_pre4',
+  require: function(libraryName) {
+    // inserting via DOM fails in Safari 2.0, so brute force approach
+    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
+  },
+  load: function() {
+    if((typeof Prototype=='undefined') ||
+      parseFloat(Prototype.Version.split(".")[0] + "." +
+                 Prototype.Version.split(".")[1]) < 1.4)
+      throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0");
+    var scriptTags = document.getElementsByTagName("script");
+    for(var i=0;i<scriptTags.length;i++) {
+      if(scriptTags[i].src && scriptTags[i].src.match(/scriptaculous\.js$/)) {
+        var path = scriptTags[i].src.replace(/scriptaculous\.js$/,'');
+        this.require(path + 'util.js');
+        this.require(path + 'effects.js');
+        this.require(path + 'dragdrop.js');
+        this.require(path + 'controls.js');
+        break;
+      }
+    }
+  }
+}
+
+Scriptaculous.load();
\ No newline at end of file
diff --git a/webcit/static/unittest.js b/webcit/static/unittest.js
new file mode 100644 (file)
index 0000000..b53d65e
--- /dev/null
@@ -0,0 +1,381 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+//           (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// experimental, Firefox-only
+Event.simulateMouse = function(element, eventName) {
+  var options = Object.extend({
+    pointerX: 0,
+    pointerY: 0,
+    buttons: 0
+  }, arguments[2] || {});
+  var oEvent = document.createEvent("MouseEvents");
+  oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
+    options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
+    false, false, false, false, 0, $(element));
+  
+  if(this.mark) Element.remove(this.mark);
+  this.mark = document.createElement('div');
+  this.mark.appendChild(document.createTextNode(" "));
+  document.body.appendChild(this.mark);
+  this.mark.style.position = 'absolute';
+  this.mark.style.top = options.pointerY + "px";
+  this.mark.style.left = options.pointerX + "px";
+  this.mark.style.width = "5px";
+  this.mark.style.height = "5px;";
+  this.mark.style.borderTop = "1px solid red;"
+  this.mark.style.borderLeft = "1px solid red;"
+  
+  if(this.step)
+    alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
+  
+  $(element).dispatchEvent(oEvent);
+};
+
+// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
+// You need to downgrade to 1.0.4 for now to get this working
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
+Event.simulateKey = function(element, eventName) {
+  var options = Object.extend({
+    ctrlKey: false,
+    altKey: false,
+    shiftKey: false,
+    metaKey: false,
+    keyCode: 0,
+    charCode: 0
+  }, arguments[2] || {});
+
+  var oEvent = document.createEvent("KeyEvents");
+  oEvent.initKeyEvent(eventName, true, true, window, 
+    options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+    options.keyCode, options.charCode );
+  $(element).dispatchEvent(oEvent);
+};
+
+Event.simulateKeys = function(element, command) {
+  for(var i=0; i<command.length; i++) {
+    Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
+  }
+};
+
+var Test = {}
+Test.Unit = {};
+
+// security exception workaround
+Test.Unit.inspect = function(obj) {
+  var info = [];
+
+  if(typeof obj=="string" || 
+     typeof obj=="number") {
+    return obj;
+  } else {
+    for(property in obj)
+      if(typeof obj[property]!="function")
+        info.push(property + ' => ' + 
+          (typeof obj[property] == "string" ?
+            '"' + obj[property] + '"' :
+            obj[property]));
+  }
+
+  return ("'" + obj + "' #" + typeof obj + 
+    ": {" + info.join(", ") + "}");
+}
+
+Test.Unit.Logger = Class.create();
+Test.Unit.Logger.prototype = {
+  initialize: function(log) {
+    this.log = $(log);
+    if (this.log) {
+      this._createLogTable();
+    }
+  },
+  start: function(testName) {
+    if (!this.log) return;
+    this.testName = testName;
+    this.lastLogLine = document.createElement('tr');
+    this.statusCell = document.createElement('td');
+    this.nameCell = document.createElement('td');
+    this.nameCell.appendChild(document.createTextNode(testName));
+    this.messageCell = document.createElement('td');
+    this.lastLogLine.appendChild(this.statusCell);
+    this.lastLogLine.appendChild(this.nameCell);
+    this.lastLogLine.appendChild(this.messageCell);
+    this.loglines.appendChild(this.lastLogLine);
+  },
+  finish: function(status, summary) {
+    if (!this.log) return;
+    this.lastLogLine.className = status;
+    this.statusCell.innerHTML = status;
+    this.messageCell.innerHTML = this._toHTML(summary);
+  },
+  message: function(message) {
+    if (!this.log) return;
+    this.messageCell.innerHTML = this._toHTML(message);
+  },
+  summary: function(summary) {
+    if (!this.log) return;
+    this.logsummary.innerHTML = this._toHTML(summary);
+  },
+  _createLogTable: function() {
+    this.log.innerHTML =
+    '<div id="logsummary"></div>' +
+    '<table id="logtable">' +
+    '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
+    '<tbody id="loglines"></tbody>' +
+    '</table>';
+    this.logsummary = $('logsummary')
+    this.loglines = $('loglines');
+  },
+  _toHTML: function(txt) {
+    return txt.escapeHTML().replace(/\n/g,"<br/>");
+  }
+}
+
+Test.Unit.Runner = Class.create();
+Test.Unit.Runner.prototype = {
+  initialize: function(testcases) {
+    this.options = Object.extend({
+      testLog: 'testlog'
+    }, arguments[1] || {});
+    this.options.resultsURL = this.parseResultsURLQueryParameter();
+    if (this.options.testLog) {
+      this.options.testLog = $(this.options.testLog) || null;
+    }
+    if(this.options.tests) {
+      this.tests = [];
+      for(var i = 0; i < this.options.tests.length; i++) {
+        if(/^test/.test(this.options.tests[i])) {
+          this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
+        }
+      }
+    } else {
+      if (this.options.test) {
+        this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
+      } else {
+        this.tests = [];
+        for(var testcase in testcases) {
+          if(/^test/.test(testcase)) {
+            this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"]));
+          }
+        }
+      }
+    }
+    this.currentTest = 0;
+    this.logger = new Test.Unit.Logger(this.options.testLog);
+    setTimeout(this.runTests.bind(this), 1000);
+  },
+  parseResultsURLQueryParameter: function() {
+    return window.location.search.parseQuery()["resultsURL"];
+  },
+  // Returns:
+  //  "ERROR" if there was an error,
+  //  "FAILURE" if there was a failure, or
+  //  "SUCCESS" if there was neither
+  getResult: function() {
+    var hasFailure = false;
+    for(var i=0;i<this.tests.length;i++) {
+      if (this.tests[i].errors > 0) {
+        return "ERROR";
+      }
+      if (this.tests[i].failures > 0) {
+        hasFailure = true;
+      }
+    }
+    if (hasFailure) {
+      return "FAILURE";
+    } else {
+      return "SUCCESS";
+    }
+  },
+  postResults: function() {
+    if (this.options.resultsURL) {
+      new Ajax.Request(this.options.resultsURL, 
+        { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
+    }
+  },
+  runTests: function() {
+    var test = this.tests[this.currentTest];
+    if (!test) {
+      // finished!
+      this.postResults();
+      this.logger.summary(this.summary());
+      return;
+    }
+    if(!test.isWaiting) {
+      this.logger.start(test.name);
+    }
+    test.run();
+    if(test.isWaiting) {
+      this.logger.message("Waiting for " + test.timeToWait + "ms");
+      setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
+    } else {
+      this.logger.finish(test.status(), test.summary());
+      this.currentTest++;
+      // tail recursive, hopefully the browser will skip the stackframe
+      this.runTests();
+    }
+  },
+  summary: function() {
+    var assertions = 0;
+    var failures = 0;
+    var errors = 0;
+    var messages = [];
+    for(var i=0;i<this.tests.length;i++) {
+      assertions +=   this.tests[i].assertions;
+      failures   +=   this.tests[i].failures;
+      errors     +=   this.tests[i].errors;
+    }
+    return (
+      this.tests.length + " tests, " + 
+      assertions + " assertions, " + 
+      failures   + " failures, " +
+      errors     + " errors");
+  }
+}
+
+Test.Unit.Assertions = Class.create();
+Test.Unit.Assertions.prototype = {
+  initialize: function() {
+    this.assertions = 0;
+    this.failures   = 0;
+    this.errors     = 0;
+    this.messages   = [];
+  },
+  summary: function() {
+    return (
+      this.assertions + " assertions, " + 
+      this.failures   + " failures, " +
+      this.errors     + " errors" + "\n" +
+      this.messages.join("\n"));
+  },
+  pass: function() {
+    this.assertions++;
+  },
+  fail: function(message) {
+    this.failures++;
+    this.messages.push("Failure: " + message);
+  },
+  error: function(error) {
+    this.errors++;
+    this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
+  },
+  status: function() {
+    if (this.failures > 0) return 'failed';
+    if (this.errors > 0) return 'error';
+    return 'passed';
+  },
+  assert: function(expression) {
+    var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
+    try { expression ? this.pass() : 
+      this.fail(message); }
+    catch(e) { this.error(e); }
+  },
+  assertEqual: function(expected, actual) {
+    var message = arguments[2] || "assertEqual";
+    try { (expected == actual) ? this.pass() :
+      this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
+        '", actual "' + Test.Unit.inspect(actual) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertNotEqual: function(expected, actual) {
+    var message = arguments[2] || "assertNotEqual";
+    try { (expected != actual) ? this.pass() : 
+      this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertNull: function(obj) {
+    var message = arguments[1] || 'assertNull'
+    try { (obj==null) ? this.pass() : 
+      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertHidden: function(element) {
+    var message = arguments[1] || 'assertHidden';
+    this.assertEqual("none", element.style.display, message);
+  },
+  assertNotNull: function(object) {
+    var message = arguments[1] || 'assertNotNull';
+    this.assert(object != null, message);
+  },
+  assertInstanceOf: function(expected, actual) {
+    var message = arguments[2] || 'assertInstanceOf';
+    try { 
+      (actual instanceof expected) ? this.pass() : 
+      this.fail(message + ": object was not an instance of the expected type"); }
+    catch(e) { this.error(e); } 
+  },
+  assertNotInstanceOf: function(expected, actual) {
+    var message = arguments[2] || 'assertNotInstanceOf';
+    try { 
+      !(actual instanceof expected) ? this.pass() : 
+      this.fail(message + ": object was an instance of the not expected type"); }
+    catch(e) { this.error(e); } 
+  },
+  _isVisible: function(element) {
+    element = $(element);
+    if(!element.parentNode) return true;
+    this.assertNotNull(element);
+    if(element.style && Element.getStyle(element, 'display') == 'none')
+      return false;
+    
+    return this._isVisible(element.parentNode);
+  },
+  assertNotVisible: function(element) {
+    this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
+  },
+  assertVisible: function(element) {
+    this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
+  }
+}
+
+Test.Unit.Testcase = Class.create();
+Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
+  initialize: function(name, test, setup, teardown) {
+    Test.Unit.Assertions.prototype.initialize.bind(this)();
+    this.name           = name;
+    this.test           = test || function() {};
+    this.setup          = setup || function() {};
+    this.teardown       = teardown || function() {};
+    this.isWaiting      = false;
+    this.timeToWait     = 1000;
+  },
+  wait: function(time, nextPart) {
+    this.isWaiting = true;
+    this.test = nextPart;
+    this.timeToWait = time;
+  },
+  run: function() {
+    try {
+      try {
+        if (!this.isWaiting) this.setup.bind(this)();
+        this.isWaiting = false;
+        this.test.bind(this)();
+      } finally {
+        if(!this.isWaiting) {
+          this.teardown.bind(this)();
+        }
+      }
+    }
+    catch(e) { this.error(e); }
+  }
+});
\ No newline at end of file
diff --git a/webcit/static/util.js b/webcit/static/util.js
new file mode 100644 (file)
index 0000000..9170a95
--- /dev/null
@@ -0,0 +1,429 @@
+// small but works-for-me stuff for testing javascripts
+// not ready for "production" use
+
+Object.inspect = function(obj) {
+  var info = [];
+  
+  if(typeof obj in ["string","number"]) {
+    return obj;
+  } else {
+    for(property in obj)
+      if(typeof obj[property]!="function")
+        info.push(property + ' => ' + 
+          (typeof obj[property] == "string" ?
+            '"' + obj[property] + '"' :
+            obj[property]));
+  }
+  
+  return ("'" + obj + "' #" + typeof obj + 
+    ": {" + info.join(", ") + "}");
+}
+
+// borrowed from http://www.schuerig.de/michael/javascript/stdext.js
+// Copyright (c) 2005, Michael Schuerig, michael@schuerig.de
+
+Array.flatten = function(array, excludeUndefined) {
+  if (excludeUndefined === undefined) {
+    excludeUndefined = false;
+  }
+  var result = [];
+  var len = array.length;
+  for (var i = 0; i < len; i++) {
+    var el = array[i];
+    if (el instanceof Array) {
+      var flat = el.flatten(excludeUndefined);
+      result = result.concat(flat);
+    } else if (!excludeUndefined || el != undefined) {
+      result.push(el);
+    }
+  }
+  return result;
+};
+
+if (!Array.prototype.flatten) {
+  Array.prototype.flatten = function(excludeUndefined) {
+    return Array.flatten(this, excludeUndefined);
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Builder = {
+  node: function(elementName) {
+    var element = document.createElement('div');
+    element.innerHTML = 
+      "<" + elementName + "></" + elementName + ">";
+
+    // attributes (or text)
+    if(arguments[1])
+      if(this._isStringOrNumber(arguments[1]) ||
+        (arguments[1] instanceof Array)) {
+          this._children(element.firstChild, arguments[1]);
+        } else {
+          var attrs = this._attributes(arguments[1]);
+          if(attrs.length) 
+            element.innerHTML = "<" +elementName + " " +
+              attrs + "></" + elementName + ">";
+        } 
+
+    // text, or array of children
+    if(arguments[2])
+      this._children(element.firstChild, arguments[2]);
+
+     return element.firstChild;
+  },
+  _text: function(text) {
+     return document.createTextNode(text);
+  },
+  _attributes: function(attributes) {
+    var attrs = [];
+    for(attribute in attributes)
+      attrs.push((attribute=='className' ? 'class' : attribute) +
+          '="' + attributes[attribute].toString().escapeHTML() + '"');
+    return attrs.join(" ");
+  },
+  _children: function(element, children) {
+    if(typeof children=='object') { // array can hold nodes and text
+      children = children.flatten();
+      for(var i = 0; i<children.length; i++)
+        if(typeof children[i]=='object')
+          element.appendChild(children[i]);
+        else
+          if(this._isStringOrNumber(children[i]))
+            element.appendChild(this._text(children[i]));
+    } else
+      if(this._isStringOrNumber(children)) 
+         element.appendChild(this._text(children));
+  },
+  _isStringOrNumber: function(param) {
+    return(typeof param=='string' || typeof param=='number');
+  }
+}
+
+/* ------------- element ext -------------- */
+
+// adapted from http://dhtmlkitchen.com/learn/js/setstyle/index4.jsp
+// note: Safari return null on elements with display:none; see http://bugzilla.opendarwin.org/show_bug.cgi?id=4125
+// instead of "auto" values returns null so it's easier to use with || constructs
+
+String.prototype.camelize = function() {
+  var oStringList = this.split('-');
+  if(oStringList.length == 1)    
+    return oStringList[0];
+  var ret = this.indexOf("-") == 0 ? 
+    oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) : oStringList[0];
+  for(var i = 1, len = oStringList.length; i < len; i++){
+    var s = oStringList[i];
+    ret += s.charAt(0).toUpperCase() + s.substring(1)
+  }
+  return ret;
+}
+
+Element.getStyle = function(element, style) {
+  element = $(element);
+  var value = element.style[style.camelize()];
+  if(!value)
+    if(document.defaultView && document.defaultView.getComputedStyle) {
+      var css = document.defaultView.getComputedStyle(element, null);
+      value = (css!=null) ? css.getPropertyValue(style) : null;
+    } else if(element.currentStyle) {
+      value = element.currentStyle[style.camelize()];  
+    }
+  if(value=='auto') value = null;
+  return value;
+}
+
+Element.makePositioned = function(element) {
+  element = $(element);
+  if(Element.getStyle(element, 'position')=='static')
+    element.style.position = "relative";
+}
+
+Element.makeClipping = function(element) {
+  element = $(element);
+  element._overflow = Element.getStyle(element, 'overflow') || 'visible';
+  if(element._overflow!='hidden') element.style.overflow = 'hidden';
+}
+
+Element.undoClipping = function(element) {
+  element = $(element);
+  if(element._overflow!='hidden') element.style.overflow = element._overflow;
+}
+
+Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
+  var children = $(element).childNodes;
+  var text     = "";
+  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");
+
+  for (var i = 0; i < children.length; i++) {
+    if(children[i].nodeType==3) {
+      text+=children[i].nodeValue;
+    } else {
+      if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
+        text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
+    }
+  }
+
+  return text;
+}
+
+/*--------------------------------------------------------------------------*/
+
+Position.positionedOffset = function(element) {
+  var valueT = 0, valueL = 0;
+  do {
+    valueT += element.offsetTop  || 0;
+    valueL += element.offsetLeft || 0;
+    element = element.offsetParent;
+    if (element) {
+      p = Element.getStyle(element,'position');
+      if(p == 'relative' || p == 'absolute') break;
+    }
+  } while (element);
+  return [valueL, valueT];
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely positioned.
+// for performance reasons, we create a specialized version of Position.positionedOffset for
+// KHTML/WebKit only
+
+if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      
+      if (element.offsetParent==document.body) 
+        if (Element.getStyle(element,'position')=='absolute') break;
+        
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  }
+}
+
+Position.page = function(forElement) {
+  if(element == document.body) return [0, 0];
+  var valueT = 0, valueL = 0;
+
+  var element = forElement;
+  do {
+    valueT += element.offsetTop  || 0;
+    valueL += element.offsetLeft || 0;
+
+    // Safari fix
+    if (element.offsetParent==document.body)
+      if (Element.getStyle(element,'position')=='absolute') break;
+      
+  } while (element = element.offsetParent);
+
+  element = forElement;
+  do {
+    valueT -= element.scrollTop  || 0;
+    valueL -= element.scrollLeft || 0;    
+  } while (element = element.parentNode);
+
+  return [valueL, valueT];
+}
+
+// elements with display:none don't return an offsetParent, 
+// fall back to  manual calculation
+Position.offsetParent = function(element) {
+  if(element.offsetParent) return element.offsetParent;
+  if(element == document.body) return element;
+  
+  while ((element = element.parentNode) && element != document.body)
+    if (Element.getStyle(element,'position')!='static')
+      return element;
+  
+  return document.body;
+}
+
+Position.clone = function(source, target) {
+  var options = Object.extend({
+    setLeft:    true,
+    setTop:     true,
+    setWidth:   true,
+    setHeight:  true,
+    offsetTop:  0,
+    offsetLeft: 0
+  }, arguments[2] || {})
+  
+  // find page position of source
+  source = $(source);
+  var p = Position.page(source);
+
+  // find coordinate system to use
+  target = $(target);
+  var delta = [0, 0];
+  var parent = null;
+  // delta [0,0] will do fine with position: fixed elements, 
+  // position:absolute needs offsetParent deltas
+  if (Element.getStyle(target,'position') == 'absolute') {
+    parent = Position.offsetParent(target);
+    delta = Position.page(parent);
+  }
+  
+  // correct by body offsets (fixes Safari)
+  if (parent==document.body) {
+    delta[0] -= document.body.offsetLeft;
+    delta[1] -= document.body.offsetTop; 
+  }
+
+  // set position
+  if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + "px";
+  if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + "px";
+  if(options.setWidth)  target.style.width = source.offsetWidth + "px";
+  if(options.setHeight) target.style.height = source.offsetHeight + "px";
+}
+
+Position.absolutize = function(element) {
+  element = $(element);
+  if(element.style.position=='absolute') return;
+  Position.prepare();
+
+  var offsets = Position.positionedOffset(element);
+  var top     = offsets[1];
+  var left    = offsets[0];
+  var width   = element.clientWidth;
+  var height  = element.clientHeight;
+
+  element._originalLeft   = left - parseFloat(element.style.left  || 0);
+  element._originalTop    = top  - parseFloat(element.style.top || 0);
+  element._originalWidth  = element.style.width;
+  element._originalHeight = element.style.height;
+
+  element.style.position = 'absolute';
+  element.style.top    = top + 'px';;
+  element.style.left   = left + 'px';;
+  element.style.width  = width + 'px';;
+  element.style.height = height + 'px';;
+}
+
+Position.relativize = function(element) {
+  element = $(element);
+  if(element.style.position=='relative') return;
+  Position.prepare();
+
+  element.style.position = 'relative';
+  var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+  var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+  element.style.top    = top + 'px';
+  element.style.left   = left + 'px';
+  element.style.height = element._originalHeight;
+  element.style.width  = element._originalWidth;
+}
+
+/*--------------------------------------------------------------------------*/
+
+Element.Class = {
+    // Element.toggleClass(element, className) toggles the class being on/off
+    // Element.toggleClass(element, className1, className2) toggles between both classes,
+    //   defaulting to className1 if neither exist
+    toggle: function(element, className) {
+      if(Element.Class.has(element, className)) {
+        Element.Class.remove(element, className);
+        if(arguments.length == 3) Element.Class.add(element, arguments[2]);
+      } else {
+        Element.Class.add(element, className);
+        if(arguments.length == 3) Element.Class.remove(element, arguments[2]);
+      }
+    },
+
+    // gets space-delimited classnames of an element as an array
+    get: function(element) {
+      element = $(element);
+      return element.className.split(' ');
+    },
+
+    // functions adapted from original functions by Gavin Kistner
+    remove: function(element) {
+      element = $(element);
+      var regEx;
+      for(var i = 1; i < arguments.length; i++) {
+        regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)", 'g');
+        element.className = element.className.replace(regEx, '')
+      }
+    },
+
+    add: function(element) {
+      element = $(element);
+      for(var i = 1; i < arguments.length; i++) {
+        Element.Class.remove(element, arguments[i]);
+        element.className += (element.className.length > 0 ? ' ' : '') + arguments[i];
+      }
+    },
+
+    // returns true if all given classes exist in said element
+    has: function(element) {
+      element = $(element);
+      if(!element || !element.className) return false;
+      var regEx;
+      for(var i = 1; i < arguments.length; i++) {
+        if((typeof arguments[i] == 'object') && 
+          (arguments[i].constructor == Array)) {
+          for(var j = 0; j < arguments[i].length; j++) {
+            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");
+            if(!regEx.test(element.className)) return false;
+          }
+        } else {
+          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");
+          if(!regEx.test(element.className)) return false;
+        }
+      }
+      return true;
+    },
+
+    // expects arrays of strings and/or strings as optional paramters
+    // Element.Class.has_any(element, ['classA','classB','classC'], 'classD')
+    has_any: function(element) {
+      element = $(element);
+      if(!element || !element.className) return false;
+      var regEx;
+      for(var i = 1; i < arguments.length; i++) {
+        if((typeof arguments[i] == 'object') && 
+          (arguments[i].constructor == Array)) {
+          for(var j = 0; j < arguments[i].length; j++) {
+            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");
+            if(regEx.test(element.className)) return true;
+          }
+        } else {
+          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");
+          if(regEx.test(element.className)) return true;
+        }
+      }
+      return false;
+    },
+
+    childrenWith: function(element, className) {
+      var children = $(element).getElementsByTagName('*');
+      var elements = new Array();
+
+      for (var i = 0; i < children.length; i++) {
+        if (Element.Class.has(children[i], className)) {
+          elements.push(children[i]);
+          break;
+        }
+      }
+
+      return elements;
+    }
+}
+
+/*--------------------------------------------------------------------------*/
+
+String.prototype.parseQuery = function() {
+  var str = this;
+  if(str.substring(0,1) == '?') {
+    str = this.substring(1);
+  }
+  var result = {};
+  var pairs = str.split('&');
+  for(var i = 0; i < pairs.length; i++) {
+    var pair = pairs[i].split('=');
+    result[pair[0]] = pair[1];
+  }
+  return result;
+}
\ No newline at end of file
index c42449997422084a1ac761031591c9b2464b59f8..90266777e3539a610c141261968dd46d8add54a8 100644 (file)
@@ -41,6 +41,6 @@ function hide_imsg_popup_old() {
 }
 
 function hide_imsg_popup() {
-       new Effect.FadeTo('important_message', 0.0, 1000, 20, {complete:function() { hide_imsg_popup_old(); }} );
-       // we still do it the old way afterwards, just in case the browser didn't dazzle us enough
+       // new Effect.FadeTo('important_message', 0.0, 1000, 20, {complete:function() { hide_imsg_popup_old(); }} );
+       hide_imsg_popup_old();  // Do it the old way for now, to avoid library conflicts
 }