[Jifty-commit] r5294 - in jifty/trunk: . lib/Jifty lib/Jifty/Manual lib/Jifty/Plugin share/plugins/Jifty/Plugin/I18N/web/static/js share/plugins/Jifty/Plugin/Prototypism share/plugins/Jifty/Plugin/Prototypism/web share/plugins/Jifty/Plugin/Prototypism/web/static share/plugins/Jifty/Plugin/Prototypism/web/static/js share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous share/plugins/Jifty/Plugin/SinglePage share/plugins/Jifty/Plugin/SinglePage/web share/plugins/Jifty/Plugin/SinglePage/web/static share/plugins/Jifty/Plugin/SinglePage/web/static/js share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh share/web/static/js share/web/static/js/scriptaculous share/web/templates/__jifty t/TestApp-JiftyJS/etc t/TestApp-JiftyJS/lib/TestApp/JiftyJS t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Model t/TestApp-JiftyJS/share/web/static/js t/TestApp-JiftyJS/share/web/static/js-test t/TestApp-JiftyJS/t t/TestApp-JiftyJS2 t/TestApp-JiftyJS2/bin t/TestApp-JiftyJS2/doc t/TestApp-JiftyJS2/etc t/TestApp-JiftyJS2/log t/TestApp-JiftyJS2/var t/TestApp-Plugin-OnClick/etc t/TestApp-Plugin-OnClick/t t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage t/TestApp-Plugin-SinglePage/t

Jifty commits jifty-commit at lists.jifty.org
Wed Apr 9 00:13:37 EDT 2008


Author: hlb
Date: Wed Apr  9 00:12:34 2008
New Revision: 5294

Added:
   jifty/trunk/lib/Jifty/Manual/JavaScript.pod
   jifty/trunk/lib/Jifty/Manual/jQueryMigrationGuide.pod
   jifty/trunk/lib/Jifty/Plugin/Prototypism.pm
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/jifty_compatible.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/prototype.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/builder.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/controls.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/dragdrop.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/effects.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/scriptaculous.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/slider.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/unittest.js   (contents, props changed)
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/LICENSE.txt
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/blank.html
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/rsh.js
   jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/spa.js
   jifty/trunk/share/web/static/js/iautocompleter.js   (contents, props changed)
   jifty/trunk/share/web/static/js/iutil.js   (contents, props changed)
   jifty/trunk/share/web/static/js/jifty_interface.js   (contents, props changed)
   jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action/Play2.pm
   jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Dispatcher.pm
   jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Model/Offer.pm
   jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/
   jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/
   jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/en.json
   jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/en_us.json
   jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/zh_tw.json
   jifty/trunk/t/TestApp-JiftyJS/t/00-action-Play2.t
   jifty/trunk/t/TestApp-JiftyJS/t/00-model-Offer.t
   jifty/trunk/t/TestApp-JiftyJS/t/6-offer-actions.t
   jifty/trunk/t/TestApp-JiftyJS/t/7-redirect.t
   jifty/trunk/t/TestApp-JiftyJS/t/8-placeholder.t
   jifty/trunk/t/TestApp-JiftyJS2/
   jifty/trunk/t/TestApp-JiftyJS2/Makefile.PL
   jifty/trunk/t/TestApp-JiftyJS2/bin/
   jifty/trunk/t/TestApp-JiftyJS2/bin/jifty   (contents, props changed)
   jifty/trunk/t/TestApp-JiftyJS2/doc/
   jifty/trunk/t/TestApp-JiftyJS2/etc/
   jifty/trunk/t/TestApp-JiftyJS2/etc/config.yml
   jifty/trunk/t/TestApp-JiftyJS2/lib   (contents, props changed)
   jifty/trunk/t/TestApp-JiftyJS2/log/
   jifty/trunk/t/TestApp-JiftyJS2/share   (contents, props changed)
   jifty/trunk/t/TestApp-JiftyJS2/t   (contents, props changed)
   jifty/trunk/t/TestApp-JiftyJS2/var/
   jifty/trunk/t/TestApp-Plugin-SinglePage/t/history.t
Removed:
   jifty/trunk/share/web/static/js/prototype.js
   jifty/trunk/share/web/static/js/scriptaculous/
Modified:
   jifty/trunk/   (props changed)
   jifty/trunk/lib/Jifty/Config.pm
   jifty/trunk/lib/Jifty/Plugin/Halo.pm
   jifty/trunk/lib/Jifty/Plugin/SinglePage.pm
   jifty/trunk/lib/Jifty/Web.pm
   jifty/trunk/share/plugins/Jifty/Plugin/I18N/web/static/js/loc.js
   jifty/trunk/share/web/static/js/bps_util.js
   jifty/trunk/share/web/static/js/calendar.js
   jifty/trunk/share/web/static/js/halo.js
   jifty/trunk/share/web/static/js/jifty.js
   jifty/trunk/share/web/static/js/jifty_subs.js
   jifty/trunk/share/web/static/js/jifty_utils.js
   jifty/trunk/share/web/static/js/key_bindings.js
   jifty/trunk/share/web/templates/__jifty/halo
   jifty/trunk/t/TestApp-JiftyJS/etc/config.yml
   jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action/Play.pm
   jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/View.pm
   jifty/trunk/t/TestApp-JiftyJS/share/web/static/js-test/02.action.html
   jifty/trunk/t/TestApp-JiftyJS/t/1-jifty-update.t
   jifty/trunk/t/TestApp-JiftyJS/t/2-behaviour.t
   jifty/trunk/t/TestApp-JiftyJS/t/4-tangent.t
   jifty/trunk/t/TestApp-JiftyJS/t/5-action.t
   jifty/trunk/t/TestApp-Plugin-OnClick/etc/config.yml
   jifty/trunk/t/TestApp-Plugin-OnClick/t/onclick.t
   jifty/trunk/t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/View.pm

Log:
merge jQuery branch into trunk


Modified: jifty/trunk/lib/Jifty/Config.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Config.pm	(original)
+++ jifty/trunk/lib/Jifty/Config.pm	Wed Apr  9 00:12:34 2008
@@ -346,7 +346,7 @@
 sub initial_config {
     my $self = shift;
     my $guess = $self->guess(@_);
-    $guess->{'framework'}->{'ConfigFileVersion'} = 3;
+    $guess->{'framework'}->{'ConfigFileVersion'} = 4;
 
     # These are the plugins which new apps will get by default
     $guess->{'framework'}->{'Plugins'} = [
@@ -396,6 +396,12 @@
         );
     }
 
+    if ( $config->{'framework'}->{'ConfigFileVersion'} < 4) {
+        unshift (@{$config->{'framework'}->{'Plugins'}}, 
+            { Prototypism        => {}, }
+        );
+    }
+
     return $config;
 }
 

Added: jifty/trunk/lib/Jifty/Manual/JavaScript.pod
==============================================================================
--- (empty file)
+++ jifty/trunk/lib/Jifty/Manual/JavaScript.pod	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,224 @@
+
+=head1 NAME
+
+JavaScript programming guide for Jifty
+
+=head1 DESCRIPTION
+
+jQuery took over Prototype and becoming the core of Jifty's Javascript
+development. Besides re-implement core javascript libraries with
+jQuery, some good refactor is also being done.
+
+This document is written to help JavaScript programmers working for
+a Jifty project to understand what's the different before jQuery landed,
+and provide a quick reference for prototypism believers to learn the new wave
+of JavaScript programming in Jifty.
+
+=head1 Migration to jQuery
+
+This section provides a simple guide through jQuery's core functions
+that's used to replace Prototype javascript library.
+
+=head2 Selecting elements with jQuery()
+
+Invokin jQuery function with exactly one string argument will return
+a jQuery object that represents a list of elements. The string is
+a CSS selector. For example:
+
+    jQuery("span.message")
+
+This works very similar to Prototype's $$() function, but with one
+difference. The return value is I<not> an Array, it's a jQuery
+object that acts likes a Enumerable object (but still, not one). If you
+really want a Array, you can do:
+
+   my array_of_message = jQuery("span.message").get()
+
+For most cases, C<jQuery("#" + id).get(0)> can be a replacement pattern
+to C<$(id)>. Selecting elements with C<jQuery()> function always
+returns a jQuery object, but not element it self. There are two notice
+especially for Jifty world.
+
+First of all, Jifty develoeprs should always use C<Jifty.$>. Deep in
+the design of Jifty, there are many kind of elements with C<":">
+character in their id. Sadly it is a feature in jQuery to do more
+powerful seleciton with C<":"> character. For example, this selects
+current mouse-overed elements:
+
+    jQuery(":hover")
+
+C<jifty.js> internally use C<Jifty.$> as the direct replacement to
+C<$()> function defined in the Prototype library.
+
+However, for application developers it's quite safe to use
+C<jQuery("#id")> to select elements they created.
+
+=head2 Document ready with jQuery()
+
+The way to do execute some javascript right after the DOM is ready is
+to bind a handler for C<"ready"> event on the C<document> object:
+
+    jQuery(document).ready(function() {
+        ...
+    });
+
+Since is done quite often, there's a shortcut:
+
+    jQuery(function() {
+        ...
+    });
+
+=head1 METHODS
+
+This section list those public functions under C<Jifty> namespace.
+They are defined in jifty.js.
+
+=over
+
+=item Jifty.$( element_id )
+
+This is a shorthand of document.getElementById function, like the C<$>
+function defined in Prototype library. It is also internally used a
+lot because many form specific element ID does not get along with
+jQuery's selector, which expect the ":" character is used for special
+purpose.
+
+element_id should be a string. If not, element_id itself is returned.
+Therefore, this convention:
+
+    element = Jifty.$(element)
+
+Can work when the variable C<element> is either a string, or a HTML
+element object.
+
+=item Jifty.Effect(element, effect_name, option)
+
+When called, instantly pefrom a js effect on give element. "element" is an
+element object.
+
+The last arg "option" is a hash. Currently it's only used for
+specificing callbacks. There are two possible callbacks, before and
+after. You may specify them like this:
+
+    Jifty.Effect(element, "Fade", { duration: 2.0 }, {
+        before: function() { ... },
+        after: function() { ... }
+    });
+
+The "before" callback is called right before the effect starts.
+The "after" callback is called right after it's started, but not
+necessarily ended.
+
+This function is written to make it possible that a Jifty plugin
+can override default effects with other fancy javascript libraries.
+By default, it delegates all the real work to jQuery's built-in
+effect functions.
+
+=item Jifty.Form.getElements(element)
+
+Given a form element, returns all input fields inside this form. That
+includes INPUT, SELECT, tags. The returned value is an array of HTML
+elements.
+
+=item Jifty.Form.getActions(element)
+
+Given a form element, returns a array of elements that are defined as
+Jifty actions.
+
+=item Jifty.Form.clearPlaceholders(element)
+
+=item Jifty.Form.Element.getMoniker( element )
+
+Given an element, or an element id, return a string representing a
+moniker of this element. It returs null if the given element is
+considered having no moniker at all.
+
+=item Jifty.Form.Element.getAction( element )
+
+Takes an element or an element id. Get the action for this form
+element. The returned value is an Action object.
+
+=item Jifty.Form.Element.getType( element )
+
+Takes an element or an element id, returns the type associated with
+this element. Possible return values are "registraion", "value",
+"fallback", or null if the element does not belongs to any of these
+types.
+
+=item Jifty.Form.Element.getField( element )
+
+Takes an element or an element id, returns the name of it. Returns
+null if the element given does not have a name.
+
+=item Jifty.Form.Element.getValue( element )
+
+Tkaes an element or an element id, returns the element value. If the
+element is a CHECKBOX or a RADIO button but it's un-checked, returns
+null.
+
+=item Jifty.Form.Element.validate( element )
+
+Validates the action this form element is part of.
+
+=item Jifty.Form.Element.disableValidation( element )
+
+Temporarily disable validation.
+
+=item Jifty.Form.Element.enableValidation( element )
+
+Re-enable validation.
+
+=item Jifty.Form.Element.getForm( element )
+
+Look up the form that this element is part of.
+
+This is sometimes more complicated than you'd think because the form
+may not exist anymore, or the element may have been inserted into a
+new form.  Hence, we may need to walk the DOM.
+
+Upon the failure of searching, null is returned.
+
+=item Jifty.Form.Element.buttonArguments( element )
+
+Takes an element or an element id that is considered as a "button",
+which can be an <INPUT type="submit"> tag, or a <A> tag, returns the
+arguments on this element.
+
+If none, an empty object is returned.
+
+=item Jifty.Form.Element.buttonActions( element )
+
+Takes an element or an element id that is considered as a "button",
+return array of actions on this element.
+
+If none, an empty array is returned.
+
+=item Jifty.Form.Element.buttonFormElements( element )
+
+Takes an element or an element id that is considered as a "button",
+return an array of form elements that's just constructed based on the
+arguments on this element.
+
+If none, an empty array is returned.
+
+=item Jifty.Form.Element.clickDefaultButton( element )
+
+Click the first button that will submit the action associated with the
+form element.
+
+=item Jifty.Form.Element.handleEnter( event )
+
+Trap "Enter" key, and prevent it from doing any browser default
+behaviours.
+
+=back
+
+=head1 REFERENCE
+
+L<http://www.slideshare.net/simon/jquery-in-15-minutes/>
+L<http://www.slideshare.net/simon/learning-jquery-in-30-minutes/>
+L<http://www.slideshare.net/remy.sharp/prototype-jquery-going-from-one-to-the-other/>
+L<http://docs.jquery.com/>
+
+=cut
+

Added: jifty/trunk/lib/Jifty/Manual/jQueryMigrationGuide.pod
==============================================================================
--- (empty file)
+++ jifty/trunk/lib/Jifty/Manual/jQueryMigrationGuide.pod	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,136 @@
+=head1 NAME
+
+jQueryMigrationGuide - How to migrate your code to use jQuery.
+
+=head1 Migrate your jifty app to jquery
+
+Application developers may start the migration by modifying
+F<config.yml>, set the C<ConfigFileVersion> to 4. If you did not write
+any custom javascript code for your app, then its done. Everything
+should just work.
+
+If you did wrote some javascript code before, but you did not use any
+of those funcitons defined in F<jifty*.js>, F<prototype.js> or
+F<scriptaculous.js>, then you're still good to go.
+
+Otherwise, your code might need to be modified a little bit. Since
+both F<prototype.js> and F<scriptaculous.js> are removed by default,
+one trivial choice is to simply bring them back. That is as easy as
+adding a B<Prototypism> plugin to your Jifty application.
+
+If you dislike the whole Prototypism like us, you can choose to
+re-write your code with jQuery. In the section L</"From Prototype
+to jQuery"> below, we provide some known pattern that can be applied to
+rewrite prototypism code with jQuery, or with just normal javascript.
+
+If you hack Jifty internal, please make sure you've read the following
+L</"Jifty API"> section and L<Jifty::Manual::JavaScript> to catch the
+Javascript API update since the removal of C<prototype.js>.
+
+Although we've removed C<prototype.js>, we still prefer to use
+non-conflict mode of jQuery at this moment. That is, C<$> function is
+now undefined, but not an alias of jQuery. This is to ensure that it's
+not conflicting with Prototypism at all conditions. If you'd like
+to use C<$> function, create that alias in your C<app.js> like this:
+
+    $ = jQuery;
+
+However, instead of making a global alias, it's always recommended to
+always make this alias with a closure to really localize it.
+
+    (function($) {
+        // $ is an alias to jQuery to the end of this closure
+
+        $(".message").show();
+    })(jQuery);
+
+=head1 Jifty API
+
+Jifty javascript libraries embraced a major re-architect after jQuery
+landed. Especially those internal functions to process form elements.
+
+The Prototype-based old way is to extend Form object and the
+Form.Element object. Since the removal of Prototype library, it is
+dangerous to name those functions under Form. Because loading
+Prototype library can destroy those Jifty functions.
+
+The new jQuery-fashioned way is to always extend internal functions
+under Jifty object. C<Form becomes C<Jifty.Form>, C<Form.Element> becomes
+C<Jifty.Form.Element>, and so on. The detail list of these defined
+functions are given in L<Jifty::Manual::Javascript>. Most of
+those functions are internal functions that you probably should not
+use them directly.
+
+=head1 From Prototype to jQuery
+
+If you've ever written javascript code on your Jifty applications, and
+you'd like to remove PrototypeJS library, here are some dummy rules to
+re-write prototypejs-based javascript code with jQuery.
+
+=head3 Array iteration
+
+From:
+
+    A.each( function( $_ ) { ... } )
+
+To:
+
+    jQuery.each(A, function(index, value ) {
+        // "this" is an alias to current value.
+    })
+
+=head2 Hash key iteration
+
+From:
+
+    H = new Hash({...});
+
+    H.each(function( pair ) {
+        // pair.key is the key
+        // pair.value is the value
+    });
+
+jQuery.each is designed to work on both C<Array> and C<Object> in the
+same way. So there's not much difference.
+
+To:
+
+    // H can be any kind of "Object"
+
+    jQuery.each(H, function(key, value) {
+        // "this" is an alias to current value.
+    })
+
+=head2 Object extend
+
+From:
+
+    obj.extend({ ... }}
+
+To:
+
+    jQuery.extend( obj, { ... } )
+
+=head2 JSON
+
+jQuery does not build-in with JSON stringify function, but since it
+neither altered the native Array, nor defined its own Hash, it's
+prefered and fine to just use C<JSON.stringify> from C<json.js>.
+
+From:
+
+    // obj need to be one of those objects defined in C<prototype.js>
+    obj.toJSON();
+
+To:
+
+    JSON.stringify( obj )
+
+=head2 Effects
+
+jQuery has a small set of default effects built-in to its core. They
+have different naming then those defined in C<scriptaculous.js>. The
+internal way to do effect is via the C<Jifty.Effect> method. Please
+see the detail usage in L<Jifty::Manual::JavaScript>.
+
+=cut

Modified: jifty/trunk/lib/Jifty/Plugin/Halo.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Plugin/Halo.pm	(original)
+++ jifty/trunk/lib/Jifty/Plugin/Halo.pm	Wed Apr  9 00:12:34 2008
@@ -198,7 +198,7 @@
                     my $expanded = Jifty->web->serial;
                     my $yaml = Jifty->web->escape(Jifty::YAML::Dump($value));
                     #$out .= qq{<a href="#" onclick="Element.toggle('$expanded'); return false">$ref</a><div id="$expanded" style="display: none; position: absolute; left: 200px; border: 1px solid black; background: #ccc; padding: 1em; padding-top: 0; width: 300px; height: 500px; overflow: auto"><pre>$yaml</pre></div>};
-                    $out .= qq{<a href="#" onclick="Element.toggle('$expanded'); return false">$ref</a><div id="$expanded" class="halo-argument" style="display: none"><pre>$yaml</pre></div>};
+                    $out .= qq{<a href="#" onclick="jQuery(Jifty.$('$expanded')).toggle()); return false">$ref</a><div id="$expanded" class="halo-argument" style="display: none"><pre>$yaml</pre></div>};
                 }
                 elsif (defined $value) {
                     $out .= Jifty->web->escape($value);

Added: jifty/trunk/lib/Jifty/Plugin/Prototypism.pm
==============================================================================
--- (empty file)
+++ jifty/trunk/lib/Jifty/Plugin/Prototypism.pm	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,42 @@
+use strict;
+use warnings;
+
+package Jifty::Plugin::Prototypism;
+use base 'Jifty::Plugin';
+
+=head1 NAME
+
+Jifty::Plugin::Prototypism
+
+=head1 SYNOPSIS
+
+# In your jifty config.yml under the framework section:
+
+  Plugins:
+    - Prototypism
+        cdn: 'http://yourcdn.for.static.prefix/'
+
+=cut
+
+__PACKAGE__->mk_accessors(qw(cdn));
+
+sub init {
+    my $self = shift;
+    return if $self->_pre_init;
+
+    my %opt  = @_;
+    $self->cdn( $opt{ cdn } || '' );
+    my @js = qw(
+        prototype
+        scriptaculous/builder
+        scriptaculous/effects
+        scriptaculous/controls
+    );
+
+    push @js, 'jifty_compatible' if Jifty->config->framework('ConfigFileVersion') < 4;
+    Jifty->web->add_javascript( "prototypism/$_.js" ) for @js;
+
+}
+
+1;
+

Modified: jifty/trunk/lib/Jifty/Plugin/SinglePage.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Plugin/SinglePage.pm	(original)
+++ jifty/trunk/lib/Jifty/Plugin/SinglePage.pm	Wed Apr  9 00:12:34 2008
@@ -20,6 +20,11 @@
 
 =cut
 
+Jifty->web->add_javascript(
+    'singlepage/rsh/rsh.js',
+    'singlepage/spa.js'
+);
+
 sub init {
     my $self = shift;
     return if $self->_pre_init;
@@ -61,12 +66,13 @@
             $self->_push_onclick($args, {
                 region       => $self->region_name,
                 replace_with => $url,
+                beforeclick  => qq{SPA.historyChange('$url', { 'continuation':{}, 'actions':{}, 'fragments':[{'mode':'Replace','args':@{[ Jifty::JSON::objToJson($args->{parameters})]},'region':'__page','path':'$url'}],'action_arguments':{}}, true);},
                 args         => { %{$args->{parameters}}} });
         }
         elsif (exists $args->{submit} && !$args->{onclick}) {
 	    if ($args->{_form} && $args->{_form}{submit_to}) {
 		my $to = $args->{_form}{submit_to};
-		$self->_push_onclick($args, { beforeclick => qq{return _sp_submit_form(this, event, "$to");} });
+		$self->_push_onclick($args, { beforeclick => qq{return SPA._sp_submit_form(this, event, "$to");} });
 	    }
 	    else {
 		$self->_push_onclick($args, { refresh_self => 1, submit => $args->{submit} });

Modified: jifty/trunk/lib/Jifty/Web.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Web.pm	(original)
+++ jifty/trunk/lib/Jifty/Web.pm	Wed Apr  9 00:12:34 2008
@@ -38,13 +38,12 @@
     jsan/Upgrade/Array/push.js
     jsan/DOM/Events.js
     json.js
-    prototype.js
     jquery-1.2.1.js
+    iutil.js
+    iautocompleter.js
+    jifty_interface.js
     jquery_noconflict.js
     behaviour.js
-    scriptaculous/builder.js
-    scriptaculous/effects.js
-    scriptaculous/controls.js
     formatDate.js
     template_declare.js
     jifty.js
@@ -58,7 +57,6 @@
     key_bindings.js
     context_menu.js
     bps_util.js
-    rico.js
     yui/yahoo.js
     yui/dom.js
     yui/event.js

Modified: jifty/trunk/share/plugins/Jifty/Plugin/I18N/web/static/js/loc.js
==============================================================================
--- jifty/trunk/share/plugins/Jifty/Plugin/I18N/web/static/js/loc.js	(original)
+++ jifty/trunk/share/plugins/Jifty/Plugin/I18N/web/static/js/loc.js	Wed Apr  9 00:12:34 2008
@@ -1,4 +1,4 @@
-Localization = Object.extend(new Object(), {
+Localization = {
     init: function(params) {
         this.lang = params.lang || 'en';
         if (params["dict_path"]) {
@@ -10,22 +10,21 @@
         this.dict = this.load_dict(lang);
     },
     load_dict: function(lang) {
-        var d;
-        new Ajax.Request(
-            this.dict_path + "/" + lang + ".json",
-            {
-                method: 'get',
-                asynchronous: false,
-                onComplete: function(t, obj) {
-                    eval("d = " + t.responseText || "{}");
-                }
+        var d = {};
+        jQuery.ajax({
+            url: this.dict_path + "/" + lang + ".json",
+            type: 'get',
+            asynchronous: fasle,
+            success: function(dict) {
+                eval("d = " + dict || "{}");
             }
-        );
+        });
+
         return d;
     },
     loc: function(str) {
         var dict = this.dict;
-        if (dict[str]) {
+        if (dict[str] != null) {
             return dict[str];
         }
         return str;
@@ -58,8 +57,10 @@
         if (minutes < 2880) return _('about one day');
         else return (Math.round(minutes / 1440) + _(' days'))
     }
-
-});
+};
 
 Localization.dict = {};
-_ = Localization.loc.bind(Localization);
+
+window._ = function() {
+    return Localization.loc.apply(Localization, arguments);
+}

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/jifty_compatible.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/jifty_compatible.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,33 @@
+// Compatible notice for old school
+
+(function($) {
+    $.each(
+        Jifty.Form,
+        function(k, v) {
+            if ( $.isFunction(v) && Form[k] == null ) {
+                Form[k] = function() {
+                    alert("Form." + k +
+                          " is going to be depcreated. Please use Jifty.Form." + k +
+                          " instead.");
+                    v.apply(Jifty.Form, arguments);
+                }
+            }
+        }
+    );
+
+    $.each(
+        Jifty.Form.Element,
+        function(k, v) {
+            if ( $.isFunction(v) && Form.Element[k] == null ) {
+                Form.Element[k] = function() {
+                    alert("Form.Element" + k +
+                          " is going to be depcreated. Please use Jifty.Form.Element" + k +
+                          " instead.");
+                    v.apply(Jifty.Form.Element, arguments);
+                }
+            }
+        }
+    );
+
+})(jQuery);
+

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/prototype.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/prototype.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,4204 @@
+/*  Prototype JavaScript framework, version 1.6.0
+ *  (c) 2005-2007 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.6.0',
+
+  Browser: {
+    IE:     !!(window.attachEvent && !window.opera),
+    Opera:  !!window.opera,
+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
+    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+  },
+
+  BrowserFeatures: {
+    XPath: !!document.evaluate,
+    ElementExtensions: !!window.HTMLElement,
+    SpecificElementExtensions:
+      document.createElement('div').__proto__ &&
+      document.createElement('div').__proto__ !==
+        document.createElement('form').__proto__
+  },
+
+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  emptyFunction: function() { },
+  K: function(x) { return x }
+};
+
+if (Prototype.Browser.MobileSafari)
+  Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+if (Prototype.Browser.WebKit)
+  Prototype.BrowserFeatures.XPath = false;
+
+/* Based on Alex Arnell's inheritance implementation. */
+var Class = {
+  create: function() {
+    var parent = null, properties = $A(arguments);
+    if (Object.isFunction(properties[0]))
+      parent = properties.shift();
+
+    function klass() {
+      this.initialize.apply(this, arguments);
+    }
+
+    Object.extend(klass, Class.Methods);
+    klass.superclass = parent;
+    klass.subclasses = [];
+
+    if (parent) {
+      var subclass = function() { };
+      subclass.prototype = parent.prototype;
+      klass.prototype = new subclass;
+      parent.subclasses.push(klass);
+    }
+
+    for (var i = 0; i < properties.length; i++)
+      klass.addMethods(properties[i]);
+
+    if (!klass.prototype.initialize)
+      klass.prototype.initialize = Prototype.emptyFunction;
+
+    klass.prototype.constructor = klass;
+
+    return klass;
+  }
+};
+
+Class.Methods = {
+  addMethods: function(source) {
+    var ancestor   = this.superclass && this.superclass.prototype;
+    var properties = Object.keys(source);
+
+    if (!Object.keys({ toString: true }).length)
+      properties.push("toString", "valueOf");
+
+    for (var i = 0, length = properties.length; i < length; i++) {
+      var property = properties[i], value = source[property];
+      if (ancestor && Object.isFunction(value) &&
+          value.argumentNames().first() == "$super") {
+        var method = value, value = Object.extend((function(m) {
+          return function() { return ancestor[m].apply(this, arguments) };
+        })(property).wrap(method), {
+          valueOf:  function() { return method },
+          toString: function() { return method.toString() }
+        });
+      }
+      this.prototype[property] = value;
+    }
+
+    return this;
+  }
+};
+
+var Abstract = { };
+
+Object.extend = function(destination, source) {
+  for (var property in source)
+    destination[property] = source[property];
+  return destination;
+};
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (object === undefined) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : object.toString();
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  toJSON: function(object) {
+    var type = typeof object;
+    switch (type) {
+      case 'undefined':
+      case 'function':
+      case 'unknown': return;
+      case 'boolean': return object.toString();
+    }
+
+    if (object === null) return 'null';
+    if (object.toJSON) return object.toJSON();
+    if (Object.isElement(object)) return;
+
+    var results = [];
+    for (var property in object) {
+      var value = Object.toJSON(object[property]);
+      if (value !== undefined)
+        results.push(property.toJSON() + ': ' + value);
+    }
+
+    return '{' + results.join(', ') + '}';
+  },
+
+  toQueryString: function(object) {
+    return $H(object).toQueryString();
+  },
+
+  toHTML: function(object) {
+    return object && object.toHTML ? object.toHTML() : String.interpret(object);
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({ }, object);
+  },
+
+  isElement: function(object) {
+    return object && object.nodeType == 1;
+  },
+
+  isArray: function(object) {
+    return object && object.constructor === Array;
+  },
+
+  isHash: function(object) {
+    return object instanceof Hash;
+  },
+
+  isFunction: function(object) {
+    return typeof object == "function";
+  },
+
+  isString: function(object) {
+    return typeof object == "string";
+  },
+
+  isNumber: function(object) {
+    return typeof object == "number";
+  },
+
+  isUndefined: function(object) {
+    return typeof object == "undefined";
+  }
+});
+
+Object.extend(Function.prototype, {
+  argumentNames: function() {
+    var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
+    return names.length == 1 && !names[0] ? [] : names;
+  },
+
+  bind: function() {
+    if (arguments.length < 2 && arguments[0] === undefined) return this;
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function() {
+      return __method.apply(object, args.concat($A(arguments)));
+    }
+  },
+
+  bindAsEventListener: function() {
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function(event) {
+      return __method.apply(object, [event || window.event].concat(args));
+    }
+  },
+
+  curry: function() {
+    if (!arguments.length) return this;
+    var __method = this, args = $A(arguments);
+    return function() {
+      return __method.apply(this, args.concat($A(arguments)));
+    }
+  },
+
+  delay: function() {
+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+    return window.setTimeout(function() {
+      return __method.apply(__method, args);
+    }, timeout);
+  },
+
+  wrap: function(wrapper) {
+    var __method = this;
+    return function() {
+      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+    }
+  },
+
+  methodize: function() {
+    if (this._methodized) return this._methodized;
+    var __method = this;
+    return this._methodized = function() {
+      return __method.apply(null, [this].concat($A(arguments)));
+    };
+  }
+});
+
+Function.prototype.defer = Function.prototype.delay.curry(0.01);
+
+Date.prototype.toJSON = function() {
+  return '"' + this.getUTCFullYear() + '-' +
+    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+    this.getUTCDate().toPaddedString(2) + 'T' +
+    this.getUTCHours().toPaddedString(2) + ':' +
+    this.getUTCMinutes().toPaddedString(2) + ':' +
+    this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) { }
+    }
+
+    return returnValue;
+  }
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create({
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  execute: function() {
+    this.callback(this);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.execute();
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+});
+Object.extend(String, {
+  interpret: function(value) {
+    return value == null ? '' : String(value);
+  },
+  specialChar: {
+    '\b': '\\b',
+    '\t': '\\t',
+    '\n': '\\n',
+    '\f': '\\f',
+    '\r': '\\r',
+    '\\': '\\\\'
+  }
+});
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return String(this);
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : String(this);
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var self = arguments.callee;
+    self.text.data = this;
+    return self.div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = new Element('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return { };
+
+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var key = decodeURIComponent(pair.shift());
+        var value = pair.length > 1 ? pair.join('=') : pair[0];
+        if (value != undefined) value = decodeURIComponent(value);
+
+        if (key in hash) {
+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+          hash[key].push(value);
+        }
+        else hash[key] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  times: function(count) {
+    return count < 1 ? '' : new Array(count + 1).join(this);
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function() {
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+      var character = String.specialChar[match[0]];
+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+    });
+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  },
+
+  toJSON: function() {
+    return this.inspect(true);
+  },
+
+  unfilterJSON: function(filter) {
+    return this.sub(filter || Prototype.JSONFilter, '#{1}');
+  },
+
+  isJSON: function() {
+    var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+  },
+
+  evalJSON: function(sanitize) {
+    var json = this.unfilterJSON();
+    try {
+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+    } catch (e) { }
+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+  },
+
+  include: function(pattern) {
+    return this.indexOf(pattern) > -1;
+  },
+
+  startsWith: function(pattern) {
+    return this.indexOf(pattern) === 0;
+  },
+
+  endsWith: function(pattern) {
+    var d = this.length - pattern.length;
+    return d >= 0 && this.lastIndexOf(pattern) === d;
+  },
+
+  empty: function() {
+    return this == '';
+  },
+
+  blank: function() {
+    return /^\s*$/.test(this);
+  },
+
+  interpolate: function(object, pattern) {
+    return new Template(this, pattern).evaluate(object);
+  }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+  escapeHTML: function() {
+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+  },
+  unescapeHTML: function() {
+    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (Object.isFunction(replacement)) return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+};
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+  div:  document.createElement('div'),
+  text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create({
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    if (Object.isFunction(object.toTemplateReplacements))
+      object = object.toTemplateReplacements();
+
+    return this.template.gsub(this.pattern, function(match) {
+      if (object == null) return '';
+
+      var before = match[1] || '';
+      if (before == '\\') return match[2];
+
+      var ctx = object, expr = match[3];
+      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/, match = pattern.exec(expr);
+      if (match == null) return before;
+
+      while (match != null) {
+        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+        ctx = ctx[comp];
+        if (null == ctx || '' == match[3]) break;
+        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+        match = pattern.exec(expr);
+      }
+
+      return before + String.interpret(ctx);
+    }.bind(this));
+  }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
+
+var Enumerable = {
+  each: function(iterator, context) {
+    var index = 0;
+    iterator = iterator.bind(context);
+    try {
+      this._each(function(value) {
+        iterator(value, index++);
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.collect(iterator, context);
+  },
+
+  all: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!iterator(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!iterator(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var results = [];
+    this.each(function(value, index) {
+      results.push(iterator(value, index));
+    });
+    return results;
+  },
+
+  detect: function(iterator, context) {
+    iterator = iterator.bind(context);
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator, context) {
+    iterator = iterator.bind(context);
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(filter, iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var results = [];
+
+    if (Object.isString(filter))
+      filter = new RegExp(filter);
+
+    this.each(function(value, index) {
+      if (filter.match(value))
+        results.push(iterator(value, index));
+    });
+    return results;
+  },
+
+  include: function(object) {
+    if (Object.isFunction(this.indexOf))
+      if (this.indexOf(object) != -1) return true;
+
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = fillWith === undefined ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator, context) {
+    iterator = iterator.bind(context);
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.map(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result;
+    this.each(function(value, index) {
+      value = iterator(value, index);
+      if (result == undefined || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result;
+    this.each(function(value, index) {
+      value = iterator(value, index);
+      if (result == undefined || value < result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      (iterator(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator, context) {
+    iterator = iterator.bind(context);
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator, context) {
+    iterator = iterator.bind(context);
+    return this.map(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.map();
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (Object.isFunction(args.last()))
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+};
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  filter:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray,
+  every:   Enumerable.all,
+  some:    Enumerable.any
+});
+function $A(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) return iterable.toArray();
+  var length = iterable.length, results = new Array(length);
+  while (length--) results[length] = iterable[length];
+  return results;
+}
+
+if (Prototype.Browser.WebKit) {
+  function $A(iterable) {
+    if (!iterable) return [];
+    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
+        iterable.toArray) return iterable.toArray();
+    var length = iterable.length, results = new Array(length);
+    while (length--) results[length] = iterable[length];
+    return results;
+  }
+}
+
+Array.from = $A;
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i < length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(Object.isArray(value) ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function(sorted) {
+    return this.inject([], function(array, value, index) {
+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+        array.push(value);
+      return array;
+    });
+  },
+
+  intersect: function(array) {
+    return this.uniq().findAll(function(item) {
+      return array.detect(function(value) { return item === value });
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  },
+
+  toJSON: function() {
+    var results = [];
+    this.each(function(object) {
+      var value = Object.toJSON(object);
+      if (value !== undefined) results.push(value);
+    });
+    return '[' + results.join(', ') + ']';
+  }
+});
+
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+  Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+  i || (i = 0);
+  var length = this.length;
+  if (i < 0) i = length + i;
+  for (; i < length; i++)
+    if (this[i] === item) return i;
+  return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+  var n = this.slice(0, i).reverse().indexOf(item);
+  return (n < 0) ? n : i - n - 1;
+};
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+  if (!Object.isString(string)) return [];
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+  Array.prototype.concat = function() {
+    var array = [];
+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      if (Object.isArray(arguments[i])) {
+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  };
+}
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  },
+
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
+  },
+
+  toJSON: function() {
+    return isFinite(this) ? this.toString() : 'null';
+  }
+});
+
+$w('abs round ceil floor').each(function(method){
+  Number.prototype[method] = Math[method].methodize();
+});
+function $H(object) {
+  return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+  if (function() {
+    var i = 0, Test = function(value) { this.key = value };
+    Test.prototype.key = 'foo';
+    for (var property in new Test('bar')) i++;
+    return i > 1;
+  }()) {
+    function each(iterator) {
+      var cache = [];
+      for (var key in this._object) {
+        var value = this._object[key];
+        if (cache.include(key)) continue;
+        cache.push(key);
+        var pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    }
+  } else {
+    function each(iterator) {
+      for (var key in this._object) {
+        var value = this._object[key], pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    }
+  }
+
+  function toQueryPair(key, value) {
+    if (Object.isUndefined(value)) return key;
+    return key + '=' + encodeURIComponent(String.interpret(value));
+  }
+
+  return {
+    initialize: function(object) {
+      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+    },
+
+    _each: each,
+
+    set: function(key, value) {
+      return this._object[key] = value;
+    },
+
+    get: function(key) {
+      return this._object[key];
+    },
+
+    unset: function(key) {
+      var value = this._object[key];
+      delete this._object[key];
+      return value;
+    },
+
+    toObject: function() {
+      return Object.clone(this._object);
+    },
+
+    keys: function() {
+      return this.pluck('key');
+    },
+
+    values: function() {
+      return this.pluck('value');
+    },
+
+    index: function(value) {
+      var match = this.detect(function(pair) {
+        return pair.value === value;
+      });
+      return match && match.key;
+    },
+
+    merge: function(object) {
+      return this.clone().update(object);
+    },
+
+    update: function(object) {
+      return new Hash(object).inject(this, function(result, pair) {
+        result.set(pair.key, pair.value);
+        return result;
+      });
+    },
+
+    toQueryString: function() {
+      return this.map(function(pair) {
+        var key = encodeURIComponent(pair.key), values = pair.value;
+
+        if (values && typeof values == 'object') {
+          if (Object.isArray(values))
+            return values.map(toQueryPair.curry(key)).join('&');
+        }
+        return toQueryPair(key, values);
+      }).join('&');
+    },
+
+    inspect: function() {
+      return '#<Hash:{' + this.map(function(pair) {
+        return pair.map(Object.inspect).join(': ');
+      }).join(', ') + '}>';
+    },
+
+    toJSON: function() {
+      return Object.toJSON(this.toObject());
+    },
+
+    clone: function() {
+      return new Hash(this);
+    }
+  }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+};
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+};
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (Object.isFunction(responder[callback])) {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) { }
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate:   function() { Ajax.activeRequestCount++ },
+  onComplete: function() { Ajax.activeRequestCount-- }
+});
+
+Ajax.Base = Class.create({
+  initialize: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   '',
+      evalJSON:     true,
+      evalJS:       true
+    };
+    Object.extend(this.options, options || { });
+
+    this.options.method = this.options.method.toLowerCase();
+    if (Object.isString(this.options.parameters))
+      this.options.parameters = this.options.parameters.toQueryParams();
+  }
+});
+
+Ajax.Request = Class.create(Ajax.Base, {
+  _complete: false,
+
+  initialize: function($super, url, options) {
+    $super(options);
+    this.transport = Ajax.getTransport();
+    this.request(url);
+  },
+
+  request: function(url) {
+    this.url = url;
+    this.method = this.options.method;
+    var params = Object.clone(this.options.parameters);
+
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    this.parameters = params;
+
+    if (params = Object.toQueryString(params)) {
+      // when GET, append parameters to URL
+      if (this.method == 'get')
+        this.url += (this.url.include('?') ? '&' : '?') + params;
+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+        params += '&_=';
+    }
+
+    try {
+      var response = new Ajax.Response(this);
+      if (this.options.onCreate) this.options.onCreate(response);
+      Ajax.Responders.dispatch('onCreate', this, response);
+
+      this.transport.open(this.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+      this.transport.send(this.body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+          headers['Content-Type'] = this.options.contentType +
+            (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) {
+            headers['Connection'] = 'close';
+          }
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (Object.isFunction(extras.push))
+        for (var i = 0, length = extras.length; i < length; i += 2) {
+          headers[extras[i]] = extras[i+1];
+        }
+      else
+        $H(extras).each(function(pair) {
+            headers[pair.key] = pair.value 
+        });
+    }
+
+    for (var name in headers) {
+        if ( typeof headers[name] == "string" ) 
+            this.transport.setRequestHeader(name, headers[name]);
+    }
+
+  },
+
+  success: function() {
+    var status = this.getStatus();
+    return !status || (status >= 200 && status < 300);
+  },
+
+  getStatus: function() {
+    try {
+      return this.transport.status || 0;
+    } catch (e) { return 0 }
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + response.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(response, response.headerJSON);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      var contentType = response.getHeader('Content-type');
+      if (this.options.evalJS == 'force'
+          || (this.options.evalJS && contentType
+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+        this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval((this.transport.responseText || '').unfilterJSON());
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+  initialize: function(request){
+    this.request = request;
+    var transport  = this.transport  = request.transport,
+        readyState = this.readyState = transport.readyState;
+
+    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+      this.status       = this.getStatus();
+      this.statusText   = this.getStatusText();
+      this.responseText = String.interpret(transport.responseText);
+      this.headerJSON   = this._getHeaderJSON();
+    }
+
+    if(readyState == 4) {
+      var xml = transport.responseXML;
+      this.responseXML  = xml === undefined ? null : xml;
+      this.responseJSON = this._getResponseJSON();
+    }
+  },
+
+  status:      0,
+  statusText: '',
+
+  getStatus: Ajax.Request.prototype.getStatus,
+
+  getStatusText: function() {
+    try {
+      return this.transport.statusText || '';
+    } catch (e) { return '' }
+  },
+
+  getHeader: Ajax.Request.prototype.getHeader,
+
+  getAllHeaders: function() {
+    try {
+      return this.getAllResponseHeaders();
+    } catch (e) { return null }
+  },
+
+  getResponseHeader: function(name) {
+    return this.transport.getResponseHeader(name);
+  },
+
+  getAllResponseHeaders: function() {
+    return this.transport.getAllResponseHeaders();
+  },
+
+  _getHeaderJSON: function() {
+    var json = this.getHeader('X-JSON');
+    if (!json) return null;
+    json = decodeURIComponent(escape(json));
+    try {
+      return json.evalJSON(this.request.options.sanitizeJSON);
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  },
+
+  _getResponseJSON: function() {
+    var options = this.request.options;
+    if (!options.evalJSON || (options.evalJSON != 'force' &&
+      !(this.getHeader('Content-type') || '').include('application/json')))
+        return null;
+    try {
+      return this.transport.responseText.evalJSON(options.sanitizeJSON);
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+  initialize: function($super, container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    };
+
+    options = options || { };
+    var onComplete = options.onComplete;
+    options.onComplete = (function(response, param) {
+      this.updateContent(response.responseText);
+      if (Object.isFunction(onComplete)) onComplete(response, param);
+    }).bind(this);
+
+    $super(url, options);
+  },
+
+  updateContent: function(responseText) {
+    var receiver = this.container[this.success() ? 'success' : 'failure'],
+        options = this.options;
+
+    if (!options.evalScripts) responseText = responseText.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (options.insertion) {
+        if (Object.isString(options.insertion)) {
+          var insertion = { }; insertion[options.insertion] = responseText;
+          receiver.insert(insertion);
+        }
+        else options.insertion(receiver, responseText);
+      }
+      else receiver.update(responseText);
+    }
+
+    if (this.success()) {
+      if (this.onComplete) this.onComplete.bind(this).defer();
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+  initialize: function($super, container, url, options) {
+    $super(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = { };
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(response) {
+    if (this.options.decay) {
+      this.decay = (response.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = response.responseText;
+    }
+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (Object.isString(element))
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(Element.extend(query.snapshotItem(i)));
+    return results;
+  };
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+  // DOM level 2 ECMAScript Language Binding
+  Object.extend(Node, {
+    ELEMENT_NODE: 1,
+    ATTRIBUTE_NODE: 2,
+    TEXT_NODE: 3,
+    CDATA_SECTION_NODE: 4,
+    ENTITY_REFERENCE_NODE: 5,
+    ENTITY_NODE: 6,
+    PROCESSING_INSTRUCTION_NODE: 7,
+    COMMENT_NODE: 8,
+    DOCUMENT_NODE: 9,
+    DOCUMENT_TYPE_NODE: 10,
+    DOCUMENT_FRAGMENT_NODE: 11,
+    NOTATION_NODE: 12
+  });
+}
+
+(function() {
+  var element = this.Element;
+  this.Element = function(tagName, attributes) {
+    attributes = attributes || { };
+    tagName = tagName.toLowerCase();
+    var cache = Element.cache;
+    if (Prototype.Browser.IE && attributes.name) {
+      tagName = '<' + tagName + ' name="' + attributes.name + '">';
+      delete attributes.name;
+      return Element.writeAttribute(document.createElement(tagName), attributes);
+    }
+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+  };
+  Object.extend(this.Element, element || { });
+}).call(window);
+
+Element.cache = { };
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+    content = Object.toHTML(content);
+    element.innerHTML = content.stripScripts();
+    content.evalScripts.bind(content).defer();
+    return element;
+  },
+
+  replace: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    else if (!Object.isElement(content)) {
+      content = Object.toHTML(content);
+      var range = element.ownerDocument.createRange();
+      range.selectNode(element);
+      content.evalScripts.bind(content).defer();
+      content = range.createContextualFragment(content.stripScripts());
+    }
+    element.parentNode.replaceChild(content, element);
+    return element;
+  },
+
+  insert: function(element, insertions) {
+    element = $(element);
+
+    if (Object.isString(insertions) || Object.isNumber(insertions) ||
+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+          insertions = {bottom:insertions};
+
+    var content, t, range;
+
+    for (position in insertions) {
+      if ( position == 'extend' ) continue;
+      content  = insertions[position];
+      position = position.toLowerCase();
+      t = Element._insertionTranslations[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        t.insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+
+      range = element.ownerDocument.createRange();
+      t.initializeRange(element, range);
+      t.insert(element, range.createContextualFragment(content.stripScripts()));
+
+      content.evalScripts.bind(content).defer();
+    }
+
+    return element;
+  },
+
+  wrap: function(element, wrapper, attributes) {
+    element = $(element);
+    if (Object.isElement(wrapper))
+      $(wrapper).writeAttribute(attributes || { });
+    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+    else wrapper = new Element('div', wrapper);
+    if (element.parentNode)
+      element.parentNode.replaceChild(wrapper, element);
+    wrapper.appendChild(element);
+    return wrapper;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $A($(element).getElementsByTagName('*')).each(Element.extend);
+  },
+
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (Object.isString(selector))
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return expression ? Selector.findElement(ancestors, expression, index) :
+      ancestors[index || 0];
+  },
+
+  down: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return element.firstDescendant();
+    var descendants = element.descendants();
+    return expression ? Selector.findElement(descendants, expression, index) :
+      descendants[index || 0];
+  },
+
+  previous: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return expression ? Selector.findElement(previousSiblings, expression, index) :
+      previousSiblings[index || 0];
+  },
+
+  next: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return expression ? Selector.findElement(nextSiblings, expression, index) :
+      nextSiblings[index || 0];
+  },
+
+  select: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  adjacent: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element.parentNode, args).without(element);
+  },
+
+  identify: function(element) {
+    element = $(element);
+    var id = element.readAttribute('id'), self = arguments.callee;
+    if (id) return id;
+    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+    element.writeAttribute('id', id);
+    return id;
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (Prototype.Browser.IE) {
+      var t = Element._attributeTranslations.read;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name]) name = t.names[name];
+      if (name.include(':')) {
+        return (!element.attributes || !element.attributes[name]) ? null :
+         element.attributes[name].value;
+      }
+    }
+    return element.getAttribute(name);
+  },
+
+  writeAttribute: function(element, name, value) {
+    element = $(element);
+    var attributes = { }, t = Element._attributeTranslations.write;
+
+    if (typeof name == 'object') attributes = name;
+    else attributes[name] = value === undefined ? true : value;
+
+    for (var attr in attributes) {
+      var name = t.names[attr] || attr, value = attributes[attr];
+      if (t.values[attr]) name = t.values[attr](element, value);
+      if (value === false || value === null)
+        element.removeAttribute(name);
+      else if (value === true)
+        element.setAttribute(name, name);
+      else element.setAttribute(name, value);
+    }
+    return element;
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    return (elementClassName.length > 0 && (elementClassName == className ||
+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    if (!element.hasClassName(className))
+      element.className += (element.className ? ' ' : '') + className;
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    element.className = element.className.replace(
+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return element[element.hasClassName(className) ?
+      'removeClassName' : 'addClassName'](className);
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.blank();
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+
+    if (element.compareDocumentPosition)
+      return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+    if (element.sourceIndex && !Prototype.Browser.Opera) {
+      var e = element.sourceIndex, a = ancestor.sourceIndex,
+       nextAncestor = ancestor.nextSibling;
+      if (!nextAncestor) {
+        do { ancestor = ancestor.parentNode; }
+        while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
+      }
+      if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex);
+    }
+
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = element.cumulativeOffset();
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    style = style == 'float' ? 'cssFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value) {
+      var css = document.defaultView.getComputedStyle(element, null);
+      value = css ? css[style] : null;
+    }
+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+    return value == 'auto' ? null : value;
+  },
+
+  getOpacity: function(element) {
+    return $(element).getStyle('opacity');
+  },
+
+  setStyle: function(element, styles) {
+    element = $(element);
+    var elementStyle = element.style, match;
+    if (Object.isString(styles)) {
+      element.style.cssText += ';' + styles;
+      return styles.include('opacity') ?
+        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+    }
+    for (var property in styles)
+      if (property == 'opacity') element.setOpacity(styles[property]);
+      else
+        elementStyle[(property == 'float' || property == 'cssFloat') ?
+          (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
+            property] = styles[property];
+
+    return element;
+  },
+
+  setOpacity: function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = $(element).getStyle('display');
+    if (display != 'none' && display != null) // Safari bug
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+    if (element._overflow !== 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if (element.tagName == 'BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'absolute') return;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    var offsets = element.positionedOffset();
+    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';
+    return element;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'relative') return;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    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;
+    return element;
+  },
+
+  cumulativeScrollOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  getOffsetParent: 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);
+  },
+
+  viewportOffset: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent == document.body &&
+        Element.getStyle(element, 'position') == 'absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  clonePosition: function(element, source) {
+    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 = source.viewportOffset();
+
+    // find coordinate system to use
+    element = $(element);
+    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(element, 'position') == 'absolute') {
+      parent = element.getOffsetParent();
+      delta = parent.viewportOffset();
+    }
+
+    // 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)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
+    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+    return element;
+  }
+};
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+  getElementsBySelector: Element.Methods.select,
+  childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+  write: {
+    names: {
+      className: 'class',
+      htmlFor:   'for'
+    },
+    values: { }
+  }
+};
+
+
+if (!document.createRange || Prototype.Browser.Opera) {
+  Element.Methods.insert = function(element, insertions) {
+    element = $(element);
+
+    if (Object.isString(insertions) || Object.isNumber(insertions) ||
+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+          insertions = { bottom: insertions };
+
+    var t = Element._insertionTranslations, content, position, pos, tagName;
+
+    for (position in insertions) {
+      content  = insertions[position];
+      position = position.toLowerCase();
+      pos      = t[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        pos.insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+      tagName = ((position == 'before' || position == 'after')
+        ? element.parentNode : element).tagName.toUpperCase();
+
+      if (t.tags[tagName]) {
+        var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+        if (position == 'top' || position == 'after') fragments.reverse();
+        fragments.each(pos.insert.curry(element));
+      }
+      // Sartak: pos.adjacency may be undefined. IE6 gets very unhappy if you
+      // try to pass undef to insertAdjacentHTML
+      else if (pos.adjacency) {
+          element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
+      }
+
+      content.evalScripts.bind(content).defer();
+    }
+
+    return element;
+  };
+}
+
+if (Prototype.Browser.Opera) {
+  Element.Methods._getStyle = Element.Methods.getStyle;
+  Element.Methods.getStyle = function(element, style) {
+    switch(style) {
+      case 'left':
+      case 'top':
+      case 'right':
+      case 'bottom':
+        if (Element._getStyle(element, 'position') == 'static') return null;
+      default: return Element._getStyle(element, style);
+    }
+  };
+  Element.Methods._readAttribute = Element.Methods.readAttribute;
+  Element.Methods.readAttribute = function(element, attribute) {
+    if (attribute == 'title') return element.title;
+    return Element._readAttribute(element, attribute);
+  };
+}
+
+else if (Prototype.Browser.IE) {
+  $w('positionedOffset getOffsetParent viewportOffset').each(function(method) {
+    Element.Methods[method] = Element.Methods[method].wrap(
+      function(proceed, element) {
+        element = $(element);
+        var position = element.getStyle('position');
+        if (position != 'static') return proceed(element);
+        element.setStyle({ position: 'relative' });
+        var value = proceed(element);
+        element.setStyle({ position: position });
+        return value;
+      }
+    );
+  });
+
+  Element.Methods.getStyle = function(element, style) {
+    element = $(element);
+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value && element.currentStyle) value = element.currentStyle[style];
+
+    if (style == 'opacity') {
+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if (value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+
+    if (value == 'auto') {
+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+        return element['offset' + style.capitalize()] + 'px';
+      return null;
+    }
+    return value;
+  };
+
+  Element.Methods.setOpacity = function(element, value) {
+    function stripAlpha(filter){
+      return filter.replace(/alpha\([^\)]*\)/gi,'');
+    }
+    element = $(element);
+    var currentStyle = element.currentStyle;
+    if ((currentStyle && !currentStyle.hasLayout) ||
+      (!currentStyle && element.style.zoom == 'normal'))
+        element.style.zoom = 1;
+
+    var filter = element.getStyle('filter'), style = element.style;
+    if (value == 1 || value === '') {
+      (filter = stripAlpha(filter)) ?
+        style.filter = filter : style.removeAttribute('filter');
+      return element;
+    } else if (value < 0.00001) value = 0;
+    style.filter = stripAlpha(filter) +
+      'alpha(opacity=' + (value * 100) + ')';
+    return element;
+  };
+
+  Element._attributeTranslations = {
+    read: {
+      names: {
+        'class': 'className',
+        'for':   'htmlFor'
+      },
+      values: {
+        _getAttr: function(element, attribute) {
+          return element.getAttribute(attribute, 2);
+        },
+        _getAttrNode: function(element, attribute) {
+          var node = element.getAttributeNode(attribute);
+          return node ? node.value : "";
+        },
+        _getEv: function(element, attribute) {
+          var attribute = element.getAttribute(attribute);
+          return attribute ? attribute.toString().slice(23, -2) : null;
+        },
+        _flag: function(element, attribute) {
+          return $(element).hasAttribute(attribute) ? attribute : null;
+        },
+        style: function(element) {
+          return element.style.cssText.toLowerCase();
+        },
+        title: function(element) {
+          return element.title;
+        }
+      }
+    }
+  };
+
+  Element._attributeTranslations.write = {
+    names: Object.clone(Element._attributeTranslations.read.names),
+    values: {
+      checked: function(element, value) {
+        element.checked = !!value;
+      },
+
+      style: function(element, value) {
+        element.style.cssText = value ? value : '';
+      }
+    }
+  };
+
+  Element._attributeTranslations.has = {};
+
+  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+      'encType maxLength readOnly longDesc').each(function(attr) {
+    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+  });
+
+  (function(v) {
+    Object.extend(v, {
+      href:        v._getAttr,
+      src:         v._getAttr,
+      type:        v._getAttr,
+      action:      v._getAttrNode,
+      disabled:    v._flag,
+      checked:     v._flag,
+      readonly:    v._flag,
+      multiple:    v._flag,
+      onload:      v._getEv,
+      onunload:    v._getEv,
+      onclick:     v._getEv,
+      ondblclick:  v._getEv,
+      onmousedown: v._getEv,
+      onmouseup:   v._getEv,
+      onmouseover: v._getEv,
+      onmousemove: v._getEv,
+      onmouseout:  v._getEv,
+      onfocus:     v._getEv,
+      onblur:      v._getEv,
+      onkeypress:  v._getEv,
+      onkeydown:   v._getEv,
+      onkeyup:     v._getEv,
+      onsubmit:    v._getEv,
+      onreset:     v._getEv,
+      onselect:    v._getEv,
+      onchange:    v._getEv
+    });
+  })(Element._attributeTranslations.read.values);
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1) ? 0.999999 :
+      (value === '') ? '' : (value < 0.00001) ? 0 : value;
+    return element;
+  };
+}
+
+else if (Prototype.Browser.WebKit) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+
+    if (value == 1)
+      if(element.tagName == 'IMG' && element.width) {
+        element.width++; element.width--;
+      } else try {
+        var n = document.createTextNode(' ');
+        element.appendChild(n);
+        element.removeChild(n);
+      } catch (e) { }
+
+    return element;
+  };
+
+  // Safari returns margins on body which is incorrect if the child is absolutely
+  // positioned.  For performance reasons, redefine Position.cumulativeOffset for
+  // KHTML/WebKit only.
+  Element.Methods.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 Element._returnOffset(valueL, valueT);
+  };
+}
+
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+  Element.Methods.update = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+
+    content = Object.toHTML(content);
+    var tagName = element.tagName.toUpperCase();
+
+    if (tagName in Element._insertionTranslations.tags) {
+      $A(element.childNodes).each(function(node) { element.removeChild(node) });
+      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+        .each(function(node) { element.appendChild(node) });
+    }
+    else element.innerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+if (document.createElement('div').outerHTML) {
+  Element.Methods.replace = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) {
+      element.parentNode.replaceChild(content, element);
+      return element;
+    }
+
+    content = Object.toHTML(content);
+    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+    if (Element._insertionTranslations.tags[tagName]) {
+      var nextSibling = element.next();
+      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+      parent.removeChild(element);
+      if (nextSibling)
+        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+      else
+        fragments.each(function(node) { parent.appendChild(node) });
+    }
+    else element.outerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+Element._returnOffset = function(l, t) {
+  var result = [l, t];
+  result.left = l;
+  result.top = t;
+  return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+  div.innerHTML = t[0] + html + t[1];
+  t[2].times(function() { div = div.firstChild });
+  return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+  before: {
+    adjacency: 'beforeBegin',
+    insert: function(element, node) {
+      element.parentNode.insertBefore(node, element);
+    },
+    initializeRange: function(element, range) {
+      range.setStartBefore(element);
+    }
+  },
+  top: {
+    adjacency: 'afterBegin',
+    insert: function(element, node) {
+      element.insertBefore(node, element.firstChild);
+    },
+    initializeRange: function(element, range) {
+      range.selectNodeContents(element);
+      range.collapse(true);
+    }
+  },
+  bottom: {
+    adjacency: 'beforeEnd',
+    insert: function(element, node) {
+      element.appendChild(node);
+    }
+  },
+  after: {
+    adjacency: 'afterEnd',
+    insert: function(element, node) {
+      element.parentNode.insertBefore(node, element.nextSibling);
+    },
+    initializeRange: function(element, range) {
+      range.setStartAfter(element);
+    }
+  },
+  tags: {
+    TABLE:  ['<table>',                '</table>',                   1],
+    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
+    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
+    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+    SELECT: ['<select>',               '</select>',                  1]
+  }
+};
+
+(function() {
+  this.bottom.initializeRange = this.top.initializeRange;
+  Object.extend(this.tags, {
+    THEAD: this.tags.TBODY,
+    TFOOT: this.tags.TBODY,
+    TH:    this.tags.TD
+  });
+}).call(Element._insertionTranslations);
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    attribute = Element._attributeTranslations.has[attribute] || attribute;
+    var node = $(element).getAttributeNode(attribute);
+    return node && node.specified;
+  }
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+    document.createElement('div').__proto__) {
+  window.HTMLElement = { };
+  window.HTMLElement.prototype = document.createElement('div').__proto__;
+  Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.extend = (function() {
+  if (Prototype.BrowserFeatures.SpecificElementExtensions)
+    return Prototype.K;
+
+  var Methods = { }, ByTag = Element.Methods.ByTag;
+
+  var extend = Object.extend(function(element) {
+    if (!element || element._extendedByPrototype ||
+        element.nodeType != 1 || element == window) return element;
+
+    var methods = Object.clone(Methods),
+      tagName = element.tagName, property, value;
+
+    // extend methods for specific tags
+    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+    for (property in methods) {
+      value = methods[property];
+      if (Object.isFunction(value) && !(property in element))
+        element[property] = value.methodize();
+    }
+
+    element._extendedByPrototype = Prototype.emptyFunction;
+    return element;
+
+  }, {
+    refresh: function() {
+      // extend methods for all tags (Safari doesn't need this)
+      if (!Prototype.BrowserFeatures.ElementExtensions) {
+        Object.extend(Methods, Element.Methods);
+        Object.extend(Methods, Element.Methods.Simulated);
+      }
+    }
+  });
+
+  extend.refresh();
+  return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+  if (element.hasAttribute) return element.hasAttribute(attribute);
+  return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+  if (!methods) {
+    Object.extend(Form, Form.Methods);
+    Object.extend(Form.Element, Form.Element.Methods);
+    Object.extend(Element.Methods.ByTag, {
+      "FORM":     Object.clone(Form.Methods),
+      "INPUT":    Object.clone(Form.Element.Methods),
+      "SELECT":   Object.clone(Form.Element.Methods),
+      "TEXTAREA": Object.clone(Form.Element.Methods)
+    });
+  }
+
+  if (arguments.length == 2) {
+    var tagName = methods;
+    methods = arguments[1];
+  }
+
+  if (!tagName) Object.extend(Element.Methods, methods || { });
+  else {
+    if (Object.isArray(tagName)) tagName.each(extend);
+    else extend(tagName);
+  }
+
+  function extend(tagName) {
+    tagName = tagName.toUpperCase();
+    if (!Element.Methods.ByTag[tagName])
+      Element.Methods.ByTag[tagName] = { };
+    Object.extend(Element.Methods.ByTag[tagName], methods);
+  }
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    for (var property in methods) {
+      var value = methods[property];
+      // don't copy update, temporarily 
+      if (!Object.isFunction(value) || property == 'update') continue;
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = value.methodize();
+    }
+  }
+
+  function findDOMClass(tagName) {
+    var klass;
+    var trans = {
+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+      "FrameSet", "IFRAME": "IFrame"
+    };
+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName.capitalize() + 'Element';
+    if (window[klass]) return window[klass];
+
+    window[klass] = { };
+    window[klass].prototype = document.createElement(tagName).__proto__;
+    return window[klass];
+  }
+
+  if (F.ElementExtensions) {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+  }
+
+  if (F.SpecificElementExtensions) {
+    for (var tag in Element.Methods.ByTag) {
+      var klass = findDOMClass(tag);
+      if (Object.isUndefined(klass)) continue;
+      copy(T[tag], klass.prototype);
+    }
+  }
+
+  Object.extend(Element, Element.Methods);
+  delete Element.ByTag;
+
+  if (Element.extend.refresh) Element.extend.refresh();
+  Element.cache = { };
+};
+
+document.viewport = {
+  getDimensions: function() {
+    var dimensions = { };
+    $w('width height').each(function(d) {
+      var D = d.capitalize();
+      dimensions[d] = self['inner' + D] ||
+       (document.documentElement['client' + D] || document.body['client' + D]);
+    });
+    return dimensions;
+  },
+
+  getWidth: function() {
+    return this.getDimensions().width;
+  },
+
+  getHeight: function() {
+    return this.getDimensions().height;
+  },
+
+  getScrollOffsets: function() {
+    return Element._returnOffset(
+      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+  }
+};
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license.  Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create({
+  initialize: function(expression) {
+    this.expression = expression.strip();
+    this.compileMatcher();
+  },
+
+  compileMatcher: function() {
+    // Selectors with namespaced attributes can't use the XPath version
+    if (Prototype.BrowserFeatures.XPath && !(/(\[[\w-]*?:|:checked)/).test(this.expression))
+      return this.compileXPathMatcher();
+
+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+        c = Selector.criteria, le, p, m;
+
+    if (Selector._cache[e]) {
+      this.matcher = Selector._cache[e];
+      return;
+    }
+
+    this.matcher = ["this.matcher = function(root) {",
+                    "var r = root, h = Selector.handlers, c = false, n;"];
+
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+    	      new Template(c[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.matcher.push("return h.unique(n);\n}");
+    eval(this.matcher.join('\n'));
+    Selector._cache[this.expression] = this.matcher;
+  },
+
+  compileXPathMatcher: function() {
+    var e = this.expression, ps = Selector.patterns,
+        x = Selector.xpath, le, m;
+
+    if (Selector._cache[e]) {
+      this.xpath = Selector._cache[e]; return;
+    }
+
+    this.matcher = ['.//*'];
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        if (m = e.match(ps[i])) {
+          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+            new Template(x[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.xpath = this.matcher.join('');
+    Selector._cache[this.expression] = this.xpath;
+  },
+
+  findElements: function(root) {
+    root = root || document;
+    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+    return this.matcher(root);
+  },
+
+  match: function(element) {
+    this.tokens = [];
+
+    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+    var le, p, m;
+
+    while (e && le !== e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          // use the Selector.assertions methods unless the selector
+          // is too complex.
+          if (as[i]) {
+            this.tokens.push([i, Object.clone(m)]);
+            e = e.replace(m[0], '');
+          } else {
+            // reluctantly do a document-wide search
+            // and look for a match in the array
+            return this.findElements(document).include(element);
+          }
+        }
+      }
+    }
+
+    var match = true, name, matches;
+    for (var i = 0, token; token = this.tokens[i]; i++) {
+      name = token[0], matches = token[1];
+      if (!Selector.assertions[name](element, matches)) {
+        match = false; break;
+      }
+    }
+
+    return match;
+  },
+
+  toString: function() {
+    return this.expression;
+  },
+
+  inspect: function() {
+    return "#<Selector:" + this.expression.inspect() + ">";
+  }
+});
+
+Object.extend(Selector, {
+  _cache: { },
+
+  xpath: {
+    descendant:   "//*",
+    child:        "/*",
+    adjacent:     "/following-sibling::*[1]",
+    laterSibling: '/following-sibling::*',
+    tagName:      function(m) {
+      if (m[1] == '*') return '';
+      return "[local-name()='" + m[1].toLowerCase() +
+             "' or local-name()='" + m[1].toUpperCase() + "']";
+    },
+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+    id:           "[@id='#{1}']",
+    attrPresence: "[@#{1}]",
+    attr: function(m) {
+      m[3] = m[5] || m[6];
+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+    },
+    pseudo: function(m) {
+      var h = Selector.xpath.pseudos[m[1]];
+      if (!h) return '';
+      if (Object.isFunction(h)) return h(m);
+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+    },
+    operators: {
+      '=':  "[@#{1}='#{3}']",
+      '!=': "[@#{1}!='#{3}']",
+      '^=': "[starts-with(@#{1}, '#{3}')]",
+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+      '*=': "[contains(@#{1}, '#{3}')]",
+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+    },
+    pseudos: {
+      'first-child': '[not(preceding-sibling::*)]',
+      'last-child':  '[not(following-sibling::*)]',
+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
+      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+      'checked':     "[@checked]",
+      'disabled':    "[@disabled]",
+      'enabled':     "[not(@disabled)]",
+      'not': function(m) {
+        var e = m[6], p = Selector.patterns,
+            x = Selector.xpath, le, m, v;
+
+        var exclusion = [];
+        while (e && le != e && (/\S/).test(e)) {
+          le = e;
+          for (var i in p) {
+            if (m = e.match(p[i])) {
+              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+              e = e.replace(m[0], '');
+              break;
+            }
+          }
+        }
+        return "[not(" + exclusion.join(" and ") + ")]";
+      },
+      'nth-child':      function(m) {
+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+      },
+      'nth-last-child': function(m) {
+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+      },
+      'nth-of-type':    function(m) {
+        return Selector.xpath.pseudos.nth("position() ", m);
+      },
+      'nth-last-of-type': function(m) {
+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+      },
+      'first-of-type':  function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+      },
+      'last-of-type':   function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+      },
+      'only-of-type':   function(m) {
+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+      },
+      nth: function(fragment, m) {
+        var mm, formula = m[6], predicate;
+        if (formula == 'even') formula = '2n+0';
+        if (formula == 'odd')  formula = '2n+1';
+        if (mm = formula.match(/^(\d+)$/)) // digit only
+          return '[' + fragment + "= " + mm[1] + ']';
+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+          if (mm[1] == "-") mm[1] = -1;
+          var a = mm[1] ? Number(mm[1]) : 1;
+          var b = mm[2] ? Number(mm[2]) : 0;
+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+          "((#{fragment} - #{b}) div #{a} >= 0)]";
+          return new Template(predicate).evaluate({
+            fragment: fragment, a: a, b: b });
+        }
+      }
+    }
+  },
+
+  criteria: {
+    tagName:      'n = h.tagName(n, r, "#{1}", c);   c = false;',
+    className:    'n = h.className(n, r, "#{1}", c); c = false;',
+    id:           'n = h.id(n, r, "#{1}", c);        c = false;',
+    attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+    attr: function(m) {
+      m[3] = (m[5] || m[6]);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+    },
+    pseudo: function(m) {
+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+    },
+    descendant:   'c = "descendant";',
+    child:        'c = "child";',
+    adjacent:     'c = "adjacent";',
+    laterSibling: 'c = "laterSibling";'
+  },
+
+  patterns: {
+    // combinators must be listed first
+    // (and descendant needs to be last combinator)
+    laterSibling: /^\s*~\s*/,
+    child:        /^\s*>\s*/,
+    adjacent:     /^\s*\+\s*/,
+    descendant:   /^\s/,
+
+    // selectors follow
+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
+    id:           /^#([\w\-\*]+)(\b|$)/,
+    className:    /^\.([\w\-\*]+)(\b|$)/,
+    pseudo:       /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/,
+    attrPresence: /^\[([\w]+)\]/,
+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+  },
+
+  // for Selector.match and Element#match
+  assertions: {
+    tagName: function(element, matches) {
+      return matches[1].toUpperCase() == element.tagName.toUpperCase();
+    },
+
+    className: function(element, matches) {
+      return Element.hasClassName(element, matches[1]);
+    },
+
+    id: function(element, matches) {
+      return element.id === matches[1];
+    },
+
+    attrPresence: function(element, matches) {
+      return Element.hasAttribute(element, matches[1]);
+    },
+
+    attr: function(element, matches) {
+      var nodeValue = Element.readAttribute(element, matches[1]);
+      return Selector.operators[matches[2]](nodeValue, matches[3]);
+    }
+  },
+
+  handlers: {
+    // UTILITY FUNCTIONS
+    // joins two collections
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        a.push(node);
+      return a;
+    },
+
+    // marks an array of nodes for counting
+    mark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = true;
+      return nodes;
+    },
+
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = undefined;
+      return nodes;
+    },
+
+    // mark each child node with its position (for nth calls)
+    // "ofType" flag indicates whether we're indexing for nth-of-type
+    // rather than nth-child
+    index: function(parentNode, reverse, ofType) {
+      parentNode._counted = true;
+      if (reverse) {
+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+          var node = nodes[i];
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+        }
+      } else {
+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+      }
+    },
+
+    // filters out duplicates and extends all nodes
+    unique: function(nodes) {
+      if (nodes.length == 0) return nodes;
+      var results = [], n;
+      for (var i = 0, l = nodes.length; i < l; i++)
+        if (!(n = nodes[i])._counted) {
+          n._counted = true;
+          results.push(Element.extend(n));
+        }
+      return Selector.handlers.unmark(results);
+    },
+
+    // COMBINATOR FUNCTIONS
+    descendant: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, node.getElementsByTagName('*'));
+      return results;
+    },
+
+    child: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+      }
+      return results;
+    },
+
+    adjacent: function(nodes) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        var next = this.nextElementSibling(node);
+        if (next) results.push(next);
+      }
+      return results;
+    },
+
+    laterSibling: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, Element.nextSiblings(node));
+      return results;
+    },
+
+    nextElementSibling: function(node) {
+      while (node = node.nextSibling)
+	      if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    previousElementSibling: function(node) {
+      while (node = node.previousSibling)
+        if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    // TOKEN FUNCTIONS
+    tagName: function(nodes, root, tagName, combinator) {
+      tagName = tagName.toUpperCase();
+      var results = [], h = Selector.handlers;
+      if (nodes) {
+        if (combinator) {
+          // fastlane for ordinary descendant combinators
+          if (combinator == "descendant") {
+            for (var i = 0, node; node = nodes[i]; i++)
+              h.concat(results, node.getElementsByTagName(tagName));
+            return results;
+          } else nodes = this[combinator](nodes);
+          if (tagName == "*") return nodes;
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.tagName.toUpperCase() == tagName) results.push(node);
+        return results;
+      } else return root.getElementsByTagName(tagName);
+    },
+
+    id: function(nodes, root, id, combinator) {
+      var targetNode = $(id), h = Selector.handlers;
+      if (!targetNode) return [];
+      if (!nodes && root == document) return [targetNode];
+      if (nodes) {
+        if (combinator) {
+          if (combinator == 'child') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (targetNode.parentNode == node) return [targetNode];
+          } else if (combinator == 'descendant') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Element.descendantOf(targetNode, node)) return [targetNode];
+          } else if (combinator == 'adjacent') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Selector.handlers.previousElementSibling(targetNode) == node)
+                return [targetNode];
+          } else nodes = h[combinator](nodes);
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node == targetNode) return [targetNode];
+        return [];
+      }
+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+    },
+
+    className: function(nodes, root, className, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      return Selector.handlers.byClassName(nodes, root, className);
+    },
+
+    byClassName: function(nodes, root, className) {
+      if (!nodes) nodes = Selector.handlers.descendant([root]);
+      var needle = ' ' + className + ' ';
+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+        nodeClassName = node.className;
+        if (nodeClassName.length == 0) continue;
+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+          results.push(node);
+      }
+      return results;
+    },
+
+    attrPresence: function(nodes, root, attr) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      var results = [];
+      for (var i = 0, node; node = nodes[i]; i++)
+        if (Element.hasAttribute(node, attr)) results.push(node);
+      return results;
+    },
+
+    attr: function(nodes, root, attr, value, operator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      var handler = Selector.operators[operator], results = [];
+      for (var i = 0, node; node = nodes[i]; i++) {
+        var nodeValue = Element.readAttribute(node, attr);
+        if (nodeValue === null) continue;
+        if (handler(nodeValue, value)) results.push(node);
+      }
+      return results;
+    },
+
+    pseudo: function(nodes, name, value, root, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      return Selector.pseudos[name](nodes, value, root);
+    }
+  },
+
+  pseudos: {
+    'first-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.previousElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'last-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.nextElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'only-child': function(nodes, value, root) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+          results.push(node);
+      return results;
+    },
+    'nth-child':        function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root);
+    },
+    'nth-last-child':   function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true);
+    },
+    'nth-of-type':      function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, false, true);
+    },
+    'nth-last-of-type': function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true, true);
+    },
+    'first-of-type':    function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, false, true);
+    },
+    'last-of-type':     function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, true, true);
+    },
+    'only-of-type':     function(nodes, formula, root) {
+      var p = Selector.pseudos;
+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+    },
+
+    // handles the an+b logic
+    getIndices: function(a, b, total) {
+      if (a == 0) return b > 0 ? [b] : [];
+      return $R(1, total).inject([], function(memo, i) {
+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+        return memo;
+      });
+    },
+
+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+    nth: function(nodes, formula, root, reverse, ofType) {
+      if (nodes.length == 0) return [];
+      if (formula == 'even') formula = '2n+0';
+      if (formula == 'odd')  formula = '2n+1';
+      var h = Selector.handlers, results = [], indexed = [], m;
+      h.mark(nodes);
+      for (var i = 0, node; node = nodes[i]; i++) {
+        if (!node.parentNode._counted) {
+          h.index(node.parentNode, reverse, ofType);
+          indexed.push(node.parentNode);
+        }
+      }
+      if (formula.match(/^\d+$/)) { // just a number
+        formula = Number(formula);
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.nodeIndex == formula) results.push(node);
+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+        if (m[1] == "-") m[1] = -1;
+        var a = m[1] ? Number(m[1]) : 1;
+        var b = m[2] ? Number(m[2]) : 0;
+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+          for (var j = 0; j < l; j++)
+            if (node.nodeIndex == indices[j]) results.push(node);
+        }
+      }
+      h.unmark(nodes);
+      h.unmark(indexed);
+      return results;
+    },
+
+    'empty': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        // IE treats comments as element nodes
+        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+        results.push(node);
+      }
+      return results;
+    },
+
+    'not': function(nodes, selector, root) {
+      var h = Selector.handlers, selectorType, m;
+      var exclusions = new Selector(selector).findElements(root);
+      h.mark(exclusions);
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node._counted) results.push(node);
+      h.unmark(exclusions);
+      return results;
+    },
+
+    'enabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node.disabled) results.push(node);
+      return results;
+    },
+
+    'disabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.disabled) results.push(node);
+      return results;
+    },
+
+    'checked': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.checked) results.push(node);
+      return results;
+    }
+  },
+
+  operators: {
+    '=':  function(nv, v) { return nv == v; },
+    '!=': function(nv, v) { return nv != v; },
+    '^=': function(nv, v) { return nv.startsWith(v); },
+    '$=': function(nv, v) { return nv.endsWith(v); },
+    '*=': function(nv, v) { return nv.include(v); },
+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+  },
+
+  matchElements: function(elements, expression) {
+    var matches = new Selector(expression).findElements(), h = Selector.handlers;
+    h.mark(matches);
+    for (var i = 0, results = [], element; element = elements[i]; i++)
+      if (element._counted) results.push(element);
+    h.unmark(matches);
+    return results;
+  },
+
+  findElement: function(elements, expression, index) {
+    if (Object.isNumber(expression)) {
+      index = expression; expression = false;
+    }
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    var exprs = expressions.join(','), expressions = [];
+    exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    var results = [], h = Selector.handlers;
+    for (var i = 0, l = expressions.length, selector; i < l; i++) {
+      selector = new Selector(expressions[i].strip());
+      h.concat(results, selector.findElements(element));
+    }
+    return (l > 1) ? h.unique(results) : results;
+  }
+});
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements, options) {
+    if (typeof options != 'object') options = { hash: !!options };
+    else if (options.hash === undefined) options.hash = true;
+    var key, value, submitted = false, submit = options.submit;
+
+    var data = elements.inject({ }, function(result, element) {
+      if (!element.disabled && element.name) {
+        key = element.name; value = $(element).getValue();
+        if (value != null && (element.type != 'submit' || (!submitted &&
+            submit !== false && (!submit || key == submit) && (submitted = true)))) {
+          if (key in result) {
+            // a key is already present; construct an array of values
+            if (!Object.isArray(result[key])) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return options.hash ? data : Object.toQueryString(data);
+  }
+};
+
+Form.Methods = {
+  serialize: function(form, options) {
+    return Form.serializeElements(Form.getElements(form), options);
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) || (name && input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('disable');
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('enable');
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    var elements = $(form).getElements().findAll(function(element) {
+      return 'hidden' != element.type && !element.disabled;
+    });
+    var firstByIndex = elements.findAll(function(element) {
+      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+    }).sortBy(function(element) { return element.tabIndex }).first();
+
+    return firstByIndex ? firstByIndex : elements.find(function(element) {
+      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  },
+
+  request: function(form, options) {
+    form = $(form), options = Object.clone(options || { });
+
+    var params = options.parameters, action = form.readAttribute('action') || '';
+    if (action.blank()) action = window.location.href;
+    options.parameters = form.serialize(true);
+
+    if (params) {
+      if (Object.isString(params)) params = params.toQueryParams();
+      Object.extend(options.parameters, params);
+    }
+
+    if (form.hasAttribute('method') && !options.method)
+      options.method = form.method;
+
+    return new Ajax.Request(action, options);
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+};
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        // XXX: Jifty: this used to be:
+        //     var pair = { };
+        //     pair[element.name] = value
+        //     return Object.toQueryString(pair)
+        // but that included the pair.extend function, which occurred
+        // a lot whenever we validated an action. since we're only encoding
+        // the key (element.name) and value, do what we actually mean
+        return encodeURIComponent(element.name)
+             + '='
+             + encodeURIComponent(value);
+      }
+    }
+    return '';
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    return Form.Element.Serializers[method](element);
+  },
+
+  setValue: function(element, value) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    Form.Element.Serializers[method](element, value);
+    return element;
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    try {
+      element.focus();
+      if (element.select && (element.tagName.toLowerCase() != 'input' ||
+          !['button', 'reset', 'submit'].include(element.type)))
+        element.select();
+    } catch (e) { }
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.disabled = false;
+    return element;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element, value) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element, value);
+      default:
+        return Form.Element.Serializers.textarea(element, value);
+    }
+  },
+
+  inputSelector: function(element, value) {
+    if (value === undefined) return element.checked ? element.value : null;
+    else element.checked = !!value;
+  },
+
+  textarea: function(element, value) {
+    if (value === undefined) return element.value;
+    else element.value = value;
+  },
+
+  select: function(element, index) {
+    if (index === undefined)
+      return this[element.type == 'select-one' ?
+        'selectOne' : 'selectMany'](element);
+    else {
+      var opt, value, single = !Object.isArray(index);
+      for (var i = 0, length = element.length; i < length; i++) {
+        opt = element.options[i];
+        value = this.optionValue(opt);
+        if (single) {
+          if (value == index) {
+            opt.selected = true;
+            return;
+          }
+        }
+        else opt.selected = index.include(value);
+      }
+    }
+  },
+
+  selectOne: function(element) {
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
+  },
+
+  selectMany: function(element) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) values.push(this.optionValue(opt));
+    }
+    return values;
+  },
+
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+  initialize: function($super, element, frequency, callback) {
+    $super(callback, frequency);
+    this.element   = $(element);
+    this.lastValue = this.getValue();
+  },
+
+  execute: function() {
+    var value = this.getValue();
+    if (Object.isString(this.lastValue) && Object.isString(value) ?
+        this.lastValue != value : String(this.lastValue) != String(value)) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create(Abstract.TimedObserver, {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = Class.create({
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    Form.getElements(this.element).each(this.registerCallback, this);
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+});
+
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create(Abstract.EventObserver, {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) var Event = { };
+
+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,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+  KEY_INSERT:   45,
+
+  cache: { },
+
+  relatedTarget: function(event) {
+    var element;
+    switch(event.type) {
+      case 'mouseover': element = event.fromElement; break;
+      case 'mouseout':  element = event.toElement;   break;
+      default: return null;
+    }
+    return Element.extend(element);
+  }
+});
+
+Event.Methods = (function() {
+  var isButton;
+
+  if (Prototype.Browser.IE) {
+    var buttonMap = { 0: 1, 1: 4, 2: 2 };
+    isButton = function(event, code) {
+      return event.button == buttonMap[code];
+    };
+
+  } else if (Prototype.Browser.WebKit) {
+    isButton = function(event, code) {
+      switch (code) {
+        case 0: return event.which == 1 && !event.metaKey;
+        case 1: return event.which == 1 && event.metaKey;
+        default: return false;
+      }
+    };
+
+  } else {
+    isButton = function(event, code) {
+      return event.which ? (event.which === code + 1) : (event.button === code);
+    };
+  }
+
+  return {
+    isLeftClick:   function(event) { return isButton(event, 0) },
+    isMiddleClick: function(event) { return isButton(event, 1) },
+    isRightClick:  function(event) { return isButton(event, 2) },
+
+    element: function(event) {
+      var node = Event.extend(event).target;
+      return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
+    },
+
+    findElement: function(event, expression) {
+      var element = Event.element(event);
+      return element.match(expression) ? element : element.up(expression);
+    },
+
+    pointer: function(event) {
+      return {
+        x: event.pageX || (event.clientX +
+          (document.documentElement.scrollLeft || document.body.scrollLeft)),
+        y: event.pageY || (event.clientY +
+          (document.documentElement.scrollTop || document.body.scrollTop))
+      };
+    },
+
+    pointerX: function(event) { return Event.pointer(event).x },
+    pointerY: function(event) { return Event.pointer(event).y },
+
+    stop: function(event) {
+      Event.extend(event);
+      event.preventDefault();
+      event.stopPropagation();
+      event.stopped = true;
+    }
+  };
+})();
+
+Event.extend = (function() {
+  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+    m[name] = Event.Methods[name].methodize();
+    return m;
+  });
+
+  if (Prototype.Browser.IE) {
+    Object.extend(methods, {
+      stopPropagation: function() { this.cancelBubble = true },
+      preventDefault:  function() { this.returnValue = false },
+      inspect: function() { return "[object Event]" }
+    });
+
+    return function(event) {
+      if (!event) return false;
+      if (event._extendedByPrototype) return event;
+
+      event._extendedByPrototype = Prototype.emptyFunction;
+      var pointer = Event.pointer(event);
+      Object.extend(event, {
+        target: event.srcElement,
+        relatedTarget: Event.relatedTarget(event),
+        pageX:  pointer.x,
+        pageY:  pointer.y
+      });
+      return Object.extend(event, methods);
+    };
+
+  } else {
+    Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;
+    Object.extend(Event.prototype, methods);
+    return Prototype.K;
+  }
+})();
+
+Object.extend(Event, (function() {
+  var cache = Event.cache;
+
+  function getEventID(element) {
+    if (element._eventID) return element._eventID;
+    arguments.callee.id = arguments.callee.id || 1;
+    return element._eventID = ++arguments.callee.id;
+  }
+
+  function getDOMEventName(eventName) {
+    if (eventName && eventName.include(':')) return "dataavailable";
+    return eventName;
+  }
+
+  function getCacheForID(id) {
+    return cache[id] = cache[id] || { };
+  }
+
+  function getWrappersForEventName(id, eventName) {
+    var c = getCacheForID(id);
+    return c[eventName] = c[eventName] || [];
+  }
+
+  function createWrapper(element, eventName, handler) {
+    var id = getEventID(element);
+    var c = getWrappersForEventName(id, eventName);
+    if (c.pluck("handler").include(handler)) return false;
+
+    var wrapper = function(event) {
+      if (!Event || !Event.extend ||
+        (event.eventName && event.eventName != eventName))
+          return false;
+
+      Event.extend(event);
+      handler.call(element, event)
+    };
+
+    wrapper.handler = handler;
+    c.push(wrapper);
+    return wrapper;
+  }
+
+  function findWrapper(id, eventName, handler) {
+    var c = getWrappersForEventName(id, eventName);
+    return c.find(function(wrapper) { return wrapper.handler == handler });
+  }
+
+  function destroyWrapper(id, eventName, handler) {
+    var c = getCacheForID(id);
+    if (!c[eventName]) return false;
+    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+  }
+
+  function destroyCache() {
+    for (var id in cache)
+      for (var eventName in cache[id])
+        cache[id][eventName] = null;
+  }
+
+  if (window.attachEvent) {
+    window.attachEvent("onunload", destroyCache);
+  }
+
+  return {
+    observe: function(element, eventName, handler) {
+      element = $(element);
+      var name = getDOMEventName(eventName);
+
+      var wrapper = createWrapper(element, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.addEventListener) {
+        element.addEventListener(name, wrapper, false);
+      } else {
+        element.attachEvent("on" + name, wrapper);
+      }
+
+      return element;
+    },
+
+    stopObserving: function(element, eventName, handler) {
+      element = $(element);
+      var id = getEventID(element), name = getDOMEventName(eventName);
+
+      if (!handler && eventName) {
+        getWrappersForEventName(id, eventName).each(function(wrapper) {
+          element.stopObserving(eventName, wrapper.handler);
+        });
+        return element;
+
+      } else if (!eventName) {
+        Object.keys(getCacheForID(id)).each(function(eventName) {
+          element.stopObserving(eventName);
+        });
+        return element;
+      }
+
+      var wrapper = findWrapper(id, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.removeEventListener) {
+        element.removeEventListener(name, wrapper, false);
+      } else {
+        element.detachEvent("on" + name, wrapper);
+      }
+
+      destroyWrapper(id, eventName, handler);
+
+      return element;
+    },
+
+    fire: function(element, eventName, memo) {
+      element = $(element);
+      if (element == document && document.createEvent && !element.dispatchEvent)
+        element = document.documentElement;
+
+      if (document.createEvent) {
+        var event = document.createEvent("HTMLEvents");
+        event.initEvent("dataavailable", true, true);
+      } else {
+        var event = document.createEventObject();
+        event.eventType = "ondataavailable";
+      }
+
+      event.eventName = eventName;
+      event.memo = memo || { };
+
+      if (document.createEvent) {
+        element.dispatchEvent(event);
+      } else {
+        element.fireEvent(event.eventType, event);
+      }
+
+      return event;
+    }
+  };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+  fire:          Event.fire,
+  observe:       Event.observe,
+  stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+  fire:          Element.Methods.fire.methodize(),
+  observe:       Element.Methods.observe.methodize(),
+  stopObserving: Element.Methods.stopObserving.methodize()
+});
+
+(function() {
+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+     Matthias Miller, Dean Edwards and John Resig. */
+
+  var timer, fired = false;
+
+  function fireContentLoadedEvent() {
+    if (fired) return;
+    if (timer) window.clearInterval(timer);
+    document.fire("dom:loaded");
+    fired = true;
+  }
+
+  if (document.addEventListener) {
+    if (Prototype.Browser.WebKit) {
+      timer = window.setInterval(function() {
+        if (/loaded|complete/.test(document.readyState))
+          fireContentLoadedEvent();
+      }, 0);
+
+      Event.observe(window, "load", fireContentLoadedEvent);
+
+    } else {
+      document.addEventListener("DOMContentLoaded",
+        fireContentLoadedEvent, false);
+    }
+
+  } else {
+    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+    $("__onDOMContentLoaded").onreadystatechange = function() {
+      if (this.readyState == "complete") {
+        this.onreadystatechange = null;
+        fireContentLoadedEvent();
+      }
+    };
+  }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+  Before: function(element, content) {
+    return Element.insert(element, {before:content});
+  },
+
+  Top: function(element, content) {
+    return Element.insert(element, {top:content});
+  },
+
+  Bottom: function(element, content) {
+    return Element.insert(element, {bottom:content});
+  },
+
+  After: function(element, content) {
+    return Element.insert(element, {after:content});
+  }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
+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;
+  },
+
+  // 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 = Element.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = Element.cumulativeScrollOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = Element.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  // Deprecation layer -- use newer Element methods now (1.5.2).
+
+  cumulativeOffset: Element.Methods.cumulativeOffset,
+
+  positionedOffset: Element.Methods.positionedOffset,
+
+  absolutize: function(element) {
+    Position.prepare();
+    return Element.absolutize(element);
+  },
+
+  relativize: function(element) {
+    Position.prepare();
+    return Element.relativize(element);
+  },
+
+  realOffset: Element.Methods.cumulativeScrollOffset,
+
+  offsetParent: Element.Methods.getOffsetParent,
+
+  page: Element.Methods.viewportOffset,
+
+  clone: function(source, target, options) {
+    options = options || { };
+    return Element.clonePosition(target, source, options);
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+  function iter(name) {
+    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+  }
+
+  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+  function(element, className) {
+    className = className.toString().strip();
+    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+  } : function(element, className) {
+    className = className.toString().strip();
+    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+    if (!classNames && !className) return elements;
+
+    var nodes = $(element).getElementsByTagName('*');
+    className = ' ' + className + ' ';
+
+    for (var i = 0, child, cn; child = nodes[i]; i++) {
+      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+          (classNames && classNames.all(function(name) {
+            return !name.toString().blank() && cn.include(' ' + name + ' ');
+          }))))
+        elements.push(Element.extend(child));
+    }
+    return elements;
+  };
+
+  return function(className, parentElement) {
+    return $(parentElement || document.body).getElementsByClassName(className);
+  };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
+
+Element.addMethods();

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/builder.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/builder.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,101 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// See scriptaculous.js for full license.
+
+var Builder = {
+  NODEMAP: {
+    AREA: 'map',
+    CAPTION: 'table',
+    COL: 'table',
+    COLGROUP: 'table',
+    LEGEND: 'fieldset',
+    OPTGROUP: 'select',
+    OPTION: 'select',
+    PARAM: 'object',
+    TBODY: 'table',
+    TD: 'table',
+    TFOOT: 'table',
+    TH: 'table',
+    THEAD: 'table',
+    TR: 'table'
+  },
+  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
+  //       due to a Firefox bug
+  node: function(elementName) {
+    elementName = elementName.toUpperCase();
+    
+    // try innerHTML approach
+    var parentTag = this.NODEMAP[elementName] || 'div';
+    var parentElement = document.createElement(parentTag);
+    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
+    } catch(e) {}
+    var element = parentElement.firstChild || null;
+      
+    // see if browser added wrapping tags
+    if(element && (element.tagName != elementName))
+      element = element.getElementsByTagName(elementName)[0];
+    
+    // fallback to createElement approach
+    if(!element) element = document.createElement(elementName);
+    
+    // abort if nothing could be created
+    if(!element) return;
+
+    // attributes (or text)
+    if(arguments[1])
+      if(this._isStringOrNumber(arguments[1]) ||
+        (arguments[1] instanceof Array)) {
+          this._children(element, arguments[1]);
+        } else {
+          var attrs = this._attributes(arguments[1]);
+          if(attrs.length) {
+            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+              parentElement.innerHTML = "<" +elementName + " " +
+                attrs + "></" + elementName + ">";
+            } catch(e) {}
+            element = parentElement.firstChild || null;
+            // workaround firefox 1.0.X bug
+            if(!element) {
+              element = document.createElement(elementName);
+              for(attr in arguments[1]) 
+                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
+            }
+            if(element.tagName != elementName)
+              element = parentElement.getElementsByTagName(elementName)[0];
+            }
+        } 
+
+    // text, or array of children
+    if(arguments[2])
+      this._children(element, arguments[2]);
+
+     return element;
+  },
+  _text: function(text) {
+     return document.createTextNode(text);
+  },
+  _attributes: function(attributes) {
+    var attrs = [];
+    for(attribute in attributes)
+      attrs.push((attribute=='className' ? 'class' : attribute) +
+          '="' + attributes[attribute].toString().escapeHTML() + '"');
+    return attrs.join(" ");
+  },
+  _children: function(element, children) {
+    if(typeof children=='object') { // array can hold nodes and text
+      children.flatten().each( function(e) {
+        if(typeof e=='object')
+          element.appendChild(e)
+        else
+          if(Builder._isStringOrNumber(e))
+            element.appendChild(Builder._text(e));
+      });
+    } else
+      if(Builder._isStringOrNumber(children)) 
+         element.appendChild(Builder._text(children));
+  },
+  _isStringOrNumber: function(param) {
+    return(typeof param=='string' || typeof param=='number');
+  }
+}
\ No newline at end of file

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/controls.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/controls.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,776 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+//  Rob Wills
+// 
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality 
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least, 
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method 
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most 
+// useful when one of the tokens is \n (a newline), as it 
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+  baseInitialize: function(element, update, options) {
+    this.element     = $(element); 
+    this.update      = $(update);  
+    this.hasFocus    = false; 
+    this.changed     = false; 
+    this.active      = false; 
+    this.index       = 0;     
+    this.entryCount  = 0;
+
+    if (this.setOptions)
+      this.setOptions(options);
+    else
+      this.options = options || {};
+
+    this.options.paramName    = this.options.paramName || this.element.name;
+    this.options.tokens       = this.options.tokens || [];
+    this.options.frequency    = this.options.frequency || 0.4;
+    this.options.minChars     = this.options.minChars || 1;
+    this.options.onShow       = this.options.onShow || 
+    function(element, update){ 
+      if(!update.style.position || update.style.position=='absolute') {
+        update.style.position = 'absolute';
+        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+      }
+      Effect.Appear(update,{duration:0.15});
+    };
+    this.options.onHide = this.options.onHide || 
+    function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+    if (typeof(this.options.tokens) == 'string') 
+      this.options.tokens = new Array(this.options.tokens);
+
+    this.observer = null;
+    
+    this.element.setAttribute('autocomplete','off');
+
+    Element.hide(this.update);
+
+    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+  },
+
+  show: function() {
+    /* Next line added by TRS, 07 July 2006 */
+    if ( this.options.beforeShow ) this.options.beforeShow(this);
+    
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix && 
+      (navigator.appVersion.indexOf('MSIE')>0) &&
+      (navigator.userAgent.indexOf('Opera')<0) &&
+      (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
+       '<iframe id="' + this.update.id + '_iefix" '+
+       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+      this.iefix = $(this.update.id+'_iefix');
+    }
+    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+  },
+  
+  fixIEOverlapping: function() {
+    Position.clone(this.update, this.iefix);
+    this.iefix.style.zIndex = 1;
+    this.update.style.zIndex = 2;
+    Element.show(this.iefix);
+  },
+
+  hide: function() {
+    /* Next line added by TRS, 07 July 2006 */
+    if ( this.options.beforeHide ) this.options.beforeHide(this);
+    
+    this.stopIndicator();
+    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+    if(this.iefix) Element.hide(this.iefix);
+  },
+
+  startIndicator: function() {
+    if(this.options.indicator) Element.show(this.options.indicator);
+  },
+
+  stopIndicator: function() {
+    if(this.options.indicator) Element.hide(this.options.indicator);
+  },
+
+  onKeyPress: function(event) {
+    if(this.active)
+      switch(event.keyCode) {
+       case Event.KEY_TAB:
+       case Event.KEY_RETURN:
+         this.selectEntry();
+         Event.stop(event);
+       case Event.KEY_ESC:
+         this.hide();
+         this.active = false;
+         Event.stop(event);
+         return;
+       case Event.KEY_LEFT:
+       case Event.KEY_RIGHT:
+         return;
+       case Event.KEY_UP:
+         this.markPrevious();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+       case Event.KEY_DOWN:
+         this.markNext();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+      }
+     else 
+      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
+        return;
+
+    this.changed = true;
+    this.hasFocus = true;
+
+    if(this.observer) clearTimeout(this.observer);
+      this.observer = 
+        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+  },
+
+  onHover: function(event) {
+    var element = Event.findElement(event, 'LI');
+    if(this.index != element.autocompleteIndex) 
+    {
+        this.index = element.autocompleteIndex;
+        this.render();
+    }
+    Event.stop(event);
+  },
+  
+  onClick: function(event) {
+    var element = Event.findElement(event, 'LI');
+    this.index = element.autocompleteIndex;
+    this.selectEntry();
+    this.hide();
+  },
+  
+  onBlur: function(event) {
+    // needed to make click events working
+    setTimeout(this.hide.bind(this), 250);
+    this.hasFocus = false;
+    this.active = false;     
+  },
+
+  render: function() {
+    if(this.entryCount > 0) {
+      for (var i = 0; i < this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),"selected") : 
+          Element.removeClassName(this.getEntry(i),"selected");
+        
+      if(this.hasFocus) { 
+        this.show();
+        this.active = true;
+      }
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+  
+  markPrevious: function() {
+    if(this.index > 0) this.index--
+      else this.index = this.entryCount-1;
+  },
+  
+  markNext: function() {
+    if(this.index < this.entryCount-1) this.index++
+      else this.index = 0;
+  },
+  
+  getEntry: function(index) {
+    return this.update.firstChild.childNodes[index];
+  },
+  
+  getCurrentEntry: function() {
+    return this.getEntry(this.index);
+  },
+  
+  selectEntry: function() {
+    this.active = false;
+    this.updateElement(this.getCurrentEntry());
+  },
+
+  updateElement: function(selectedElement) {
+    if (this.options.updateElement) {
+      this.options.updateElement(selectedElement);
+      return;
+    }
+    var value = '';
+    if (this.options.select) {
+      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+    } else
+      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    
+    var lastTokenPos = this.findLastToken();
+    if (lastTokenPos != -1) {
+      var newValue = this.element.value.substr(0, lastTokenPos + 1);
+      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+      if (whitespace)
+        newValue += whitespace[0];
+      this.element.value = newValue + value;
+    } else {
+      this.element.value = value;
+    }
+    this.element.focus();
+    
+    if (this.options.afterUpdateElement)
+      this.options.afterUpdateElement(this.element, selectedElement);
+  },
+
+  updateChoices: function(choices) {
+    if(!this.changed && this.hasFocus) {
+      this.update.innerHTML = choices;
+      Element.cleanWhitespace(this.update);
+      Element.cleanWhitespace(this.update.firstChild);
+
+      if(this.update.firstChild && this.update.firstChild.childNodes) {
+        this.entryCount = 
+          this.update.firstChild.childNodes.length;
+        for (var i = 0; i < this.entryCount; i++) {
+          var entry = this.getEntry(i);
+          entry.autocompleteIndex = i;
+          this.addObservers(entry);
+        }
+      } else { 
+        this.entryCount = 0;
+      }
+
+      this.stopIndicator();
+
+      this.index = 0;
+      this.render();
+    }
+  },
+
+  addObservers: function(element) {
+    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+  },
+
+  onObserverEvent: function() {
+    this.changed = false;   
+    if(this.getToken().length>=this.options.minChars) {
+      this.startIndicator();
+      this.getUpdatedChoices();
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+
+  getToken: function() {
+    var tokenPos = this.findLastToken();
+    if (tokenPos != -1)
+      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+    else
+      var ret = this.element.value;
+
+    return /\n/.test(ret) ? '' : ret;
+  },
+
+  findLastToken: function() {
+    var lastTokenPos = -1;
+
+    for (var i=0; i<this.options.tokens.length; i++) {
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos > lastTokenPos)
+        lastTokenPos = thisTokenPos;
+    }
+    return lastTokenPos;
+  }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+  initialize: function(element, update, url, options) {
+	  this.baseInitialize(element, update, options);
+    this.options.asynchronous  = true;
+    this.options.onComplete    = this.onComplete.bind(this);
+    this.options.defaultParams = this.options.parameters || null;
+    this.url                   = url;
+  },
+
+  getUpdatedChoices: function() {
+    entry = encodeURIComponent(this.options.paramName) + '=' + 
+      encodeURIComponent(this.getToken());
+
+    this.options.parameters = this.options.callback ?
+      this.options.callback(this.element, entry) : entry;
+
+    if(this.options.defaultParams) 
+      this.options.parameters += '&' + this.options.defaultParams;
+
+    new Ajax.Request(this.url, this.options);
+  },
+
+  onComplete: function(request) {
+    this.updateChoices(request.responseText);
+  }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+//                    text only at the beginning of strings in the 
+//                    autocomplete array. Defaults to true, which will
+//                    match text at the beginning of any *word* in the
+//                    strings in the autocomplete array. If you want to
+//                    search anywhere in the string, additionally set
+//                    the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+//                   a partial match (unlike minChars, which defines
+//                   how many characters are required to do any match
+//                   at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+//                 Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector' 
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+  initialize: function(element, update, array, options) {
+    this.baseInitialize(element, update, options);
+    this.options.array = array;
+  },
+
+  getUpdatedChoices: function() {
+    this.updateChoices(this.options.selector(this));
+  },
+
+  setOptions: function(options) {
+    this.options = Object.extend({
+      choices: 10,
+      partialSearch: true,
+      partialChars: 2,
+      ignoreCase: true,
+      fullSearch: false,
+      selector: function(instance) {
+        var ret       = []; // Beginning matches
+        var partial   = []; // Inside matches
+        var entry     = instance.getToken();
+        var count     = 0;
+
+        for (var i = 0; i < instance.options.array.length &&  
+          ret.length < instance.options.choices ; i++) { 
+
+          var elem = instance.options.array[i];
+          var foundPos = instance.options.ignoreCase ? 
+            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
+            elem.indexOf(entry);
+
+          while (foundPos != -1) {
+            if (foundPos == 0 && elem.length != entry.length) { 
+              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
+                elem.substr(entry.length) + "</li>");
+              break;
+            } else if (entry.length >= instance.options.partialChars && 
+              instance.options.partialSearch && foundPos != -1) {
+              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+                  foundPos + entry.length) + "</li>");
+                break;
+              }
+            }
+
+            foundPos = instance.options.ignoreCase ? 
+              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
+              elem.indexOf(entry, foundPos + 1);
+
+          }
+        }
+        if (partial.length)
+          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+        return "<ul>" + ret.join('') + "</ul>";
+      }
+    }, options || {});
+  }
+});
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+  setTimeout(function() {
+    Field.activate(field);
+  }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+  initialize: function(element, url, options) {
+    this.url = url;
+    this.element = $(element);
+
+    this.options = Object.extend({
+      okButton: true,
+      okText: "ok",
+      cancelLink: true,
+      cancelText: "cancel",
+      savingText: "Saving...",
+      clickToEditText: "Click to edit",
+      okText: "ok",
+      rows: 1,
+      onComplete: function(transport, element) {
+        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+      },
+      onFailure: function(transport) {
+        alert("Error communicating with the server: " + transport.responseText.stripTags());
+      },
+      callback: function(form) {
+        return Form.serialize(form);
+      },
+      handleLineBreaks: true,
+      loadingText: 'Loading...',
+      savingClassName: 'inplaceeditor-saving',
+      loadingClassName: 'inplaceeditor-loading',
+      formClassName: 'inplaceeditor-form',
+      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+      highlightendcolor: "#FFFFFF",
+      externalControl:	null,
+      submitOnBlur: false,
+      ajaxOptions: {}
+    }, options || {});
+
+    if(!this.options.formId && this.element.id) {
+      this.options.formId = this.element.id + "-inplaceeditor";
+      if ($(this.options.formId)) {
+        // there's already a form with that name, don't specify an id
+        this.options.formId = null;
+      }
+    }
+    
+    if (this.options.externalControl) {
+      this.options.externalControl = $(this.options.externalControl);
+    }
+    
+    this.originalBackground = Element.getStyle(this.element, 'background-color');
+    if (!this.originalBackground) {
+      this.originalBackground = "transparent";
+    }
+    
+    this.element.title = this.options.clickToEditText;
+    
+    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+    Event.observe(this.element, 'click', this.onclickListener);
+    Event.observe(this.element, 'mouseover', this.mouseoverListener);
+    Event.observe(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.observe(this.options.externalControl, 'click', this.onclickListener);
+      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  },
+  enterEditMode: function(evt) {
+    if (this.saving) return;
+    if (this.editing) return;
+    this.editing = true;
+    this.onEnterEditMode();
+    if (this.options.externalControl) {
+      Element.hide(this.options.externalControl);
+    }
+    Element.hide(this.element);
+    this.createForm();
+    this.element.parentNode.insertBefore(this.form, this.element);
+    Field.scrollFreeActivate(this.editField);
+    // stop the event to avoid a page refresh in Safari
+    if (evt) {
+      Event.stop(evt);
+    }
+    return false;
+  },
+  createForm: function() {
+    this.form = document.createElement("form");
+    this.form.id = this.options.formId;
+    Element.addClassName(this.form, this.options.formClassName)
+    this.form.onsubmit = this.onSubmit.bind(this);
+
+    this.createEditField();
+
+    if (this.options.textarea) {
+      var br = document.createElement("br");
+      this.form.appendChild(br);
+    }
+
+    if (this.options.okButton) {
+      okButton = document.createElement("input");
+      okButton.type = "submit";
+      okButton.value = this.options.okText;
+      this.form.appendChild(okButton);
+    }
+
+    if (this.options.cancelLink) {
+      cancelLink = document.createElement("a");
+      cancelLink.href = "#";
+      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+      cancelLink.onclick = this.onclickCancel.bind(this);
+      this.form.appendChild(cancelLink);
+    }
+  },
+  hasHTMLLineBreaks: function(string) {
+    if (!this.options.handleLineBreaks) return false;
+    return string.match(/<br/i) || string.match(/<p>/i);
+  },
+  convertHTMLLineBreaks: function(string) {
+    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+  },
+  createEditField: function() {
+    var text;
+    if(this.options.loadTextURL) {
+      text = this.options.loadingText;
+    } else {
+      text = this.getText();
+    }
+
+    var obj = this;
+    
+    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+      this.options.textarea = false;
+      var textField = document.createElement("input");
+      textField.obj = this;
+      textField.type = "text";
+      textField.name = "value";
+      textField.value = text;
+      textField.style.backgroundColor = this.options.highlightcolor;
+      var size = this.options.size || this.options.cols || 0;
+      if (size != 0) textField.size = size;
+      if (this.options.submitOnBlur)
+        textField.onblur = this.onSubmit.bind(this);
+      this.editField = textField;
+    } else {
+      this.options.textarea = true;
+      var textArea = document.createElement("textarea");
+      textArea.obj = this;
+      textArea.name = "value";
+      textArea.value = this.convertHTMLLineBreaks(text);
+      textArea.rows = this.options.rows;
+      textArea.cols = this.options.cols || 40;
+      if (this.options.submitOnBlur)
+        textArea.onblur = this.onSubmit.bind(this);
+      this.editField = textArea;
+    }
+    
+    if(this.options.loadTextURL) {
+      this.loadExternalText();
+    }
+    this.form.appendChild(this.editField);
+  },
+  getText: function() {
+    return this.element.innerHTML;
+  },
+  loadExternalText: function() {
+    Element.addClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = true;
+    new Ajax.Request(
+      this.options.loadTextURL,
+      Object.extend({
+        asynchronous: true,
+        onComplete: this.onLoadedExternalText.bind(this)
+      }, this.options.ajaxOptions)
+    );
+  },
+  onLoadedExternalText: function(transport) {
+    Element.removeClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = false;
+    this.editField.value = transport.responseText.stripTags();
+  },
+  onclickCancel: function() {
+    this.onComplete();
+    this.leaveEditMode();
+    return false;
+  },
+  onFailure: function(transport) {
+    this.options.onFailure(transport);
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+      this.oldInnerHTML = null;
+    }
+    return false;
+  },
+  onSubmit: function() {
+    // onLoading resets these so we need to save them away for the Ajax call
+    var form = this.form;
+    var value = this.editField.value;
+    
+    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+    // to be displayed indefinitely
+    this.onLoading();
+    
+    new Ajax.Updater(
+      { 
+        success: this.element,
+         // don't update on failure (this could be an option)
+        failure: null
+      },
+      this.url,
+      Object.extend({
+        parameters: this.options.callback(form, value),
+        onComplete: this.onComplete.bind(this),
+        onFailure: this.onFailure.bind(this)
+      }, this.options.ajaxOptions)
+    );
+    // stop the event to avoid a page refresh in Safari
+    if (arguments.length > 1) {
+      Event.stop(arguments[0]);
+    }
+    return false;
+  },
+  onLoading: function() {
+    this.saving = true;
+    this.removeForm();
+    this.leaveHover();
+    this.showSaving();
+  },
+  showSaving: function() {
+    this.oldInnerHTML = this.element.innerHTML;
+    this.element.innerHTML = this.options.savingText;
+    Element.addClassName(this.element, this.options.savingClassName);
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+  },
+  removeForm: function() {
+    if(this.form) {
+      if (this.form.parentNode) Element.remove(this.form);
+      this.form = null;
+    }
+  },
+  enterHover: function() {
+    if (this.saving) return;
+    this.element.style.backgroundColor = this.options.highlightcolor;
+    if (this.effect) {
+      this.effect.cancel();
+    }
+    Element.addClassName(this.element, this.options.hoverClassName)
+  },
+  leaveHover: function() {
+    if (this.options.backgroundColor) {
+      this.element.style.backgroundColor = this.oldBackground;
+    }
+    Element.removeClassName(this.element, this.options.hoverClassName)
+    if (this.saving) return;
+    this.effect = new Effect.Highlight(this.element, {
+      startcolor: this.options.highlightcolor,
+      endcolor: this.options.highlightendcolor,
+      restorecolor: this.originalBackground
+    });
+  },
+  leaveEditMode: function() {
+    Element.removeClassName(this.element, this.options.savingClassName);
+    this.removeForm();
+    this.leaveHover();
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+    if (this.options.externalControl) {
+      Element.show(this.options.externalControl);
+    }
+    this.editing = false;
+    this.saving = false;
+    this.oldInnerHTML = null;
+    this.onLeaveEditMode();
+  },
+  onComplete: function(transport) {
+    this.leaveEditMode();
+    this.options.onComplete.bind(this)(transport, this.element);
+  },
+  onEnterEditMode: function() {},
+  onLeaveEditMode: function() {},
+  dispose: function() {
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+    }
+    this.leaveEditMode();
+    Event.stopObserving(this.element, 'click', this.onclickListener);
+    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  }
+};
+
+// Delayed observer, like Form.Element.Observer, 
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+  initialize: function(element, delay, callback) {
+    this.delay     = delay || 0.5;
+    this.element   = $(element);
+    this.callback  = callback;
+    this.timer     = null;
+    this.lastValue = $F(this.element); 
+    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+  },
+  delayedListener: function(event) {
+    if(this.lastValue == $F(this.element)) return;
+    if(this.timer) clearTimeout(this.timer);
+    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+    this.lastValue = $F(this.element);
+  },
+  onTimerEvent: function() {
+    this.timer = null;
+    this.callback(this.element, $F(this.element));
+  }
+};

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/dragdrop.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/dragdrop.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,585 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// See scriptaculous.js for full license.
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+  drops: [],
+
+  remove: function(element) {
+    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+  },
+
+  add: function(element) {
+    element = $(element);
+    var options = Object.extend({
+      greedy:     true,
+      hoverclass: null  
+    }, arguments[1] || {});
+
+    // cache containers
+    if(options.containment) {
+      options._containers = [];
+      var containment = options.containment;
+      if((typeof containment == 'object') && 
+        (containment.constructor == Array)) {
+        containment.each( function(c) { options._containers.push($(c)) });
+      } else {
+        options._containers.push($(containment));
+      }
+    }
+    
+    if(options.accept) options.accept = [options.accept].flatten();
+
+    Element.makePositioned(element); // fix IE
+    options.element = element;
+
+    this.drops.push(options);
+  },
+
+  isContained: function(element, drop) {
+    var parentNode = element.parentNode;
+    return drop._containers.detect(function(c) { return parentNode == c });
+  },
+
+  isAffected: function(point, element, drop) {
+    return (
+      (drop.element!=element) &&
+      ((!drop._containers) ||
+        this.isContained(element, drop)) &&
+      ((!drop.accept) ||
+        (Element.classNames(element).detect( 
+          function(v) { return drop.accept.include(v) } ) )) &&
+      Position.within(drop.element, point[0], point[1]) );
+  },
+
+  deactivate: function(drop) {
+    if(drop.hoverclass)
+      Element.removeClassName(drop.element, drop.hoverclass);
+    this.last_active = null;
+  },
+
+  activate: function(drop) {
+    if(drop.hoverclass)
+      Element.addClassName(drop.element, drop.hoverclass);
+    this.last_active = drop;
+  },
+
+  show: function(point, element) {
+    if(!this.drops.length) return;
+    
+    if(this.last_active) this.deactivate(this.last_active);
+    this.drops.each( function(drop) {
+      if(Droppables.isAffected(point, element, drop)) {
+        if(drop.onHover)
+           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+        if(drop.greedy) { 
+          Droppables.activate(drop);
+          throw $break;
+        }
+      }
+    });
+  },
+
+  fire: function(event, element) {
+    if(!this.last_active) return;
+    Position.prepare();
+
+    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+      if (this.last_active.onDrop) 
+        this.last_active.onDrop(element, this.last_active.element, event);
+  },
+
+  reset: function() {
+    if(this.last_active)
+      this.deactivate(this.last_active);
+  }
+}
+
+var Draggables = {
+  drags: [],
+  observers: [],
+  
+  register: function(draggable) {
+    if(this.drags.length == 0) {
+      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+      
+      Event.observe(document, "mouseup", this.eventMouseUp);
+      Event.observe(document, "mousemove", this.eventMouseMove);
+      Event.observe(document, "keypress", this.eventKeypress);
+    }
+    this.drags.push(draggable);
+  },
+  
+  unregister: function(draggable) {
+    this.drags = this.drags.reject(function(d) { return d==draggable });
+    if(this.drags.length == 0) {
+      Event.stopObserving(document, "mouseup", this.eventMouseUp);
+      Event.stopObserving(document, "mousemove", this.eventMouseMove);
+      Event.stopObserving(document, "keypress", this.eventKeypress);
+    }
+  },
+  
+  activate: function(draggable) {
+    window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+    this.activeDraggable = draggable;
+  },
+  
+  deactivate: function(draggbale) {
+    this.activeDraggable = null;
+  },
+  
+  updateDrag: function(event) {
+    if(!this.activeDraggable) return;
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    // Mozilla-based browsers fire successive mousemove events with
+    // the same coordinates, prevent needless redrawing (moz bug?)
+    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+    this._lastPointer = pointer;
+    this.activeDraggable.updateDrag(event, pointer);
+  },
+  
+  endDrag: function(event) {
+    if(!this.activeDraggable) return;
+    this._lastPointer = null;
+    this.activeDraggable.endDrag(event);
+    this.activeDraggable = null;
+  },
+  
+  keyPress: function(event) {
+    if(this.activeDraggable)
+      this.activeDraggable.keyPress(event);
+  },
+  
+  addObserver: function(observer) {
+    this.observers.push(observer);
+    this._cacheObserverCallbacks();
+  },
+  
+  removeObserver: function(element) {  // element instead of observer fixes mem leaks
+    this.observers = this.observers.reject( function(o) { return o.element==element });
+    this._cacheObserverCallbacks();
+  },
+  
+  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
+    if(this[eventName+'Count'] > 0)
+      this.observers.each( function(o) {
+        if(o[eventName]) o[eventName](eventName, draggable, event);
+      });
+  },
+  
+  _cacheObserverCallbacks: function() {
+    ['onStart','onEnd','onDrag'].each( function(eventName) {
+      Draggables[eventName+'Count'] = Draggables.observers.select(
+        function(o) { return o[eventName]; }
+      ).length;
+    });
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable.prototype = {
+  initialize: function(element) {
+    var options = Object.extend({
+      handle: false,
+      starteffect: function(element) { 
+        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
+      },
+      reverteffect: function(element, top_offset, left_offset) {
+        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+        element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
+      },
+      endeffect: function(element) { 
+        new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
+      },
+      zindex: 1000,
+      revert: false,
+      snap: false   // false, or xy or [x,y] or function(x,y){ return [x,y] }
+    }, arguments[1] || {});
+
+    this.element = $(element);
+    
+    if(options.handle && (typeof options.handle == 'string'))
+      this.handle = Element.childrenWithClassName(this.element, options.handle)[0];  
+    if(!this.handle) this.handle = $(options.handle);
+    if(!this.handle) this.handle = this.element;
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.delta    = this.currentDelta();
+    this.options  = options;
+    this.dragging = false;   
+
+    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+    Event.observe(this.handle, "mousedown", this.eventMouseDown);
+    
+    Draggables.register(this);
+  },
+  
+  destroy: function() {
+    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+    Draggables.unregister(this);
+  },
+  
+  currentDelta: function() {
+    return([
+      parseInt(Element.getStyle(this.element,'left') || '0'),
+      parseInt(Element.getStyle(this.element,'top') || '0')]);
+  },
+  
+  initDrag: function(event) {
+    if(Event.isLeftClick(event)) {    
+      // abort on form elements, fixes a Firefox issue
+      var src = Event.element(event);
+      if(src.tagName && (
+        src.tagName=='INPUT' ||
+        src.tagName=='SELECT' ||
+        src.tagName=='BUTTON' ||
+        src.tagName=='TEXTAREA')) return;
+        
+      if(this.element._revert) {
+        this.element._revert.cancel();
+        this.element._revert = null;
+      }
+      
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var pos     = Position.cumulativeOffset(this.element);
+      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+      
+      Draggables.activate(this);
+      Event.stop(event);
+    }
+  },
+  
+  startDrag: function(event) {
+    this.dragging = true;
+    
+    if(this.options.zindex) {
+      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+      this.element.style.zIndex = this.options.zindex;
+    }
+    
+    if(this.options.ghosting) {
+      this._clone = this.element.cloneNode(true);
+      Position.absolutize(this.element);
+      this.element.parentNode.insertBefore(this._clone, this.element);
+    }
+    
+    Draggables.notify('onStart', this, event);
+    if(this.options.starteffect) this.options.starteffect(this.element);
+  },
+  
+  updateDrag: function(event, pointer) {
+    if(!this.dragging) this.startDrag(event);
+    Position.prepare();
+    Droppables.show(pointer, this.element);
+    Draggables.notify('onDrag', this, event);
+    this.draw(pointer);
+    if(this.options.change) this.options.change(this);
+    
+    // fix AppleWebKit rendering
+    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+    Event.stop(event);
+  },
+  
+  finishDrag: function(event, success) {
+    this.dragging = false;
+
+    if(this.options.ghosting) {
+      Position.relativize(this.element);
+      Element.remove(this._clone);
+      this._clone = null;
+    }
+
+    if(success) Droppables.fire(event, this.element);
+    Draggables.notify('onEnd', this, event);
+
+    var revert = this.options.revert;
+    if(revert && typeof revert == 'function') revert = revert(this.element);
+    
+    var d = this.currentDelta();
+    if(revert && this.options.reverteffect) {
+      this.options.reverteffect(this.element, 
+        d[1]-this.delta[1], d[0]-this.delta[0]);
+    } else {
+      this.delta = d;
+    }
+
+    if(this.options.zindex)
+      this.element.style.zIndex = this.originalZ;
+
+    if(this.options.endeffect) 
+      this.options.endeffect(this.element);
+
+    Draggables.deactivate(this);
+    Droppables.reset();
+  },
+  
+  keyPress: function(event) {
+    if(!event.keyCode==Event.KEY_ESC) return;
+    this.finishDrag(event, false);
+    Event.stop(event);
+  },
+  
+  endDrag: function(event) {
+    if(!this.dragging) return;
+    this.finishDrag(event, true);
+    Event.stop(event);
+  },
+  
+  draw: function(point) {
+    var pos = Position.cumulativeOffset(this.element);
+    var d = this.currentDelta();
+    pos[0] -= d[0]; pos[1] -= d[1];
+    
+    var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this));
+    
+    if(this.options.snap) {
+      if(typeof this.options.snap == 'function') {
+        p = this.options.snap(p[0],p[1]);
+      } else {
+      if(this.options.snap instanceof Array) {
+        p = p.map( function(v, i) {
+          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+      } else {
+        p = p.map( function(v) {
+          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+      }
+    }}
+    
+    var style = this.element.style;
+    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+      style.left = p[0] + "px";
+    if((!this.options.constraint) || (this.options.constraint=='vertical'))
+      style.top  = p[1] + "px";
+    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+  initialize: function(element, observer) {
+    this.element   = $(element);
+    this.observer  = observer;
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  
+  onStart: function() {
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  
+  onEnd: function() {
+    Sortable.unmark();
+    if(this.lastValue != Sortable.serialize(this.element))
+      this.observer(this.element)
+  }
+}
+
+var Sortable = {
+  sortables: new Array(),
+  
+  options: function(element){
+    element = $(element);
+    return this.sortables.detect(function(s) { return s.element == element });
+  },
+  
+  destroy: function(element){
+    element = $(element);
+    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
+      Draggables.removeObserver(s.element);
+      s.droppables.each(function(d){ Droppables.remove(d) });
+      s.draggables.invoke('destroy');
+    });
+    this.sortables = this.sortables.reject(function(s) { return s.element == element });
+  },
+  
+  create: function(element) {
+    element = $(element);
+    var options = Object.extend({ 
+      element:     element,
+      tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,      // fixme: unimplemented
+      overlap:     'vertical', // one of 'vertical', 'horizontal'
+      constraint:  'vertical', // one of 'vertical', 'horizontal', false
+      containment: element,    // also takes array of elements (or id's); or false
+      handle:      false,      // or a CSS class
+      only:        false,
+      hoverclass:  null,
+      ghosting:    false,
+      format:      null,
+      onChange:    Prototype.emptyFunction,
+      onUpdate:    Prototype.emptyFunction
+    }, arguments[1] || {});
+
+    // clear any old sortable with same element
+    this.destroy(element);
+
+    // build options for the draggables
+    var options_for_draggable = {
+      revert:      true,
+      ghosting:    options.ghosting,
+      constraint:  options.constraint,
+      handle:      options.handle };
+
+    if(options.starteffect)
+      options_for_draggable.starteffect = options.starteffect;
+
+    if(options.reverteffect)
+      options_for_draggable.reverteffect = options.reverteffect;
+    else
+      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+        element.style.top  = 0;
+        element.style.left = 0;
+      };
+
+    if(options.endeffect)
+      options_for_draggable.endeffect = options.endeffect;
+
+    if(options.zindex)
+      options_for_draggable.zindex = options.zindex;
+
+    // build options for the droppables  
+    var options_for_droppable = {
+      overlap:     options.overlap,
+      containment: options.containment,
+      hoverclass:  options.hoverclass,
+      onHover:     Sortable.onHover,
+      greedy:      !options.dropOnEmpty
+    }
+
+    // fix for gecko engine
+    Element.cleanWhitespace(element); 
+
+    options.draggables = [];
+    options.droppables = [];
+
+    // make it so
+
+    // drop on empty handling
+    if(options.dropOnEmpty) {
+      Droppables.add(element,
+        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
+      options.droppables.push(element);
+    }
+
+    (this.findElements(element, options) || []).each( function(e) {
+      // handles are per-draggable
+      var handle = options.handle ? 
+        Element.childrenWithClassName(e, options.handle)[0] : e;    
+      options.draggables.push(
+        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+      Droppables.add(e, options_for_droppable);
+      options.droppables.push(e);      
+    });
+
+    // keep reference
+    this.sortables.push(options);
+
+    // for onupdate
+    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+  },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    if(!element.hasChildNodes()) return null;
+    var elements = [];
+    $A(element.childNodes).each( function(e) {
+      if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() &&
+        (!options.only || (Element.hasClassName(e, options.only))))
+          elements.push(e);
+      if(options.tree) {
+        var grandchildren = this.findElements(e, options);
+        if(grandchildren) elements.push(grandchildren);
+      }
+    });
+
+    return (elements.length>0 ? elements.flatten() : null);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(overlap>0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, dropon);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    } else {
+      Sortable.mark(dropon, 'after');
+      var nextElement = dropon.nextSibling || null;
+      if(nextElement != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, nextElement);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    }
+  },
+
+  onEmptyHover: function(element, dropon) {
+    if(element.parentNode!=dropon) {
+      var oldParentNode = element.parentNode;
+      dropon.appendChild(element);
+      Sortable.options(oldParentNode).onChange(element);
+      Sortable.options(dropon).onChange(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Element.hide(Sortable._marker);
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable && !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = $('dropmarker') || document.createElement('DIV');
+      Element.hide(Sortable._marker);
+      Element.addClassName(Sortable._marker, 'dropmarker');
+      Sortable._marker.style.position = 'absolute';
+      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.style.left = offsets[0] + 'px';
+    Sortable._marker.style.top = offsets[1] + 'px';
+    
+    if(position=='after')
+      if(sortable.overlap == 'horizontal') 
+        Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
+      else
+        Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+    
+    Element.show(Sortable._marker);
+  },
+
+  serialize: function(element) {
+    element = $(element);
+    var sortableOptions = this.options(element);
+    var options = Object.extend({
+      tag:  sortableOptions.tag,
+      only: sortableOptions.only,
+      name: element.id,
+      format: sortableOptions.format || /^[^_]*_(.*)$/
+    }, arguments[1] || {});
+    return $(this.findElements(element, options) || []).map( function(item) {
+      return (encodeURIComponent(options.name) + "[]=" + 
+              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
+    }).join("&");
+  }
+}
\ No newline at end of file

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/effects.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/effects.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,903 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+//  Justin Palmer (http://encytemedia.com/)
+//  Mark Pilgrim (http://diveintomark.org/)
+//  Martin Bialasinki
+// 
+// See scriptaculous.js for full license.  
+
+/* ------------- element ext -------------- */  
+ 
+// converts rgb() and #xxx to #xxxxxx format,  
+// returns self (or first argument) if not convertable  
+String.prototype.parseColor = function() {  
+  var color = '#';  
+  if(this.slice(0,4) == 'rgb(') {  
+    var cols = this.slice(4,this.length-1).split(',');  
+    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
+  } else {  
+    if(this.slice(0,1) == '#') {  
+      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
+      if(this.length==7) color = this.toLowerCase();  
+    }  
+  }  
+  return(color.length==7 ? color : (arguments[0] || this));  
+}
+
+Element.collectTextNodes = function(element) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+  }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 
+        Element.collectTextNodes(node) : ''));
+  }).flatten().join('');
+}
+
+Element.setStyle = function(element, style) {
+  element = $(element);
+  for(k in style) element.style[k.camelize()] = style[k];
+}
+
+Element.setContentZoom = function(element, percent) {  
+  Element.setStyle(element, {fontSize: (percent/100) + 'em'});   
+  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);  
+}
+
+Element.getOpacity = function(element){  
+  var opacity;
+  if (opacity = Element.getStyle(element, 'opacity'))  
+    return parseFloat(opacity);  
+  if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))  
+    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
+  return 1.0;  
+}
+
+Element.setOpacity = function(element, value){  
+  element= $(element);  
+  if (value == 1){
+    Element.setStyle(element, { opacity: 
+      (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
+      0.999999 : null });
+    if(/MSIE/.test(navigator.userAgent))  
+      Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
+  } else {  
+    if(value < 0.00001) value = 0;  
+    Element.setStyle(element, {opacity: value});
+    if(/MSIE/.test(navigator.userAgent))  
+     Element.setStyle(element, 
+       { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
+                 'alpha(opacity='+value*100+')' });  
+  }   
+}  
+ 
+Element.getInlineOpacity = function(element){  
+  return $(element).style.opacity || '';
+}  
+
+Element.childrenWithClassName = function(element, className) {  
+  return $A($(element).getElementsByTagName('*')).select(
+    function(c) { return Element.hasClassName(c, className) });
+}
+
+Array.prototype.call = function() {
+  var args = arguments;
+  this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+  tagifyText: function(element) {
+    var tagifyStyle = 'position:relative';
+    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
+    element = $(element);
+    $A(element.childNodes).each( function(child) {
+      if(child.nodeType==3) {
+        child.nodeValue.toArray().each( function(character) {
+          element.insertBefore(
+            Builder.node('span',{style: tagifyStyle},
+              character == ' ' ? String.fromCharCode(160) : character), 
+              child);
+        });
+        Element.remove(child);
+      }
+    });
+  },
+  multiple: function(element, effect) {
+    var elements;
+    if(((typeof element == 'object') || 
+        (typeof element == 'function')) && 
+       (element.length))
+      elements = element;
+    else
+      elements = $(element).childNodes;
+      
+    var options = Object.extend({
+      speed: 0.1,
+      delay: 0.0
+    }, arguments[2] || {});
+    var masterDelay = options.delay;
+
+    $A(elements).each( function(element, index) {
+      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+    });
+  },
+  PAIRS: {
+    'slide':  ['SlideDown','SlideUp'],
+    'blind':  ['BlindDown','BlindUp'],
+    'appear': ['Appear','Fade']
+  },
+  toggle: function(element, effect) {
+    element = $(element);
+    effect = (effect || 'appear').toLowerCase();
+    var options = Object.extend({
+      queue: { position:'end', scope:(element.id || 'global') }
+    }, arguments[2] || {});
+    Effect[Element.visible(element) ? 
+      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+  }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {}
+
+Effect.Transitions.linear = function(pos) {
+  return pos;
+}
+Effect.Transitions.sinoidal = function(pos) {
+  return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+Effect.Transitions.reverse  = function(pos) {
+  return 1-pos;
+}
+Effect.Transitions.flicker = function(pos) {
+  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+}
+Effect.Transitions.wobble = function(pos) {
+  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+Effect.Transitions.pulse = function(pos) {
+  return (Math.floor(pos*10) % 2 == 0 ? 
+    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+Effect.Transitions.none = function(pos) {
+  return 0;
+}
+Effect.Transitions.full = function(pos) {
+  return 1;
+}
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+  initialize: function() {
+    this.effects  = [];
+    this.interval = null;
+  },
+  _each: function(iterator) {
+    this.effects._each(iterator);
+  },
+  add: function(effect) {
+    var timestamp = new Date().getTime();
+    
+    var position = (typeof effect.options.queue == 'string') ? 
+      effect.options.queue : effect.options.queue.position;
+    
+    switch(position) {
+      case 'front':
+        // move unstarted effects after this effect  
+        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+            e.startOn  += effect.finishOn;
+            e.finishOn += effect.finishOn;
+          });
+        break;
+      case 'end':
+        // start effect after last queued effect has finished
+        timestamp = this.effects.pluck('finishOn').max() || timestamp;
+        break;
+    }
+    
+    effect.startOn  += timestamp;
+    effect.finishOn += timestamp;
+    this.effects.push(effect);
+    if(!this.interval) 
+      this.interval = setInterval(this.loop.bind(this), 40);
+  },
+  remove: function(effect) {
+    this.effects = this.effects.reject(function(e) { return e==effect });
+    if(this.effects.length == 0) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+  },
+  loop: function() {
+    var timePos = new Date().getTime();
+    this.effects.invoke('loop', timePos);
+  }
+});
+
+Effect.Queues = {
+  instances: $H(),
+  get: function(queueName) {
+    if(typeof queueName != 'string') return queueName;
+    
+    if(!this.instances.get(queueName))
+      this.instances.set(queueName, new Effect.ScopedQueue());
+      
+    return this.instances.get(queueName);
+  }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+  transition: Effect.Transitions.sinoidal,
+  duration:   1.0,   // seconds
+  fps:        25.0,  // max. 25fps due to Effect.Queue implementation
+  sync:       false, // true for combining
+  from:       0.0,
+  to:         1.0,
+  delay:      0.0,
+  queue:      'parallel'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+  position: null,
+  start: function(options) {
+    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+    this.currentFrame = 0;
+    this.state        = 'idle';
+    this.startOn      = this.options.delay*1000;
+    this.finishOn     = this.startOn + (this.options.duration*1000);
+    this.event('beforeStart');
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).add(this);
+  },
+  loop: function(timePos) {
+    if(timePos >= this.startOn) {
+      if(timePos >= this.finishOn) {
+        this.render(1.0);
+        this.cancel();
+        this.event('beforeFinish');
+        if(this.finish) this.finish(); 
+        this.event('afterFinish');
+        return;  
+      }
+      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
+      var frame = Math.round(pos * this.options.fps * this.options.duration);
+      if(frame > this.currentFrame) {
+        this.render(pos);
+        this.currentFrame = frame;
+      }
+    }
+  },
+  render: function(pos) {
+    if(this.state == 'idle') {
+      this.state = 'running';
+      this.event('beforeSetup');
+      if(this.setup) this.setup();
+      this.event('afterSetup');
+    }
+    if(this.state == 'running') {
+      if(this.options.transition) pos = this.options.transition(pos);
+      pos *= (this.options.to-this.options.from);
+      pos += this.options.from;
+      this.position = pos;
+      this.event('beforeUpdate');
+      if(this.update) this.update(pos);
+      this.event('afterUpdate');
+    }
+  },
+  cancel: function() {
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).remove(this);
+    this.state = 'finished';
+  },
+  event: function(eventName) {
+    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+    if(this.options[eventName]) this.options[eventName](this);
+  },
+  inspect: function() {
+    return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
+  }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+  initialize: function(effects) {
+    this.effects = effects || [];
+    this.start(arguments[1]);
+  },
+  update: function(position) {
+    this.effects.invoke('render', position);
+  },
+  finish: function(position) {
+    this.effects.each( function(effect) {
+      effect.render(1.0);
+      effect.cancel();
+      effect.event('beforeFinish');
+      if(effect.finish) effect.finish(position);
+      effect.event('afterFinish');
+    });
+  }
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    // make this work on IE on elements without 'layout'
+    if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
+      Element.setStyle(this.element, {zoom: 1});
+    var options = Object.extend({
+      from: Element.getOpacity(this.element) || 0.0,
+      to:   1.0
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  update: function(position) {
+    Element.setOpacity(this.element, position);
+  }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    var options = Object.extend({
+      x:    0,
+      y:    0,
+      mode: 'relative'
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Bug in Opera: Opera returns the "real" position of a static element or
+    // relative element that does not have top/left explicitly set.
+    // ==> Always set top and left for position relative elements in your stylesheets 
+    // (to 0 if you do not need them) 
+    Element.makePositioned(this.element);
+    this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
+    this.originalTop  = parseFloat(Element.getStyle(this.element,'top')  || '0');
+    if(this.options.mode == 'absolute') {
+      // absolute movement, so we need to calc deltaX and deltaY
+      this.options.x = this.options.x - this.originalLeft;
+      this.options.y = this.options.y - this.originalTop;
+    }
+  },
+  update: function(position) {
+    Element.setStyle(this.element, {
+      left: this.options.x  * position + this.originalLeft + 'px',
+      top:  this.options.y  * position + this.originalTop  + 'px'
+    });
+  }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+  return new Effect.Move(element, 
+    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+  initialize: function(element, percent) {
+    this.element = $(element)
+    var options = Object.extend({
+      scaleX: true,
+      scaleY: true,
+      scaleContent: true,
+      scaleFromCenter: false,
+      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
+      scaleFrom: 100.0,
+      scaleTo:   percent
+    }, arguments[2] || {});
+    this.start(options);
+  },
+  setup: function() {
+    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+    this.elementPositioning = Element.getStyle(this.element,'position');
+    
+    this.originalStyle = {};
+    ['top','left','width','height','fontSize'].each( function(k) {
+      this.originalStyle[k] = this.element.style[k];
+    }.bind(this));
+      
+    this.originalTop  = this.element.offsetTop;
+    this.originalLeft = this.element.offsetLeft;
+    
+    var fontSize = Element.getStyle(this.element,'font-size') || '100%';
+    ['em','px','%'].each( function(fontSizeType) {
+      if(fontSize.indexOf(fontSizeType)>0) {
+        this.fontSize     = parseFloat(fontSize);
+        this.fontSizeType = fontSizeType;
+      }
+    }.bind(this));
+    
+    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+    
+    this.dims = null;
+    if(this.options.scaleMode=='box')
+      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+    if(/^content/.test(this.options.scaleMode))
+      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+    if(!this.dims)
+      this.dims = [this.options.scaleMode.originalHeight,
+                   this.options.scaleMode.originalWidth];
+  },
+  update: function(position) {
+    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+    if(this.options.scaleContent && this.fontSize)
+      Element.setStyle(this.element, {fontSize: this.fontSize * currentScale + this.fontSizeType });
+    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+  },
+  finish: function(position) {
+    if (this.restoreAfterFinish) Element.setStyle(this.element, this.originalStyle);
+  },
+  setDimensions: function(height, width) {
+    var d = {};
+    if(this.options.scaleX) d.width = width + 'px';
+    if(this.options.scaleY) d.height = height + 'px';
+    if(this.options.scaleFromCenter) {
+      var topd  = (height - this.dims[0])/2;
+      var leftd = (width  - this.dims[1])/2;
+      if(this.elementPositioning == 'absolute') {
+        if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+        if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+      } else {
+        if(this.options.scaleY) d.top = -topd + 'px';
+        if(this.options.scaleX) d.left = -leftd + 'px';
+      }
+    }
+    Element.setStyle(this.element, d);
+  }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Prevent executing on elements not in the layout flow
+    if(Element.getStyle(this.element, 'display')=='none') { this.cancel(); return; }
+    // Disable background image during the effect
+    this.oldStyle = {
+      backgroundImage: Element.getStyle(this.element, 'background-image') };
+    Element.setStyle(this.element, {backgroundImage: 'none'});
+    if(!this.options.endcolor)
+      this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
+    if(!this.options.restorecolor)
+      this.options.restorecolor = Element.getStyle(this.element, 'background-color');
+    // init color calculations
+    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+  },
+  update: function(position) {
+    Element.setStyle(this.element,{backgroundColor: $R(0,2).inject('#',function(m,v,i){
+      return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+  },
+  finish: function() {
+    Element.setStyle(this.element, Object.extend(this.oldStyle, {
+      backgroundColor: this.options.restorecolor
+    }));
+  }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    this.start(arguments[1] || {});
+  },
+  setup: function() {
+    Position.prepare();
+    var offsets = Position.cumulativeOffset(this.element);
+    if(this.options.offset) offsets[1] += this.options.offset;
+    var max = window.innerHeight ? 
+      window.height - window.innerHeight :
+      document.body.scrollHeight - 
+        (document.documentElement.clientHeight ? 
+          document.documentElement.clientHeight : document.body.clientHeight);
+    this.scrollStart = Position.deltaY;
+    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+  },
+  update: function(position) {
+    Position.prepare();
+    window.scrollTo(Position.deltaX, 
+      this.scrollStart + (position*this.delta));
+  }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+  var oldOpacity = Element.getInlineOpacity(element);
+  var options = Object.extend({
+  from: Element.getOpacity(element) || 1.0,
+  to:   0.0,
+  afterFinishInternal: function(effect) { with(Element) { 
+    if(effect.options.to!=0) return;
+    hide(effect.element);
+    setStyle(effect.element, {opacity: oldOpacity}); }}
+  }, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+  var options = Object.extend({
+  from: (Element.getStyle(element, 'display') == 'none' ? 0.0 : Element.getOpacity(element) || 0.0),
+  to:   1.0,
+  beforeSetup: function(effect) { with(Element) {
+    setOpacity(effect.element, effect.options.from);
+    show(effect.element); }}
+  }, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+  element = $(element);
+  var oldStyle = { opacity: Element.getInlineOpacity(element), position: Element.getStyle(element, 'position') };
+  return new Effect.Parallel(
+   [ new Effect.Scale(element, 200, 
+      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
+     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
+     Object.extend({ duration: 1.0, 
+      beforeSetupInternal: function(effect) { with(Element) {
+        setStyle(effect.effects[0].element, {position: 'absolute'}); }},
+      afterFinishInternal: function(effect) { with(Element) {
+         hide(effect.effects[0].element);
+         setStyle(effect.effects[0].element, oldStyle); }}
+     }, arguments[1] || {})
+   );
+}
+
+Effect.BlindUp = function(element) {
+  element = $(element);
+  Element.makeClipping(element);
+  return new Effect.Scale(element, 0, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      restoreAfterFinish: true,
+      afterFinishInternal: function(effect) { with(Element) {
+        [hide, undoClipping].call(effect.element); }} 
+    }, arguments[1] || {})
+  );
+}
+
+Effect.BlindDown = function(element) {
+  element = $(element);
+  var oldHeight = Element.getStyle(element, 'height');
+  var elementDimensions = Element.getDimensions(element);
+  return new Effect.Scale(element, 100, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false,
+      scaleFrom: 0,
+      scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+      restoreAfterFinish: true,
+      afterSetup: function(effect) { with(Element) {
+        makeClipping(effect.element);
+        setStyle(effect.element, {height: '0px'});
+        show(effect.element); 
+      }},  
+      afterFinishInternal: function(effect) { with(Element) {
+        undoClipping(effect.element);
+        setStyle(effect.element, {height: oldHeight});
+      }}
+    }, arguments[1] || {})
+  );
+}
+
+Effect.SwitchOff = function(element) {
+  element = $(element);
+  var oldOpacity = Element.getInlineOpacity(element);
+  return new Effect.Appear(element, { 
+    duration: 0.4,
+    from: 0,
+    transition: Effect.Transitions.flicker,
+    afterFinishInternal: function(effect) {
+      new Effect.Scale(effect.element, 1, { 
+        duration: 0.3, scaleFromCenter: true,
+        scaleX: false, scaleContent: false, restoreAfterFinish: true,
+        beforeSetup: function(effect) { with(Element) {
+          [makePositioned,makeClipping].call(effect.element);
+        }},
+        afterFinishInternal: function(effect) { with(Element) {
+          [hide,undoClipping,undoPositioned].call(effect.element);
+          setStyle(effect.element, {opacity: oldOpacity});
+        }}
+      })
+    }
+  });
+}
+
+Effect.DropOut = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: Element.getStyle(element, 'top'),
+    left: Element.getStyle(element, 'left'),
+    opacity: Element.getInlineOpacity(element) };
+  return new Effect.Parallel(
+    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
+      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+    Object.extend(
+      { duration: 0.5,
+        beforeSetup: function(effect) { with(Element) {
+          makePositioned(effect.effects[0].element); }},
+        afterFinishInternal: function(effect) { with(Element) {
+          [hide, undoPositioned].call(effect.effects[0].element);
+          setStyle(effect.effects[0].element, oldStyle); }} 
+      }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: Element.getStyle(element, 'top'),
+    left: Element.getStyle(element, 'left') };
+	  return new Effect.Move(element, 
+	    { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+	  new Effect.Move(effect.element,
+	    { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+	  new Effect.Move(effect.element,
+	    { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+	  new Effect.Move(effect.element,
+	    { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+	  new Effect.Move(effect.element,
+	    { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+	  new Effect.Move(effect.element,
+	    { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { with(Element) {
+        undoPositioned(effect.element);
+        setStyle(effect.element, oldStyle);
+  }}}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+  element = $(element);
+  Element.cleanWhitespace(element);
+  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+  var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom');
+  var elementDimensions = Element.getDimensions(element);
+  return new Effect.Scale(element, 100, Object.extend({ 
+    scaleContent: false, 
+    scaleX: false, 
+    scaleFrom: 0,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+    restoreAfterFinish: true,
+    afterSetup: function(effect) { with(Element) {
+      makePositioned(effect.element);
+      makePositioned(effect.element.firstChild);
+      if(window.opera) setStyle(effect.element, {top: ''});
+      makeClipping(effect.element);
+      setStyle(effect.element, {height: '0px'});
+      show(element); }},
+    afterUpdateInternal: function(effect) { with(Element) {
+      setStyle(effect.element.firstChild, {bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); }},
+    afterFinishInternal: function(effect) { with(Element) {
+      undoClipping(effect.element); 
+      undoPositioned(effect.element.firstChild);
+      undoPositioned(effect.element);
+      setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }}
+    }, arguments[1] || {})
+  );
+}
+  
+Effect.SlideUp = function(element) {
+  element = $(element);
+  Element.cleanWhitespace(element);
+  var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom');
+  return new Effect.Scale(element, 0, 
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleMode: 'box',
+    scaleFrom: 100,
+    restoreAfterFinish: true,
+    beforeStartInternal: function(effect) { with(Element) {
+      makePositioned(effect.element);
+      makePositioned(effect.element.firstChild);
+      if(window.opera) setStyle(effect.element, {top: ''});
+      makeClipping(effect.element);
+      show(element); }},  
+    afterUpdateInternal: function(effect) { with(Element) {
+      setStyle(effect.element.firstChild, {bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); }},
+    afterFinishInternal: function(effect) { with(Element) {
+        [hide, undoClipping].call(effect.element); 
+        undoPositioned(effect.element.firstChild);
+        undoPositioned(effect.element);
+        setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }}
+   }, arguments[1] || {})
+  );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish 
+Effect.Squish = function(element) {
+  return new Effect.Scale(element, window.opera ? 1 : 0, 
+    { restoreAfterFinish: true,
+      beforeSetup: function(effect) { with(Element) {
+        makeClipping(effect.element); }},  
+      afterFinishInternal: function(effect) { with(Element) {
+        hide(effect.element); 
+        undoClipping(effect.element); }}
+  });
+}
+
+Effect.Grow = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransistion: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.full
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: Element.getInlineOpacity(element) };
+
+  var dims = Element.getDimensions(element);    
+  var initialMoveX, initialMoveY;
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      break;
+    case 'top-right':
+      initialMoveX = dims.width;
+      initialMoveY = moveY = 0;
+      moveX = -dims.width;
+      break;
+    case 'bottom-left':
+      initialMoveX = moveX = 0;
+      initialMoveY = dims.height;
+      moveY = -dims.height;
+      break;
+    case 'bottom-right':
+      initialMoveX = dims.width;
+      initialMoveY = dims.height;
+      moveX = -dims.width;
+      moveY = -dims.height;
+      break;
+    case 'center':
+      initialMoveX = dims.width / 2;
+      initialMoveY = dims.height / 2;
+      moveX = -dims.width / 2;
+      moveY = -dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Move(element, {
+    x: initialMoveX,
+    y: initialMoveY,
+    duration: 0.01, 
+    beforeSetup: function(effect) { with(Element) {
+      hide(effect.element);
+      makeClipping(effect.element);
+      makePositioned(effect.element);
+    }},
+    afterFinishInternal: function(effect) {
+      new Effect.Parallel(
+        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+          new Effect.Scale(effect.element, 100, {
+            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
+            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+        ], Object.extend({
+             beforeSetup: function(effect) { with(Element) {
+               setStyle(effect.effects[0].element, {height: '0px'});
+               show(effect.effects[0].element); }},
+             afterFinishInternal: function(effect) { with(Element) {
+               [undoClipping, undoPositioned].call(effect.effects[0].element); 
+               setStyle(effect.effects[0].element, oldStyle); }}
+           }, options)
+      )
+    }
+  });
+}
+
+Effect.Shrink = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransistion: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.none
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: Element.getInlineOpacity(element) };
+
+  var dims = Element.getDimensions(element);
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      moveX = moveY = 0;
+      break;
+    case 'top-right':
+      moveX = dims.width;
+      moveY = 0;
+      break;
+    case 'bottom-left':
+      moveX = 0;
+      moveY = dims.height;
+      break;
+    case 'bottom-right':
+      moveX = dims.width;
+      moveY = dims.height;
+      break;
+    case 'center':  
+      moveX = dims.width / 2;
+      moveY = dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Parallel(
+    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+    ], Object.extend({            
+         beforeStartInternal: function(effect) { with(Element) {
+           [makePositioned, makeClipping].call(effect.effects[0].element) }},
+         afterFinishInternal: function(effect) { with(Element) {
+           [hide, undoClipping, undoPositioned].call(effect.effects[0].element);
+           setStyle(effect.effects[0].element, oldStyle); }}
+       }, options)
+  );
+}
+
+Effect.Pulsate = function(element) {
+  element = $(element);
+  var options    = arguments[1] || {};
+  var oldOpacity = Element.getInlineOpacity(element);
+  var transition = options.transition || Effect.Transitions.sinoidal;
+  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
+  reverser.bind(transition);
+  return new Effect.Opacity(element, 
+    Object.extend(Object.extend({  duration: 3.0, from: 0,
+      afterFinishInternal: function(effect) { Element.setStyle(effect.element, {opacity: oldOpacity}); }
+    }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height };
+  Element.makeClipping(element);
+  return new Effect.Scale(element, 5, Object.extend({   
+    scaleContent: false,
+    scaleX: false,
+    afterFinishInternal: function(effect) {
+    new Effect.Scale(element, 1, { 
+      scaleContent: false, 
+      scaleY: false,
+      afterFinishInternal: function(effect) { with(Element) {
+        [hide, undoClipping].call(effect.element); 
+        setStyle(effect.element, oldStyle);
+      }} });
+  }}, arguments[1] || {}));
+}

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/scriptaculous.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/scriptaculous.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,45 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var Scriptaculous = {
+  Version: '1.5.1',
+  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");
+    
+    $A(document.getElementsByTagName("script")).findAll( function(s) {
+      return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))
+    }).each( function(s) {
+      var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');
+      var includes = s.src.match(/\?.*load=([a-z,]*)/);
+      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider').split(',').each(
+       function(include) { Scriptaculous.require(path+include+'.js') });
+    });
+  }
+}
+
+Scriptaculous.load();
\ No newline at end of file

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/slider.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/slider.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,283 @@
+// Copyright (c) 2005 Marty Haught, Thomas Fuchs 
+//
+// See http://script.aculo.us for more info
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+if(!Control) var Control = {};
+Control.Slider = Class.create();
+
+// options:
+//  axis: 'vertical', or 'horizontal' (default)
+//
+// callbacks:
+//  onChange(value)
+//  onSlide(value)
+Control.Slider.prototype = {
+  initialize: function(handle, track, options) {
+    var slider = this;
+    
+    if(handle instanceof Array) {
+      this.handles = handle.collect( function(e) { return $(e) });
+    } else {
+      this.handles = [$(handle)];
+    }
+    
+    this.track   = $(track);
+    this.options = options || {};
+
+    this.axis      = this.options.axis || 'horizontal';
+    this.increment = this.options.increment || 1;
+    this.step      = parseInt(this.options.step || '1');
+    this.range     = this.options.range || $R(0,1);
+    
+    this.value     = 0; // assure backwards compat
+    this.values    = this.handles.map( function() { return 0 });
+    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
+    this.options.startSpan = $(this.options.startSpan || null);
+    this.options.endSpan   = $(this.options.endSpan || null);
+
+    this.restricted = this.options.restricted || false;
+
+    this.maximum   = this.options.maximum || this.range.end;
+    this.minimum   = this.options.minimum || this.range.start;
+
+    // Will be used to align the handle onto the track, if necessary
+    this.alignX = parseInt(this.options.alignX || '0');
+    this.alignY = parseInt(this.options.alignY || '0');
+    
+    this.trackLength = this.maximumOffset() - this.minimumOffset();
+    this.handleLength = this.isVertical() ? this.handles[0].offsetHeight : this.handles[0].offsetWidth;
+
+    this.active   = false;
+    this.dragging = false;
+    this.disabled = false;
+
+    if(this.options.disabled) this.setDisabled();
+
+    // Allowed values array
+    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
+    if(this.allowedValues) {
+      this.minimum = this.allowedValues.min();
+      this.maximum = this.allowedValues.max();
+    }
+
+    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+    this.eventMouseMove = this.update.bindAsEventListener(this);
+
+    // Initialize handles in reverse (make sure first handle is active)
+    this.handles.each( function(h,i) {
+      i = slider.handles.length-1-i;
+      slider.setValue(parseFloat(
+        (slider.options.sliderValue instanceof Array ? 
+          slider.options.sliderValue[i] : slider.options.sliderValue) || 
+         slider.range.start), i);
+      Element.makePositioned(h); // fix IE
+      Event.observe(h, "mousedown", slider.eventMouseDown);
+    });
+    
+    Event.observe(this.track, "mousedown", this.eventMouseDown);
+    Event.observe(document, "mouseup", this.eventMouseUp);
+    Event.observe(document, "mousemove", this.eventMouseMove);
+    
+    this.initialized = true;
+  },
+  dispose: function() {
+    var slider = this;    
+    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
+    Event.stopObserving(document, "mouseup", this.eventMouseUp);
+    Event.stopObserving(document, "mousemove", this.eventMouseMove);
+    this.handles.each( function(h) {
+      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
+    });
+  },
+  setDisabled: function(){
+    this.disabled = true;
+  },
+  setEnabled: function(){
+    this.disabled = false;
+  },  
+  getNearestValue: function(value){
+    if(this.allowedValues){
+      if(value >= this.allowedValues.max()) return(this.allowedValues.max());
+      if(value <= this.allowedValues.min()) return(this.allowedValues.min());
+      
+      var offset = Math.abs(this.allowedValues[0] - value);
+      var newValue = this.allowedValues[0];
+      this.allowedValues.each( function(v) {
+        var currentOffset = Math.abs(v - value);
+        if(currentOffset <= offset){
+          newValue = v;
+          offset = currentOffset;
+        } 
+      });
+      return newValue;
+    }
+    if(value > this.range.end) return this.range.end;
+    if(value < this.range.start) return this.range.start;
+    return value;
+  },
+  setValue: function(sliderValue, handleIdx){
+    if(!this.active) {
+      this.activeHandle    = this.handles[handleIdx];
+      this.activeHandleIdx = handleIdx;
+      this.updateStyles();
+    }
+    handleIdx = handleIdx || this.activeHandleIdx || 0;
+    if(this.initialized && this.restricted) {
+      if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
+        sliderValue = this.values[handleIdx-1];
+      if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
+        sliderValue = this.values[handleIdx+1];
+    }
+    sliderValue = this.getNearestValue(sliderValue);
+    this.values[handleIdx] = sliderValue;
+    this.value = this.values[0]; // assure backwards compat
+    
+    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = 
+      this.translateToPx(sliderValue);
+    
+    this.drawSpans();
+    if(!this.dragging || !this.event) this.updateFinished();
+  },
+  setValueBy: function(delta, handleIdx) {
+    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
+      handleIdx || this.activeHandleIdx || 0);
+  },
+  translateToPx: function(value) {
+    return Math.round(
+      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * 
+      (value - this.range.start)) + "px";
+  },
+  translateToValue: function(offset) {
+    return ((offset/(this.trackLength-this.handleLength) * 
+      (this.range.end-this.range.start)) + this.range.start);
+  },
+  getRange: function(range) {
+    var v = this.values.sortBy(Prototype.K); 
+    range = range || 0;
+    return $R(v[range],v[range+1]);
+  },
+  minimumOffset: function(){
+    return(this.isVertical() ? this.alignY : this.alignX);
+  },
+  maximumOffset: function(){
+    return(this.isVertical() ?
+      this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX);
+  },  
+  isVertical:  function(){
+    return (this.axis == 'vertical');
+  },
+  drawSpans: function() {
+    var slider = this;
+    if(this.spans)
+      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
+    if(this.options.startSpan)
+      this.setSpan(this.options.startSpan,
+        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
+    if(this.options.endSpan)
+      this.setSpan(this.options.endSpan, 
+        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
+  },
+  setSpan: function(span, range) {
+    if(this.isVertical()) {
+      span.style.top = this.translateToPx(range.start);
+      span.style.height = this.translateToPx(range.end - range.start);
+    } else {
+      span.style.left = this.translateToPx(range.start);
+      span.style.width = this.translateToPx(range.end - range.start);
+    }
+  },
+  updateStyles: function() {
+    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
+    Element.addClassName(this.activeHandle, 'selected');
+  },
+  startDrag: function(event) {
+    if(Event.isLeftClick(event)) {
+      if(!this.disabled){
+        this.active = true;
+        
+        var handle = Event.element(event);
+        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
+        if(handle==this.track) {
+          var offsets  = Position.cumulativeOffset(this.track); 
+          this.event = event;
+          this.setValue(this.translateToValue( 
+           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
+          ));
+          var offsets  = Position.cumulativeOffset(this.activeHandle);
+          this.offsetX = (pointer[0] - offsets[0]);
+          this.offsetY = (pointer[1] - offsets[1]);
+        } else {
+          // find the handle (prevents issues with Safari)
+          while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
+            handle = handle.parentNode;
+        
+          this.activeHandle    = handle;
+          this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
+          this.updateStyles();
+        
+          var offsets  = Position.cumulativeOffset(this.activeHandle);
+          this.offsetX = (pointer[0] - offsets[0]);
+          this.offsetY = (pointer[1] - offsets[1]);
+        }
+      }
+      Event.stop(event);
+    }
+  },
+  update: function(event) {
+   if(this.active) {
+      if(!this.dragging) this.dragging = true;
+      this.draw(event);
+      // fix AppleWebKit rendering
+      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+      Event.stop(event);
+   }
+  },
+  draw: function(event) {
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    var offsets = Position.cumulativeOffset(this.track);
+    pointer[0] -= this.offsetX + offsets[0];
+    pointer[1] -= this.offsetY + offsets[1];
+    this.event = event;
+    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
+    if(this.initialized && this.options.onSlide)
+      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
+  },
+  endDrag: function(event) {
+    if(this.active && this.dragging) {
+      this.finishDrag(event, true);
+      Event.stop(event);
+    }
+    this.active = false;
+    this.dragging = false;
+  },  
+  finishDrag: function(event, success) {
+    this.active = false;
+    this.dragging = false;
+    this.updateFinished();
+  },
+  updateFinished: function() {
+    if(this.initialized && this.options.onChange) 
+      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
+    this.event = null;
+  }
+}
\ No newline at end of file

Added: jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/unittest.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/Prototypism/web/static/js/prototypism/scriptaculous/unittest.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,363 @@
+// 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/)
+//
+// See scriptaculous.js for full license.
+
+// 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

Added: jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/LICENSE.txt
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/LICENSE.txt	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,15 @@
+Copyright (c) 2007 Brian Dillard and Brad Neuberg:
+Brian Dillard | Project Lead | bdillard at pathf.com | http://blogs.pathf.com/agileajax/
+Brad Neuberg | Original Project Creator | http://codinginparadise.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.

Added: jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/blank.html
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/blank.html	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,39 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<!--
+Copyright (c) 2007 Brian Dillard and Brad Neuberg:
+Brian Dillard | Project Lead | bdillard at pathf.com | http://blogs.pathf.com/agileajax/
+Brad Neuberg | Original Project Creator | http://codinginparadise.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.
+-->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+
+<script language="JavaScript">
+function pageLoaded() {
+	window.parent.dhtmlHistory.iframeLoaded(window.location);
+	document.getElementById("output").innerHTML = window.location;
+}
+</script>
+
+</head>
+
+<body onload="pageLoaded();" style="width:700px;padding:2px;margin:0;">
+
+	<p>blank.html - Needed for Internet Explorer's hidden iframe</p>
+	<p id="output"></p>
+
+</body>
+</html>

Added: jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/rsh.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/rsh/rsh.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,667 @@
+/*
+Copyright (c) 2007 Brian Dillard and Brad Neuberg:
+Brian Dillard | Project Lead | bdillard at pathf.com | http://blogs.pathf.com/agileajax/
+Brad Neuberg | Original Project Creator | http://codinginparadise.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.
+*/
+
+/*
+   Modified version. The path of blank.html has been changed to fit Jifty's directory structure and cache mechanism.
+*/
+
+/*
+	dhtmlHistory: An object that provides history, history data, and bookmarking for DHTML and Ajax applications.
+	
+	dependencies:
+		* the historyStorage object included in this file.
+
+*/
+window.dhtmlHistory = {
+	
+	/*Public: User-agent booleans*/
+	isIE: false,
+	isOpera: false,
+	isSafari: false,
+	isKonquerer: false,
+	isGecko: false,
+	isSupported: false,
+	
+	/*Public: Create the DHTML history infrastructure*/
+	create: function(options) {
+		
+		/*
+			options - object to store initialization parameters
+			options.debugMode - boolean that causes hidden form fields to be shown for development purposes.
+			options.toJSON - function to override default JSON stringifier
+			options.fromJSON - function to override default JSON parser
+		*/
+
+		var that = this;
+
+		/*set user-agent flags*/
+		var UA = navigator.userAgent.toLowerCase();
+		var platform = navigator.platform.toLowerCase();
+		var vendor = navigator.vendor || "";
+		if (vendor === "KDE") {
+			this.isKonqueror = true;
+			this.isSupported = false;
+		} else if (typeof window.opera !== "undefined") {
+			this.isOpera = true;
+			this.isSupported = true;
+		} else if (typeof document.all !== "undefined") {
+			this.isIE = true;
+			this.isSupported = true;
+		} else if (vendor.indexOf("Apple Computer, Inc.") > -1) {
+			this.isSafari = true;
+			this.isSupported = (platform.indexOf("mac") > -1);
+		} else if (UA.indexOf("gecko") != -1) {
+			this.isGecko = true;
+			this.isSupported = true;
+		}
+
+		/*Set up the historyStorage object; pass in init parameters*/
+		window.historyStorage.setup(options);
+
+		/*Execute browser-specific setup methods*/
+		if (this.isSafari) {
+			this.createSafari();
+		} else if (this.isOpera) {
+			this.createOpera();
+		}
+		
+		/*Get our initial location*/
+		var initialHash = this.getCurrentLocation();
+
+		/*Save it as our current location*/
+		this.currentLocation = initialHash;
+
+		/*Now that we have a hash, create IE-specific code*/
+		if (this.isIE) {
+			this.createIE(initialHash);
+		}
+
+		/*Add an unload listener for the page; this is needed for FF 1.5+ because this browser caches all dynamic updates to the
+		page, which can break some of our logic related to testing whether this is the first instance a page has loaded or whether
+		it is being pulled from the cache*/
+
+		var unloadHandler = function() {
+			that.firstLoad = null;
+		};
+		
+		this.addEventListener(window,'unload',unloadHandler);		
+
+		/*Determine if this is our first page load; for IE, we do this in this.iframeLoaded(), which is fired on pageload. We do it
+		there because we have no historyStorage at this point, which only exists after the page is finished loading in IE*/
+		if (this.isIE) {
+			/*The iframe will get loaded on page load, and we want to ignore this fact*/
+			this.ignoreLocationChange = true;
+		} else {
+			if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
+				/*This is our first page load, so ignore the location change and add our special history entry*/
+				this.ignoreLocationChange = true;
+				this.firstLoad = true;
+				historyStorage.put(this.PAGELOADEDSTRING, true);
+			} else {
+				/*This isn't our first page load, so indicate that we want to pay attention to this location change*/
+				this.ignoreLocationChange = false;
+				/*For browsers other than IE, fire a history change event; on IE, the event will be thrown automatically when its
+				hidden iframe reloads on page load. Unfortunately, we don't have any listeners yet; indicate that we want to fire
+				an event when a listener is added.*/
+				this.fireOnNewListener = true;
+			}
+		}
+
+		/*Other browsers can use a location handler that checks at regular intervals as their primary mechanism; we use it for IE as
+		well to handle an important edge case; see checkLocation() for details*/
+		var locationHandler = function() {
+			that.checkLocation();
+		};
+		setInterval(locationHandler, 100);
+	},	
+	
+	/*Public: Initialize our DHTML history. You must call this after the page is finished loading.*/
+	initialize: function() {
+		/*IE needs to be explicitly initialized. IE doesn't autofill form data until the page is finished loading, so we have to wait*/
+		if (this.isIE) {
+			/*If this is the first time this page has loaded*/
+			if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
+				/*For IE, we do this in initialize(); for other browsers, we do it in create()*/
+				this.fireOnNewListener = false;
+				this.firstLoad = true;
+				historyStorage.put(this.PAGELOADEDSTRING, true);
+			}
+			/*Else if this is a fake onload event*/
+			else {
+				this.fireOnNewListener = true;
+				this.firstLoad = false;   
+			}
+		}
+	},
+
+	/*Public: Adds a history change listener. Note that only one listener is supported at this time.*/
+	addListener: function(listener) {
+		this.listener = listener;
+		/*If the page was just loaded and we should not ignore it, fire an event to our new listener now*/
+		if (this.fireOnNewListener) {
+			this.fireHistoryEvent(this.currentLocation);
+			this.fireOnNewListener = false;
+		}
+	},
+	
+	/*Public: Generic utility function for attaching events*/
+	addEventListener: function(o,e,l) {
+		if (o.addEventListener) {
+			o.addEventListener(e,l,false);
+		} else if (o.attachEvent) {
+			o.attachEvent('on'+e,function() {
+				l(window.event);
+			});
+		}
+	},
+	
+	/*Public: Add a history point.*/
+	add: function(newLocation, historyData) {
+		
+		if (this.isSafari) {
+			
+			/*Remove any leading hash symbols on newLocation*/
+			newLocation = this.removeHash(newLocation);
+
+			/*Store the history data into history storage*/
+			historyStorage.put(newLocation, historyData);
+
+			/*Save this as our current location*/
+			this.currentLocation = newLocation;
+	
+			/*Change the browser location*/
+			window.location.hash = newLocation;
+		
+			/*Save this to the Safari form field*/
+			this.putSafariState(newLocation);
+
+		} else {
+			
+			/*Most browsers require that we wait a certain amount of time before changing the location, such
+			as 200 MS; rather than forcing external callers to use window.setTimeout to account for this,
+			we internally handle it by putting requests in a queue.*/
+			var that = this;
+			var addImpl = function() {
+
+				/*Indicate that the current wait time is now less*/
+				if (that.currentWaitTime > 0) {
+					that.currentWaitTime = that.currentWaitTime - that.waitTime;
+				}
+			
+				/*Remove any leading hash symbols on newLocation*/
+				newLocation = that.removeHash(newLocation);
+
+				/*IE has a strange bug; if the newLocation is the same as _any_ preexisting id in the
+				document, then the history action gets recorded twice; throw a programmer exception if
+				there is an element with this ID*/
+				if (document.getElementById(newLocation) && that.debugMode) {
+					var e = "Exception: History locations can not have the same value as _any_ IDs that might be in the document,"
+					+ " due to a bug in IE; please ask the developer to choose a history location that does not match any HTML"
+					+ " IDs in this document. The following ID is already taken and cannot be a location: " + newLocation;
+					throw new Error(e); 
+				}
+
+				/*Store the history data into history storage*/
+				historyStorage.put(newLocation, historyData);
+
+				/*Indicate to the browser to ignore this upcomming location change since we're making it programmatically*/
+				that.ignoreLocationChange = true;
+
+				/*Indicate to IE that this is an atomic location change block*/
+				that.ieAtomicLocationChange = true;
+
+				/*Save this as our current location*/
+				that.currentLocation = newLocation;
+		
+				/*Change the browser location*/
+				window.location.hash = newLocation;
+
+				/*Change the hidden iframe's location if on IE*/
+				if (that.isIE) {
+					that.iframe.src = "/static/js/singlepage/rsh/blank.html?" + newLocation;
+				}
+
+				/*End of atomic location change block for IE*/
+				that.ieAtomicLocationChange = false;
+			};
+
+			/*Now queue up this add request*/
+			window.setTimeout(addImpl, this.currentWaitTime);
+
+			/*Indicate that the next request will have to wait for awhile*/
+			this.currentWaitTime = this.currentWaitTime + this.waitTime;
+		}
+	},
+
+	/*Public*/
+	isFirstLoad: function() {
+		return this.firstLoad;
+	},
+
+	/*Public*/
+	getVersion: function() {
+		return "0.6";
+	},
+
+	/*Get browser's current hash location; for Safari, read value from a hidden form field*/
+
+	/*Public*/
+	getCurrentLocation: function() {
+		var r = (this.isSafari
+			? this.getSafariState()
+			: this.getCurrentHash()
+		);
+		return r;
+	},
+	
+	/*Public: Manually parse the current url for a hash; tip of the hat to YUI*/
+    getCurrentHash: function() {
+		var r = window.location.href;
+		var i = r.indexOf("#");
+		return (i >= 0
+			? r.substr(i+1)
+			: ""
+		);
+    },
+	
+	/*- - - - - - - - - - - -*/
+	
+	/*Private: Constant for our own internal history event called when the page is loaded*/
+	PAGELOADEDSTRING: "DhtmlHistory_pageLoaded",
+	
+	/*Private: Our history change listener.*/
+	listener: null,
+
+	/*Private: MS to wait between add requests - will be reset for certain browsers*/
+	waitTime: 200,
+	
+	/*Private: MS before an add request can execute*/
+	currentWaitTime: 0,
+
+	/*Private: Our current hash location, without the "#" symbol.*/
+	currentLocation: null,
+
+	/*Private: Hidden iframe used to IE to detect history changes*/
+	iframe: null,
+
+	/*Private: Flags and DOM references used only by Safari*/
+	safariHistoryStartPoint: null,
+	safariStack: null,
+	safariLength: null,
+
+	/*Private: Flag used to keep checkLocation() from doing anything when it discovers location changes we've made ourselves
+	programmatically with the add() method. Basically, add() sets this to true. When checkLocation() discovers it's true,
+	it refrains from firing our listener, then resets the flag to false for next cycle. That way, our listener only gets fired on
+	history change events triggered by the user via back/forward buttons and manual hash changes. This flag also helps us set up
+	IE's special iframe-based method of handling history changes.*/
+	ignoreLocationChange: null,
+
+	/*Private: A flag that indicates that we should fire a history change event when we are ready, i.e. after we are initialized and
+	we have a history change listener. This is needed due to an edge case in browsers other than IE; if you leave a page entirely
+	then return, we must fire this as a history change event. Unfortunately, we have lost all references to listeners from earlier,
+	because JavaScript clears out.*/
+	fireOnNewListener: null,
+
+	/*Private: A variable that indicates whether this is the first time this page has been loaded. If you go to a web page, leave it
+	for another one, and then return, the page's onload listener fires again. We need a way to differentiate between the first page
+	load and subsequent ones. This variable works hand in hand with the pageLoaded variable we store into historyStorage.*/
+	firstLoad: null,
+
+	/*Private: A variable to handle an important edge case in IE. In IE, if a user manually types an address into their browser's
+	location bar, we must intercept this by calling checkLocation() at regular intervals. However, if we are programmatically
+	changing the location bar ourselves using the add() method, we need to ignore these changes in checkLocation(). Unfortunately,
+	these changes take several lines of code to complete, so for the duration of those lines of code, we set this variable to true.
+	That signals to checkLocation() to ignore the change-in-progress. Once we're done with our chunk of location-change code in
+	add(), we set this back to false. We'll do the same thing when capturing user-entered address changes in checkLocation itself.*/
+	ieAtomicLocationChange: null,
+	
+	/*Private: Create IE-specific DOM nodes and overrides*/
+	createIE: function(initialHash) {
+		/*write out a hidden iframe for IE and set the amount of time to wait between add() requests*/
+		this.waitTime = 400;/*IE needs longer between history updates*/
+		var styles = (historyStorage.debugMode
+			? 'width: 800px;height:80px;border:1px solid black;'
+			: historyStorage.hideStyles
+		);
+		var iframeID = "rshHistoryFrame";
+		var iframeHTML = '<iframe frameborder="0" id="' + iframeID + '" style="' + styles + '" src="/static/js/singlepage/rsh/blank.html?' + initialHash + '"></iframe>';
+		document.write(iframeHTML);
+		this.iframe = document.getElementById(iframeID);
+	},
+	
+	/*Private: Create Opera-specific DOM nodes and overrides*/
+	createOpera: function() {
+		this.waitTime = 400;/*Opera needs longer between history updates*/
+		var imgHTML = '<img src="javascript:location.href=\'javascript:dhtmlHistory.checkLocation();\';" style="' + historyStorage.hideStyles + '" />';
+		document.write(imgHTML);
+	},
+	
+	/*Private: Create Safari-specific DOM nodes and overrides*/
+	createSafari: function() {
+		var formID = "rshSafariForm";
+		var stackID = "rshSafariStack";
+		var lengthID = "rshSafariLength";
+		var formStyles = historyStorage.debugMode ? historyStorage.showStyles : historyStorage.hideStyles;
+		var inputStyles = (historyStorage.debugMode
+			? 'width:800px;height:20px;border:1px solid black;margin:0;padding:0;'
+			: historyStorage.hideStyles
+		);
+		var safariHTML = '<form id="' + formID + '" style="' + formStyles + '">'
+			+ '<input type="text" style="' + inputStyles + '" id="' + stackID + '" value="[]"/>'
+			+ '<input type="text" style="' + inputStyles + '" id="' + lengthID + '" value=""/>'
+		+ '</form>';
+		document.write(safariHTML);
+		this.safariStack = document.getElementById(stackID);
+		this.safariLength = document.getElementById(lengthID);
+		if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
+			this.safariHistoryStartPoint = history.length;
+			this.safariLength.value = this.safariHistoryStartPoint;
+		} else {
+			this.safariHistoryStartPoint = this.safariLength.value;
+		}
+	},
+	
+	/*Private: Safari method to read the history stack from a hidden form field*/
+	getSafariStack: function() {
+		var r = this.safariStack.value;
+		return historyStorage.fromJSON(r);
+	},
+
+	/*Private: Safari method to read from the history stack*/
+	getSafariState: function() {
+		var stack = this.getSafariStack();
+		var state = stack[history.length - this.safariHistoryStartPoint - 1];
+		return state;
+	},			
+	/*Private: Safari method to write the history stack to a hidden form field*/
+	putSafariState: function(newLocation) {
+	    var stack = this.getSafariStack();
+	    stack[history.length - this.safariHistoryStartPoint] = newLocation;
+	    this.safariStack.value = historyStorage.toJSON(stack);
+	},
+
+	/*Private: Notify the listener of new history changes.*/
+	fireHistoryEvent: function(newHash) {
+		/*extract the value from our history storage for this hash*/
+		var historyData = historyStorage.get(newHash);
+		/*call our listener*/
+		this.listener.call(null, newHash, historyData);
+	},
+	
+	/*Private: See if the browser has changed location. This is the primary history mechanism for Firefox. For IE, we use this to
+	handle an important edge case: if a user manually types in a new hash value into their IE location bar and press enter, we want to
+	to intercept this and notify any history listener.*/
+	checkLocation: function() {
+		
+		/*Ignore any location changes that we made ourselves for browsers other than IE*/
+		if (!this.isIE && this.ignoreLocationChange) {
+			this.ignoreLocationChange = false;
+			return;
+		}
+
+		/*If we are dealing with IE and we are in the middle of making a location change from an iframe, ignore it*/
+		if (!this.isIE && this.ieAtomicLocationChange) {
+			return;
+		}
+		
+		/*Get hash location*/
+		var hash = this.getCurrentLocation();
+
+		/*Do nothing if there's been no change*/
+		if (hash == this.currentLocation) {
+			return;
+		}
+
+		/*In IE, users manually entering locations into the browser; we do this by comparing the browser's location against the
+		iframe's location; if they differ, we are dealing with a manual event and need to place it inside our history, otherwise
+		we can return*/
+		this.ieAtomicLocationChange = true;
+
+		if (this.isIE && this.getIframeHash() != hash) {
+			this.iframe.src = "/static/js/singlepage/rsh/blank.html?" + hash;
+		}
+		else if (this.isIE) {
+			/*the iframe is unchanged*/
+			return;
+		}
+
+		/*Save this new location*/
+		this.currentLocation = hash;
+
+		this.ieAtomicLocationChange = false;
+
+		/*Notify listeners of the change*/
+		this.fireHistoryEvent(hash);
+	},
+
+	/*Private: Get the current location of IE's hidden iframe.*/
+	getIframeHash: function() {
+		var doc = this.iframe.contentWindow.document;
+		var hash = String(doc.location.search);
+		if (hash.length == 1 && hash.charAt(0) == "?") {
+			hash = "";
+		}
+		else if (hash.length >= 2 && hash.charAt(0) == "?") {
+			hash = hash.substring(1);
+		}
+		return hash;
+	},
+
+	/*Private: Remove any leading hash that might be on a location.*/
+	removeHash: function(hashValue) {
+		var r;
+		if (hashValue === null || hashValue === undefined) {
+			r = null;
+		}
+		else if (hashValue === "") {
+			r = "";
+		}
+		else if (hashValue.length == 1 && hashValue.charAt(0) == "#") {
+			r = "";
+		}
+		else if (hashValue.length > 1 && hashValue.charAt(0) == "#") {
+			r = hashValue.substring(1);
+		}
+		else {
+			r = hashValue;
+		}
+		return r;
+	},
+
+	/*Private: For IE, tell when the hidden iframe has finished loading.*/
+	iframeLoaded: function(newLocation) {
+		/*ignore any location changes that we made ourselves*/
+		if (this.ignoreLocationChange) {
+			this.ignoreLocationChange = false;
+			return;
+		}
+
+		/*Get the new location*/
+		var hash = String(newLocation.search);
+		if (hash.length == 1 && hash.charAt(0) == "?") {
+			hash = "";
+		}
+		else if (hash.length >= 2 && hash.charAt(0) == "?") {
+			hash = hash.substring(1);
+		}
+		/*Keep the browser location bar in sync with the iframe hash*/
+		window.location.hash = hash;
+
+		/*Notify listeners of the change*/
+		this.fireHistoryEvent(hash);
+	}
+
+};
+
+/*
+	historyStorage: An object that uses a hidden form to store history state across page loads. The mechanism for doing so relies on
+	the fact that browsers save the text in form data for the life of the browser session, which means the text is still there when
+	the user navigates back to the page. This object can be used independently of the dhtmlHistory object for caching of Ajax
+	session information.
+	
+	dependencies: 
+		* json2007.js (included in a separate file) or alternate JSON methods passed in through an options bundle.
+*/
+window.historyStorage = {
+	
+	/*Public: Set up our historyStorage object for use by dhtmlHistory or other objects*/
+	setup: function(options) {
+		
+		/*
+			options - object to store initialization parameters - passed in from dhtmlHistory or directly into historyStorage
+			options.debugMode - boolean that causes hidden form fields to be shown for development purposes.
+			options.toJSON - function to override default JSON stringifier
+			options.fromJSON - function to override default JSON parser
+		*/
+		
+		/*process init parameters*/
+		if (typeof options !== "undefined") {
+			if (options.debugMode) {
+				this.debugMode = options.debugMode;
+			}
+			if (options.toJSON) {
+				this.toJSON = options.toJSON;
+			}
+			if (options.fromJSON) {
+				this.fromJSON = options.fromJSON;
+			}
+		}		
+		
+		/*write a hidden form and textarea into the page; we'll stow our history stack here*/
+		var formID = "rshStorageForm";
+		var textareaID = "rshStorageField";
+		var formStyles = this.debugMode ? historyStorage.showStyles : historyStorage.hideStyles;
+		var textareaStyles = (historyStorage.debugMode
+			? 'width: 800px;height:80px;border:1px solid black;'
+			: historyStorage.hideStyles
+		);
+		var textareaHTML = '<form id="' + formID + '" style="' + formStyles + '">'
+			+ '<textarea id="' + textareaID + '" style="' + textareaStyles + '"></textarea>'
+		+ '</form>';
+		document.write(textareaHTML);
+		this.storageField = document.getElementById(textareaID);
+		if (typeof window.opera !== "undefined") {
+			this.storageField.focus();/*Opera needs to focus this element before persisting values in it*/
+		}
+	},
+	
+	/*Public*/
+	put: function(key, value) {
+		this.assertValidKey(key);
+		/*if we already have a value for this, remove the value before adding the new one*/
+		if (this.hasKey(key)) {
+			this.remove(key);
+		}
+		/*store this new key*/
+		this.storageHash[key] = value;
+		/*save and serialize the hashtable into the form*/
+		this.saveHashTable();
+	},
+
+	/*Public*/
+	get: function(key) {
+		this.assertValidKey(key);
+		/*make sure the hash table has been loaded from the form*/
+		this.loadHashTable();
+		var value = this.storageHash[key];
+		if (value === undefined) {
+			value = null;
+		}
+		return value;
+	},
+
+	/*Public*/
+	remove: function(key) {
+		this.assertValidKey(key);
+		/*make sure the hash table has been loaded from the form*/
+		this.loadHashTable();
+		/*delete the value*/
+		delete this.storageHash[key];
+		/*serialize and save the hash table into the form*/
+		this.saveHashTable();
+	},
+
+	/*Public: Clears out all saved data.*/
+	reset: function() {
+		this.storageField.value = "";
+		this.storageHash = {};
+	},
+
+	/*Public*/
+	hasKey: function(key) {
+		this.assertValidKey(key);
+		/*make sure the hash table has been loaded from the form*/
+		this.loadHashTable();
+		return (typeof this.storageHash[key] !== "undefined");
+	},
+
+	/*Public*/
+	isValidKey: function(key) {
+		return (typeof key === "string");
+	},
+	
+	/*Public - CSS strings utilized by both objects to hide or show behind-the-scenes DOM elements*/
+	showStyles: 'border:0;margin:0;padding:0;',
+	hideStyles: 'left:-1000px;top:-1000px;width:1px;height:1px;border:0;position:absolute;',
+	
+	/*Public - debug mode flag*/
+	debugMode: false,
+	
+	/*- - - - - - - - - - - -*/
+
+	/*Private: Our hash of key name/values.*/
+	storageHash: {},
+
+	/*Private: If true, we have loaded our hash table out of the storage form.*/
+	hashLoaded: false, 
+
+	/*Private: DOM reference to our history field*/
+	storageField: null,
+
+	/*Private: Assert that a key is valid; throw an exception if it not.*/
+	assertValidKey: function(key) {
+		var isValid = this.isValidKey(key);
+		if (!isValid && this.debugMode) {
+			throw new Error("Please provide a valid key for window.historyStorage. Invalid key = " + key + ".");
+		}
+	},
+
+	/*Private: Load the hash table up from the form.*/
+	loadHashTable: function() {
+		if (!this.hashLoaded) {	
+			var serializedHashTable = this.storageField.value;
+			if (serializedHashTable !== "" && serializedHashTable !== null) {
+				this.storageHash = this.fromJSON(serializedHashTable);
+				this.hashLoaded = true;
+			}
+		}
+	},
+	/*Private: Save the hash table into the form.*/
+	saveHashTable: function() {
+		this.loadHashTable();
+		var serializedHashTable = this.toJSON(this.storageHash);
+		this.storageField.value = serializedHashTable;
+	},
+	/*Private: Bridges for our JSON implementations - both rely on 2007 JSON.org library - can be overridden by options bundle*/
+	toJSON: function(o) {
+		return o.toJSONString();
+	},
+	fromJSON: function(s) {
+		return s.parseJSON();
+	}
+};

Added: jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/spa.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/plugins/Jifty/Plugin/SinglePage/web/static/js/singlepage/spa.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,97 @@
+(function($) {
+    SPA = {
+        initialHash: "spa_start",
+        currentHash: null,
+        currentLocation: null,
+        historyChange: function(newLocation, historyData, first) {
+
+            /* reload if user goes to the first page */
+            if (newLocation == SPA.initialHash) {
+                location.href = location.pathname;
+            }
+
+            if (first) {
+                dhtmlHistory.add(newLocation, historyData);
+            } else {
+                if (historyStorage.hasKey(newLocation)) {
+                    Jifty.update(historyStorage.get(newLocation), "");
+                }
+            }
+        },
+        _sp_submit_form: function(elt, event, submit_to) {
+            if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true;
+
+            var form = Jifty.Form.Element.getForm(elt);
+            var elements = Jifty.Form.getElements(form);
+
+            // Three things need to get merged -- hidden defaults, defaults
+            // from buttons, and form values.  Hence, we build up three lists
+            // and then merge them.
+            var hiddens = {};
+            var buttons = {};
+            var inputs = {};
+            for (var i = 0; i < elements.length; i++) {
+                var e = elements[i];
+                var parsed = e.getAttribute("name").match(/^J:V-region-__page\.(.*)/);
+                var extras = Jifty.Form.Element.buttonArguments(e);
+
+                var extras_key_length = 0;
+                $.each(extras, function() { extras_key_length++ });
+
+                if (extras_key_length > 1) {
+                    // Button with values
+                    $.each(extras, function(k, v) {
+                        if (k == 'extend') return;
+                        parsed = k.match(/^J:V-region-__page\.(.*)/);
+                        if ((parsed != null) && (parsed.length == 2)) {
+                            buttons[ parsed[1] ] = v;
+                        } else if (v.length > 0) {
+                            input[ k ] = v;
+                        }
+                    });
+                } else if ((parsed != null) && (parsed.length == 2)) {
+                    // Hidden default
+                    hiddens[ parsed[1] ] = $(e).val();
+                } else if (e.name.length > 0) {
+                    // Straight up values
+                    inputs[ e.name ] = $(e).val();
+                }
+            }
+
+            var args = $.extend({}, hiddens, buttons, inputs);
+
+            return Jifty.update( {'continuation':{},'actions':null,'fragments':[{'mode':'Replace','args':args,'region':'__page','path': submit_to}]}, elt );
+        }
+    };
+
+    /*
+     * If user paste /#/abc in location bar, or click the reload button,
+     * then we should redirect him to the right page
+     */
+    SPA.currentHash = location.hash;
+    if (SPA.currentHash.length) {
+        if (SPA.currentHash.charAt(0) == '#' && SPA.currentHash.charAt(1) == '/') {
+            SPA.currentLocation = SPA.currentHash.slice(1);
+            location.href = SPA.currentLocation;
+        }
+    }
+
+    $(document).ready(function(){
+        dhtmlHistory.initialize();
+        dhtmlHistory.addListener(SPA.historyChange);
+        if (dhtmlHistory.isFirstLoad()) {
+            dhtmlHistory.add(SPA.initialHash, "");
+        }
+    });
+    
+})(jQuery);
+
+
+window.dhtmlHistory.create({
+    toJSON: function(o) {
+        return JSON.stringify(o);
+    }
+    , fromJSON: function(s) {
+        return JSON.parse(s);
+    }
+});

Modified: jifty/trunk/share/web/static/js/bps_util.js
==============================================================================
--- jifty/trunk/share/web/static/js/bps_util.js	(original)
+++ jifty/trunk/share/web/static/js/bps_util.js	Wed Apr  9 00:12:34 2008
@@ -23,7 +23,7 @@
     link.setAttribute("href","#");
     link.setAttribute("name",e.getAttribute("name"));
 
-    var form = Form.Element.getForm(e);
+    var form = Jifty.Form.Element.getForm(e);
     var onclick = e.getAttribute("onclick");
 
     /* Simple buttons that don't use any JS need us to create an onclick

Modified: jifty/trunk/share/web/static/js/calendar.js
==============================================================================
--- jifty/trunk/share/web/static/js/calendar.js	(original)
+++ jifty/trunk/share/web/static/js/calendar.js	Wed Apr  9 00:12:34 2008
@@ -4,7 +4,7 @@
 
 Jifty.Calendar = {
     registerDateWidget: function(id) {
-        var input = $(id);
+        var input = document.getElementById(id);
         
         if ( !input ) return false;
 
@@ -22,7 +22,7 @@
     toggleCalendar: function(ev) {
         var calId  = "cal_" + ev.target.id;
         var wrapId = calId + "_wrap";
-        var wrap   = $(wrapId);
+        var wrap   = document.getElementById(wrapId);
         var input  = ev.target;
 
         if ( Jifty.Calendar.openCalendar == wrapId ) {
@@ -34,7 +34,7 @@
         
         /* We need to delay Jifty's canonicalization until after we've
            selected a value via the calendar */
-        Form.Element.disableValidation(input);
+        Jifty.Form.Element.disableValidation(input);
         
         wrap = document.createElement("div");
         wrap.setAttribute( "id", wrapId );
@@ -96,7 +96,7 @@
     openCalendar: "",
 
     hideOpenCalendar: function() {
-        if ( Jifty.Calendar.openCalendar && $( Jifty.Calendar.openCalendar ) ) {
+        if ( Jifty.Calendar.openCalendar && document.getElementById( Jifty.Calendar.openCalendar ) ) {
 
             /* Get the input's ID */
             var inputId = Jifty.Calendar.openCalendar;
@@ -105,11 +105,11 @@
 
             Element.remove(Jifty.Calendar.openCalendar);
 
-            var input = $( inputId );
+            var input = document.getElementById( inputId );
 
             /* Reenable canonicalization, and do it */
-            Form.Element.enableValidation(input);
-            Form.Element.validate(input);
+            Jifty.Form.Element.enableValidation(input);
+            Jifty.Form.Element.validate(input);
 
             Jifty.Calendar.openCalendar = "";
         }

Modified: jifty/trunk/share/web/static/js/halo.js
==============================================================================
--- jifty/trunk/share/web/static/js/halo.js	(original)
+++ jifty/trunk/share/web/static/js/halo.js	Wed Apr  9 00:12:34 2008
@@ -7,45 +7,43 @@
 
 function halo_toggle (id) {
     if (halo_shown && (id != halo_shown)) {
-        halo_top   = $('halo-'+halo_shown+'-menu').style.top;
-        halo_left  = $('halo-'+halo_shown+'-menu').style.left;
-        halo_width = $('halo-'+halo_shown+'-menu').style.width;
-        Element.hide('halo-'+halo_shown+'-menu');
-    }
-    $('halo-'+id+'-menu').style.top   = halo_top;
-    $('halo-'+id+'-menu').style.left  = halo_left;
-    $('halo-'+id+'-menu').style.width = halo_width;
-    Element.toggle('halo-'+id+'-menu');
+        halo_top   = Jifty.$('halo-'+halo_shown+'-menu').style.top;
+        halo_left  = Jifty.$('halo-'+halo_shown+'-menu').style.left;
+        halo_width = Jifty.$('halo-'+halo_shown+'-menu').style.width;
+        jQuery('#halo-'+halo_shown+'-menu').hide();
+    }
+
+    jQuery("#halo-"+id+"-menu").css({
+        top: halo_top,
+        left: halo_left,
+        width: halo_width
+    }).toggle();
 
-    Drag.init( $('halo-'+id+'-title'), $('halo-'+id+'-menu') );
-    init_resize($('halo-'+id+'-resize'), $('halo-'+id+'-menu') );
+    Drag.init( Jifty.$('halo-'+id+'-title'), Jifty.$('halo-'+id+'-menu') );
+    init_resize(Jifty.$('halo-'+id+'-resize'), Jifty.$('halo-'+id+'-menu') );
 
-    var e = $('halo-'+id);
-    if (Element.visible('halo-'+id+'-menu')) {
+    var e = jQuery('#halo-'+id).get(0);
+    if (jQuery('#halo-'+id+'-menu').is(":visible")) {
         halo_shown = id;
-        Element.setStyle(e, {background: '#ffff80'});
+        jQuery(e).css({ background: '#ffff80' });
     } else {
-        halo_top   = $('halo-'+halo_shown+'-menu').style.top;
-        halo_left  = $('halo-'+halo_shown+'-menu').style.left;
-        halo_width = $('halo-'+halo_shown+'-menu').style.width;
+        halo_top   = Jifty.$('halo-'+halo_shown+'-menu').style.top;
+        halo_left  = Jifty.$('halo-'+halo_shown+'-menu').style.left;
+        halo_width = Jifty.$('halo-'+halo_shown+'-menu').style.width;
         halo_shown = null;
-        Element.setStyle(e, {background: 'inherit'});
+        jQuery(e).css({ background: 'inherit' });
     }
 
     return false;
 }
 
 function halo_over (id) {
-    var e = $('halo-'+id);
-    if (e) {
-        Element.setStyle(e, {background: '#ffff80'});
-    }
+    jQuery('#halo-'+id).css({ background: '#ffff80' });
 }
 
 function halo_out (id) {
-    var e = $('halo-'+id);
-    if (e && ! Element.visible('halo-'+id+'-menu')) {
-        Element.setStyle(e, {background: 'inherit'});
+    if (! jQuery("#halo-"+id+"-menu").is(":visible")) {
+        jQuery('#halo-'+id).css({ background: 'inherit' });
     }
 }
 
@@ -79,50 +77,48 @@
         halo_padding        = '3px';
     }
 
-    $("render_info-draw_halos").innerHTML = halos_drawn ? "Hide halos" : "Draw halos";
+    jQuery("#render_info-draw_halos").text(halos_drawn ? "Hide halos" : "Draw halos");
+
+    jQuery(".halo-header").css({
+        display: halo_header_display
+    });
+
+    jQuery(".halo").css({
+        'border-width': halo_border_width,
+        'margin': halo_margin,
+        'padding': halo_padding
+    })
 
-    YAHOO.util.Dom.getElementsByClassName("halo-header", null, null,
-        function (e) {
-            e.style.display = halo_header_display;
-        }
-    );
-
-    YAHOO.util.Dom.getElementsByClassName("halo", null, null,
-        function (e) {
-            e.style.borderWidth = halo_border_width;
-            e.style.margin = halo_margin;
-            e.style.padding = halo_padding;
-        }
-    );
 }
 
 function render_info_tree() {
-    Element.toggle("render_info_tree");
+    jQuery("#render_info_tree").toggle();
 }
 
 function halo_render(id, name) {
     halo_reset(id);
-    $('halo-button-'+name+'-'+id).style.fontWeight = 'bold';
 
-    var e = $('halo-rendered-'+id);
+    jQuery('#halo-button-'+name+'-'+id).css("font-weight", "bold");
+
+    var e = jQuery('#halo-rendered-'+id).get(0);
 
     if (name == 'source') {
         e.halo_rendered = e.innerHTML;
-        e.innerHTML = '<div class="halo-source">' + e.innerHTML.escapeHTML() + '</div>';
+        jQuery(e).html('<div class="halo-source"></div>').find("div").text(e.halo_rendered);
     }
     else if (name == 'render') {
         /* ignore */
     }
     else {
         e.style.display = 'none';
-        $('halo-info-'+id).style.display = 'block';
-        $('halo-info-'+name+'-'+id).style.display = 'block';
+        jQuery("#halo-info-"+id).show();
+        jQuery('#halo-info-'+name+'-'+id).show();
     }
 }
 
 function halo_reset(id) {
     /* restore all buttons to nonbold */
-    for (var child = $('halo-rendermode-'+id).firstChild;
+    for (var child = jQuery('#halo-rendermode-'+id).firstChild;
          child != null;
          child = child.nextSibling) {
             if (child.style) {
@@ -131,8 +127,9 @@
     }
 
     /* hide all the info divs */
-    $('halo-info-'+id).style.display = 'none';
-    for (var child = $('halo-info-'+id).firstChild;
+    jQuery('#halo-info-'+id).hide();
+
+    for (var child = jQuery('#halo-info-'+id).firstChild;
          child != null;
          child = child.nextSibling) {
             if (child.style) {
@@ -141,7 +138,7 @@
     }
 
     /* restore the rendered div */
-    var e = $('halo-rendered-'+id);
+    var e = jQuery('#halo-rendered-'+id).get(0);
     e.style.display = 'block';
     if (e.halo_rendered) {
         e.innerHTML = e.halo_rendered;
@@ -150,7 +147,7 @@
 }
 
 function remove_link(id, name) {
-    var link = $('halo-button-'+name+'-'+id);
+    var link = jQuery('#halo-button-'+name+'-'+id).get(0);
     var newlink = document.createElement("span");
     newlink.appendChild(link.childNodes[0]);
     link.parentNode.replaceChild(newlink, link);

Added: jifty/trunk/share/web/static/js/iautocompleter.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/web/static/js/iautocompleter.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,536 @@
+/**
+ * Interface Elements for jQuery
+ * Autocompleter
+ * 
+ * http://interface.eyecon.ro
+ * 
+ * Copyright (c) 2006 Stefan Petre
+ * Dual licensed under the MIT (MIT-LICENSE.txt) 
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *  
+ */
+
+/**
+ * Attach AJAX driven autocomplete/sugestion box to text input fields.
+ *
+ * 
+ * 
+ * @name Autocomplete
+ * @description Attach AJAX driven autocomplete/sugestion box to text input fields.
+ * @param Hash hash A hash of parameters
+ * @option String source the URL to request
+ * @option Integer delay (optional) the delayed time to start the AJAX request
+ * @option Boolean autofill (optional) when true the first sugested value fills the input
+ * @option String helperClass (optional) the CSS class applied to sugestion box
+ * @option String selectClass (optional) the CSS class applied to selected/hovered item
+ * @option Integer minchars (optional) the number of characters needed before starting AJAX request
+ * @option Hash fx (optional) {type:[slide|blind|fade]; duration: integer} the fx type to apply to sugestion box and duration for that fx
+ * @option Function onSelect (optional) A function to be executed whenever an item it is selected
+ * @option Function onShow (optional) A function to be executed whenever the suggection box is displayed
+ * @option Function onHide (optional) A function to be executed whenever the suggection box is hidden
+ * @option Function onHighlight (optional) A function to be executed whenever an item it is highlighted
+ *
+ * @type jQuery
+ * @cat Plugins/Interface
+ * @author Stefan Petre
+ */
+jQuery.iAuto = {
+	helper : null,
+	content : null,
+	iframe: null,
+	timer : null,
+	lastValue: null,
+	currentValue: null,
+	subject: null,
+	selectedItem : null,
+	items: null,
+	
+	empty : function()
+	{
+		jQuery.iAuto.content.empty();
+		if (jQuery.iAuto.iframe) {
+			jQuery.iAuto.iframe.hide();
+		}
+	},
+
+	clear : function()
+	{
+		jQuery.iAuto.items = null;
+		jQuery.iAuto.selectedItem = null;
+		jQuery.iAuto.lastValue = jQuery.iAuto.subject.value;
+		if(jQuery.iAuto.helper.css('display') == 'block') {
+			if (jQuery.iAuto.subject.autoCFG.fx) {
+				switch(jQuery.iAuto.subject.autoCFG.fx.type) {
+					case 'fade':
+						jQuery.iAuto.helper.fadeOut(jQuery.iAuto.subject.autoCFG.fx.duration, jQuery.iAuto.empty);
+						break;
+					case 'slide':
+						jQuery.iAuto.helper.SlideOutUp(jQuery.iAuto.subject.autoCFG.fx.duration, jQuery.iAuto.empty);
+						break;
+					case 'blind':
+						jQuery.iAuto.helper.BlindUp(jQuery.iAuto.subject.autoCFG.fx.duration, jQuery.iAuto.empty);
+						break;
+				}
+			} else {
+				jQuery.iAuto.helper.hide();
+			}
+			if (jQuery.iAuto.subject.autoCFG.onHide)
+				jQuery.iAuto.subject.autoCFG.onHide.apply(jQuery.iAuto.subject, [jQuery.iAuto.helper, jQuery.iAuto.iframe]);
+		} else {
+			jQuery.iAuto.empty();
+		}
+		window.clearTimeout(jQuery.iAuto.timer);
+	},
+
+	update : function ()
+	{
+		var subject = jQuery.iAuto.subject;
+		var subjectValue = jQuery.iAuto.getFieldValues(subject);
+		//var selectionStart = jQuery.iAuto.getSelectionStart(subject);
+		if (subject && subjectValue.item != jQuery.iAuto.lastValue && subjectValue.item.length >= subject.autoCFG.minchars) {
+			jQuery.iAuto.lastValue = subjectValue.item;
+			jQuery.iAuto.currentValue = subjectValue.item;
+
+			data = {
+				field: jQuery(subject).attr('name')||'field',
+				value: subjectValue.item
+			};
+
+			jQuery.ajax(
+				{
+					type: 'POST',
+					data: jQuery.param(data),
+					success: function(xml)
+					{
+						subject.autoCFG.lastSuggestion = jQuery('item',xml);
+						size = subject.autoCFG.lastSuggestion.size();
+						if (size > 0) {
+							var toWrite = '';
+							subject.autoCFG.lastSuggestion.each(
+								function(nr)
+								{
+									toWrite += '<li rel="' + jQuery('value', this).text() + '" dir="' + nr + '" style="cursor: default;">' + jQuery('text', this).text() + '</li>';
+								}
+							);
+							if (subject.autoCFG.autofill) {
+								var valueToAdd = jQuery('value', subject.autoCFG.lastSuggestion.get(0)).text();
+								subject.value = subjectValue.pre + valueToAdd + subject.autoCFG.multipleSeparator + subjectValue.post;
+								jQuery.iAuto.selection(
+									subject, 
+									subjectValue.item.length != valueToAdd.length ? (subjectValue.pre.length + subjectValue.item.length) : valueToAdd.length,
+									subjectValue.item.length != valueToAdd.length ? (subjectValue.pre.length + valueToAdd.length) : valueToAdd.length
+								);
+							}
+							
+							if (size > 0) {
+								jQuery.iAuto.writeItems(subject, toWrite);
+							} else {
+								jQuery.iAuto.clear();
+							}
+						} else {
+							jQuery.iAuto.clear();
+						}
+					},
+					url : subject.autoCFG.source
+				}
+			);
+		}
+	},
+	
+	writeItems : function(subject, toWrite)
+	{
+		jQuery.iAuto.content.html(toWrite);
+		jQuery.iAuto.items = jQuery('li', jQuery.iAuto.content.get(0));
+		jQuery.iAuto.items
+			.mouseover(jQuery.iAuto.hoverItem)
+			.bind('click', jQuery.iAuto.clickItem);
+		var position = jQuery.iUtil.getPosition(subject);
+		var size = jQuery.iUtil.getSize(subject);
+		jQuery.iAuto.helper
+			.css('top', position.y + size.hb + 'px')
+			.css('left', position.x +  'px')
+			.addClass(subject.autoCFG.helperClass);
+		if (jQuery.iAuto.iframe) {
+			jQuery.iAuto.iframe
+				.css('display', 'block')
+				.css('top', position.y + size.hb + 'px')
+				.css('left', position.x +  'px')
+				.css('width', jQuery.iAuto.helper.css('width'))
+				.css('height', jQuery.iAuto.helper.css('height'));
+		}
+		jQuery.iAuto.selectedItem = 0;
+		jQuery.iAuto.items.get(0).className = subject.autoCFG.selectClass;
+		jQuery.iAuto.applyOn(subject,subject.autoCFG.lastSuggestion.get(0), 'onHighlight');
+		
+		if (jQuery.iAuto.helper.css('display') == 'none') {
+			if (subject.autoCFG.inputWidth) {
+				var borders = jQuery.iUtil.getPadding(subject, true);
+				var paddings = jQuery.iUtil.getBorder(subject, true);
+				jQuery.iAuto.helper.css('width', subject.offsetWidth - (jQuery.boxModel ? (borders.l + borders.r + paddings.l + paddings.r) : 0 ) + 'px');
+			}
+			if (subject.autoCFG.fx) {
+				switch(subject.autoCFG.fx.type) {
+					case 'fade':
+						jQuery.iAuto.helper.fadeIn(subject.autoCFG.fx.duration);
+						break;
+					case 'slide':
+						jQuery.iAuto.helper.SlideInUp(subject.autoCFG.fx.duration);
+						break;
+					case 'blind':
+						jQuery.iAuto.helper.BlindDown(subject.autoCFG.fx.duration);
+						break;
+				}
+			} else {
+				jQuery.iAuto.helper.show();
+			}
+			
+			if (jQuery.iAuto.subject.autoCFG.onShow)
+				jQuery.iAuto.subject.autoCFG.onShow.apply(jQuery.iAuto.subject, [jQuery.iAuto.helper, jQuery.iAuto.iframe]);
+		}
+	},
+	
+	checkCache : function()
+	{
+		var subject = this;
+		if (subject.autoCFG.lastSuggestion) {
+			
+			jQuery.iAuto.lastValue = subject.value;
+			jQuery.iAuto.currentValue = subject.value;
+			
+			var toWrite = '';
+			subject.autoCFG.lastSuggestion.each(
+				function(nr)
+				{
+					value = jQuery('value', this).text().toLowerCase();
+					inputValue = subject.value.toLowerCase();
+					if (value.indexOf(inputValue) == 0) {
+						toWrite += '<li rel="' + jQuery('value', this).text() + '" dir="' + nr + '" style="cursor: default;">' + jQuery('text', this).text() + '</li>';
+					}
+				}
+			);
+			
+			if (toWrite != '') {
+				jQuery.iAuto.writeItems(subject, toWrite);
+				
+				this.autoCFG.inCache = true;
+				return;
+			}
+		}
+		subject.autoCFG.lastSuggestion = null;
+		this.autoCFG.inCache = false;
+	},
+
+	selection : function(field, start, end)
+	{
+		if (field.createTextRange) {
+			var selRange = field.createTextRange();
+			selRange.collapse(true);
+			selRange.moveStart("character", start);
+			selRange.moveEnd("character", - end + start);
+			selRange.select();
+		} else if (field.setSelectionRange) {
+			field.setSelectionRange(start, end);
+		} else {
+			if (field.selectionStart) {
+				field.selectionStart = start;
+				field.selectionEnd = end;
+			}
+		}
+		field.focus();
+	},
+	
+	getSelectionStart : function(field)
+	{
+		if (field.selectionStart)
+			return field.selectionStart;
+		else if(field.createTextRange) {
+			var selRange = document.selection.createRange();
+			var selRange2 = selRange.duplicate();
+			return 0 - selRange2.moveStart('character', -100000);
+			//result.end = result.start + range.text.length;
+			/*var selRange = document.selection.createRange();
+			var isCollapsed = selRange.compareEndPoints("StartToEnd", selRange) == 0;
+			if (!isCollapsed)
+				selRange.collapse(true);
+			var bookmark = selRange.getBookmark();
+			return bookmark.charCodeAt(2) - 2;*/
+		}
+	},
+	
+	getFieldValues : function(field)
+	{
+		var fieldData = {
+			value: field.value,
+			pre: '',
+			post: '',
+			item: ''
+		};
+		
+		if(field.autoCFG.multiple) {
+			var finishedPre = false;
+			var selectionStart = jQuery.iAuto.getSelectionStart(field)||0;
+			var chunks = fieldData.value.split(field.autoCFG.multipleSeparator);
+			for (var i=0; i<chunks.length; i++) {
+				if(
+					(fieldData.pre.length + chunks[i].length >= selectionStart
+					 || 
+					selectionStart == 0)
+					 && 
+					!finishedPre 
+				) {
+					if (fieldData.pre.length <= selectionStart)
+						fieldData.item = chunks[i];
+					else 
+						fieldData.post += chunks[i] + (chunks[i] != '' ? field.autoCFG.multipleSeparator : '');
+					finishedPre = true;
+				} else if (finishedPre){
+					fieldData.post += chunks[i] + (chunks[i] != '' ? field.autoCFG.multipleSeparator : '');
+				}
+				if(!finishedPre) {
+					fieldData.pre += chunks[i] + (chunks.length > 1 ? field.autoCFG.multipleSeparator : '');
+				}
+			}
+		} else {
+			fieldData.item = fieldData.value;
+		}
+		return fieldData;
+	},
+	
+	autocomplete : function(e)
+	{
+		window.clearTimeout(jQuery.iAuto.timer);
+		var subject = jQuery.iAuto.getFieldValues(this);
+
+		var pressedKey = e.charCode || e.keyCode || -1;
+		if (/13|27|35|36|38|40|9/.test(pressedKey) && jQuery.iAuto.items) {
+			if (window.event) {
+				window.event.cancelBubble = true;
+				window.event.returnValue = false;
+			} else {
+				e.preventDefault();
+				e.stopPropagation();
+			}
+			if (jQuery.iAuto.selectedItem != null) 
+				jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).className = '';
+			else
+				jQuery.iAuto.selectedItem = -1;
+			switch(pressedKey) {
+				//enter
+				case 9:
+				case 13:
+					if (jQuery.iAuto.selectedItem == -1)
+						jQuery.iAuto.selectedItem = 0;
+					var selectedItem = jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0);
+					var valueToAdd = selectedItem.getAttribute('rel');
+					this.value = subject.pre + valueToAdd + this.autoCFG.multipleSeparator + subject.post;
+					jQuery.iAuto.lastValue = subject.item;
+					jQuery.iAuto.selection(
+						this, 
+						subject.pre.length + valueToAdd.length + this.autoCFG.multipleSeparator.length, 
+						subject.pre.length + valueToAdd.length + this.autoCFG.multipleSeparator.length
+					);
+					jQuery.iAuto.clear();
+					if (this.autoCFG.onSelect) {
+						iteration = parseInt(selectedItem.getAttribute('dir'))||0;
+						jQuery.iAuto.applyOn(this,this.autoCFG.lastSuggestion.get(iteration), 'onSelect');
+					}
+					if (this.scrollIntoView)
+						this.scrollIntoView(false);
+					return pressedKey != 13;
+					break;
+				//escape
+				case 27:
+					this.value = subject.pre + jQuery.iAuto.lastValue + this.autoCFG.multipleSeparator + subject.post;
+					this.autoCFG.lastSuggestion = null;
+					jQuery.iAuto.clear();
+					if (this.scrollIntoView)
+						this.scrollIntoView(false);
+					return false;
+					break;
+				//end
+				case 35:
+					jQuery.iAuto.selectedItem = jQuery.iAuto.items.size() - 1;
+					break;
+				//home
+				case 36:
+					jQuery.iAuto.selectedItem = 0;
+					break;
+				//up
+				case 38:
+					jQuery.iAuto.selectedItem --;
+					if (jQuery.iAuto.selectedItem < 0)
+						jQuery.iAuto.selectedItem = jQuery.iAuto.items.size() - 1;
+					break;
+				case 40:
+					jQuery.iAuto.selectedItem ++;
+					if (jQuery.iAuto.selectedItem == jQuery.iAuto.items.size())
+						jQuery.iAuto.selectedItem = 0;
+					break;
+			}
+			jQuery.iAuto.applyOn(this,this.autoCFG.lastSuggestion.get(jQuery.iAuto.selectedItem||0), 'onHighlight');
+			jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).className = this.autoCFG.selectClass;
+			if (jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).scrollIntoView)
+				jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).scrollIntoView(false);
+			if(this.autoCFG.autofill) {
+				var valToAdd = jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).getAttribute('rel');
+				this.value = subject.pre + valToAdd + this.autoCFG.multipleSeparator + subject.post;
+				if(jQuery.iAuto.lastValue.length != valToAdd.length)
+					jQuery.iAuto.selection(
+						this, 
+						subject.pre.length + jQuery.iAuto.lastValue.length, 
+						subject.pre.length + valToAdd.length
+					);
+			}
+			return false;
+		}
+		jQuery.iAuto.checkCache.apply(this);
+		
+		if (this.autoCFG.inCache == false) {
+			if (subject.item != jQuery.iAuto.lastValue && subject.item.length >= this.autoCFG.minchars)
+				jQuery.iAuto.timer = window.setTimeout(jQuery.iAuto.update, this.autoCFG.delay);
+			if (jQuery.iAuto.items) {
+				jQuery.iAuto.clear();
+			}
+		}
+		return true;
+	},
+
+	applyOn: function(field, item, type)
+	{
+		if (field.autoCFG[type]) {
+			var data = {};
+			childs = item.getElementsByTagName('*');
+			for(i=0; i<childs.length; i++){
+				data[childs[i].tagName] = childs[i].firstChild.nodeValue;
+			}
+			field.autoCFG[type].apply(field,[data]);
+		}
+	},
+	
+	hoverItem : function(e)
+	{
+		if (jQuery.iAuto.items) {
+			if (jQuery.iAuto.selectedItem != null) 
+				jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).className = '';
+			jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).className = '';
+			jQuery.iAuto.selectedItem = parseInt(this.getAttribute('dir'))||0;
+			jQuery.iAuto.items.get(jQuery.iAuto.selectedItem||0).className = jQuery.iAuto.subject.autoCFG.selectClass;
+		}
+	},
+
+	clickItem : function(event)
+	{	
+		window.clearTimeout(jQuery.iAuto.timer);
+		
+		event = event || jQuery.event.fix( window.event );
+		event.preventDefault();
+		event.stopPropagation();
+		var subject = jQuery.iAuto.getFieldValues(jQuery.iAuto.subject);
+		var valueToAdd = this.getAttribute('rel');
+		jQuery.iAuto.subject.value = subject.pre + valueToAdd + jQuery.iAuto.subject.autoCFG.multipleSeparator + subject.post;
+		jQuery.iAuto.lastValue = this.getAttribute('rel');
+		jQuery.iAuto.selection(
+			jQuery.iAuto.subject, 
+			subject.pre.length + valueToAdd.length + jQuery.iAuto.subject.autoCFG.multipleSeparator.length, 
+			subject.pre.length + valueToAdd.length + jQuery.iAuto.subject.autoCFG.multipleSeparator.length
+		);
+		jQuery.iAuto.clear();
+		if (jQuery.iAuto.subject.autoCFG.onSelect) {
+			iteration = parseInt(this.getAttribute('dir'))||0;
+			jQuery.iAuto.applyOn(jQuery.iAuto.subject,jQuery.iAuto.subject.autoCFG.lastSuggestion.get(iteration), 'onSelect');
+		}
+
+		return false;
+	},
+
+	protect : function(e)
+	{
+		pressedKey = e.charCode || e.keyCode || -1;
+		if (/13|27|35|36|38|40/.test(pressedKey) && jQuery.iAuto.items) {
+			if (window.event) {
+				window.event.cancelBubble = true;
+				window.event.returnValue = false;
+			} else {
+				e.preventDefault();
+				e.stopPropagation();
+			}
+			return false;
+		}
+	},
+
+	build : function(options)
+	{
+		if (!options.source || !jQuery.iUtil) {
+			return;
+		}
+
+		if (!jQuery.iAuto.helper) {
+			if (jQuery.browser.msie) {
+				jQuery('body', document).append('<iframe style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" id="autocompleteIframe" src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+				jQuery.iAuto.iframe = jQuery('#autocompleteIframe');
+			}
+			jQuery('body', document).append('<div id="autocompleteHelper" style="position: absolute; top: 0; left: 0; z-index: 30001; display: none;"><ul style="margin: 0;padding: 0; list-style: none; z-index: 30002;">&nbsp;</ul></div>');
+			jQuery.iAuto.helper = jQuery('#autocompleteHelper');
+			jQuery.iAuto.content = jQuery('ul', jQuery.iAuto.helper);
+		}
+
+		return this.each(
+			function()
+			{
+				if (this.tagName != 'INPUT' && this.getAttribute('type') != 'text' )
+					return;
+				this.autoCFG = {};
+				this.autoCFG.source = options.source;
+				this.autoCFG.minchars = Math.abs(parseInt(options.minchars)||1);
+				this.autoCFG.helperClass = options.helperClass ? options.helperClass : '';
+				this.autoCFG.selectClass = options.selectClass ? options.selectClass : '';
+				this.autoCFG.onSelect = options.onSelect && options.onSelect.constructor == Function ? options.onSelect : null;
+				this.autoCFG.onShow = options.onShow && options.onShow.constructor == Function ? options.onShow : null;
+				this.autoCFG.onHide = options.onHide && options.onHide.constructor == Function ? options.onHide : null;
+				this.autoCFG.onHighlight = options.onHighlight && options.onHighlight.constructor == Function ? options.onHighlight : null;
+				this.autoCFG.inputWidth = options.inputWidth||false;
+				this.autoCFG.multiple = options.multiple||false;
+				this.autoCFG.multipleSeparator = this.autoCFG.multiple ? (options.multipleSeparator||', '):'';
+				this.autoCFG.autofill = options.autofill ? true : false;
+				this.autoCFG.delay = Math.abs(parseInt(options.delay)||1000);
+				if (options.fx && options.fx.constructor == Object) {
+					if (!options.fx.type || !/fade|slide|blind/.test(options.fx.type)) {
+						options.fx.type = 'slide';
+					}
+					if (options.fx.type == 'slide' && !jQuery.fx.slide)
+						return;
+					if (options.fx.type == 'blind' && !jQuery.fx.BlindDirection)
+						return;
+
+					options.fx.duration = Math.abs(parseInt(options.fx.duration)||400);
+					if (options.fx.duration > this.autoCFG.delay) {
+						options.fx.duration = this.autoCFG.delay - 100;
+					}
+					this.autoCFG.fx = options.fx;
+				}
+				this.autoCFG.lastSuggestion = null;
+				this.autoCFG.inCache = false;
+
+				jQuery(this)
+					.attr('autocomplete', 'off')
+					.focus(
+						function()
+						{
+							jQuery.iAuto.subject = this;
+							jQuery.iAuto.lastValue = this.value;
+						}
+					)
+					.keypress(jQuery.iAuto.protect)
+					.keyup(jQuery.iAuto.autocomplete)
+					
+					.blur(
+						function()
+						{
+							jQuery.iAuto.timer = window.setTimeout(jQuery.iAuto.clear, 200);
+						}
+					);
+			}
+		);
+	}
+};
+jQuery.fn.Autocomplete = jQuery.iAuto.build;
\ No newline at end of file

Added: jifty/trunk/share/web/static/js/iutil.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/web/static/js/iutil.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,245 @@
+/**
+ * Interface Elements for jQuery
+ * utility function
+ *
+ * http://interface.eyecon.ro
+ *
+ * Copyright (c) 2006 Stefan Petre
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ *
+ */
+
+jQuery.iUtil = {
+	getPosition : function(e)
+	{
+		var x = 0;
+		var y = 0;
+		var es = e.style;
+		var restoreStyles = false;
+		if (jQuery(e).css('display') == 'none') {
+			var oldVisibility = es.visibility;
+			var oldPosition = es.position;
+			restoreStyles = true;
+			es.visibility = 'hidden';
+			es.display = 'block';
+			es.position = 'absolute';
+		}
+		var el = e;
+		while (el){
+			x += el.offsetLeft + (el.currentStyle && !jQuery.browser.opera ?parseInt(el.currentStyle.borderLeftWidth)||0:0);
+			y += el.offsetTop + (el.currentStyle && !jQuery.browser.opera ?parseInt(el.currentStyle.borderTopWidth)||0:0);
+			el = el.offsetParent;
+		}
+		el = e;
+		while (el && el.tagName  && el.tagName.toLowerCase() != 'body')
+		{
+			x -= el.scrollLeft||0;
+			y -= el.scrollTop||0;
+			el = el.parentNode;
+		}
+		if (restoreStyles == true) {
+			es.display = 'none';
+			es.position = oldPosition;
+			es.visibility = oldVisibility;
+		}
+		return {x:x, y:y};
+	},
+	getPositionLite : function(el)
+	{
+		var x = 0, y = 0;
+		while(el) {
+			x += el.offsetLeft || 0;
+			y += el.offsetTop || 0;
+			el = el.offsetParent;
+		}
+		return {x:x, y:y};
+	},
+	getSize : function(e)
+	{
+		var w = jQuery.css(e,'width');
+		var h = jQuery.css(e,'height');
+		var wb = 0;
+		var hb = 0;
+		var es = e.style;
+		if (jQuery(e).css('display') != 'none') {
+			wb = e.offsetWidth;
+			hb = e.offsetHeight;
+		} else {
+			var oldVisibility = es.visibility;
+			var oldPosition = es.position;
+			es.visibility = 'hidden';
+			es.display = 'block';
+			es.position = 'absolute';
+			wb = e.offsetWidth;
+			hb = e.offsetHeight;
+			es.display = 'none';
+			es.position = oldPosition;
+			es.visibility = oldVisibility;
+		}
+		return {w:w, h:h, wb:wb, hb:hb};
+	},
+	getSizeLite : function(el)
+	{
+		return {
+			wb:el.offsetWidth||0,
+			hb:el.offsetHeight||0
+		};
+	},
+	getClient : function(e)
+	{
+		var h, w, de;
+		if (e) {
+			w = e.clientWidth;
+			h = e.clientHeight;
+		} else {
+			de = document.documentElement;
+			w = window.innerWidth || self.innerWidth || (de&&de.clientWidth) || document.body.clientWidth;
+			h = window.innerHeight || self.innerHeight || (de&&de.clientHeight) || document.body.clientHeight;
+		}
+		return {w:w,h:h};
+	},
+	getScroll : function (e)
+	{
+		var t=0, l=0, w=0, h=0, iw=0, ih=0;
+		if (e && e.nodeName.toLowerCase() != 'body') {
+			t = e.scrollTop;
+			l = e.scrollLeft;
+			w = e.scrollWidth;
+			h = e.scrollHeight;
+			iw = 0;
+			ih = 0;
+		} else  {
+			if (document.documentElement) {
+				t = document.documentElement.scrollTop;
+				l = document.documentElement.scrollLeft;
+				w = document.documentElement.scrollWidth;
+				h = document.documentElement.scrollHeight;
+			} else if (document.body) {
+				t = document.body.scrollTop;
+				l = document.body.scrollLeft;
+				w = document.body.scrollWidth;
+				h = document.body.scrollHeight;
+			}
+			iw = self.innerWidth||document.documentElement.clientWidth||document.body.clientWidth||0;
+			ih = self.innerHeight||document.documentElement.clientHeight||document.body.clientHeight||0;
+		}
+		return { t: t, l: l, w: w, h: h, iw: iw, ih: ih };
+	},
+	getMargins : function(e, toInteger)
+	{
+		var el = jQuery(e);
+		var t = el.css('marginTop') || '';
+		var r = el.css('marginRight') || '';
+		var b = el.css('marginBottom') || '';
+		var l = el.css('marginLeft') || '';
+		if (toInteger)
+			return {
+				t: parseInt(t)||0,
+				r: parseInt(r)||0,
+				b: parseInt(b)||0,
+				l: parseInt(l)
+			};
+		else
+			return {t: t, r: r,	b: b, l: l};
+	},
+	getPadding : function(e, toInteger)
+	{
+		var el = jQuery(e);
+		var t = el.css('paddingTop') || '';
+		var r = el.css('paddingRight') || '';
+		var b = el.css('paddingBottom') || '';
+		var l = el.css('paddingLeft') || '';
+		if (toInteger)
+			return {
+				t: parseInt(t)||0,
+				r: parseInt(r)||0,
+				b: parseInt(b)||0,
+				l: parseInt(l)
+			};
+		else
+			return {t: t, r: r,	b: b, l: l};
+	},
+	getBorder : function(e, toInteger)
+	{
+		var el = jQuery(e);
+		var t = el.css('borderTopWidth') || '';
+		var r = el.css('borderRightWidth') || '';
+		var b = el.css('borderBottomWidth') || '';
+		var l = el.css('borderLeftWidth') || '';
+		if (toInteger)
+			return {
+				t: parseInt(t)||0,
+				r: parseInt(r)||0,
+				b: parseInt(b)||0,
+				l: parseInt(l)||0
+			};
+		else
+			return {t: t, r: r,	b: b, l: l};
+	},
+	getPointer : function(event)
+	{
+		var x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)) || 0;
+		var y = event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)) || 0;
+		return {x:x, y:y};
+	},
+	traverseDOM : function(nodeEl, func)
+	{
+		func(nodeEl);
+		nodeEl = nodeEl.firstChild;
+		while(nodeEl){
+			jQuery.iUtil.traverseDOM(nodeEl, func);
+			nodeEl = nodeEl.nextSibling;
+		}
+	},
+	purgeEvents : function(nodeEl)
+	{
+		jQuery.iUtil.traverseDOM(
+			nodeEl,
+			function(el)
+			{
+				for(var attr in el){
+					if(typeof el[attr] === 'function') {
+						el[attr] = null;
+					}
+				}
+			}
+		);
+	},
+	centerEl : function(el, axis)
+	{
+		var clientScroll = jQuery.iUtil.getScroll();
+		var windowSize = jQuery.iUtil.getSize(el);
+		if (!axis || axis == 'vertically')
+			jQuery(el).css(
+				{
+					top: clientScroll.t + ((Math.max(clientScroll.h,clientScroll.ih) - clientScroll.t - windowSize.hb)/2) + 'px'
+				}
+			);
+		if (!axis || axis == 'horizontally')
+			jQuery(el).css(
+				{
+					left:	clientScroll.l + ((Math.max(clientScroll.w,clientScroll.iw) - clientScroll.l - windowSize.wb)/2) + 'px'
+				}
+			);
+	},
+	fixPNG : function (el, emptyGIF) {
+		var images = jQuery('img[@src*="png"]', el||document), png;
+		images.each( function() {
+			png = this.src;				
+			this.src = emptyGIF;
+			this.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + png + "')";
+		});
+	}
+};
+
+// Helper function to support older browsers!
+[].indexOf || (Array.prototype.indexOf = function(v, n){
+	n = (n == null) ? 0 : n;
+	var m = this.length;
+	for (var i=n; i<m; i++)
+		if (this[i] == v)
+			return i;
+	return -1;
+});

Modified: jifty/trunk/share/web/static/js/jifty.js
==============================================================================
--- jifty/trunk/share/web/static/js/jifty.js	(original)
+++ jifty/trunk/share/web/static/js/jifty.js	Wed Apr  9 00:12:34 2008
@@ -1,14 +1,38 @@
 /* An empty class so we can create things inside it */
-var Jifty = Class.create();
+var Jifty = {};
 
-Jifty.Web = Class.create();
-Jifty.Web.current_actions = new Array;
+Jifty.$ = function(id) {
+    if (typeof id == 'string')
+        return document.getElementById(id)
+    return id;
+}
+
+Jifty.hasAjaxTransport = function() {
+    var r = false;
+    jQuery.each(
+        [
+            function() {return new XMLHttpRequest()},
+            function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+            function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+        ],
+        function(i, v) {
+            try {
+                r = v();
+                if (r) return false;
+            } catch(e) {}
+        })
+    return r ? true : false;
+}();
+
+Jifty.Web = {};
+Jifty.Web.current_actions = [];
 Jifty.Web.new_action = function() {
     var args = _get_named_args(arguments);
     var a;
-    Jifty.Web.current_actions.each(function(x) { if (x.moniker == args.moniker) a = x });
+    jQuery(Jifty.Web.current_actions).each(function(i, v) {
+        if (v.moniker == args.moniker) a = v;
+    });
     if (!a) throw "hate";
-    
     return a;
 };
 
@@ -93,7 +117,11 @@
 }
 
 /* Actions */
-var Action = Class.create();
+var Action = function() {
+    this.initialize.apply(this, arguments);
+    return this;
+};
+
 Action.prototype = {
     // New takes the moniker (a string), and an optional array of form
     // elements to additionally take into consideration
@@ -101,20 +129,23 @@
         this.moniker = moniker;
 
         // Extra form parameters
-        this.extras = $A();
+        this.extras = [];
         if (arguments.length > 1) {
             this.extras = arguments[1];
         }
 
-        this.register = $('J:A-' + this.moniker);  // Simple case -- no ordering information
+        this.register = Jifty.$('J:A-' + this.moniker); // Simple case -- no ordering information
         if (! this.register) {
+            var elements = [];
             // We need to go looking -- this also goes looking through this.extras, from above
-            var elements = $A(document.getElementsByTagName('input'));
-            for (var i = 0; i < this.extras.length; i++)
-                elements.push(this.extras[i]);
-            for (var i = 0; i < elements.length; i++) {
-                if ((Form.Element.getMoniker(elements[i]) == this.moniker)
-                 && (Form.Element.getType(elements[i]) == "registration")) {
+
+            var add_to_elements = function(){ elements.push(this) };
+            jQuery('input').each(add_to_elements);
+            jQuery.each(this.extras, add_to_elements);
+
+            for (var i = 0, l = elements.length; i < l; i++) {
+                if ((Jifty.Form.Element.getMoniker(elements[i]) == this.moniker)
+                    && (Jifty.Form.Element.getType(elements[i]) == "registration")) {
                     this.register = elements[i];
                     break;
                 }
@@ -122,7 +153,7 @@
         }
 
         if (this.register) {
-            this.form = Form.Element.getForm(this.register);
+            this.form = Jifty.Form.Element.getForm(this.register);
             this.actionClass = this.register.value;
         }
     },
@@ -130,14 +161,14 @@
     // Returns an Array of all fields in this Action
     fields: function() {
         if(!this.cached_fields) {
-            var elements = new Array;
-            var possible = Form.getElements(this.form);
+            var elements = [];
+            var possible = Jifty.Form.getElements(this.form);
             // Also pull from extra query parameters
-            for (var i = 0; i < this.extras.length; i++)
+            for (var i = 0, l = this.extras.length; i < l; i++)
                 possible.push(this.extras[i]);
 
-            for (var i = 0; i < possible.length; i++) {
-                if (Form.Element.getMoniker(possible[i]) == this.moniker)
+            for (var i = 0, l = possible.length; i < l; i++) {
+                if (Jifty.Form.Element.getMoniker(possible[i]) == this.moniker)
                     elements.push(possible[i]);
             }
             this.cached_fields = elements;
@@ -147,10 +178,10 @@
 
     buttons: function() {
         var elements = new Array();
-        var possible = Form.getElements(this.form);
+        var possible = Jifty.Form.getElements(this.form);
         for(var i = 0; i < possible.length; i++) {
             if(possible[i].nodeName == 'INPUT' && possible[i].getAttribute("type") == 'submit') {
-                actions = Form.Element.buttonActions(possible[i]);
+                actions = Jifty.Form.Element.buttonActions(possible[i]);
                 //If the button has no actions explicitly associated
                 //with it, it's associated with all the actions in the
                 //form
@@ -166,7 +197,7 @@
     getField: function(name) {
         var elements = this.fields();
         for (var i = 0; i < elements.length; i++) {
-            if (Form.Element.getField(elements[i]) == name)
+            if (Jifty.Form.Element.getField(elements[i]) == name)
                 return elements[i];
         }
         return null;
@@ -177,16 +208,17 @@
         var fields = this.fields();
         var serialized = new Array;
 
-        for (var i = 0; i < fields.length; i++) {
-            serialized.push(Form.Element.serialize(fields[i]));
-        }
+        jQuery.each(fields, function() {
+            serialized.push( jQuery(this).serialize() )
+        });
+
         return serialized.join('&');
     },
 
     // Returns true if there is a file upload form as one of our elements
     hasUpload: function() {
         var fields = this.fields();
-        for (var i = 0; i < fields.length; i++) {
+        for (var i = 0, l = fields.length; i < l; i++) {
             if ((fields[i].getAttribute("type") == "file") && fields[i].value)
                 return true;
         }
@@ -204,21 +236,22 @@
         for (var i = 0; i < fields.length; i++) {
             var f = fields[i];
 
-            if (   (Form.Element.getType(f) != "registration")
-                && (Form.Element.getValue(f) != null)
+            if (   (Jifty.Form.Element.getType(f) != "registration")
+                && (Jifty.Form.Element.getValue(f) != null)
                 && (!Jifty.Placeholder.hasPlaceholder(f)))
             {
-                if (! a['fields'][Form.Element.getField(f)])
-                    a['fields'][Form.Element.getField(f)] = {};
-                var field = Form.Element.getField(f);
-                var type = Form.Element.getType(f);
+                if (! a['fields'][Jifty.Form.Element.getField(f)])
+                    a['fields'][Jifty.Form.Element.getField(f)] = {};
+                var field = Jifty.Form.Element.getField(f);
+                var type = Jifty.Form.Element.getType(f);
 
                 // XXX: fallback value being an array makes server
                 // upset, we don't think that should happen anyway
                 if (type == 'fallback' && a['fields'][field][type])
-                    continue                    
+                    continue
                 a['fields'][field][type] = this._mergeValues(a['fields'][field][type],
-                                                             Form.Element.getValue(f));
+                                                             Jifty.Form.Element.getValue(f));
+
             }
         }
 
@@ -241,80 +274,75 @@
         show_wait_message();
         var id = this.register.id;
 
-        new Ajax.Request(
-            '/__jifty/validator.xml',  // Right now, the URL is actually completely irrelevant
-            {
-                asynchronous: 1,
-                method: "get",
-                parameters: this.serialize() + "&J:VALIDATE=1",
-                onComplete:
-                    function (request) {
-                        var response  = request.responseXML.documentElement;
-                        for (var action = response.firstChild; action != null; action = action.nextSibling) {
-                            if ((action.nodeName == 'validationaction') && (action.getAttribute("id") == id)) {
-                                for (var field = action.firstChild; field != null; field = field.nextSibling) {
-                                    // Possibilities for field.nodeName: it could be #text (whitespace),
-                                    // or 'blank' (the field was blank, don't mess with the error div), or 'ok'
-                                    // (clear the error and warning div!) or 'error' (fill in the error div, clear 
-                                    // the warning div!) or 'warning' (fill in the warning div and clear the error div!)
-                                    if (field.nodeName == 'error' || field.nodeName == 'warning') {
-                                        var err_div = document.getElementById(field.getAttribute("id"));
-                                        if (err_div != null) {
-                                            Element.show(err_div);
-                                            err_div.innerHTML = field.firstChild.data;
-                                        }
-                                    } else if (field.nodeName == 'ok') {
-                                        var err_div = document.getElementById(field.getAttribute("id"));
-                                        if (err_div != null) {
-                                            Element.hide(err_div);
-                                            err_div.innerHTML = '';
-                                        }
-                                    }
+        jQuery.ajax({
+            url: '/__jifty/validator.xml',  // Right now, the URL is actually completely irrelevant
+            type: "get",
+            data: this.serialize() + "&J:VALIDATE=1",
+            complete: function (request, status) {
+                var response  = request.responseXML.documentElement;
+                for (var action = response.firstChild; action != null; action = action.nextSibling) {
+                    if ((action.nodeName == 'validationaction') && (action.getAttribute("id") == id)) {
+                        for (var field = action.firstChild; field != null; field = field.nextSibling) {
+                            // Possibilities for field.nodeName: it could be #text (whitespace),
+                            // or 'blank' (the field was blank, don't mess with the error div), or 'ok'
+                            // (clear the error and warning div!) or 'error' (fill in the error div, clear
+                            // the warning div!) or 'warning' (fill in the warning div and clear the error div!)
+                            if (field.nodeName == 'error' || field.nodeName == 'warning') {
+                                var err_div = document.getElementById(field.getAttribute("id"));
+                                if (err_div != null) {
+                                    jQuery(err_div).show().html(field.firstChild.data);
                                 }
-                            } else if ((action.nodeName == 'canonicalizeaction') && (action.getAttribute("id") == id)) {
-                                for (var field = action.firstChild; field != null; field = field.nextSibling) {
-                                    // Possibilities for field.nodeName: it could be 'ignored', 'blank' , 'update', or 'info'
-                                    // info is a separate action from the update
-                                    if (field.nodeName == 'canonicalization_note')  {
-                                        var note_div= document.getElementById(field.getAttribute("id"));
-                                        if (note_div != null) {
-                                            Element.show(note_div);
-                                            note_div.innerHTML = field.firstChild.data;
-                                        }
-                                    }
-
-                                    if (field.nodeName == 'update') {
-                                        var field_name = field.getAttribute("name");
-                                        for (var form_number = 0 ; form_number < document.forms.length; form_number++) {
-                                            var form_field = document.forms[form_number].elements[field_name];
-                                            if (form_field == null || !form_field.hasClassName('ajaxcanonicalization'))
-                                                continue;
-                                            form_field.value = field.firstChild.data;
-                                        }
-                                    }
+                            } else if (field.nodeName == 'ok') {
+                                var err_div = document.getElementById(field.getAttribute("id"));
+                                if (err_div != null) {
+                                    jQuery(err_div).hide().html('');
+                                }
+                            }
+                        }
+                    } else if ((action.nodeName == 'canonicalizeaction') && (action.getAttribute("id") == id)) {
+                        for (var field = action.firstChild; field != null; field = field.nextSibling) {
+                            // Possibilities for field.nodeName: it could be 'ignored', 'blank' , 'update', or 'info'
+                            // info is a separate action from the update
+                            if (field.nodeName == 'canonicalization_note')  {
+                                var note_div= document.getElementById(field.getAttribute("id"));
+                                if (note_div != null) {
+                                    jQuery(note_div).show().html(field.firstChild.data);
+                                }
+                            }
+
+                            if (field.nodeName == 'update') {
+                                var field_name = field.getAttribute("name");
+                                for (var form_number = 0 ; form_number < document.forms.length; form_number++) {
+                                    var form_field = document.forms[form_number].elements[field_name];
+                                    if (form_field  == null || !jQuery(form_field).is('.ajaxcanonicalization'))
+                                        continue;
+                                    form_field.value = field.firstChild.data;
                                 }
                             }
                         }
-                        return true;
                     }
-            }
-        ); 
+                }
+                return true;
+            }            
+        });
+        
         hide_wait_message();
         return false;
     },
 
     submit: function() {
         show_wait_message();
-        new Ajax.Request(
-            '/empty',
-            { parameters: this.serialize() }
-        );
+        jQuery.ajax({
+            url: '/empty',
+            type: 'post',
+            data: this.serialize()
+        });
         hide_wait_message();
     },
 
     disable_input_fields: function(disabled_elements) {
         var disable = function() {
-            var elt = arguments[0];
+            var elt = this;
             // Disabling hidden elements seems to  make IE sad for some reason
             if(elt.type != 'hidden') {
                 // Triggers https://bugzilla.mozilla.org/show_bug.cgi?id=236791
@@ -323,14 +351,14 @@
                 disabled_elements.push(elt);
             }
         };
-        this.fields().each(disable);
-        this.buttons().each(disable);
+        jQuery.each(this.fields(), disable);
+        jQuery.each(this.buttons(), disable);
     },
 
     enable_input_fields: function() {
-        var enable = function() { arguments[0].disabled = false; };
-        this.fields().each( enable );
-        this.buttons().each( enable );
+        var enable = function() { this.disabled = false; };
+        jQuery.each(this.fields(), disable);
+        jQuery.each(this.buttons(), disable);
     },
 
 
@@ -339,9 +367,9 @@
         if (!this.s_a) {
             /* XXX: make REST client accessible */
             var Todo = new AsynapseRecord('todo');
-            this.s_a = $H(Todo.eval_ajax_get('/=/action/'+this.actionClass+'.js'));
+            this.s_a = jQuery.extend({}, Todo.eval_ajax_get('/=/action/'+this.actionClass+'.js'));
         }
-        
+
         return this.s_a
     },
     argument_names: function() {
@@ -360,7 +388,11 @@
 
 var SERIAL_postfix = Math.ceil(10000*Math.random());
 var SERIAL = 0;
-ActionField = Class.create();
+var ActionField = function() {
+    this.initialize.apply(this, arguments);
+    return this;
+};
+
 ActionField.prototype = {
  initialize: function(name, args, action) {
         this.name = name;
@@ -472,58 +504,46 @@
 };
 
 /* Forms */
-Jifty.Form = Class.create();
 
-Object.extend(Jifty.Form, {
+Jifty.Form = {};
+
+jQuery.extend(Jifty.Form, {
     getElements: function(element) {
-        return Form.getElements(element);
+        return jQuery(":input", element).get();
     },
+
     // Return an Array of Actions that are in this form
     getActions: function (element) {
-        var elements = new Array;
-        var possible = Form.getElements(element);
+        var elements = [];
+
+        jQuery(":input", element).each(function() {
+            if (Jifty.Form.Element.getType(this) == "registration")
+                elements.push(Jifty.Form.Element.getAction(this));
+        });
 
-        for (var i = 0; i < possible.length; i++) {
-            if (Jifty.Form.Element.getType(possible[i]) == "registration")
-                elements.push(Jifty.Form.Element.getAction(possible[i]));
-        }
-        
         return elements;
     },
 
     clearPlaceholders: function(element) {
-        var elements = Form.getElements(element);
+        var elements = Jifty.Form.getElements(element);
         for(var i = 0; i < elements.length; i++) {
             Jifty.Placeholder.clearPlaceholder(elements[i]);
         }
     }
 });
 
-Object.extend(Form, {
-    // Return an Array of Actions that are in this form
-    getActions: function (element) {
-        // DEPRECATED: use Jifty.Form.getActions instead
-        return Jifty.Form.getActions(element);
-    },
-
-    clearPlaceholders: function(element) {
-        // DEPRECATED: use Jifty.Form.clearPlaceholders instead
-        return Jifty.Form.clearPlaceholders(element);
-    }
-});
-
+var current_actions = {};
 
-var current_actions = $H();
+Jifty.Form.Element = {};
 
 /* Fields */
-Jifty.Form.Element = Class.create();
 
-Object.extend(Jifty.Form.Element, {
+jQuery.extend(Jifty.Form.Element, {
     // Get the moniker for this form element
     // Takes an element or an element id
     getMoniker: function (element) {
-        element = $(element);    
-
+        element = Jifty.$(element);
+        
         if (/^J:A(:F)+-[^-]+-.+$/.test(element.name)) {
             var bits = element.name.match(/^J:A(?::F)+-[^-]+-(.+)$/);
             return bits[1];
@@ -538,16 +558,16 @@
     // Get the Action for this form element
     // Takes an element or an element id
     getAction: function (element) {
-        element = $(element);    
+        element = Jifty.$(element);
         var moniker = Jifty.Form.Element.getMoniker(element);
-        if (!current_actions.get(moniker))
-            current_actions.set(moniker, new Action(moniker));
-        return current_actions.get(moniker);
+        if (!current_actions[moniker])
+            current_actions[moniker] = new Action(moniker);
+        return current_actions[moniker];
     },
 
     // Returns the name of the field
     getField: function (element) {
-        element = $(element);    
+        element = Jifty.$(element);
 
         if (/^J:A(:F)+-[^-]+-.+$/.test(element.name)) {
             var bits = element.name.match(/^J:A(?::F)+-([^-]+)-.+$/);
@@ -559,8 +579,7 @@
 
     // The type of Jifty form element
     getType: function (element) {
-        element = $(element);
-
+        element = Jifty.$(element);
         if (/^J:A-/.test(element.name)) {
             return "registration";
         } else if (/^J:A:F-/.test(element.name)) {
@@ -572,22 +591,30 @@
         }
     },
 
+    getValue: function(element) {
+        var $el = jQuery(Jifty.$(element));
+        if ( $el.is(":checkbox, :radio") ) {
+            return $el.is(":checked") ? $el.val() : null
+        }
+        return $el.val();
+    },
+
     // Validates the action this form element is part of
     validate: function (element) {
-            if(!Element.hasClassName(element, 'validation_disabled')) {
-                Jifty.Form.Element.getAction(element).validate();
-            }
+        if ( !jQuery(element).is('.validation_disabled') ) {
+            Jifty.Form.Element.getAction(element).validate();
+        }
     },
 
     // Temporarily disable validation
-            disableValidation: function(element) {
-            Element.addClassName(element, 'validation_disabled');
-        },
+    disableValidation: function(element) {
+        jQuery(element).addClass('validation_disabled');
+    },
 
-            //Reenable validation            
-            enableValidation: function(element) {
-            Element.removeClassName(element, 'validation_disabled');
-        },
+    //Reenable validation
+    enableValidation: function(element) {
+        jQuery(element).removeClass('validation_disabled');
+    },
 
 
     // Look up the form that this element is part of -- this is sometimes
@@ -595,7 +622,7 @@
     // anymore, or the element may have been inserted into a new form.
     // Hence, we may need to walk the DOM.
     getForm: function (element) {
-        element = $(element);
+        element = Jifty.$(element);
 
         if (!element)
             return null;
@@ -610,61 +637,62 @@
             if (elt.nodeName == 'FORM') {
                 element.form = elt;
                 return elt;
-            } 
+            }
        }
         return null;
     },
 
     buttonArguments: function(element) {
-        element = $(element);
+        element = Jifty.$(element);
         if (!element)
-            return $H();
+            return {}
 
         if (((element.nodeName != 'INPUT') || (element.getAttribute("type") != "submit"))
          && ((element.nodeName != 'A')     || (! element.getAttribute("name"))))
-            return $H();
+            return {}
 
         if (element.getAttribute("name").length == 0)
-            return $H();
+            return {}
 
-        var extras = $H();
+        var extras = {}
 
         // Split other arguments out, if we're on a button
         var pairs = element.getAttribute("name").split("|");
         for (var i = 0; i < pairs.length; i++) {
             var bits = pairs[i].split('=',2);
-            extras.set(bits[0], bits[1]);
+            extras[ bits[0] ] = bits[1];
         }
         return extras;
     },
 
     buttonActions: function(element) {
-        element = $(element);
-        var actions = Jifty.Form.Element.buttonArguments(element).get('J:ACTIONS');
+        element = Jifty.$(element);
+        var actions = Jifty.Form.Element.buttonArguments(element)['J:ACTIONS'];
         if(actions) {
             return actions.split(",");
         } else {
             return new Array();
         }
-    },  
+    },
 
     buttonFormElements: function(element) {
-        element = $(element);
+        element = Jifty.$(element);
 
-        var extras = $A();
+        var extras = [];
         if (!element)
             return extras;
 
         var args = Jifty.Form.Element.buttonArguments(element);
-        var keys = args.keys();
-        for (var i = 0; i < keys.length; i++) {
+
+        jQuery.each(args, function(k, v) {
             var e = document.createElement("input");
             e.setAttribute("type", "hidden");
-            e.setAttribute("name", keys[i]);
-            e.setAttribute("value", args.get(keys[i]));
+            e.setAttribute("name", k);
+            e.setAttribute("value", v);
             e['virtualform'] = Jifty.Form.Element.getForm(element);
             extras.push(e);
-        }
+        });
+
         return extras;
     },
 
@@ -699,116 +727,29 @@
 
 });
 
-Object.extend(Form.Element, {
-    // Get the moniker for this form element
-    // Takes an element or an element id
-    getMoniker: function (element) {
-        // DEPRECATED: use Jifty.Form.Element.getMoniker instead
-        return Jifty.Form.Element.getMoniker(element);
-    },
-
-    // Get the Action for this form element
-    // Takes an element or an element id
-    getAction: function (element) {
-        // DEPRECATED: use Jifty.Form.Element.getAction instead
-        return Jifty.Form.Element.getAction(element);
-    },
-
-    // Returns the name of the field
-    getField: function (element) {
-        // DEPRECATED: use Jifty.Form.Element.getField instead
-        return Jifty.Form.Element.getField(element);
-    },
-
-    // The type of Jifty form element
-    getType: function (element) {
-        // DEPRECATED: use Jifty.Form.Element.getType instead
-        return Jifty.Form.Element.getType(element);
-    },
-
-    // Validates the action this form element is part of
-    validate: function (element) {
-        // DEPRECATED: use Jifty.Form.Element.validate instead
-        return Jifty.Form.Element.validate(element);
-    },
-
-    // Temporarily disable validation
-            disableValidation: function(element) {
-                // DEPRECATED: use Jifty.Form.Element.disableValidation instead
-                return Jifty.Form.Element.disableValidation(element);
-        },
-
-            //Reenable validation            
-            enableValidation: function(element) {
-                // DEPRECATED: use Jifty.Form.Element.enableValidation instead
-                return Jifty.Form.Element.enableValidation(element);
-        },
-
-
-    // Look up the form that this element is part of -- this is sometimes
-    // more complicated than you'd think because the form may not exist
-    // anymore, or the element may have been inserted into a new form.
-    // Hence, we may need to walk the DOM.
-    getForm: function (element) {
-        // DEPRECATED: use Jifty.Form.Element.getForm instead
-        return Jifty.Form.Element.getForm(element);
-    },
-
-    buttonArguments: function(element) {
-        // DEPRECATED: use Jifty.Form.Element.buttonArguments instead
-        return Jifty.Form.Element.buttonArguments(element);
-    },
-
-    buttonActions: function(element) {
-        // DEPRECATED: use Jifty.Form.Element.buttonActions instead
-        return Jifty.Form.Element.buttonActions(element);
-    },  
-
-    buttonFormElements: function(element) {
-        // DEPRECATED: use Jifty.Form.Element.buttonFormElements instead
-        return Jifty.Form.Element.buttonFormElements(element);
-    },
-
-    /* Someday Jifty may have the concept of "default"
-       buttons.  For now, this clicks the first button that will
-       submit the action associated with the form element.
-     */
-    clickDefaultButton: function(element) {
-        // DEPRECATED: use Jifty.Form.Element.clickDefaultButton instead
-        return Jifty.Form.Element.clickDefaultButton(element);
-    },
-
-    handleEnter: function(event) {
-        // DEPRECATED: use Jifty.Form.Element.handleEnter instead
-        return Jifty.Form.Element.handleEnter(event);
-    }
-
-});
-
-JSAN.use("DOM.Events");
-
-
 // Form elements should focus if the CSS says so.
-Behaviour.register( { ".focus": function(e) {
-    /* Check to see if the element is already focused */
-    if ( !Element.hasClassName(e, "focused") ) {
-        e.focus();
-        Element.addClassName(e, "focused");
+Behaviour.register({
+    ".focus": function(e) {
+        /* Check to see if the element is already focused */
+        if (!jQuery(e).hasClass("focused")) {
+            e.focus();
+            jQuery(e).addClass("focused")
+        }
     }
-    } });
+});
 
 
 // Form elements should AJAX validate if the CSS says so
 Behaviour.register({
     'input.ajaxvalidation, textarea.ajaxvalidation, input.ajaxcanonicalization, textarea.ajaxcanonicalization': function(elt) {
-        DOM.Events.addListener(elt, "blur", function () {
-                Form.Element.validate(elt);
-            });
+        jQuery(elt).bind('blur', function () {
+            Jifty.Form.Element.validate(elt);
+        });
     },
     'input.date': function(e) {
-        if ( !Element.hasClassName( e, 'has_calendar_link' ) ) {
+        if ( !jQuery(e).hasClass('has_calendar_link') ) {
             createCalendarLink(e);
-            Element.addClassName( e, 'has_calendar_link' );
+            jQuery(e).addClass('has_calendar_link');
         }
     },
     'input.button_as_link': function(e) {
@@ -819,100 +760,101 @@
            when the autocomplete is active so we can use it on autocompleted
            fields
          */
-        if (   !Element.hasClassName( e, "jifty_enter_handler_attached" )
-            && !Element.hasClassName( e, "ajaxautocompletes" ) )
+        if (   !jQuery(e).hasClass("jifty_enter_handler_attached" )
+               && !jQuery(e).hasClass("ajaxautocompletes" ) )
         {
             /* Do not use keydown as the event, it will not work as expected in Safari */
-            DOM.Events.addListener( e, "keypress", Form.Element.handleEnter );
-            Element.addClassName( e, "jifty_enter_handler_attached" );
+            jQuery(e).bind('keypress', Jifty.Form.Element.handleEnter).addClass("jifty_enter_handler_attached");
         }
     },
     ".messages": function(e) {
-        if (   !Element.hasClassName( e, "jifty_enter_handler_attached" ) ) {
-            e.innerHTML= 
-              '<a  href="#" id="dismiss_'+e.id+'" title="Dismiss" onmousedown="this.onfocus=this.blur;" onmouseup="this.onfocus=window.clientInformation?null:window.undefined" onclick="Effect.Fade(this.parentNode); return false;">Dismiss</a>' + e.innerHTML;
+        if ( !jQuery(e).hasClass('jifty_enter_handler_attached') ) {
+            jQuery(e)
+            .prepend('<a  href="#" id="dismiss_'+e.id+'" title="Dismiss" onmousedown="this.onfocus=this.blur;" onmouseup="this.onfocus=window.clientInformation?null:window.undefined" onclick="Jifty.Effect(this.parentNode, \'Fade\'); return false;">Dismiss</a>')
+            .addClass("jifty_enter_handler_attached" );
 
-            Element.addClassName( e, "jifty_enter_handler_attached" );
         }
     },
     '.form_field .error, .form_field .warning, .form_field .canonicalization_note': function(e) {
-        if ( e.innerHTML == "" ) Element.hide(e);
+        if ( e.innerHTML == "" ) jQuery(e).hide();
     }
 });
 
 
 /* Regions */
 // Keep track of the fragments on the page
-var fragments = $H();
-var Region = Class.create();
+Jifty.fragments = {};
+
+// Todo: This "fragments" variable should be localized. External access should be restricted
+// to use "Jifty.fragments" instead.
+var fragments = Jifty.fragments;
+
+var Region = function() {
+    this.initialize.apply(this, arguments);
+    return this;
+};
+
 Region.prototype = {
     initialize: function(name, args, path, parent, in_form) {
         this.name = name;
-        this.args = $H(args);
+        this.args = jQuery.extend({}, args);
         this.path = path;
-        this.parent = parent ? fragments.get(parent) : null;
+        this.parent = parent ? fragments[parent] : null;
         this.in_form = in_form;
-        if (fragments.get(name)) {
+        if (fragments[name]) {
             // If this fragment already existed, we want to wipe out
             // whatever evil lies we might have said earlier; do this
             // by clearing out everything that looks relevant
-            var keys = current_args.keys();
-            for (var i = 0; i < keys.length; i++) {
-                var k = keys[i];
+            jQuery.each(current_args, function(k, v) {
                 var parsed = k.match(/^(.*?)\.(.*)/);
                 if ((parsed != null) && (parsed.length == 3) && (parsed[1] == this.name)) {
-                    current_args.unset(k);
-                }
-            }
+                    current_args[k] = null;
+                }                
+            });
         }
 
-        fragments.set(name, this);
+        fragments[name] = this;
     },
 
     setPath: function(supplied) {
+        var self = this;
+
         // Merge in from current_args
-        var keys = current_args.keys();
-        for (var i = 0; i < keys.length; i++) {
-            var k = keys[i];
-            if (k == this.name) {
-                this.path = current_args.get(k);
+        jQuery.each(current_args, function(k, v) {
+            if (k == self.name) {
+                self.path = v
             }
-        }
+        });
 
         // Update with supplied
         if (supplied != null) {
             this.path = supplied;
         }
-        
+
         // Propagate back to current args
-        current_args.set(this.name, this.path);
+        current_args[ this.name ] = this.path;
 
         // Return new value
         return this.path;
     },
 
     setArgs: function(supplied) {
-        supplied = $H(supplied);
+        var self = this;
         // Merge in current args
-        var keys = current_args.keys();
-        for (var i = 0; i < keys.length; i++) {
-            var k = keys[i];
+        jQuery.each(current_args, function(k, v) {
             var parsed = k.match(/^(.*?)\.(.*)/);
             if ((parsed != null) && (parsed.length == 3) && (parsed[1] == this.name)) {
-                this.args.set(parsed[2], current_args.get(k));
+                self.args[ parsed[2] ] = v
             }
-        }
-
+        });
         // Merge in any supplied parameters
-        this.args = this.args.merge(supplied);
+        jQuery.extend(this.args, supplied);
 
         // Fill supplied parameters into current args
-        keys = supplied.keys();
-        for (var i = 0; i < keys.length; i++) {
-            var k = keys[i];
-            current_args.set(this.name+'.'+k, supplied.get(k));
-        }
-        
+        jQuery.each(supplied, function(k, v) {
+            current_args[ self.name + '.' + k ] = v;
+        });
+
         // Return new values
         return this.args;
     },
@@ -942,7 +884,7 @@
 
 
 // Keep track of the state variables.
-var current_args = $H();
+var current_args = {};
 
 // Prepare element for use in update()
 //  - 'fragment' is a hash, see fragments in update()
@@ -951,13 +893,9 @@
         var name = f['region'];
 
         // Find where we are going to go
-        var element = $('region-' + f['region']);
+        var element = document.getElementById('region-' + f['region']);
         if (f['element']) {
-            var possible = cssQuery(f['element']);
-            if (possible.length == 0)
-                element = null;
-            else
-                element = possible[0];
+            element = jQuery(f['element'])[0];
         }
         f['element'] = element;
 
@@ -968,12 +906,12 @@
         // If we're removing the element, do it now
         // XXX TODO: Effects on this?
         if (f['mode'] == "Delete") {
-            fragments.set(name, null);
-            Element.remove(element);
+            fragments[name] = null;
+            jQuery(element).remove();
             return;
         }
 
-        f['is_new'] = (fragments.get(name) ? false : true);
+        f['is_new'] = (fragments[name] ? false : true);
         // If it's new, we need to create it so we can dump it
         if (f['is_new']) {
             // Find what region we're inside
@@ -991,18 +929,18 @@
             }
 
             // Make the region (for now)
-            new Region(name, f['args'], f['path'], f['parent'], f['parent'] ? fragments.get(f['parent']).in_form : null);
-        } else if ((f['path'] != null) && f['toggle'] && (f['path'] == fragments.get(name).path)) {
+            new Region(name, f['args'], f['path'], f['parent'], f['parent'] ? fragments[f['parent']].in_form : null);
+        } else if ((f['path'] != null) && f['toggle'] && (f['path'] == fragments[name].path)) {
             // If they set the 'toggle' flag, and clicking wouldn't change the path
-            Element.update(element, '');
-            fragments.get(name).path = null;
+            jQuery(element).empty();
+            fragments[name].path = null;
             return;
         } else if (f['path'] == null) {
             // If they didn't know the path, fill it in now
-            f['path'] == fragments.get(name).path;
+            f['path'] == fragments[name].path;
         }
 
-    return f;    
+    return f;
 }
 
 var CACHE = {};
@@ -1027,12 +965,12 @@
                 textContent = fragment_bit.textContent;
             } else if (fragment_bit.firstChild) {
                 textContent = fragment_bit.firstChild.nodeValue;
-            } 
+            }
             try {
                 var cache_func = eval(textContent);
                 CACHE[f['path']] = { 'type': c_type, 'content': cache_func };
             }
-            catch(e) { 
+            catch(e) {
                 alert(e);
                 alert(textContent);
             }
@@ -1045,63 +983,76 @@
 //   - f: fragment spec
 var apply_fragment_updates = function(fragment, f) {
     // We found the right fragment
-    var dom_fragment = fragments.get(f['region']);
-    var new_dom_args = $H();
+    var dom_fragment = fragments[ f['region'] ];
+    var new_dom_args = {};
 
     var element = f['element'];
-    walk_node(fragment,
-    { argument: function(fragment_bit) {
-            // First, update the fragment's arguments
-            // with what the server actually used --
-            // this is needed in case there was
-            // argument mapping going on
-            var textContent = '';
-            if (fragment_bit.textContent) {
-                textContent = fragment_bit.textContent;
-            } else if (fragment_bit.firstChild) {
-                textContent = fragment_bit.firstChild.nodeValue;
-            }
-            new_dom_args.set(fragment_bit.getAttribute("name"), textContent);
-        },
-      content: function(fragment_bit) {
-            var textContent = '';
-            if (fragment_bit.textContent) {
-                textContent = fragment_bit.textContent;
-            } else if (fragment_bit.firstChild) {
-                textContent = fragment_bit.firstChild.nodeValue;
-            }
-                    
-            // Once we find it, do the insertion
-            if (f['mode'] && (f['mode'] != 'Replace')) {
-                var insertion = eval('Insertion.'+f['mode']);
-                new insertion(element, textContent.stripScripts());
-                element = document.getElementById('region-' + f['region']);
-            } else {
-                Element.update(element, textContent.stripScripts());
+    walk_node(
+        fragment,
+        {
+            argument: function(fragment_bit) {
+                // First, update the fragment's arguments
+                // with what the server actually used --
+                // this is needed in case there was
+                // argument mapping going on
+                var textContent = '';
+                if (fragment_bit.textContent) {
+                    textContent = fragment_bit.textContent;
+                } else if (fragment_bit.firstChild) {
+                    textContent = fragment_bit.firstChild.nodeValue;
+                }
+                new_dom_args[ fragment_bit.getAttribute("name") ] = textContent;
+            },
+            content: function(fragment_bit) {
+                var textContent = '';
+                if (fragment_bit.textContent) {
+                    textContent = fragment_bit.textContent;
+                } else if (fragment_bit.firstChild) {
+                    textContent = fragment_bit.firstChild.nodeValue;
+                }
+
+                // Re-arrange all <script> tags to the end of textContent.
+                // This approach easily deal with the uncertain amount of
+                // time we need to wait before the region is ready for running
+                // some javascript.
+
+                var re = new RegExp('<script[^>]*>([\\S\\s]*?)<\/script>', 'img');
+                var scripts = (textContent.match(re) || []).join("");
+                var textContentWithoutScript = textContent.replace(re, '');
+                textContent = textContentWithoutScript + scripts;
+
+                // Once we find it, do the insertion
+                if (f['mode'] && (f['mode'] != 'Replace')) {
+                    var method = ({
+                        After: 'after',
+                        Before: 'before',
+                        Bottom: 'append',
+                        Top: 'prepend'
+                    })[ f['mode'] ];
+
+                    (jQuery(element)[method])( textContent );
+                } else {
+                    jQuery(element).html( textContent );
+                }
+                Behaviour.apply(element);
             }
-            // We need to give the browser some "settle" time before
-            // we eval scripts in the body
-            YAHOO.util.Event.onAvailable(element.id, function() {
-                (function() { this.evalScripts() }).bind(textContent)();
-            });
-            Behaviour.apply(element);
         }
-    });
+    );
     dom_fragment.setArgs(new_dom_args);
 
     // Also, set us up the effect
     if (f['effect']) {
-        try {
-            var effect = eval('Effect.'+f['effect']);
-            var effect_args  = f['effect_args'] || {};
-            if (effect) {
-                if (f['is_new'])
-                    Element.hide($('region-'+f['region']));
-                (effect)($('region-'+f['region']), effect_args);
+        Jifty.Effect(
+            Jifty.$('region-'+f['region']),
+            f['effect'],
+            f['effect_args'],
+            {
+                before: function() {
+                    if(f['is_new']) 
+                        jQuery(this).hide();
+                }
             }
-        } catch ( e ) {
-            // Don't be sad if the effect doesn't exist
-        }
+        );
     }
 }
 
@@ -1114,11 +1065,11 @@
 //     - 'args' is a hash of arguments to override
 //     - 'path' is the path of the fragment (if this is a new fragment)
 //     - 'element' is the CSS selector of the element to update, if 'region' isn't supplied
-//     - 'mode' is one of 'Replace', or the name of a Prototype Insertion
+//     - 'mode' is one of 'Replace', 'Top', 'Bottom', 'Before', or 'After'
 //     - 'effect' is the name of a Prototype Effect
 Jifty.update = function () {
     // loads
-    if(!Ajax.getTransport()) return true;
+    if (!Jifty.hasAjaxTransport) return true;
     // XXX: prevent default behavior in IE
     if(window.event) {
         window.event.returnValue = false;
@@ -1128,44 +1079,52 @@
     var trigger    = arguments[1];
 
     // The YAML/JSON data structure that will be sent
-    var request = $H();
+    var request = {};
 
     // Keep track of disabled elements
-    var disabled_elements = $A();
+    var disabled_elements = [];
 
     // Set request base path
-    request.set('path', '/__jifty/webservices/xml');
+    request.path = '/__jifty/webservices/xml';
 
     // Grab extra arguments (from a button)
-    var button_args = Form.Element.buttonFormElements(trigger);
+    var button_args = Jifty.Form.Element.buttonFormElements(trigger);
 
-    var form = Form.Element.getForm(trigger);
+    var form = Jifty.Form.Element.getForm(trigger);
     // If the action is null, take all actions
     if (named_args['actions'] == null) {
         named_args['actions'] = {};
         // default to disable fields
         if (form)
-            Form.getActions(form).map(function(x){
+            Jifty.Form.getActions(form).map(function(x){
                 named_args['actions'][x.moniker] = 1;
             });
     }
     var optional_fragments;
-    if (form && form['J:CALL']) 
+    if (form && form['J:CALL'])
         optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': null}) ];
     // Build actions structure
     var has_request = 0;
-    request.set('actions', $H());
+    request['actions'] = {};
+
     for (var moniker in named_args['actions']) {
         if (moniker == 'extend')
             continue;
         var disable = named_args['actions'][moniker];
         var a = new Action(moniker, button_args);
-            current_actions.set(moniker, a); // XXX: how do i make this bloody singleton?
+        current_actions[moniker] = a;
         // Special case for Redirect, allow optional, implicit __page
         // from the response to be used.
-        if (a.actionClass == 'Jifty::Action::Redirect')
-            optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': a.fields().last().value}) ];
-        a.result = {}; a.result.field_error = {};
+        if (a.actionClass == 'Jifty::Action::Redirect') {
+            (function() {
+                var fields = a.fields();
+                var path = fields[fields.length - 1];
+                optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': path}) ];
+            })()
+        }
+        a.result = {};
+        a.result.field_error = {};
+
         if (a.register) {
             if (a.hasUpload())
                 return true;
@@ -1183,13 +1142,12 @@
                     fields[argname] = { value: override[argname] };
                 }
             }
-            request.get('actions').set(moniker, param);
+            request['actions'][moniker] = param;
             ++has_request;
         }
-
     }
 
-    request.set('fragments', $H());
+    request.fragments = {};
     var update_from_cache = new Array;
 
     // Build fragments structure
@@ -1204,7 +1162,7 @@
             var content_node = document.createElement('content');
             var cached_result;
 
-            Jifty.Web.current_region = fragments.get(f['region']);
+            Jifty.Web.current_region = fragments[ f['region'] ];
             try { cached_result = apply_cached_for_action(cached['content'], []) }
             catch (e) { alert(e) }
 
@@ -1224,9 +1182,9 @@
             my_fragment.setAttribute('id', f['region']);
             update_from_cache.push(function(){
                     var cached_result;
-                    Jifty.Web.current_region = fragments.get(f['region']);
+                    Jifty.Web.current_region = fragments[ f['region'] ];
                     try {
-                        cached_result = apply_cached_for_action(cached['content'], Form.getActions(form));
+                        cached_result = apply_cached_for_action(cached['content'], Jifty.Form.getActions(form));
                     }
                     catch (e) { alert(e); throw e }
                     content_node.textContent = cached_result;
@@ -1235,7 +1193,7 @@
             continue;
         }
         else if (cached && cached['type'] == 'crudview') {
-            try { // XXX: get model class etc as metadata in cache 
+            try { // XXX: get model class etc as metadata in cache
                 // XXX: kill dup code
             var Todo = new AsynapseRecord('todo');
             var record = Todo.find(f['args']['id']);
@@ -1251,17 +1209,17 @@
 
         // Update with all new values
         var name = f['region'];
-        var fragment_request = fragments.get(name).data_structure(f['path'], f['args']);
+        var fragment_request = fragments[ name ].data_structure(f['path'], f['args']);
 
         if (f['is_new'])
             // Ask for the wrapper if we are making a new region
             fragment_request['wrapper'] = 1;
 
-        if (fragments.get(name).in_form)
+        if (fragments[name].in_form)
             fragment_request['in_form'] = 1;
 
         // Push it onto the request stack
-        request.get('fragments').set(name, fragment_request);
+        request.fragments[name] = fragment_request;
         ++has_request;
     }
 
@@ -1274,9 +1232,9 @@
     show_wait_message();
 
     // And when we get the result back..
-    var onSuccess = function(transport, object) {
+    var onSuccess = function(responseXML, object) {
         // Grab the XML response
-        var response = transport.responseXML.documentElement;
+        var response = responseXML.documentElement;
 
         // Get action results
         walk_node(response,
@@ -1289,7 +1247,7 @@
                                       var text = error.textContent
                                           ? error.textContent
                                           : (error.firstChild ? error.firstChild.nodeValue : '');
-                                      var action = current_actions.get(moniker);
+                                      var action = current_actions[moniker];
                                       action.result.field_error[field.getAttribute("name")] = text;
                                       }
                               }});
@@ -1308,7 +1266,14 @@
              response_fragment = response_fragment.nextSibling) {
 
             var exp_id = response_fragment.getAttribute("id");
-            var f = expected_fragments.find(function(f) { return exp_id == f['region'] });
+
+            var f;
+            jQuery.each(expected_fragments, function() {
+                if (exp_id == this['region']) {
+                    f = this;
+                    return false;
+                }
+            });
             if (!f)
                 continue;
 
@@ -1318,7 +1283,10 @@
             extract_cacheable(response_fragment, f);
         }
 
-        update_from_cache.each(function(x) { x() });
+        jQuery.each(update_from_cache, function() {
+            this();
+        });
+        // update_from_cache.each(function(x) { x() });
 
         walk_node(response,
         { result: function(result) {
@@ -1331,8 +1299,9 @@
           redirect: function(redirect) {
                 document.location =  redirect.firstChild.firstChild.nodeValue;
         }});
-        current_actions = $H();
+        current_actions = {}
     };
+
     var onFailure = function(transport, object) {
         hide_wait_message_now();
 
@@ -1346,48 +1315,43 @@
     };
 
     // Build variable structure
-    request.set('variables', $H());
-    var keys = current_args.keys();
-    for (var i = 0; i < keys.length; i++) {
-        var k = keys[i];
-        request.get('variables').set('region-'+k, current_args.get(k));
-    }
+    request.variables = {};
+    jQuery.each(current_args, function(k, v) {
+        request.variables['region-'+k] = v;
+    });
 
     // Build continuation structure
-    request.set('continuation', named_args['continuation']);
+    request.continuation = named_args['continuation'];
 
     // Push any state variables which we set into the forms
     for (var i = 0; i < document.forms.length; i++) {
         var form = document.forms[i];
-        var keys = current_args.keys();
-        for (var j = 0; j < keys.length; j++) {
-            var n = keys[j];
-            if (form['J:V-region-'+n]) {
-                form['J:V-region-'+n].value = current_args.get(n);
+
+        jQuery.each(current_args, function(k, v) {
+            if (form['J:V-region-'+k]) {
+                form['J:V-region-'+k].value = v;
             } else {
                 var hidden = document.createElement('input');
                 hidden.setAttribute('type',  'hidden');
-                hidden.setAttribute('name',  'J:V-region-'+n);
-                hidden.setAttribute('id',    'J:V-region-'+n);
-                hidden.setAttribute('value', current_args.get(n));
+                hidden.setAttribute('name',  'J:V-region-'+k);
+                hidden.setAttribute('id',    'J:V-region-'+k);
+                hidden.setAttribute('value', v);
                 form.appendChild(hidden);
             }
-        }
+        })
     }
 
-    // Set up our options
-    var options = { postBody: request.toJSON(), //JSON.stringify(request.toObject),
-                    onSuccess: onSuccess,
-                    onException: onFailure,
-                    onFailure: onFailure,
-                    onComplete: function(){hide_wait_message()},
-                    requestHeaders: ['Content-Type', 'text/x-json']
-    };
-
     // Go!
-    new Ajax.Request(document.URL,
-                     options
-                    );
+    jQuery.ajax({
+        url: document.URL,
+        type: 'post',
+        dataType: 'xml',
+        data: JSON.stringify(request),
+        contentType: 'text/x-json',
+        error: onFailure,
+        complete: function(){hide_wait_message()},
+        success: onSuccess
+    });
     return false;
 }
 
@@ -1404,23 +1368,20 @@
 
 
 function show_wait_message (){
-    if ($('jifty-wait-message'))
-        new Effect.Appear('jifty-wait-message', {duration: 0.5});
+    jQuery('#jifty-wait-message').fadeIn(500);
 }
 
 function hide_wait_message (){
-    if ($('jifty-wait-message'))
-        new Effect.Fade('jifty-wait-message', {duration: 0.2});
+    jQuery('#jifty-wait-message').fadeOut(200);
 }
 
 function hide_wait_message_now() {
-    if ($('jifty-wait-message'))
-        Element.hide('jifty-wait-message');
+    jQuery('#jifty-wait-message').hide();
 }
 
 function show_action_result() {
-    var popup = $('jifty-result-popup');
-    if(!popup) return;
+    var $popup = jQuery('#jifty-result-popup');
+    if($popup.size() == 0) return;
 
     var moniker = arguments[0];
     var result = arguments[1];
@@ -1448,7 +1409,7 @@
     node.setAttribute('id', node_id);
     node.className = "popup_notification result-" + status;
     node.innerHTML = text;
-    
+
     var wrap1 = document.createElement("div");
     wrap1.className = "dropshadow_wrap1";
     var wrap2 = document.createElement("div");
@@ -1459,132 +1420,111 @@
     wrap1.appendChild(wrap2);
     wrap2.appendChild(wrap3);
     wrap3.appendChild(node);
-    
-    if(popup.hasChildNodes()) {
-        popup.insertBefore(wrap1, popup.firstChild);
-    } else {
-        popup.appendChild(wrap1);
-    }
-    
+
+    $popup.prepend( wrap1 );
+
     setTimeout(function () {
-           new Effect.Fade(wrap1, {duration: 3.0});
+        jQuery(wrap1).fadeOut(3000);
     }, 3500);
 }
 
-Jifty.Autocompleter = Class.create();
-Object.extend(Object.extend(Jifty.Autocompleter.prototype, Ajax.Autocompleter.prototype), {
-  initialize: function(field, div) {
-    this.field  = $(field);
-    this.action = Form.Element.getAction(this.field);
-    this.url    = '/__jifty/autocomplete.xml';
-
-    Event.observe(this.field, "focus", this.onFocus.bindAsEventListener(this));
-    this.baseInitialize(this.field, $(div), {
-        minChars: "0",
-        beforeShow: this.beforeShow,
-        beforeHide: this.beforeHide,
-        frequency: 0.1,
-        onShow: this.onShow,
-        onHide: this.onHide,
-        afterUpdateElement: this.afterUpdate
-    });
+Jifty.Autocompleter = function() {
+    this.initialize.apply(this, arguments);
+    return this;
+};
 
-    if ((document.all)&&(navigator.appVersion.indexOf("MSIE")!=-1)) {
-        Event.observe(this.element, "keydown", this.onKeyPress.bindAsEventListener(this));
-    }
-  },
+jQuery.extend(Jifty.Autocompleter.prototype, {
+    initialize: function(field, div) {
+        this.field  = Jifty.$(field);
+        this.action = Jifty.Form.Element.getAction(this.field);
+        this.url    = '/__jifty/autocomplete.xml';
+
+        var self = this;
+        jQuery(this.field).bind("focus", function(event) {
+            self.changed  = true;
+            self.hasFocus = true;
+            Jifty.current_autocompleter_object = self;
+        });
+        
+        jQuery(this.field).Autocomplete({
+            source: this.url,
+            minchars: -1,
+            delay: 100,
+            helperClass: 'autocomplete',
+            selectClass: 'selected'
+        });
+    },
 
-  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});
-      }
-      Element.show( update );
-  },
+    beforeShow: function() {
+        /* Prevents the race for canonicalization and updating via autocomplete */
+        if ( this.field.onblur ) {
+            this.element._onblur = this.element.onblur;
+            this.element.onblur  = null;
+        }
+    },
 
-  onHide: function(element, update) {
-      Element.hide( update );
-  },
+    beforeHide: function() {
+        /* Restore onblur and config option */
+        if ( this.element._onblur ) {
+            this.element.onblur  = this.element._onblur;
+            this.element._onblur = null;
+        }
+    },
 
-  beforeShow: function(obj) {
-    /* Prevents the race for canonicalization and updating
-       via autocomplete */
-    if ( obj.element.onblur ) {
-        obj.element._onblur = obj.element.onblur;
-        obj.element.onblur  = null;
-    }
-  },
+    afterUpdate: function(field, selection) {
+        Jifty.Form.Element.validate(field);
+    },
 
-  beforeHide: function(obj) {
-    /* Restore onblur and config option */
-    if ( obj.element._onblur ) {
-        obj.element.onblur  = obj.element._onblur;
-        obj.element._onblur = null;
+    buildRequest: function() {
+        var request = { path: this.url, actions: {} };
+        var a = {};
+        a['moniker'] = 'autocomplete';
+        a['class']   = 'Jifty::Action::Autocomplete';
+        a['fields']  = {};
+        a['fields']['moniker']  = this.action.moniker;
+        a['fields']['argument'] = Jifty.Form.Element.getField(this.field);
+        request['actions']['autocomplete'] = a;
+        request['actions'][this.action.moniker] = this.action.data_structure();
+        request['actions'][this.action.moniker]['active']  = 0;
+        return request;
     }
-  },
-
-  onFocus: function(event) {
-    this.changed  = true;
-    this.hasFocus = true;
-
-    if (this.observer)
-        clearTimeout(this.observer);
-    
-    this.onObserverEvent();
-  },
-
-  afterUpdate: function(field, selection) {
-     Form.Element.validate(field);
-  },
-  
-  getUpdatedChoices: function() {
-      var request = { path: this.url, actions: {} };
-
-      var a = {}; //$H();
-      a['moniker'] = 'autocomplete';
-      a['class']   = 'Jifty::Action::Autocomplete';
-      a['fields']  = {}; //$H();
-      a['fields']['moniker']  = this.action.moniker;
-      a['fields']['argument'] = Form.Element.getField(this.field);
-      request['actions']['autocomplete'] = a;
-      request['actions'][this.action.moniker] = this.action.data_structure();
-      request['actions'][this.action.moniker]['active']  = 0;
-
-      var options = { postBody: JSON.stringify(request),
-                      onComplete: this.onComplete.bind(this),
-                      requestHeaders: ['Content-Type', 'text/x-json']
-      };
-
-      new Ajax.Request(this.url,
-                       options
-                       );
-  }
+});
 
 
-});
+Jifty.Placeholder = function() {
+    this.initialize.apply(this, arguments);
+    return this;
+};
 
-Jifty.Placeholder = Class.create();
-Object.extend(Jifty.Placeholder.prototype, {
+jQuery.extend(Jifty.Placeholder.prototype, {
   element: null,
   text: null,
 
   initialize: function(element, text) {
-     this.element = $(element);
+     this.element = Jifty.$(element);
      this.text = text;
      this.element.placeholderText = this.text;
 
-     Event.observe(element, 'focus', this.onFocus.bind(this));
-     Event.observe(element, 'blur', this.onBlur.bind(this));
+     var self = this;
+
+     jQuery( this.element )
+     .bind("focus", function(event) {
+         self.onFocus();
+     })
+     .bind("blur", function(event) {
+         self.onBlur();
+     });
+
      this.onBlur();
 
-     var form = Form.Element.getForm(element);
-     
+     var form = Jifty.Form.Element.getForm(element);
+
      if(form && !form.hasPlaceholders) {
          form.hasPlaceholders = true;
-         // We can't attach this event via DOM event methods because 
+         // We can't attach this event via DOM event methods because
          // we need to call form.submit() sometimes and still have a good
          // way to call this event handler
-         form.onsubmit = function () { Form.clearPlaceholders(form); };
+         form.onsubmit = function () { Jifty.Form.clearPlaceholders(form); };
      }
   },
 
@@ -1594,8 +1534,7 @@
         as the placeholder text.  This does have the effect of making it
         impossible to submit a field with the same value as the placeholder. */
      if (this.element.value == '' || this.element.value == this.text) {
-       Element.addClassName(this.element, 'placeholder');
-       this.element.value = this.text;
+         jQuery(this.element).addClass('placeholder').val(this.text);
      }
   },
 
@@ -1605,12 +1544,11 @@
 
 });
 
-Object.extend(Jifty.Placeholder, {
-
+jQuery.extend(Jifty.Placeholder, {
    hasPlaceholder: function(elt) {
-     return Element.hasClassName(elt, 'placeholder');
+       return jQuery(elt).hasClass('placeholder');
   },
-            
+
   clearPlaceholder: function(elt) {
      // If the element's text isn't the same as its placeholder text, then the
      // browser screwed up and didn't clear our placeholder. Opera on Mac with
@@ -1620,8 +1558,7 @@
      elt.placeholderText = elt.placeholderText.replace(/\r/g, '');
 
      if(Jifty.Placeholder.hasPlaceholder(elt) && elt.value == elt.placeholderText) {
-       elt.value = '';
-       Element.removeClassName(elt, 'placeholder');
+       jQuery(elt).removeClass('placeholder').val('');
      }
   }
 
@@ -1644,48 +1581,170 @@
     }
 }
 
-function _sp_submit_form(elt, event, submit_to) {
-    var form = Form.Element.getForm(elt);
-    var elements = Form.getElements(form);
-
-    // Three things need to get merged -- hidden defaults, defaults
-    // from buttons, and form values.  Hence, we build up three lists
-    // and then merge them.
-    var hiddens = $H();
-    var buttons = $H();
-    var inputs = $H();
-    for (var i = 0; i < elements.length; i++) {
-        var e = elements[i];
-        var parsed = e.getAttribute("name").match(/^J:V-region-__page\.(.*)/);
-        var extras = Form.Element.buttonArguments(e);
-        if (extras.keys().length > 1) {
-            // Button with values
-            for (var j = 0; j < extras.keys().length; j++) {
-                if ( extras.keys()[j] == 'extend' ) continue;
-                // Might also have J:V mappings on it
-                parsed = extras.keys()[j].match(/^J:V-region-__page\.(.*)/);
-                if ((parsed != null) && (parsed.length == 2)) {
-                    buttons.set(parsed[1], extras.values()[j]);
-                } else if (extras.keys()[j].length > 0) {
-                    inputs.set(extras.keys()[j], extras.values()[j]);
-                }
-                
+/*
+ * Jifty.Effect Usage:
+ * 
+ * Jifty.Effect(element, "Fade", { duration: 2.0 });
+ * 
+ * When called, instantly pefrom a js effect on give element.
+ *
+ * The last arg "option" is a hash. Currently it's only used for
+ * specificing callbacks. There are two possible callbacks, before
+ * and after. You may specify them like this:
+ * 
+ * Jifty.Effect(element, "Fade", { duration: 2.0 }, {
+ *     before: function() { ... },
+ *     after: function() { ... }
+ * });
+ *
+ * The "before" callback is called right before the effect starts.
+ * The "after" callback is called right after it's started, but not
+ * necessarily ended.
+ *
+ * This function is written to make it possible that a Jifty plugin
+ * can override default effects with other fancy javascript
+ * libraries. By default, it delegates all the real work to
+ * jQuery's built-in effect functions.
+ *
+ */
+
+Jifty.Effect = function(el, name, args, options) {
+    // Scriptaculous. TODO: This should be overrided by Jifty::Prototype plugins instead of coded in here.
+    if (typeof Effect != 'undefined') {
+        try {
+            var effect = eval('Effect.' + name);
+            var effect_args  = args || {};
+            if (effect) {
+                (effect)(el, effect_args);
             }
-        } else if ((parsed != null) && (parsed.length == 2)) {
-            // Hidden default
-            hiddens.set(parsed[1], $F(e));
-        } else if (e.name.length > 0) {
-            // Straight up values
-            inputs.set(e.name, $F(e));
-        }
+            return effect;
+        } catch ( e ) {}
     }
 
-    var args = hiddens.merge(buttons.merge(inputs));
+    if (options == null) options = {};
+    // jQuery built-ins
+    var effect =
+        name == 'Fade' ? 'fadeOut' :
+        name == 'Appear' ? 'fadeIn' :
+        name == 'SlideDown' ? 'slideDown' :
+        name == 'SlideUp' ? 'slideUp' :
+        name;
 
-    /* we want to feed a common object instead of a Hash to Jifty.update */ 
-    var args_object = {};
-    args.each( function( pair ) { args_object[pair.key] = pair.value; } );
+    if (jQuery.isFunction( jQuery(el)[ effect ] ) ) {
+        if ( jQuery.isFunction(options["before"])  ) 
+            options["before"].call( el );
 
-    if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true;
-    return Jifty.update( {'continuation':{},'actions':null,'fragments':[{'mode':'Replace','args':args_object,'region':'__page','path': submit_to}]}, elt );
-}
+        ( jQuery(el)[ effect ] )(args);
+
+        if ( jQuery.isFunction(options["after"])  ) 
+            options["after"].call( el );
+    }
+};
+
+/*
+ * Provide an alias in Global namespace for backward compatibility.
+ * Also Jifty.Form will still work even if prototype.js is loaded
+ * after jifty.js.
+ */
+
+Form = {};
+
+jQuery.extend(Form, {
+    // Return an Array of Actions that are in this form
+    getActions: function (element) {
+        // DEPRECATED: use Jifty.Form.getActions instead
+        return Jifty.Form.getActions(element);
+    },
+    clearPlaceholders: function(element) {
+        // DEPRECATED: use Jifty.Form.clearPlaceholders instead
+        return Jifty.Form.clearPlaceholders(element);
+    },
+
+    Element: {}
+});
+
+jQuery.extend(Form.Element, {
+    // Get the moniker for this form element
+    // Takes an element or an element id
+    getMoniker: function (element) {
+        // DEPRECATED: use Jifty.Form.Element.getMoniker instead
+        return Jifty.Form.Element.getMoniker(element);
+    },
+
+    // Get the Action for this form element
+    // Takes an element or an element id
+    getAction: function (element) {
+        // DEPRECATED: use Jifty.Form.Element.getAction instead
+        return Jifty.Form.Element.getAction(element);
+    },
+
+    // Returns the name of the field
+    getField: function (element) {
+        // DEPRECATED: use Jifty.Form.Element.getField instead
+        return Jifty.Form.Element.getField(element);
+    },
+
+    // The type of Jifty form element
+    getType: function (element) {
+        // DEPRECATED: use Jifty.Form.Element.getType instead
+        return Jifty.Form.Element.getType(element);
+    },
+
+    // Validates the action this form element is part of
+    validate: function (element) {
+        // DEPRECATED: use Jifty.Form.Element.validate instead
+        return Jifty.Form.Element.validate(element);
+    },
+
+    // Temporarily disable validation
+            disableValidation: function(element) {
+                // DEPRECATED: use Jifty.Form.Element.disableValidation instead
+                return Jifty.Form.Element.disableValidation(element);
+        },
+
+            //Reenable validation            
+            enableValidation: function(element) {
+                // DEPRECATED: use Jifty.Form.Element.enableValidation instead
+                return Jifty.Form.Element.enableValidation(element);
+        },
+
+
+    // Look up the form that this element is part of -- this is sometimes
+    // more complicated than you'd think because the form may not exist
+    // anymore, or the element may have been inserted into a new form.
+    // Hence, we may need to walk the DOM.
+    getForm: function (element) {
+        // DEPRECATED: use Jifty.Form.Element.getForm instead
+        return Jifty.Form.Element.getForm(element);
+    },
+
+    buttonArguments: function(element) {
+        // DEPRECATED: use Jifty.Form.Element.buttonArguments instead
+        return Jifty.Form.Element.buttonArguments(element);
+    },
+
+    buttonActions: function(element) {
+        // DEPRECATED: use Jifty.Form.Element.buttonActions instead
+        return Jifty.Form.Element.buttonActions(element);
+    },  
+
+    buttonFormElements: function(element) {
+        // DEPRECATED: use Jifty.Form.Element.buttonFormElements instead
+        return Jifty.Form.Element.buttonFormElements(element);
+    },
+
+    /* Someday Jifty may have the concept of "default"
+       buttons.  For now, this clicks the first button that will
+       submit the action associated with the form element.
+     */
+    clickDefaultButton: function(element) {
+        // DEPRECATED: use Jifty.Form.Element.clickDefaultButton instead
+        return Jifty.Form.Element.clickDefaultButton(element);
+    },
+
+    handleEnter: function(event) {
+        // DEPRECATED: use Jifty.Form.Element.handleEnter instead
+        return Jifty.Form.Element.handleEnter(event);
+    }
+
+});

Added: jifty/trunk/share/web/static/js/jifty_interface.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/web/static/js/jifty_interface.js	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,60 @@
+/**
+ * This file overrides functions defined in Interface Elements for jQuery (http://interface.eyecon.ro)
+ * so they can work with Jifty. It must be loaded after all Interface scripts, and better before
+ * jquery_noconflict.js
+ */
+
+jQuery.iAuto.update = function() {
+    var subject = jQuery.iAuto.subject;
+    var subjectValue = jQuery.iAuto.getFieldValues(subject);
+    //var selectionStart = jQuery.iAuto.getSelectionStart(subject);
+    if (subject && subjectValue.item != jQuery.iAuto.lastValue && subjectValue.item.length >= subject.autoCFG.minchars) {
+	jQuery.iAuto.lastValue = subjectValue.item;
+	jQuery.iAuto.currentValue = subjectValue.item;
+
+        var request = Jifty.current_autocompleter_object.buildRequest();
+	jQuery.ajax(
+	    {
+		type: 'post',
+		data: JSON.stringify(request),
+		success: function(xml)
+		{
+		    subject.autoCFG.lastSuggestion = jQuery('li',xml);
+		    size = subject.autoCFG.lastSuggestion.size();
+		    if (size > 0) {
+			var toWrite = '';
+
+			subject.autoCFG.lastSuggestion.each(
+			    function(nr) {
+                                var v = jQuery(this).text();
+				toWrite += '<li rel="' + v + '" dir="' + nr + '" style="cursor: default;">' + v + '</li>';
+			    }
+			);
+                        
+			if (subject.autoCFG.autofill) {
+			    var valueToAdd = jQuery('value', subject.autoCFG.lastSuggestion.get(0)).text();
+			    subject.value = subjectValue.pre + valueToAdd + subject.autoCFG.multipleSeparator + subjectValue.post;
+			    jQuery.iAuto.selection(
+				subject, 
+				subjectValue.item.length != valueToAdd.length ? (subjectValue.pre.length + subjectValue.item.length) : valueToAdd.length,
+				subjectValue.item.length != valueToAdd.length ? (subjectValue.pre.length + valueToAdd.length) : valueToAdd.length
+			    );
+			}
+
+			if (size > 0) {
+			    jQuery.iAuto.writeItems(subject, toWrite);
+			} else {
+			    jQuery.iAuto.clear();
+			}
+		    } else {
+			jQuery.iAuto.clear();
+		    }
+		},
+                beforeSend: function(xhr) {
+                    xhr.setRequestHeader('Content-Type', 'text/x-json');
+                },
+		url : subject.autoCFG.source
+	    }
+	);
+    }
+};

Modified: jifty/trunk/share/web/static/js/jifty_subs.js
==============================================================================
--- jifty/trunk/share/web/static/js/jifty_subs.js	(original)
+++ jifty/trunk/share/web/static/js/jifty_subs.js	Wed Apr  9 00:12:34 2008
@@ -1,6 +1,6 @@
 if (typeof Jifty == "undefined") Jifty = { };
 
-{
+(function(){
 
     /* onPushHandler is called for each new pushed element.
        (currently, this is always a <pushfrag>).  This routine takes
@@ -18,12 +18,19 @@
     	var mode = t.getAttribute('mode');
     	var rid =  t.firstChild.getAttribute('id');
     	var f = { region: rid, path: '', mode: mode };
+
+        // If SinglePlugin is enabled, region name will be prefixed
+        // "__page-" by the time that region was rendered. Therefore
+        // it should also be prefixed the same here.
+        if(Jifty.fragments["__page"] != null) {
+            f['region'] = "__page-" + f['region']
+        }
+
     	f = prepare_element_for_update(f);
+        if (f == null) return;
     	apply_fragment_updates(t.firstChild, f);
     };
 
-
-
     
     /* This function constructs a new Jifty.Subs object and sets
     up a callback with HTTP.Push to run our onPushHandler each time
@@ -38,34 +45,30 @@
     	var window_id = args.window_id; // XXX: not yet
     	var uri = args.uri;
     	if (!uri)
-    	    uri = "/=/subs?";
-    	//var push = new HTTP.Push({ "uri": uri, interval : 100, "onPush" : onPushHandler});
+    	    uri = "/=/subs?forever=0";
     	
     	this.start = function() {
     	    //push.start();
+            var self = this;
 
-	    new Ajax.PeriodicalUpdater({},'/=/subs?forever=0',
-	    {
-	        'decay': 1, 'frequency': 0,
-	        'asynchronous':true, 
-	        'evalScripts':false,
-	        'method': 'get',
-	        'onSuccess': onSuccess,
-	        'onFailure': onFailure
-	    });
-	    	};
+            jQuery.ajax({
+                url: uri,
+                type: "get",
+                success: function(responseText) {
+                    var container = document.createElement('div');
+                    container.innerHTML = responseText;
+                    jQuery("pushfrag", container).each(function() {
+                        onPushHandler(this);
+                    });
+
+                    setTimeout(function() {
+                        self.start();
+                    }, 1000)
+                },
+                error: function() {
+                }
+            });
+        }          
     }
 
-
-    function onSuccess(req, json) {
-        var container = document.createElement('div');
-        container.innerHTML = req.responseText;
-        var frags = container.getElementsByTagName('pushfrag');
-        for(var i = 0 ; i < frags.length; i++) {
-            onPushHandler(frags[i]);
-        }
-    }
-    function onFailure(req) { }
-
-}
-
+})();

Modified: jifty/trunk/share/web/static/js/jifty_utils.js
==============================================================================
--- jifty/trunk/share/web/static/js/jifty_utils.js	(original)
+++ jifty/trunk/share/web/static/js/jifty_utils.js	Wed Apr  9 00:12:34 2008
@@ -137,6 +137,10 @@
         
         if ( diff > 0 )
              Jifty.SmoothScroll.scrollTo( scrollTop + diff );
+    },
+
+    stripScripts: function(str) {
+        return str.replace(/<script(.|\s)*?\/script>/g, "");
     }
 };
 

Modified: jifty/trunk/share/web/static/js/key_bindings.js
==============================================================================
--- jifty/trunk/share/web/static/js/key_bindings.js	(original)
+++ jifty/trunk/share/web/static/js/key_bindings.js	Wed Apr  9 00:12:34 2008
@@ -74,7 +74,7 @@
     writeLegend: function(e) {
         if (    !document.createElement
              || !document.createTextNode
-             || Element.hasClassName(e, 'keybindings-written') )
+                || jQuery(e).is('.keybindings-written') )
             return;
         
         
@@ -109,7 +109,7 @@
             
             e.appendChild( label );
             e.appendChild( dl );
-            Element.addClassName(e, 'keybindings-written');
+            jQuery(e).addClass('keybindings-written');
         
             /* since we wrote the legend, now obey it */
             Jifty.KeyBindings.activate();

Modified: jifty/trunk/share/web/templates/__jifty/halo
==============================================================================
--- jifty/trunk/share/web/templates/__jifty/halo	(original)
+++ jifty/trunk/share/web/templates/__jifty/halo	Wed Apr  9 00:12:34 2008
@@ -99,7 +99,7 @@
 <li><b><% $e->[0] %></b>:
 % if ($e->[1]) {
 % my $expanded = Jifty->web->serial;
-<a href="#" onclick="Element.toggle('<% $expanded %>'); return false"><% $e->[1] %></a>
+<a href="#" onclick="jQuery(Jifty.$('<% $expanded %>')).toggle(); return false"><% $e->[1] %></a>
 <div id="<% $expanded %>" style="display: none; position: absolute; left: 200px; border: 1px solid black; background: #ccc; padding: 1em; padding-top: 0; width: 300px; height: 500px; overflow: auto"><pre><% Jifty::YAML::Dump($e->[2]) %></pre></div>
 % } elsif (defined $e->[2]) {
 <% $e->[2] %>

Modified: jifty/trunk/t/TestApp-JiftyJS/etc/config.yml
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/etc/config.yml	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/etc/config.yml	Wed Apr  9 00:12:34 2008
@@ -4,7 +4,7 @@
   ApplicationClass: TestApp::JiftyJS
   ApplicationName: TestApp::JiftyJS
   ApplicationUUID: F43CA57E-A4BE-11DC-A07C-465A83BE23AB
-  ConfigFileVersion: 2
+  ConfigFileVersion: 4
   Database: 
     CheckSchema: 1
     Database: testapp_jiftyjs

Modified: jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action/Play.pm
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action/Play.pm	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action/Play.pm	Wed Apr  9 00:12:34 2008
@@ -19,6 +19,10 @@
         ajax validates,
         valid are qw(happy angry normal);
 
+    param flavor =>
+        autocompleter is \&autocomplete_flavor,
+        type is 'text';
+
     param tags =>
         type is 'text',
         ajax canonicalizes;
@@ -54,5 +58,13 @@
     return $v;
 }
 
+sub autocomplete_flavor {
+    my ($self, $value) = @_;
+    return grep {
+        $_ =~ /$value/i;
+    } sort qw( berry vanilla caramel caracara honey miso blueberry strawberry );
+
+}
+
 1;
 

Added: jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action/Play2.pm
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Action/Play2.pm	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,40 @@
+use strict;
+use warnings;
+
+=head1 NAME
+
+TestApp::JiftyJS::Action::Play2
+
+=cut
+
+package TestApp::JiftyJS::Action::Play2;
+use base qw/TestApp::JiftyJS::Action Jifty::Action/;
+
+use Jifty::Param::Schema;
+use Jifty::Action schema {
+    param bogus => type is 'text';
+};
+
+=head2 take_action
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    # Custom action code
+    $self->report_success if not $self->result->failure;
+    return 1;
+}
+
+=head2 report_success
+
+=cut
+
+sub report_success {
+    my $self = shift;
+    # Your success message here
+    $self->result->message('Success');
+}
+
+1;
+

Added: jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Dispatcher.pm
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Dispatcher.pm	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,14 @@
+
+package TestApp::JiftyJS::Dispatcher;
+use Jifty::Dispatcher -base;
+use strict;
+use warnings;
+
+before '*' => run {
+    Jifty->api->allow("AddTwoNumbers");
+    Jifty->api->allow('Play');
+    Jifty->api->allow('Play2');
+};
+
+1;
+

Added: jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Model/Offer.pm
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/Model/Offer.pm	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,22 @@
+use strict;
+use warnings;
+
+package TestApp::JiftyJS::Model::Offer;
+use Jifty::DBI::Schema;
+
+use TestApp::JiftyJS::Record schema {
+    column name =>
+        type is "varchar(255)";
+
+    column is_job =>
+        type is "boolean",
+        label is _("Job Offer ?");
+
+};
+
+sub current_user_can {
+    1;
+}
+
+1;
+

Modified: jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/View.pm
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/View.pm	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/lib/TestApp/JiftyJS/View.pm	Wed Apr  9 00:12:34 2008
@@ -23,20 +23,16 @@
     );
 
     hyperlink(
-        id => 'region3',
-        label => "John",
+        id => 'region3', label => "John",
         onclick => {
             region => 'content',
             replace_with => 'hello_world',
-            args => {
-                name => "John"
-            }
+            args => { name => "John" }
         }
     );
 
     hyperlink(
-        id => 'region4',
-        label => "Smith",
+        id => 'region4', label => "Smith",
         onclick => {
             region => 'content',
             replace_with => 'hello_world',
@@ -46,11 +42,38 @@
         }
     );
 
+    hyperlink(
+        id => 'append-region', label => "Append To Region",
+        onclick => {
+            region => "content",
+            append => "hello_world"
+        }
+    );
+
+    hyperlink(
+        id => 'prepend-region', label => "Prepend To Region",
+        onclick => {
+            region => "content",
+            prepend => "hello_world"
+        }
+    );
+
+    hyperlink(
+        id => 'delete-region',
+        label => "Delete Region",
+        onclick => {
+            delete => "content"
+        }
+    );
+
+    hr {};
+
     render_region( name => 'content' );
 };
 
 template 'hello_world' => sub {
-    p { "Hello, " . ( get('name') || "World" ) }
+    p {  (get('with_time') ? "Time: " . time . ". " : "")
+             . "Hello, " . ( get('name') || "World" ) };
 };
 
 template 'region1' => sub {
@@ -144,5 +167,120 @@
     };
 };
 
+template '/effects' => page {
+    h1 { "Jifty.update() tests with effects" };
+
+    for (qw(Appear SlideDown)) {
+        hyperlink(
+            label => "Append ($_)",
+            onclick => {
+                append => 'hello_world',
+                region => 'content',
+                args => {
+                    with_time => 1,
+                    name => "Append with effect $_"
+                },
+                effect => $_,
+                effect_args => "slow"
+            }
+        );
+        outs_raw("&nbsp;");
+
+        hyperlink(
+            label => "Prepend ($_)",
+            onclick => {
+                prepend => 'hello_world',
+                region => 'content',
+                args => {
+                    with_time => 1,
+                    name => "Prepend with effect $_"
+                },
+                effect => $_,
+                effect_args => "slow"
+            }
+        );
+
+        outs_raw("&nbsp;|&nbsp;");
+    }
+
+
+    hyperlink(
+        label => "Reset",
+        onclick => {
+            region => "content",
+            replace_with => "hello_world"
+        }
+    );
+
+    hr {};
+
+    render_region( name => 'content', path => "hello_world" );
+};
+
+template '/act/play2' => page {
+    my $action = new_action(class => 'Play2', moniker => "play2");
+    form {
+        render_action($action);
+
+        form_next_page( url => "/redirected");
+        form_submit( label => "Submit" );
+    };
+};
+
+template '/act/play3' => page {
+    my $action = new_action(moniker => "play2", class => "Play2");
+    form {
+        $action
+            ->form_field('text',
+                         label => "Hi",
+                         sticky => 0,
+                         placeholder => "foobar click me to enter text")
+            ->render();
+        form_submit( label => "Submit" );
+    };
+};
+
+template '/redirected' => page {
+    p { "Redirected!" }
+};
+
+template '/p/zero' => page {
+    render_region("__page", path => "/p/one");
+};
+
+template '/p/one' => sub {
+    p {
+        outs "FooBar.";
+        hyperlink(
+            label => "Two",
+            onclick => {
+              replace_with => '/p/two'
+            },
+            as_button => 1
+        );
+    }
+};
+
+template '/p/two' => sub {
+    hyperlink(
+        label => "Two",
+        onclick => {
+            refresh_self => 1,
+        },
+        as_button => 1
+    );
+    p { "Lorem Ipsum... " } for 1..100;
+
+    outs_raw(<<E);
+    <script type="text/javascript">
+    jQuery(function() {
+        alert( jQuery("p").size() );
+    });
+    </script>
+E
+
+
+};
+
 
 1;

Modified: jifty/trunk/t/TestApp-JiftyJS/share/web/static/js-test/02.action.html
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/share/web/static/js-test/02.action.html	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/share/web/static/js-test/02.action.html	Wed Apr  9 00:12:34 2008
@@ -4,17 +4,9 @@
     <script type="text/javascript" src="/static/js/jsan/JSAN.js" charset="UTF-8"></script>
     <script type="text/javascript" src="lib/Test/Builder.js" charset="UTF-8"></script>
     <script type="text/javascript" src="lib/Test/More.js" charset="UTF-8"></script>
-
-    <script type="text/javascript" src="/static/js/prototype.js" charset="UTF-8"></script>
-    <script type="text/javascript" src="/static/js/cssquery/cssQuery.js" charset="UTF-8"></script>
-    <script type="text/javascript" src="/static/js/cssquery/cssQuery-level2.js" charset="UTF-8"></script>
-    <script type="text/javascript" src="/static/js/cssquery/cssQuery-level3.js" charset="UTF-8"></script>
-    <script type="text/javascript" src="/static/js/cssquery/cssQuery-standard.js" charset="UTF-8"></script>
+    <script type="text/javascript" src="/static/js/jquery-1.2.1.js" charset="UTF-8"></script>
+    <script type="text/javascript" src="/static/js/jquery_noconflict.js" charset="UTF-8"></script>
     <script type="text/javascript" src="/static/js/behaviour.js" charset="UTF-8"></script>
-    <script type="text/javascript" src="/static/js/scriptaculous/builder.js" charset="UTF-8"></script>
-    <script type="text/javascript" src="/static/js/scriptaculous/effects.js" charset="UTF-8"></script>
-    <script type="text/javascript" src="/static/js/scriptaculous/controls.js" charset="UTF-8"></script>
-
     <script type="text/javascript" src="/static/js/jifty.js" charset="UTF-8"></script>
     <script type="text/javascript">
     </script>
@@ -28,23 +20,24 @@
 
         <div class="hidden"><input type="hidden" value="TestApp::JiftyJS::Action::AddTwoNumbers" id="J:A-run-TestApp::JiftyJS::Action::AddTwoNumbers" name="J:A-run-TestApp::JiftyJS::Action::AddTwoNumbers"/></div>
         <div class="form_field argument-first_number">
-          <span class="preamble text argument-first_number"/>
+          <span class="preamble text argument-first_number"></span>
           <label for="J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers-S1182827" class="label text argument-first_number">first_number</label>
           <input type="text" class="widget text argument-first_number jifty_enter_handler_attached" value="" id="J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers-S1182827" name="J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers"/>
-          <span class="hints text argument-first_number"/>
-          <span id="errors-J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="error text argument-first_number" style="display: none;"/>
-          <span id="warnings-J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="warning text argument-first_number" style="display: none;"/>
-          <span id="canonicalization_note-J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="canonicalization_note text argument-first_number" style="display: none;"/>
+          <span class="hints text argument-first_number"></span>
+          <span id="errors-J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="error text argument-first_number" style="display: none;"></span>
+          <span id="warnings-J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="warning text argument-first_number" style="display: none;"></span>
+          <span id="canonicalization_note-J:A:F-first_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="canonicalization_note text argument-first_number" style="display: none;"></span>
         </div>
 
         <div class="form_field argument-second_number">
-          <span class="preamble text argument-second_number"/>
+          <span class="preamble text argument-second_number"></span>
           <label for="J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers-S1192827" class="label text argument-second_number">second_number</label>
           <input type="text" class="widget text argument-second_number jifty_enter_handler_attached" value="" id="J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers-S1192827" name="J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers"/>
-          <span class="hints text argument-second_number"/>
-          <span id="errors-J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="error text argument-second_number" style="display: none;"/>
-          <span id="warnings-J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="warning text argument-second_number" style="display: none;"/>
-          <span id="canonicalization_note-J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="canonicalization_note text argument-second_number" style="display: none;"/>
+          <span class="hints text argument-second_number"></span>
+
+          <span id="errors-J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="error text argument-second_number" style="display: none;"></span>
+          <span id="warnings-J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="warning text argument-second_number" style="display: none;"></span>
+          <span id="canonicalization_note-J:A:F-second_number-run-TestApp::JiftyJS::Action::AddTwoNumbers" class="canonicalization_note text argument-second_number" style="display: none;"></span>
         </div>
 
 

Added: jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/en.json
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/en.json	Wed Apr  9 00:12:34 2008
@@ -0,0 +1 @@
+{}
\ No newline at end of file

Added: jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/en_us.json
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/en_us.json	Wed Apr  9 00:12:34 2008
@@ -0,0 +1 @@
+{}
\ No newline at end of file

Added: jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/zh_tw.json
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/share/web/static/js/dict/zh_tw.json	Wed Apr  9 00:12:34 2008
@@ -0,0 +1 @@
+{}
\ No newline at end of file

Added: jifty/trunk/t/TestApp-JiftyJS/t/00-action-Play2.t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/t/00-action-Play2.t	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,17 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A (very) basic test harness for the Play2 action.
+
+=cut
+
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test tests => 1;
+
+# Make sure we can load the action
+use_ok('TestApp::JiftyJS::Action::Play2');
+

Added: jifty/trunk/t/TestApp-JiftyJS/t/00-model-Offer.t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/t/00-model-Offer.t	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,51 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A basic test harness for the Offer model.
+
+=cut
+
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test tests => 11;
+
+# Make sure we can load the model
+use_ok('TestApp::JiftyJS::Model::Offer');
+
+# Grab a system user
+my $system_user = TestApp::JiftyJS::CurrentUser->superuser;
+ok($system_user, "Found a system user");
+
+# Try testing a create
+my $o = TestApp::JiftyJS::Model::Offer->new(current_user => $system_user);
+my ($id) = $o->create();
+ok($id, "Offer create returned success");
+ok($o->id, "New Offer has valid id set");
+is($o->id, $id, "Create returned the right id");
+
+# And another
+$o->create();
+ok($o->id, "Offer create returned another value");
+isnt($o->id, $id, "And it is different from the previous one");
+
+# Searches in general
+my $collection =  TestApp::JiftyJS::Model::OfferCollection->new(current_user => $system_user);
+$collection->unlimit;
+is($collection->count, 2, "Finds two records");
+
+# Searches in specific
+$collection->limit(column => 'id', value => $o->id);
+is($collection->count, 1, "Finds one record with specific id");
+
+# Delete one of them
+$o->delete;
+$collection->redo_search;
+is($collection->count, 0, "Deleted row is gone");
+
+# And the other one is still there
+$collection->unlimit;
+is($collection->count, 1, "Still one left");
+

Modified: jifty/trunk/t/TestApp-JiftyJS/t/1-jifty-update.t
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/t/1-jifty-update.t	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/t/1-jifty-update.t	Wed Apr  9 00:12:34 2008
@@ -4,7 +4,7 @@
 use warnings;
 use lib 't/lib';
 use Jifty::SubTest;
-use Jifty::Test tests => 18;
+use Jifty::Test tests => 29;
 use Jifty::Test::WWW::Selenium;
 use utf8;
 
@@ -28,8 +28,30 @@
 
     $sel->click_ok("region4");
     $sel->wait_for_text_present_ok("Hello, Smith");
+
+    $sel->click_ok("append-region");
+    $sel->wait_for_text_present_ok("Hello, World");
+    my $src = $sel->get_html_source();
+
+    like $src, qr{<p>Hello, Smith</p>.+<p>Hello, World</p>}is;
+
+    $sel->click_ok("prepend-region");
+    $sel->pause();
+    $sel->wait_for_text_present_ok("Hello, World");
+
+    $src = $sel->get_html_source();
+
+    like $src, qr{<p>Hello, World</p>.+<p>Hello, Smith</p>.+<p>Hello, World</p>}is;
+
+    $sel->click_ok("delete-region");
+    $sel->pause();
+
+    ok(! $sel->is_element_present("region-content"), "'content' region is deleted." );
+
 }
 
+
+
 {
     # One click updates 3 regions, and triggers an alert.
 
@@ -42,6 +64,16 @@
     $sel->wait_for_text_present_ok("Hello, Pony");
 }
 
-$sel->stop;
-
+{
+    # Make sure there's 100 <p> element.
+    # For any region update, using Jifty.udpate(), javascript code in there are always executed
+    # after HTML is all done. This is to test how many <p> elements the javascript code
+    # can get. And ithe number should be 100.
+    $sel->open_ok('/p/zero');
+    $sel->click_ok('xpath=//input');
+    $sel->pause();
+    my $msg = $sel->get_alert();
+    is($msg, "100");
+}
 
+$sel->stop;

Modified: jifty/trunk/t/TestApp-JiftyJS/t/2-behaviour.t
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/t/2-behaviour.t	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/t/2-behaviour.t	Wed Apr  9 00:12:34 2008
@@ -4,7 +4,7 @@
 use warnings;
 use lib 't/lib';
 use Jifty::SubTest;
-use Jifty::Test tests => 4;
+use Jifty::Test qw(no_plan);
 use Jifty::Test::WWW::Selenium;
 use utf8;
 
@@ -12,8 +12,16 @@
 my $sel    = Jifty::Test::WWW::Selenium->rc_ok($server);
 my $URL    = $server->started_ok;
 
-$sel->open_ok("/static/js-test/index.html");
+for my $test_file (qw(01.behaviour.html 02.action.html)) {
+    $sel->open_ok("/static/js-test/$test_file");
+    my $html = $sel->get_text("test");
+    $html =~ /(\d+)\.\.(\d+)/;
+
+    for($1..$2) {
+        $sel->wait_for_text_present("exact:ok $_");
+        ok(! $sel->is_text_present("exact:nok $_") );
+    }
+}
 
-$sel->wait_for_text_present_ok("All tests successful.");
 $sel->stop;
 

Modified: jifty/trunk/t/TestApp-JiftyJS/t/4-tangent.t
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/t/4-tangent.t	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/t/4-tangent.t	Wed Apr  9 00:12:34 2008
@@ -12,6 +12,10 @@
 my $sel    = Jifty::Test::WWW::Selenium->rc_ok($server);
 my $URL    = $server->started_ok;
 
+$sel->open("/");
+$sel->set_speed(1000);
+$sel->pause();
+
 {
     # /tangent/page1 -- tangent --> /tangent/returner -- return --> /tangent/page1
 

Modified: jifty/trunk/t/TestApp-JiftyJS/t/5-action.t
==============================================================================
--- jifty/trunk/t/TestApp-JiftyJS/t/5-action.t	(original)
+++ jifty/trunk/t/TestApp-JiftyJS/t/5-action.t	Wed Apr  9 00:12:34 2008
@@ -4,14 +4,29 @@
 use warnings;
 use lib 't/lib';
 use Jifty::SubTest;
-use Jifty::Test tests => 10;
+use Jifty::Test;
 use Jifty::Test::WWW::Selenium;
 use utf8;
 
+BEGIN {
+    if ($ENV{'SELENIUM_RC_BROWSER'} eq '*iexplore') {
+        plan(skip_all => "Temporarily, until the 'Operation Abort' bug is solved.");
+    }
+    else {
+        plan(tests => 10);
+    }
+}
+
 my $server = Jifty::Test->make_server;
 my $sel    = Jifty::Test::WWW::Selenium->rc_ok($server);
 my $URL    = $server->started_ok;
 
+$sel->open("/");
+
+if ($ENV{'SELENIUM_RC_BROWSER'} eq '*iexplore') {
+    $sel->set_speed(1000);
+}
+
 {
     # Test "Play" action's parameter.
 
@@ -22,22 +37,28 @@
 
     # Tag is ajax canonicalized to lowercase.
 
-    $sel->set_speed(1000);
     $sel->click_ok($tags);
     $sel->type_ok($tags, "FOO");
     $sel->fire_event($tags, "blur");
 
+    $sel->pause(1000);
+
     my $tag_value = $sel->get_value($tags);
     is $tag_value, 'foo', "Tags are canonicalized to lower-case";
 
     $sel->type_ok($mood, "FOO");
     $sel->fire_event($tags, "blur");
+    $sel->pause(1000);
+
     is($sel->get_text('//span[contains(@class, "error text argument-mood")]'),
        "That doesn't look like a correct value",
        "mood validation error");
 
     $sel->type_ok($mood, "angry");
     $sel->fire_event($tags, "blur");
+
+    $sel->pause(1000);
+
     is($sel->get_text('//span[contains(@class, "error text argument-mood")]'),
        "",
        "mood validation ok");
@@ -45,3 +66,4 @@
 }
 
 $sel->stop;
+

Added: jifty/trunk/t/TestApp-JiftyJS/t/6-offer-actions.t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/t/6-offer-actions.t	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,94 @@
+# This test is for testing Jifty.update() javascript function.
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test;;
+use Jifty::Test::WWW::Selenium;
+use utf8;
+
+
+$/ = undef;
+
+my $data = <DATA>;
+$data =~ s/^#.*$//gm;
+
+my @commands = split /\n\n+/, $data;
+
+plan tests => 2+ at commands;
+
+my $server = Jifty::Test->make_server;
+my $sel    = Jifty::Test::WWW::Selenium->rc_ok($server);
+my $URL    = $server->started_ok;
+
+$sel->open("/");
+
+for (@commands) {
+    my ($cmd, $arg1, $arg2) = (split(/\n\s*/, $_, 3), "", "");
+
+    $cmd =~ s{^ *}{}g;
+    $cmd =~ s{ *$}{}g;
+    $arg1 =~ s{\s*$}{};
+    $arg2 =~ s{\s*$}{};
+
+    $cmd .= "_ok";
+    $sel->$cmd($arg1, $arg2);
+
+}
+$sel->stop;
+
+__DATA__
+open
+    /__jifty/admin/model/Offer
+
+type
+    xpath=//div[contains(@class, "jifty_admin create item")]//input[@type="text"]
+    Not A Job Offer
+
+click
+    xpath=//div[contains(@class,"submit_button")]//input
+
+pause
+    1000
+
+wait_for_element_present
+    xpath=//span[contains(@class, "value")][contains(@class, "argument-name")][contains(@class, "text")]
+
+wait_for_text_present
+    Not A Job Offer
+
+wait_for_element_present
+    xpath=//input[@type="checkbox"][contains(@class, "argument-is_job")]
+
+get_text
+    xpath=//span[contains(@class, "text")][contains(@class, "value")][contains(@class, "argument-name")]
+    Not A Job Offer
+
+####
+
+open
+    /__jifty/admin/model/Offer
+
+type
+    xpath=//div[contains(@class, "form_field")][contains(@class,"argument-name")]//input[@type="text"]
+    Offer A Job
+
+check
+    xpath=//input[starts-with(@id, "J:A:F-is_job-auto-")][@type="checkbox"]
+
+
+# Click the "Create" button
+click
+    xpath=//div[@class="submit_button"]/input[@type="submit"][contains(@name,"J:ACTIONS=auto-")]
+
+pause
+    1000
+
+wait_for_element_present
+    xpath=//input[@type="checkbox"][@checked]
+
+get_text
+    xpath=//span[contains(@class, "text")][contains(@class, "argument-name")][contains(@class, "value")]
+    Offer A Job
+

Added: jifty/trunk/t/TestApp-JiftyJS/t/7-redirect.t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/t/7-redirect.t	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,50 @@
+# This test is for testing Jifty.update() javascript function.
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test;;
+use Jifty::Test::WWW::Selenium;
+use utf8;
+
+$/ = undef;
+
+my $data = <DATA>;
+$data =~ s/^#.*$//gm;
+
+my @commands = split /\n\n+/, $data;
+
+plan tests => 2+ at commands;
+
+my $server = Jifty::Test->make_server;
+my $sel    = Jifty::Test::WWW::Selenium->rc_ok($server);
+my $URL    = $server->started_ok;
+
+for (@commands) {
+    my ($cmd, $arg1, $arg2) = (split(/\n\s*/, $_, 3), "", "");
+    $cmd =~ s{^ *}{}g;
+    $cmd =~ s{ *$}{}g;
+    $arg1 =~ s{\s*$}{};
+    $arg2 =~ s{\s*$}{};
+
+    $cmd .= "_ok";
+    $sel->$cmd($arg1, $arg2);
+
+}
+$sel->stop;
+
+__DATA__
+open
+    /act/play2
+
+type
+    xpath=//input[@type='text']
+    Not A Job Offer
+
+click
+    xpath=//input[@type='submit']
+
+wait_for_text_present
+    Redirected!
+

Added: jifty/trunk/t/TestApp-JiftyJS/t/8-placeholder.t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS/t/8-placeholder.t	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,45 @@
+# Test Action
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test;
+use Jifty::Test::WWW::Selenium;
+use utf8;
+
+BEGIN {
+    if (($ENV{'SELENIUM_RC_BROWSER'}||'') eq '*iexplore') {
+        plan(skip_all => "Temporarily, until the 'Operation Abort' bug is solved.");
+    }
+    else {
+        plan(tests => 6);
+    }
+}
+
+my $server = Jifty::Test->make_server;
+my $sel    = Jifty::Test::WWW::Selenium->rc_ok($server);
+my $URL    = $server->started_ok;
+
+$sel->open("/");
+
+$sel->set_speed(1000);
+
+{
+    # Test placeholder
+    $sel->open_ok("/act/play3");
+
+    my $input = 'css=input[name="J:A:F-text-play2"]';
+
+    $sel->is_element_present($input);
+    my $text = $sel->get_value($input);
+
+    is( $text, "foobar click me to enter text", "Initial content in the placeholder." );
+
+    $sel->click_ok($input);
+    $sel->fire_event($input, "focus");
+
+    is( $sel->get_value($input), "", "Placeholder goes empty after clicking on it" );
+}
+
+$sel->stop;

Added: jifty/trunk/t/TestApp-JiftyJS2/Makefile.PL
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS2/Makefile.PL	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,7 @@
+use inc::Module::Install;
+
+name        'TestApp::JiftyJS/';
+version     '0.01';
+requires    'Jifty' => '0.70824';
+
+WriteAll;

Added: jifty/trunk/t/TestApp-JiftyJS2/bin/jifty
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS2/bin/jifty	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+use File::Basename qw(dirname); 
+use UNIVERSAL::require;
+
+use Jifty;
+use Jifty::Script;
+
+local $SIG{INT} = sub { warn "Stopped\n"; exit; };
+Jifty::Script->dispatch();

Added: jifty/trunk/t/TestApp-JiftyJS2/etc/config.yml
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS2/etc/config.yml	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,69 @@
+--- 
+framework: 
+  AdminMode: 1
+  ApplicationClass: TestApp::JiftyJS
+  ApplicationName: TestApp::JiftyJS
+  ApplicationUUID: F43CA57E-A4BE-11DC-A07C-465A83BE23AB
+  ConfigFileVersion: 3
+  Database: 
+    CheckSchema: 1
+    Database: testapp_jiftyjs
+    Driver: SQLite
+    Host: localhost
+    Password: ''
+    RecordBaseClass: Jifty::DBI::Record::Cachable
+    User: ''
+    Version: 0.0.1
+  DevelMode: 1
+  L10N: 
+    PoDir: share/po
+    AllowedLang:
+      - en
+  LogLevel: INFO
+  Mailer: Sendmail
+  MailerArgs: []
+
+  Plugins: 
+    - 
+      LetMe: {}
+
+    - 
+      SkeletonApp: {}
+
+    - 
+      REST: {}
+
+    - 
+      Halo: {}
+
+    - 
+      ErrorTemplates: {}
+
+    - 
+      OnlineDocs: {}
+
+    - 
+      CompressedCSSandJS: { }
+
+    - 
+      AdminUI: {}
+
+  PubSub: 
+    Backend: Memcached
+    Enable: ~
+  SkipAccessControl: 0
+  TemplateClass: TestApp::JiftyJS::View
+  Web: 
+    BaseURL: http://localhost
+    DataDir: var/mason
+    Globals: []
+
+    MasonConfig: 
+      autoflush: 0
+      default_escape_flags: h
+      error_format: text
+      error_mode: fatal
+    Port: 8888
+    ServeStaticFiles: 1
+    StaticRoot: share/web/static
+    TemplateRoot: share/web/templates

Added: jifty/trunk/t/TestApp-JiftyJS2/lib
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS2/lib	Wed Apr  9 00:12:34 2008
@@ -0,0 +1 @@
+link ../TestApp-JiftyJS/lib
\ No newline at end of file

Added: jifty/trunk/t/TestApp-JiftyJS2/share
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS2/share	Wed Apr  9 00:12:34 2008
@@ -0,0 +1 @@
+link ../TestApp-JiftyJS/share
\ No newline at end of file

Added: jifty/trunk/t/TestApp-JiftyJS2/t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-JiftyJS2/t	Wed Apr  9 00:12:34 2008
@@ -0,0 +1 @@
+link ../TestApp-JiftyJS/t
\ No newline at end of file

Modified: jifty/trunk/t/TestApp-Plugin-OnClick/etc/config.yml
==============================================================================
--- jifty/trunk/t/TestApp-Plugin-OnClick/etc/config.yml	(original)
+++ jifty/trunk/t/TestApp-Plugin-OnClick/etc/config.yml	Wed Apr  9 00:12:34 2008
@@ -4,7 +4,7 @@
   ApplicationClass: TestApp::Plugin::OnClick
   ApplicationName: TestApp-Plugin-OnClick
   ApplicationUUID: 45E1B0FE-820A-11DC-9905-76B28F38D863
-  ConfigFileVersion: 2
+  ConfigFileVersion: 3
   Database: 
     CheckSchema: 1
     Database: testapp_plugin_onclick

Modified: jifty/trunk/t/TestApp-Plugin-OnClick/t/onclick.t
==============================================================================
--- jifty/trunk/t/TestApp-Plugin-OnClick/t/onclick.t	(original)
+++ jifty/trunk/t/TestApp-Plugin-OnClick/t/onclick.t	Wed Apr  9 00:12:34 2008
@@ -27,7 +27,11 @@
     'please use Jifty.update instead of update.',
     'bare update is deprecated'
 );
+
+sleep 2;
+
 $html = $sel->get_html_source;
+
 like( $html, qr/original content/, 'replace content correctly' );
 unlike( $html, qr{args:/content\.html}, 'replaced by javascript' );
 

Modified: jifty/trunk/t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/View.pm
==============================================================================
--- jifty/trunk/t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/View.pm	(original)
+++ jifty/trunk/t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/View.pm	Wed Apr  9 00:12:34 2008
@@ -49,5 +49,18 @@
     h1 { $foo };
 };
 
+template '/p/history/one' => page {
+    p { "This Is Page One" };
+};
+
+template '/p/history/two' => page {
+    p { "This Is Page Two" };
+};
+
+
+template '/p/history/three' => page {
+    p { "This Is Page Three" };
+};
+
 1;
 

Added: jifty/trunk/t/TestApp-Plugin-SinglePage/t/history.t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/TestApp-Plugin-SinglePage/t/history.t	Wed Apr  9 00:12:34 2008
@@ -0,0 +1,28 @@
+use strict;
+use warnings;
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test tests => 10;
+use Jifty::Test::WWW::Selenium;
+use utf8;
+
+my $server  = Jifty::Test->make_server;
+my $sel = Jifty::Test::WWW::Selenium->rc_ok( $server );
+my $URL = $server->started_ok;
+diag $URL;
+
+$sel->open_ok("/p/history/one");
+$sel->wait_for_text_present_ok("This Is Page One");
+
+$sel->open_ok("/p/history/two");
+$sel->wait_for_text_present_ok("This Is Page Two");
+
+$sel->open_ok("/p/history/three");
+$sel->wait_for_text_present_ok("This Is Page Three");
+
+$sel->go_back();
+$sel->wait_for_text_present_ok("This Is Page Two");
+
+$sel->go_back();
+$sel->wait_for_text_present_ok("This Is Page One");
+


More information about the Jifty-commit mailing list