Major update of Selectricity to work with Rails 2.2.2 from 1.2!
author<mako@atdot.cc> <>
Fri, 28 Nov 2008 18:35:26 +0000 (13:35 -0500)
committer<mako@atdot.cc> <>
Fri, 28 Nov 2008 18:35:26 +0000 (13:35 -0500)
This included a large number of changes:

- removal of randarray.rb (features are not in the new version or ruby)
- removal of sparklines plugin and replacement with gemified version of
  sparklines and sparklines_generator
- addition of act_as_list plugin which is no longer in rails
- disabling of all API code because action_web_service is deprecated in
  favor of active_resource (!!!!)
- large number of miscellaneous updates to do the new rails 2ish

I've commented out the new features in the views (i.e., embedded) as
this is going to replace the LIVE version of Selectricity. We should
then test this well, slowly enable the new features, and get the tests i
old_api_code back work with an active_resource interface.

50 files changed:
app/controllers/election_controller.rb
app/controllers/quickvote_controller.rb
app/controllers/sparklines_controller.rb
app/controllers/voter_controller.rb
app/helpers/front_helper.rb [deleted file]
app/helpers/sparklines_helper.rb [moved from vendor/plugins/sparklines/lib/sparklines_helper.rb with 55% similarity]
app/models/vote.rb
app/views/common/_methodinfo_condorcet.rhtml
app/views/common/_methodinfo_ssd.rhtml
app/views/common/pref_tables.rhtml
app/views/election/_overview_form.rhtml
app/views/election/_theme_upload.rhtml
app/views/election/detailed_results.rhtml
app/views/election/general_information.rhtml
app/views/layouts/basic.rhtml
app/views/layouts/frontpage.rhtml
app/views/layouts/main.rhtml
app/views/quickvote/_results_sidebar.rhtml
app/views/voter/_results_sidebar.rhtml
config/boot.rb
config/environment.rb
config/environments/development.rb
db/schema.rb
lib/randarray.rb [deleted file]
lib/tasks/rails.rake [new file with mode: 0644]
old_api_code/apis/selectricity_api.rb [moved from app/apis/selectricity_api.rb with 100% similarity]
old_api_code/graph_controller_test.rb [moved from test/functional/graph_controller_test.rb with 100% similarity]
old_api_code/selectricity_service.rb [moved from app/models/selectricity_service.rb with 100% similarity]
old_api_code/selectricity_service_controller.rb [moved from app/controllers/selectricity_service_controller.rb with 100% similarity]
old_api_code/selectricityservice_test.rb [moved from test/unit/selectricityservice_test.rb with 100% similarity]
public/javascripts/controls.js
public/javascripts/dragdrop.js
public/javascripts/effects.js
public/javascripts/prototype.js
script/dbconsole [new file with mode: 0755]
script/performance/request [new file with mode: 0755]
test/functional/sparklines_controller_test.rb [deleted file]
vendor/plugins/acts_as_list/README [new file with mode: 0644]
vendor/plugins/acts_as_list/init.rb [new file with mode: 0644]
vendor/plugins/acts_as_list/lib/active_record/acts/list.rb [new file with mode: 0644]
vendor/plugins/acts_as_list/test/list_test.rb [new file with mode: 0644]
vendor/plugins/sparklines/MIT-LICENSE [deleted file]
vendor/plugins/sparklines/README [deleted file]
vendor/plugins/sparklines/Rakefile [deleted file]
vendor/plugins/sparklines/about.yml [deleted file]
vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb [deleted file]
vendor/plugins/sparklines/generators/sparklines/templates/controller.rb [deleted file]
vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb [deleted file]
vendor/plugins/sparklines/init.rb [deleted file]
vendor/plugins/sparklines/lib/sparklines.rb [deleted file]

index 3e169646bac4d583087fc585c36304fc186f9999..5c13a8128cdfa6781240bc6b9937d0e533482afd 100644 (file)
@@ -45,6 +45,7 @@ class ElectionController < ApplicationController
     @election.user = session[:user]
     @election.anonymous = 1
     @election.startdate = Time.now
     @election.user = session[:user]
     @election.anonymous = 1
     @election.startdate = Time.now
+    @election.type = 'Election'
     
     holder = create_theme_hash
     unless holder.values.all? {|v| v.has_value?("")}
     
     holder = create_theme_hash
     unless holder.values.all? {|v| v.has_value?("")}
index 768bdea261ba28000363e25e4da6d3c24b61e7e9..c32d585dd53cc441d8e36d7a72fe74be9edaec90 100644 (file)
@@ -17,6 +17,7 @@
 # <http://www.gnu.org/licenses/>.
 
 class QuickvoteController < ApplicationController
 # <http://www.gnu.org/licenses/>.
 
 class QuickvoteController < ApplicationController
+  helper :sparklines
   layout 'main'
   require_dependency "quick_voter"
   require_dependency "quick_vote"
   layout 'main'
   require_dependency "quick_voter"
   require_dependency "quick_vote"
@@ -94,7 +95,7 @@ class QuickvoteController < ApplicationController
       end
     end
     flash.keep(:candidate_names)
       end
     end
     flash.keep(:candidate_names)
-    render_partial 'candidate_list'
+    render :partial => 'candidate_list'
   end
  
   #############################################################
   end
  
   #############################################################
@@ -163,9 +164,6 @@ class QuickvoteController < ApplicationController
       @voter.ipaddress = request.env["REMOTE_ADDR"]
       @voter.save
       
       @voter.ipaddress = request.env["REMOTE_ADDR"]
       @voter.save
       
-      # save the time the vote was made for statistical use
-      @voter.vote.time = Time.now
-      
       # toggle the confirmation bit      
       @voter.vote.confirm!
      
       # toggle the confirmation bit      
       @voter.vote.confirm!
      
index d08bbc569fda37058dd37cf748484fa50f06854d..50a330fc7afda4aaaed46a9e3b82c047766db023 100644 (file)
@@ -1,61 +1,30 @@
-# Selectricity: Voting Machinery for the Masses
-# Copyright (C) 2007, 2008 Benjamin Mako Hill <mako@atdot.cc>
-# Copyright (C) 2007 Massachusetts Institute of Technology
+
+# Handles requests for sparkline graphs.
 #
 #
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
+# You shouldn't need to edit or extend this, but you can read
+# the documentation for SparklinesHelper to see how to call it from
+# another view.
 #
 #
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Affero General Public License for more details.
+# AUTHOR
+# 
+# Geoffrey Grosenbach[mailto:boss@topfunky.com]
+#
+# http://topfunky.com
 #
 #
-# You should have received a copy of the GNU Affero General Public
-# License along with this program.  If not, see
-# <http://www.gnu.org/licenses/>.
-
 class SparklinesController < ApplicationController
 class SparklinesController < ApplicationController
-    # Handles requests for sparkline graphs from views.
-    #
-    # Params are generated by the sparkline_tag helper method.
-    #
-       def index
-               # Make array from comma-delimited list of data values
-               ary = []
-               if params.has_key?('results') && !params['results'].nil?
-                 params['results'].split(',').each do |s|
-                         ary << s.to_i
-                 end
-               end
+       layout nil
+
+       def index
+               # Make array from comma-delimited list of data values
+               ary = []
+               params['results'].split(',').each do |s|
+                       ary << s.to_i
+               end
+               
+               send_data( Sparklines.plot( ary, params ), 
+                                       :disposition => 'inline',
+                                       :type => 'image/png',
+                                       :filename => "spark_#{params[:type]}.png" )
+       end
 
 
-               send_data( Sparklines.plot( ary, params ), 
-                                       :disposition => 'inline',
-                                       :type => 'image/png',
-                                       :filename => "spark_#{params[:type]}.png" )
-       end
-    
-    def spark_pie
-      send_data(Sparklines.plot( ))
-    end
-    # Use this type of method for sparklines that can be cached. (Doesn't work with the helper.)
-    #
-    # To make caching easier, add a line like this to config/routes.rb:
-    # map.sparklines "sparklines/:action/:id/image.png", :controller => "sparklines"
-    #
-    # Then reference it with the named route:
-    #   image_tag sparklines_url(:action => 'show', :id => 42)
-    def show
-      send_data(Sparklines.plot(
-                  [42, 37, 89, 74, 70, 50, 40, 30, 40, 50],
-                  :type => 'bar', :above_color => 'orange'
-                ), 
-                :disposition => 'inline', 
-                :type => 'image/png', 
-                :filename => "sparkline.png")
-    end
-  
-  
-  
 end
 end
index 9e88e9e282b048cfe1b0e9177081fc1300424873..45b9a1a85f950cec5d54320942917be65071352c 100644 (file)
@@ -17,6 +17,7 @@
 # <http://www.gnu.org/licenses/>.
 
 class VoterController < ApplicationController
 # <http://www.gnu.org/licenses/>.
 
 class VoterController < ApplicationController
+  helper :sparklines
   layout 'main'
   require_dependency "voter"
   require_dependency "vote"
   layout 'main'
   require_dependency "voter"
   require_dependency "vote"
@@ -106,8 +107,8 @@ class VoterController < ApplicationController
   def details
     if authenticate
       @election = @voter.election
   def details
     if authenticate
       @election = @voter.election
-      @votes = @election.votes.select {|v| v.confirmed? }.randomize
-      @voters = @votes.collect {|v| v.voter}.randomize
+      @votes = @election.votes.select {|v| v.confirmed? }.shuffle
+      @voters = @votes.collect {|v| v.voter}.shuffle
       render :action => 'details'
     else
       redirect_to :action => 'index'
       render :action => 'details'
     else
       redirect_to :action => 'index'
diff --git a/app/helpers/front_helper.rb b/app/helpers/front_helper.rb
deleted file mode 100644 (file)
index c879486..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-module SiteHelper
-end
similarity index 55%
rename from vendor/plugins/sparklines/lib/sparklines_helper.rb
rename to app/helpers/sparklines_helper.rb
index c8958d3399504f525de4d389c132ba564ea6356e..9376e89b9e86fc60f5b5021897df8d8d8cb6d957 100644 (file)
@@ -1,18 +1,30 @@
+
 # Provides a tag for embedding sparklines graphs into your Rails app.
 #
 # Provides a tag for embedding sparklines graphs into your Rails app.
 #
+# To use, load it in your controller with
+#
+#   helper :sparklines
+#
+# AUTHOR
+#
+# Geoffrey Grosenbach[mailto:boss@topfunky.com]
+#
+# http://topfunky.com
+# 
+# License
+#
+# This code is licensed under the MIT license.
+#
 module SparklinesHelper
 
        # Call with an array of data and a hash of params for the Sparklines module.
 module SparklinesHelper
 
        # Call with an array of data and a hash of params for the Sparklines module.
-  #
-  #  sparkline_tag [42, 37, 43, 182], :type => 'bar', :line_color => 'black'
-  #
        # You can also pass :class => 'some_css_class' ('sparkline' by default).
        def sparkline_tag(results=[], options={})               
                url = { :controller => 'sparklines',
                        :results => results.join(',') }
                options = url.merge(options)
                
        # You can also pass :class => 'some_css_class' ('sparkline' by default).
        def sparkline_tag(results=[], options={})               
                url = { :controller => 'sparklines',
                        :results => results.join(',') }
                options = url.merge(options)
                
-               %(<img src="#{ url_for options }" class="#{options[:class] || 'sparkline'}" alt="Sparkline Graph" />)
+               "<img src=\"#{ url_for options }\" class=\"#{options[:class] || 'sparkline'}\" alt=\"Sparkline Graph\" />"
        end
 
 end
        end
 
 end
index bef35020a45b1a7d37ca25fdd86dec73259f8ab5..d3010dfeac754978883be4d75a9cc755ee5dca90 100644 (file)
@@ -39,7 +39,7 @@ class Vote < ActiveRecord::Base
       if rankings.empty?
         @votes = Array.new
       else
       if rankings.empty?
         @votes = Array.new
       else
-        @votes = rankings.sort.collect { |ranking| ranking.candidate.id }
+        @votes = self.rankings.sort.collect { |ranking| ranking.candidate.id }
       end
     end
 
       end
     end
 
@@ -51,6 +51,9 @@ class Vote < ActiveRecord::Base
   end
 
   def save_rankings
   end
 
   def save_rankings
+    self.votes # i need to initalize this before destroying rankings
+               # or else the ranks themselves show up as nil
+
     destroy_rankings
     self.votes.each_with_index do |candidate_id, index| 
       ranking = Ranking.new
     destroy_rankings
     self.votes.each_with_index do |candidate_id, index| 
       ranking = Ranking.new
index 1cb5fae3ff20417d91968a43dff94ba353360412..743dd455fd672d50198b25626863f6e55f73d856 100644 (file)
@@ -13,7 +13,7 @@ another Condorcet system.</p>
 
 <% candidates = @election.ssd_result.ranked_candidates.flatten -%>
 <% if candidates.size <= 7 -%>
 
 <% candidates = @election.ssd_result.ranked_candidates.flatten -%>
 <% if candidates.size <= 7 -%>
-  <%= render_partial 'common/pref_tables' %>
+  <%= render :partial => 'common/pref_tables' %>
 <% else %>
 
   There are too many candidates in your elections to show the result
 <% else %>
 
   There are too many candidates in your elections to show the result
index 25bc3546c38cd4c4e42e629b5c7ac21bf52351b9..00f9febe01d0270a57a13c5b840a5c4bd06bfe24 100644 (file)
@@ -25,7 +25,7 @@ Beatpath Winner, Path Voting, and Path Winner.</p>
 
 <% candidates = @election.ssd_result.ranked_candidates.flatten -%>
 <% if candidates.size <= 7 -%>
 
 <% candidates = @election.ssd_result.ranked_candidates.flatten -%>
 <% if candidates.size <= 7 -%>
-  <%= render_partial 'common/pref_tables' %>
+  <%= render :partial => 'common/pref_tables' %>
 <% else %>
 
   There are too many candidates in your elections to show the result
 <% else %>
 
   There are too many candidates in your elections to show the result
index 1215c7bcbc1aa08f6e1c052e46b94c01e305d40e..55b864fd062d29ca598d20dd5f434436ae0e1030 100644 (file)
@@ -1 +1 @@
-<%= render_partial 'common/pref_tables' %>
+<%= render :partial => 'common/pref_tables' %>
index 8d1861d00b56b3d754ab1f54c4dd2f5692cbfccb..eed92da7181a180612cb918debc641010b005473 100644 (file)
 <% type_hash = {}; ELECTION_TYPES.each {|k,v| type_hash[v] = k} %>
 <%= select_tag 'election[election_method]', options_for_select(type_hash, @election.election_method) %></p>
 
 <% type_hash = {}; ELECTION_TYPES.each {|k,v| type_hash[v] = k} %>
 <%= select_tag 'election[election_method]', options_for_select(type_hash, @election.election_method) %></p>
 
-<p><label for="election_election_method">Enable Embeddable Elections?</label>
+<!-- <p><label for="election_election_method">Enable Embeddable Elections?</label>
 <%= check_box :election, :embeddable %></p>
 
 <p><label for="election_election_method">Should results be visible before end of election?</label>
 <%= check_box :election, :early_results%></p>
 <%= check_box :election, :embeddable %></p>
 
 <p><label for="election_election_method">Should results be visible before end of election?</label>
 <%= check_box :election, :early_results%></p>
+-->
 <!--[eoform:election]-->
 
 <!--[eoform:election]-->
 
index 7c29c21035ba8c8fdab84deb1a8073ecb9fcc843..cc6baa92aa1750c4261fb40d55b34e512788c470 100644 (file)
@@ -22,4 +22,4 @@
                Top Bar/ Bottom Bar/ Alternating Backgrounds: 330px wide 53px tall
                <br />
                Default Image: 70px wide 53px tall
                Top Bar/ Bottom Bar/ Alternating Backgrounds: 330px wide 53px tall
                <br />
                Default Image: 70px wide 53px tall
-</p>
\ No newline at end of file
+</p>
index eae756ad58560e577c4ff25410709f12bd17f9c0..1f22377045021aff7704c43a61c3980792f062ef 100644 (file)
@@ -1,12 +1,10 @@
-<% %>
-
 <h2>Result</h2>
 
 <h2>Result</h2>
 
-<%= render_partial 'winner' %>
+<%= render :partial => 'winner' %>
 
 <h2>Result Details</h2>
 
 
 <h2>Result Details</h2>
 
-<%= render_partial 'winner_details' %>
+<%= render :partial => 'winner_details' %>
 
 <h2>Election Rolls for Voter Verification</h2>
 
 
 <h2>Election Rolls for Voter Verification</h2>
 
index feed407dd23bc7ed4ae5548354c0c2e7679b15bf..b7abd5cca167b9239b09eb2838708c27bca65dc6 100644 (file)
@@ -5,6 +5,6 @@
 
 <% form_tag( {:action => 'create_election'}, :multipart => true ) do %>
   <%= render :partial => 'overview_form' %>
 
 <% form_tag( {:action => 'create_election'}, :multipart => true ) do %>
   <%= render :partial => 'overview_form' %>
-  <%= render :partial => 'theme_upload' %>
+<!--  <%= render :partial => 'theme_upload' %> -->
   <%= submit_tag "Proceed to Next Step" %>
 <% end %>
   <%= submit_tag "Proceed to Next Step" %>
 <% end %>
index a7e9327fb320eb5ac638db6803f28f370ec2b138..ef374d9f7001a05cac65ec2a2c620eda39c07f19 100644 (file)
@@ -18,7 +18,7 @@
   </div>
 
     <div class="clear-div"></div>
   </div>
 
     <div class="clear-div"></div>
-    <%= render_partial 'layouts/footer' %>
+    <%= render :partial => 'layouts/footer' %>
     </div>
   </body>
 </html>
     </div>
   </body>
 </html>
index 0b6067f837c2d6ce6ca5c3b702981aee5bc7aa57..9abd31e2c58156fce26eb827b2f300fe6c2a5a90 100644 (file)
@@ -33,7 +33,7 @@
     <%= @content_for_layout %>
   </div>
   
     <%= @content_for_layout %>
   </div>
   
-  <%= render_partial 'layouts/footer' %>
+  <%= render :partial => 'layouts/footer' %>
 </div>
 </body>
 
 </div>
 </body>
 
index 106528db597c8c39ddc2b7253e4f353ae3d4cf44..75f196ae3c4c9cb9fbcc4c28bacd008802167cab 100644 (file)
@@ -55,7 +55,7 @@
 
     </div> <!-- end of right-side -->
     <div class="clear-div"></div>
 
     </div> <!-- end of right-side -->
     <div class="clear-div"></div>
-    <%= render_partial 'layouts/footer' %>
+    <%= render :partial => 'layouts/footer' %>
     </div>
   </body>
 </html>
     </div>
   </body>
 </html>
index 13e0dd057075605435bee8997f9c0db1062862e0..31974a2ed9cdb6530495ffe0b608aaa4a264e602 100644 (file)
@@ -1,5 +1,5 @@
 <!-- common data to all sidebars -->
 <!-- common data to all sidebars -->
-<%= render_partial 'common/results_sidebar' %>
+<%= render :partial => 'common/results_sidebar' %>
 
 <h2>Voter Report</h2>
 
 
 <h2>Voter Report</h2>
 
index 3a9ebb01fb95e2907de1460fe7afb47d0e454fc7..81cc9079e8b24e91a037ae3458deb0dbab1389f6 100644 (file)
@@ -1,5 +1,5 @@
 <!-- common data to all sidebars -->
 <!-- common data to all sidebars -->
-<%= render_partial 'common/results_sidebar' %>
+<%= render :partial => 'common/results_sidebar' %>
 
 <h2>Details</h2>
 
 
 <h2>Details</h2>
 
index b7af0c35c7e78314768de378587c84519ca55024..0a516880cadc333d0efc41367a56a09dd0654e87 100644 (file)
-# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
 
 
-unless defined?(RAILS_ROOT)
-  root_path = File.join(File.dirname(__FILE__), '..')
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
 
 
-  unless RUBY_PLATFORM =~ /(:?mswin|mingw)/
-    require 'pathname'
-    root_path = Pathname.new(root_path).cleanpath(true).to_s
-  end
+module Rails
+  class << self
+    def boot!
+      unless booted?
+        preinitialize
+        pick_boot.run
+      end
+    end
 
 
-  RAILS_ROOT = root_path
-end
+    def booted?
+      defined? Rails::Initializer
+    end
 
 
-unless defined?(Rails::Initializer)
-  if File.directory?("#{RAILS_ROOT}/vendor/rails")
-    require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
-  else
-    require 'rubygems'
+    def pick_boot
+      (vendor_rails? ? VendorBoot : GemBoot).new
+    end
+
+    def vendor_rails?
+      File.exist?("#{RAILS_ROOT}/vendor/rails")
+    end
 
 
-    environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join
-    environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/
-    rails_gem_version = $1
+    def preinitialize
+      load(preinitializer_path) if File.exist?(preinitializer_path)
+    end
+
+    def preinitializer_path
+      "#{RAILS_ROOT}/config/preinitializer.rb"
+    end
+  end
+
+  class Boot
+    def run
+      load_initializer
+      Rails::Initializer.run(:set_load_path)
+    end
+  end
+
+  class VendorBoot < Boot
+    def load_initializer
+      require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+      Rails::Initializer.run(:install_gem_spec_stubs)
+    end
+  end
 
 
-    if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version
-      # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems
-      rails_gem = Gem.cache.search('rails', "~>#{version}.0").sort_by { |g| g.version.version }.last
+  class GemBoot < Boot
+    def load_initializer
+      self.class.load_rubygems
+      load_rails_gem
+      require 'initializer'
+    end
 
 
-      if rails_gem
-        gem "rails", "=#{rails_gem.version.version}"
-        require rails_gem.full_gem_path + '/lib/initializer'
+    def load_rails_gem
+      if version = self.class.gem_version
+        gem 'rails', version
       else
       else
-        STDERR.puts %(Cannot find gem for Rails ~>#{version}.0:
-    Install the missing gem with 'gem install -v=#{version} rails', or
-    change environment.rb to define RAILS_GEM_VERSION with your desired version.
-  )
+        gem 'rails'
+      end
+    rescue Gem::LoadError => load_error
+      $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+      exit 1
+    end
+
+    class << self
+      def rubygems_version
+        Gem::RubyGemsVersion rescue nil
+      end
+
+      def gem_version
+        if defined? RAILS_GEM_VERSION
+          RAILS_GEM_VERSION
+        elsif ENV.include?('RAILS_GEM_VERSION')
+          ENV['RAILS_GEM_VERSION']
+        else
+          parse_gem_version(read_environment_rb)
+        end
+      end
+
+      def load_rubygems
+        require 'rubygems'
+        min_version = '1.3.1'
+        unless rubygems_version >= min_version
+          $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+          exit 1
+        end
+
+      rescue LoadError
+        $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
         exit 1
       end
         exit 1
       end
-    else
-      gem "rails"
-      require 'initializer'
+
+      def parse_gem_version(text)
+        $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+      end
+
+      private
+        def read_environment_rb
+          File.read("#{RAILS_ROOT}/config/environment.rb")
+        end
     end
   end
     end
   end
-
-  Rails::Initializer.run(:set_load_path)
 end
 end
+
+# All that for this:
+Rails.boot!
index 32d313b2ed47ea9e358256050c62d6f38f1519cd..28ecf9eca9b69a83661596fc3da8790ab4524f3c 100644 (file)
@@ -5,7 +5,7 @@
 # ENV['RAILS_ENV'] ||= 'production'
 
 # Specifies gem version of Rails to use when vendor/rails is not present
 # ENV['RAILS_ENV'] ||= 'production'
 
 # Specifies gem version of Rails to use when vendor/rails is not present
-RAILS_GEM_VERSION = '1.2.3' unless defined? RAILS_GEM_VERSION
+RAILS_GEM_VERSION = '2.2.2' unless defined? RAILS_GEM_VERSION
 
 # Bootstrap the Rails environment, frameworks, and default configuration
 require File.join(File.dirname(__FILE__), 'boot')
 
 # Bootstrap the Rails environment, frameworks, and default configuration
 require File.join(File.dirname(__FILE__), 'boot')
@@ -62,7 +62,6 @@ end
 MAIL_CONFIG = { :from => 'Selectricity <team@selectricity.org>'} 
 
 require 'uniq_token'
 MAIL_CONFIG = { :from => 'Selectricity <team@selectricity.org>'} 
 
 require 'uniq_token'
-require 'randarray'
 require 'gruff-0.2.8/lib/gruff'
 require 'sparklines'
 require 'rubyvote'
 require 'gruff-0.2.8/lib/gruff'
 require 'sparklines'
 require 'rubyvote'
index 198c2bdafe91f92df9e624308df375b3dec695b2..2d7f9789e0a4bf8cb1e9388defc71e218665520f 100644 (file)
@@ -9,13 +9,9 @@ config.cache_classes = false
 # Log error messages when you accidentally call methods on nil.
 config.whiny_nils = true
 
 # Log error messages when you accidentally call methods on nil.
 config.whiny_nils = true
 
-# Enable the breakpoint server that script/breakpointer connects to
-config.breakpoint_server = true
-
 # Show full error reports and disable caching
 config.action_controller.consider_all_requests_local = true
 config.action_controller.perform_caching             = false
 # Show full error reports and disable caching
 config.action_controller.consider_all_requests_local = true
 config.action_controller.perform_caching             = false
-config.action_view.cache_template_extensions         = false
 config.action_view.debug_rjs                         = true
 
 # Don't care if the mailer can't send
 config.action_view.debug_rjs                         = true
 
 # Don't care if the mailer can't send
index e3f8d16363eb0ecaacb2889a0cb90022b606c2d9..0d99d2e959895f1b34b81551bdd0c25e5fb8926d 100644 (file)
-# This file is autogenerated. Instead of editing this file, please use the
-# migrations feature of ActiveRecord to incrementally modify your database, and
+# This file is auto-generated from the current state of the database. Instead of editing this file, 
+# please use the migrations feature of Active Record to incrementally modify your database, and
 # then regenerate this schema definition.
 # then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your database schema. If you need
+# to create the application database on another system, you should be using db:schema:load, not running
+# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended to check this file into your version control system.
 
 ActiveRecord::Schema.define(:version => 3) do
 
   create_table "candidates", :force => true do |t|
 
 ActiveRecord::Schema.define(:version => 3) do
 
   create_table "candidates", :force => true do |t|
-    t.column "election_id", :integer,                                :null => false
-    t.column "name",        :string,  :limit => 100, :default => "", :null => false
-    t.column "description", :text
+    t.integer "election_id",                :null => false
+    t.string  "name",        :limit => 100, :null => false
+    t.text    "description"
   end
 
   create_table "elections", :force => true do |t|
   end
 
   create_table "elections", :force => true do |t|
-    t.column "name",                :string,   :limit => 100, :default => "",    :null => false
-    t.column "description",         :text,                    :default => "",    :null => false
-    t.column "anonymous",           :integer,  :limit => 4,   :default => 1,     :null => false
-    t.column "startdate",           :datetime
-    t.column "enddate",             :datetime,                                   :null => false
-    t.column "active",              :integer,  :limit => 4,   :default => 0,     :null => false
-    t.column "viewable",            :integer,  :limit => 4,   :default => 1,     :null => false
-    t.column "notices",             :integer,  :limit => 4,   :default => 0,     :null => false
-    t.column "user_id",             :integer
-    t.column "quickuser",           :string
-    t.column "election_method",     :string,   :limit => 100, :default => "ssd"
-    t.column "type",                :string,   :limit => 100, :default => "",    :null => false
-    t.column "embeddable",          :boolean,                 :default => false, :null => false
-    t.column "authenticated",       :boolean,                 :default => true,  :null => false
-    t.column "early_results",       :boolean,                 :default => false, :null => false
-    t.column "embed_custom_string", :string
+    t.string   "name",                :limit => 100,                    :null => false
+    t.text     "description",                                           :null => false
+    t.integer  "anonymous",           :limit => 1,   :default => 1,     :null => false
+    t.datetime "startdate"
+    t.datetime "enddate",                                               :null => false
+    t.integer  "active",              :limit => 1,   :default => 0,     :null => false
+    t.integer  "viewable",            :limit => 1,   :default => 1,     :null => false
+    t.integer  "notices",             :limit => 1,   :default => 0,     :null => false
+    t.integer  "user_id"
+    t.string   "quickuser"
+    t.string   "election_method",     :limit => 100, :default => "ssd"
+    t.string   "type",                :limit => 100,                    :null => false
+    t.boolean  "embeddable",                         :default => false, :null => false
+    t.boolean  "authenticated",                      :default => true,  :null => false
+    t.boolean  "early_results",                      :default => false, :null => false
+    t.string   "embed_custom_string"
   end
 
   add_index "elections", ["user_id"], :name => "fk_user_election"
 
   create_table "pictures", :force => true do |t|
   end
 
   add_index "elections", ["user_id"], :name => "fk_user_election"
 
   create_table "pictures", :force => true do |t|
-    t.column "filename",     :string
-    t.column "content_type", :string
-    t.column "size",         :integer
-    t.column "width",        :integer
-    t.column "height",       :integer
-    t.column "parent_id",    :integer
-    t.column "thumbnail",    :string
-    t.column "created_at",   :datetime
-    t.column "candidate_id", :integer
+    t.string   "filename"
+    t.string   "content_type"
+    t.integer  "size"
+    t.integer  "width"
+    t.integer  "height"
+    t.integer  "parent_id"
+    t.string   "thumbnail"
+    t.datetime "created_at"
+    t.integer  "candidate_id"
   end
 
   create_table "rankings", :force => true do |t|
   end
 
   create_table "rankings", :force => true do |t|
-    t.column "vote_id",      :integer
-    t.column "candidate_id", :integer
-    t.column "rank",         :integer
+    t.integer "vote_id"
+    t.integer "candidate_id"
+    t.integer "rank"
   end
 
   create_table "sessions", :force => true do |t|
   end
 
   create_table "sessions", :force => true do |t|
-    t.column "sessid",     :string
-    t.column "data",       :text
-    t.column "updated_at", :datetime
+    t.string   "sessid"
+    t.text     "data"
+    t.datetime "updated_at"
   end
 
   add_index "sessions", ["sessid"], :name => "session_index"
 
   create_table "sitealizer", :force => true do |t|
   end
 
   add_index "sessions", ["sessid"], :name => "session_index"
 
   create_table "sitealizer", :force => true do |t|
-    t.column "path",       :string
-    t.column "ip",         :string
-    t.column "referer",    :string
-    t.column "language",   :string
-    t.column "user_agent", :string
-    t.column "created_at", :datetime
-    t.column "created_on", :date
+    t.string   "path"
+    t.string   "ip"
+    t.string   "referer"
+    t.string   "language"
+    t.string   "user_agent"
+    t.datetime "created_at"
+    t.date     "created_on"
   end
 
   create_table "skin_pictures", :force => true do |t|
   end
 
   create_table "skin_pictures", :force => true do |t|
-    t.column "filename",     :string
-    t.column "content_type", :string
-    t.column "size",         :integer
-    t.column "width",        :integer
-    t.column "height",       :integer
-    t.column "created_at",   :datetime
-    t.column "user_id",      :integer
+    t.string   "filename"
+    t.string   "content_type"
+    t.integer  "size"
+    t.integer  "width"
+    t.integer  "height"
+    t.datetime "created_at"
+    t.integer  "user_id"
   end
 
   create_table "tokens", :force => true do |t|
   end
 
   create_table "tokens", :force => true do |t|
-    t.column "token",   :string,  :limit => 100, :default => "", :null => false
-    t.column "vote_id", :integer,                                :null => false
+    t.string  "token",   :limit => 100, :null => false
+    t.integer "vote_id",                :null => false
   end
 
   add_index "tokens", ["vote_id"], :name => "fk_vote_token"
 
   create_table "users", :force => true do |t|
   end
 
   add_index "tokens", ["vote_id"], :name => "fk_vote_token"
 
   create_table "users", :force => true do |t|
-    t.column "login",                     :text
-    t.column "ip",                        :text,                   :default => "", :null => false
-    t.column "email",                     :text
-    t.column "crypted_password",          :string,   :limit => 40
-    t.column "salt",                      :string,   :limit => 40
-    t.column "created_at",                :datetime
-    t.column "updated_at",                :datetime
-    t.column "remember_token",            :text
-    t.column "remember_token_expires_at", :datetime
+    t.text     "login"
+    t.text     "ip",                                      :null => false
+    t.text     "email"
+    t.string   "crypted_password",          :limit => 40
+    t.string   "salt",                      :limit => 40
+    t.datetime "created_at"
+    t.datetime "updated_at"
+    t.text     "remember_token"
+    t.datetime "remember_token_expires_at"
   end
 
   create_table "voters", :force => true do |t|
   end
 
   create_table "voters", :force => true do |t|
-    t.column "email",       :string,  :limit => 100
-    t.column "password",    :string,  :limit => 100
-    t.column "contacted",   :integer, :limit => 4,   :default => 0,  :null => false
-    t.column "election_id", :integer,                                :null => false
-    t.column "session_id",  :string,  :limit => 32
-    t.column "ipaddress",   :string,  :limit => 32
-    t.column "type",        :string,  :limit => 100, :default => "", :null => false
+    t.string  "email",       :limit => 100
+    t.string  "password",    :limit => 100
+    t.integer "contacted",   :limit => 1,   :default => 0, :null => false
+    t.integer "election_id",                               :null => false
+    t.string  "session_id",  :limit => 32
+    t.string  "ipaddress",   :limit => 32
+    t.string  "type",        :limit => 100,                :null => false
   end
 
   add_index "voters", ["election_id"], :name => "fk_election_voter"
 
   create_table "votes", :force => true do |t|
   end
 
   add_index "voters", ["election_id"], :name => "fk_election_voter"
 
   create_table "votes", :force => true do |t|
-    t.column "voter_id",  :integer
-    t.column "confirmed", :integer,  :limit => 4, :default => 0, :null => false
-    t.column "time",      :datetime
+    t.integer  "voter_id"
+    t.integer  "confirmed", :limit => 1, :default => 0, :null => false
+    t.datetime "time"
   end
 
   add_index "votes", ["voter_id"], :name => "fk_vote_voter"
   end
 
   add_index "votes", ["voter_id"], :name => "fk_vote_voter"
diff --git a/lib/randarray.rb b/lib/randarray.rb
deleted file mode 100644 (file)
index 17961d1..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-class Array
-  # Chooses a random array element from the receiver based on the weights
-  # provided. If _weights_ is nil, then each element is weighed equally.
-  # 
-  #   [1,2,3].random          #=> 2
-  #   [1,2,3].random          #=> 1
-  #   [1,2,3].random          #=> 3
-  #
-  # If _weights_ is an array, then each element of the receiver gets its
-  # weight from the corresponding element of _weights_. Notice that it
-  # favors the element with the highest weight.
-  #
-  #   [1,2,3].random([1,4,1]) #=> 2
-  #   [1,2,3].random([1,4,1]) #=> 1
-  #   [1,2,3].random([1,4,1]) #=> 2
-  #   [1,2,3].random([1,4,1]) #=> 2
-  #   [1,2,3].random([1,4,1]) #=> 3
-  #
-  # If _weights_ is a symbol, the weight array is constructed by calling
-  # the appropriate method on each array element in turn. Notice that
-  # it favors the longer word when using :length.
-  #
-  #   ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus"
-  #   ['dog', 'cat', 'hippopotamus'].random(:length) #=> "dog"
-  #   ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus"
-  #   ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus"
-  #   ['dog', 'cat', 'hippopotamus'].random(:length) #=> "cat"
-  def random(weights=nil)
-    return random(map {|n| n.send(weights)}) if weights.is_a? Symbol
-    
-    weights ||= Array.new(length, 1.0)
-    total = weights.inject(0.0) {|t,w| t+w}
-    point = rand * total
-    
-    zip(weights).each do |n,w|
-      return n if w >= point
-      point -= w
-    end
-  end
-  
-  # Generates a permutation of the receiver based on _weights_ as in
-  # Array#random. Notice that it favors the element with the highest
-  # weight.
-  #
-  #   [1,2,3].randomize           #=> [2,1,3]
-  #   [1,2,3].randomize           #=> [1,3,2]
-  #   [1,2,3].randomize([1,4,1])  #=> [2,1,3]
-  #   [1,2,3].randomize([1,4,1])  #=> [2,3,1]
-  #   [1,2,3].randomize([1,4,1])  #=> [1,2,3]
-  #   [1,2,3].randomize([1,4,1])  #=> [2,3,1]
-  #   [1,2,3].randomize([1,4,1])  #=> [3,2,1]
-  #   [1,2,3].randomize([1,4,1])  #=> [2,1,3]
-  def randomize(weights=nil)
-    return randomize(map {|n| n.send(weights)}) if weights.is_a? Symbol
-    
-    weights = weights.nil? ? Array.new(length, 1.0) : weights.dup
-    
-    # pick out elements until there are none left
-    list, result = self.dup, []
-    until list.empty?
-      # pick an element
-      result << list.random(weights)
-      # remove the element from the temporary list and its weight
-      weights.delete_at(list.index(result.last))
-      list.delete result.last
-    end
-    
-    result
-  end
-end
diff --git a/lib/tasks/rails.rake b/lib/tasks/rails.rake
new file mode 100644 (file)
index 0000000..99d3321
--- /dev/null
@@ -0,0 +1,32 @@
+desc "Checks your app and gently warns you if you are using deprecated code." 
+task :deprecated => :environment do
+  deprecated = {
+    '@params'    => 'Use params[] instead',
+    '@session'   => 'Use session[] instead',
+    '@flash'     => 'Use flash[] instead',
+    '@request'   => 'Use request[] instead',
+    '@env' => 'Use env[] instead',
+    'find_all'   => 'Use find(:all) instead',
+    'find_first' => 'Use find(:first) instead',
+    'render_partial' => 'Use render :partial instead',
+    'component'  => 'Use of components are frowned upon',
+    'paginate'   => 'The default paginator is slow. Writing your own may be faster',
+    'start_form_tag'   => 'Use form_for instead',
+    'end_form_tag'   => 'Use form_for instead',
+    ':post => true'   => 'Use :method => :post instead'
+  }
+
+  deprecated.each do |key, warning|
+    puts '--> ' + key
+    output = `cd '#{File.expand_path('app', RAILS_ROOT)}' && grep -n --exclude=*.svn* -r '#{key}' *`
+    unless output =~ /^$/
+      puts "  !! " + warning + " !!" 
+      puts '  ' + '.' * (warning.length + 6)
+      puts output
+    else
+      puts "  Clean! Cheers for you!" 
+    end
+    puts
+  end
+end
+
index 8c273f874f91753b0ba78389f892e18cf6335e88..ca29aefdd1fb255e3da9909437735b5c84492ae5 100644 (file)
@@ -1,22 +1,22 @@
-// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-//           (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
-//           (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
 // Contributors:
 //  Richard Livsey
 //  Rahul Bhargava
 //  Rob Wills
 // Contributors:
 //  Richard Livsey
 //  Rahul Bhargava
 //  Rob Wills
-// 
+//
 // script.aculo.us is freely distributable under the terms of an MIT-style license.
 // For details, see the script.aculo.us web site: http://script.aculo.us/
 
 // script.aculo.us is freely distributable under the terms of an MIT-style license.
 // For details, see the script.aculo.us web site: http://script.aculo.us/
 
-// Autocompleter.Base handles all the autocompletion functionality 
+// 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.
 //
 // 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, 
+// Specific autocompleters need to provide, at the very least,
 // a getUpdatedChoices function that will be invoked every time
 // a getUpdatedChoices function that will be invoked every time
-// the text inside the monitored textbox changes. This method 
+// 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
 // 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
 // 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
 // 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 
+// 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.
 
 if(typeof Effect == 'undefined')
   throw("controls.js requires including script.aculo.us' effects.js library");
 
 // allows smart autocompletion after linebreaks.
 
 if(typeof Effect == 'undefined')
   throw("controls.js requires including script.aculo.us' effects.js library");
 
-var Autocompleter = {}
-Autocompleter.Base = function() {};
-Autocompleter.Base.prototype = {
+var Autocompleter = { };
+Autocompleter.Base = Class.create({
   baseInitialize: function(element, update, options) {
   baseInitialize: function(element, update, options) {
-    this.element     = $(element); 
-    this.update      = $(update);  
-    this.hasFocus    = false; 
-    this.changed     = false; 
-    this.active      = false; 
-    this.index       = 0;     
+    element          = $(element);
+    this.element     = element;
+    this.update      = $(update);
+    this.hasFocus    = false;
+    this.changed     = false;
+    this.active      = false;
+    this.index       = 0;
     this.entryCount  = 0;
     this.entryCount  = 0;
+    this.oldElementValue = this.element.value;
 
     if(this.setOptions)
       this.setOptions(options);
     else
 
     if(this.setOptions)
       this.setOptions(options);
     else
-      this.options = options || {};
+      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.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){ 
+    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, {
         if(!update.style.position || update.style.position=='absolute') {
           update.style.position = 'absolute';
           Position.clone(element, update, {
-            setHeight: false, 
+            setHeight: false,
             offsetTop: element.offsetHeight
           });
         }
         Effect.Appear(update,{duration:0.15});
       };
             offsetTop: element.offsetHeight
           });
         }
         Effect.Appear(update,{duration:0.15});
       };
-    this.options.onHide = this.options.onHide || 
+    this.options.onHide = this.options.onHide ||
       function(element, update){ new Effect.Fade(update,{duration:0.15}) };
 
       function(element, update){ new Effect.Fade(update,{duration:0.15}) };
 
-    if(typeof(this.options.tokens) == 'string') 
+    if(typeof(this.options.tokens) == 'string')
       this.options.tokens = new Array(this.options.tokens);
       this.options.tokens = new Array(this.options.tokens);
+    // Force carriage returns as token delimiters anyway
+    if (!this.options.tokens.include('\n'))
+      this.options.tokens.push('\n');
 
     this.observer = null;
 
     this.observer = null;
-    
+
     this.element.setAttribute('autocomplete','off');
 
     Element.hide(this.update);
 
     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));
+    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
   },
 
   show: function() {
     if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
   },
 
   show: function() {
     if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
-    if(!this.iefix && 
-      (navigator.appVersion.indexOf('MSIE')>0) &&
-      (navigator.userAgent.indexOf('Opera')<0) &&
+    if(!this.iefix &&
+      (Prototype.Browser.IE) &&
       (Element.getStyle(this.update, 'position')=='absolute')) {
       (Element.getStyle(this.update, 'position')=='absolute')) {
-      new Insertion.After(this.update, 
+      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>');
        '<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>');
@@ -99,7 +102,7 @@ Autocompleter.Base.prototype = {
     }
     if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
   },
     }
     if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
   },
-  
+
   fixIEOverlapping: function() {
     Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
     this.iefix.style.zIndex = 1;
   fixIEOverlapping: function() {
     Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
     this.iefix.style.zIndex = 1;
@@ -139,23 +142,23 @@ Autocompleter.Base.prototype = {
        case Event.KEY_UP:
          this.markPrevious();
          this.render();
        case Event.KEY_UP:
          this.markPrevious();
          this.render();
-         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         Event.stop(event);
          return;
        case Event.KEY_DOWN:
          this.markNext();
          this.render();
          return;
        case Event.KEY_DOWN:
          this.markNext();
          this.render();
-         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         Event.stop(event);
          return;
       }
          return;
       }
-     else 
-       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
-         (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
+     else
+       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
 
     this.changed = true;
     this.hasFocus = true;
 
     if(this.observer) clearTimeout(this.observer);
 
     this.changed = true;
     this.hasFocus = true;
 
     if(this.observer) clearTimeout(this.observer);
-      this.observer = 
+      this.observer =
         setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
   },
 
         setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
   },
 
@@ -167,36 +170,35 @@ Autocompleter.Base.prototype = {
 
   onHover: function(event) {
     var element = Event.findElement(event, 'LI');
 
   onHover: function(event) {
     var element = Event.findElement(event, 'LI');
-    if(this.index != element.autocompleteIndex) 
+    if(this.index != element.autocompleteIndex)
     {
         this.index = element.autocompleteIndex;
         this.render();
     }
     Event.stop(event);
   },
     {
         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();
   },
   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;
   onBlur: function(event) {
     // needed to make click events working
     setTimeout(this.hide.bind(this), 250);
     this.hasFocus = false;
-    this.active = false;     
-  }, 
-  
+    this.active = false;
+  },
+
   render: function() {
     if(this.entryCount > 0) {
       for (var i = 0; i < this.entryCount; i++)
   render: function() {
     if(this.entryCount > 0) {
       for (var i = 0; i < this.entryCount; i++)
-        this.index==i ? 
-          Element.addClassName(this.getEntry(i),"selected") : 
+        this.index==i ?
+          Element.addClassName(this.getEntry(i),"selected") :
           Element.removeClassName(this.getEntry(i),"selected");
           Element.removeClassName(this.getEntry(i),"selected");
-        
-      if(this.hasFocus) { 
+      if(this.hasFocus) {
         this.show();
         this.active = true;
       }
         this.show();
         this.active = true;
       }
@@ -205,27 +207,27 @@ Autocompleter.Base.prototype = {
       this.hide();
     }
   },
       this.hide();
     }
   },
-  
+
   markPrevious: function() {
   markPrevious: function() {
-    if(this.index > 0) this.index--
+    if(this.index > 0) this.index--;
       else this.index = this.entryCount-1;
     this.getEntry(this.index).scrollIntoView(true);
   },
       else this.index = this.entryCount-1;
     this.getEntry(this.index).scrollIntoView(true);
   },
-  
+
   markNext: function() {
   markNext: function() {
-    if(this.index < this.entryCount-1) this.index++
+    if(this.index < this.entryCount-1) this.index++;
       else this.index = 0;
     this.getEntry(this.index).scrollIntoView(false);
   },
       else this.index = 0;
     this.getEntry(this.index).scrollIntoView(false);
   },
-  
+
   getEntry: function(index) {
     return this.update.firstChild.childNodes[index];
   },
   getEntry: function(index) {
     return this.update.firstChild.childNodes[index];
   },
-  
+
   getCurrentEntry: function() {
     return this.getEntry(this.index);
   },
   getCurrentEntry: function() {
     return this.getEntry(this.index);
   },
-  
+
   selectEntry: function() {
     this.active = false;
     this.updateElement(this.getCurrentEntry());
   selectEntry: function() {
     this.active = false;
     this.updateElement(this.getCurrentEntry());
@@ -238,23 +240,24 @@ Autocompleter.Base.prototype = {
     }
     var value = '';
     if (this.options.select) {
     }
     var value = '';
     if (this.options.select) {
-      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+      var nodes = $(selectedElement).select('.' + this.options.select) || [];
       if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
     } else
       value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
       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+/);
+
+    var bounds = this.getTokenBounds();
+    if (bounds[0] != -1) {
+      var newValue = this.element.value.substr(0, bounds[0]);
+      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
       if (whitespace)
         newValue += whitespace[0];
       if (whitespace)
         newValue += whitespace[0];
-      this.element.value = newValue + value;
+      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
     } else {
       this.element.value = value;
     }
     } else {
       this.element.value = value;
     }
+    this.oldElementValue = this.element.value;
     this.element.focus();
     this.element.focus();
-    
+
     if (this.options.afterUpdateElement)
       this.options.afterUpdateElement(this.element, selectedElement);
   },
     if (this.options.afterUpdateElement)
       this.options.afterUpdateElement(this.element, selectedElement);
   },
@@ -266,20 +269,20 @@ Autocompleter.Base.prototype = {
       Element.cleanWhitespace(this.update.down());
 
       if(this.update.firstChild && this.update.down().childNodes) {
       Element.cleanWhitespace(this.update.down());
 
       if(this.update.firstChild && this.update.down().childNodes) {
-        this.entryCount = 
+        this.entryCount =
           this.update.down().childNodes.length;
         for (var i = 0; i < this.entryCount; i++) {
           var entry = this.getEntry(i);
           entry.autocompleteIndex = i;
           this.addObservers(entry);
         }
           this.update.down().childNodes.length;
         for (var i = 0; i < this.entryCount; i++) {
           var entry = this.getEntry(i);
           entry.autocompleteIndex = i;
           this.addObservers(entry);
         }
-      } else { 
+      } else {
         this.entryCount = 0;
       }
 
       this.stopIndicator();
       this.index = 0;
         this.entryCount = 0;
       }
 
       this.stopIndicator();
       this.index = 0;
-      
+
       if(this.entryCount==1 && this.options.autoSelect) {
         this.selectEntry();
         this.hide();
       if(this.entryCount==1 && this.options.autoSelect) {
         this.selectEntry();
         this.hide();
@@ -295,40 +298,49 @@ Autocompleter.Base.prototype = {
   },
 
   onObserverEvent: function() {
   },
 
   onObserverEvent: function() {
-    this.changed = false;   
+    this.changed = false;
+    this.tokenBounds = null;
     if(this.getToken().length>=this.options.minChars) {
     if(this.getToken().length>=this.options.minChars) {
-      this.startIndicator();
       this.getUpdatedChoices();
     } else {
       this.active = false;
       this.hide();
     }
       this.getUpdatedChoices();
     } else {
       this.active = false;
       this.hide();
     }
+    this.oldElementValue = this.element.value;
   },
 
   getToken: function() {
   },
 
   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;
+    var bounds = this.getTokenBounds();
+    return this.element.value.substring(bounds[0], bounds[1]).strip();
+  },
+
+  getTokenBounds: function() {
+    if (null != this.tokenBounds) return this.tokenBounds;
+    var value = this.element.value;
+    if (value.strip().empty()) return [-1, 0];
+    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+    var offset = (diff == this.oldElementValue.length ? 1 : 0);
+    var prevTokenPos = -1, nextTokenPos = value.length;
+    var tp;
+    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+      if (tp > prevTokenPos) prevTokenPos = tp;
+      tp = value.indexOf(this.options.tokens[index], diff + offset);
+      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
     }
     }
-    return lastTokenPos;
+    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
   }
   }
-}
+});
 
 
-Ajax.Autocompleter = Class.create();
-Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+  var boundary = Math.min(newS.length, oldS.length);
+  for (var index = 0; index < boundary; ++index)
+    if (newS[index] != oldS[index])
+      return index;
+  return boundary;
+};
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
   initialize: function(element, update, url, options) {
     this.baseInitialize(element, update, options);
     this.options.asynchronous  = true;
   initialize: function(element, update, url, options) {
     this.baseInitialize(element, update, options);
     this.options.asynchronous  = true;
@@ -338,13 +350,15 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
   },
 
   getUpdatedChoices: function() {
   },
 
   getUpdatedChoices: function() {
-    entry = encodeURIComponent(this.options.paramName) + '=' + 
+    this.startIndicator();
+
+    var entry = encodeURIComponent(this.options.paramName) + '=' +
       encodeURIComponent(this.getToken());
 
     this.options.parameters = this.options.callback ?
       this.options.callback(this.element, entry) : entry;
 
       encodeURIComponent(this.getToken());
 
     this.options.parameters = this.options.callback ?
       this.options.callback(this.element, entry) : entry;
 
-    if(this.options.defaultParams) 
+    if(this.options.defaultParams)
       this.options.parameters += '&' + this.options.defaultParams;
 
     new Ajax.Request(this.url, this.options);
       this.options.parameters += '&' + this.options.defaultParams;
 
     new Ajax.Request(this.url, this.options);
@@ -353,7 +367,6 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
   onComplete: function(request) {
     this.updateChoices(request.responseText);
   }
   onComplete: function(request) {
     this.updateChoices(request.responseText);
   }
-
 });
 
 // The local array autocompleter. Used when you'd prefer to
 });
 
 // The local array autocompleter. Used when you'd prefer to
@@ -369,7 +382,7 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
 // - choices - How many autocompletion choices to offer
 //
 // - partialSearch - If false, the autocompleter will match entered
 // - choices - How many autocompletion choices to offer
 //
 // - partialSearch - If false, the autocompleter will match entered
-//                    text only at the beginning of strings in the 
+//                    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
 //                    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
@@ -386,13 +399,12 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
 // - ignoreCase - Whether to ignore case when autocompleting.
 //                 Defaults to true.
 //
 // - ignoreCase - Whether to ignore case when autocompleting.
 //                 Defaults to true.
 //
-// It's possible to pass in a custom function as the 'selector' 
+// 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.
 
 // 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(), {
+Autocompleter.Local = Class.create(Autocompleter.Base, {
   initialize: function(element, update, array, options) {
     this.baseInitialize(element, update, options);
     this.options.array = array;
   initialize: function(element, update, array, options) {
     this.baseInitialize(element, update, options);
     this.options.array = array;
@@ -415,20 +427,20 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
         var entry     = instance.getToken();
         var count     = 0;
 
         var entry     = instance.getToken();
         var count     = 0;
 
-        for (var i = 0; i < instance.options.array.length &&  
-          ret.length < instance.options.choices ; i++) { 
+        for (var i = 0; i < instance.options.array.length &&
+          ret.length < instance.options.choices ; i++) {
 
           var elem = instance.options.array[i];
 
           var elem = instance.options.array[i];
-          var foundPos = instance.options.ignoreCase ? 
-            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
+          var foundPos = instance.options.ignoreCase ?
+            elem.toLowerCase().indexOf(entry.toLowerCase()) :
             elem.indexOf(entry);
 
           while (foundPos != -1) {
             elem.indexOf(entry);
 
           while (foundPos != -1) {
-            if (foundPos == 0 && elem.length != entry.length) { 
-              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
+            if (foundPos == 0 && elem.length != entry.length) {
+              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
                 elem.substr(entry.length) + "</li>");
               break;
                 elem.substr(entry.length) + "</li>");
               break;
-            } else if (entry.length >= instance.options.partialChars && 
+            } 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>" +
               instance.options.partialSearch && foundPos != -1) {
               if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                 partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
@@ -438,23 +450,22 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
               }
             }
 
               }
             }
 
-            foundPos = instance.options.ignoreCase ? 
-              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
+            foundPos = instance.options.ignoreCase ?
+              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
               elem.indexOf(entry, foundPos + 1);
 
           }
         }
         if (partial.length)
               elem.indexOf(entry, foundPos + 1);
 
           }
         }
         if (partial.length)
-          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
         return "<ul>" + ret.join('') + "</ul>";
       }
         return "<ul>" + ret.join('') + "</ul>";
       }
-    }, options || {});
+    }, options || { });
   }
 });
 
   }
 });
 
-// AJAX in-place editor
-//
-// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
 
 // 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
 
 // 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
@@ -463,361 +474,480 @@ Field.scrollFreeActivate = function(field) {
   setTimeout(function() {
     Field.activate(field);
   }, 1);
   setTimeout(function() {
     Field.activate(field);
   }, 1);
-}
+};
 
 
-Ajax.InPlaceEditor = Class.create();
-Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
-Ajax.InPlaceEditor.prototype = {
+Ajax.InPlaceEditor = Class.create({
   initialize: function(element, url, options) {
     this.url = url;
   initialize: function(element, url, options) {
     this.url = url;
-    this.element = $(element);
-
-    this.options = Object.extend({
-      paramName: "value",
-      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: {},
-      evalScripts: false
-    }, 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;
-      }
+    this.element = element = $(element);
+    this.prepareOptions();
+    this._controls = { };
+    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+    Object.extend(this.options, options || { });
+    if (!this.options.formId && this.element.id) {
+      this.options.formId = this.element.id + '-inplaceeditor';
+      if ($(this.options.formId))
+        this.options.formId = '';
     }
     }
-    
-    if (this.options.externalControl) {
+    if (this.options.externalControl)
       this.options.externalControl = $(this.options.externalControl);
       this.options.externalControl = $(this.options.externalControl);
-    }
-    
-    this.originalBackground = Element.getStyle(this.element, 'background-color');
-    if (!this.originalBackground) {
-      this.originalBackground = "transparent";
-    }
-    
+    if (!this.options.externalControl)
+      this.options.externalControlOnly = false;
+    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
     this.element.title = this.options.clickToEditText;
     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);
+    this._boundCancelHandler = this.handleFormCancellation.bind(this);
+    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+    this._boundWrapperHandler = this.wrapUp.bind(this);
+    this.registerListeners();
+  },
+  checkForEscapeOrReturn: function(e) {
+    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+    if (Event.KEY_ESC == e.keyCode)
+      this.handleFormCancellation(e);
+    else if (Event.KEY_RETURN == e.keyCode)
+      this.handleFormSubmission(e);
+  },
+  createControl: function(mode, handler, extraClasses) {
+    var control = this.options[mode + 'Control'];
+    var text = this.options[mode + 'Text'];
+    if ('button' == control) {
+      var btn = document.createElement('input');
+      btn.type = 'submit';
+      btn.value = text;
+      btn.className = 'editor_' + mode + '_button';
+      if ('cancel' == mode)
+        btn.onclick = this._boundCancelHandler;
+      this._form.appendChild(btn);
+      this._controls[mode] = btn;
+    } else if ('link' == control) {
+      var link = document.createElement('a');
+      link.href = '#';
+      link.appendChild(document.createTextNode(text));
+      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+      link.className = 'editor_' + mode + '_link';
+      if (extraClasses)
+        link.className += ' ' + extraClasses;
+      this._form.appendChild(link);
+      this._controls[mode] = link;
     }
   },
     }
   },
-  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);
-    if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
-    // stop the event to avoid a page refresh in Safari
-    if (evt) {
-      Event.stop(evt);
+  createEditField: function() {
+    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+    var fld;
+    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+      fld = document.createElement('input');
+      fld.type = 'text';
+      var size = this.options.size || this.options.cols || 0;
+      if (0 < size) fld.size = size;
+    } else {
+      fld = document.createElement('textarea');
+      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+      fld.cols = this.options.cols || 40;
     }
     }
-    return false;
+    fld.name = this.options.paramName;
+    fld.value = text; // No HTML breaks conversion anymore
+    fld.className = 'editor_field';
+    if (this.options.submitOnBlur)
+      fld.onblur = this._boundSubmitHandler;
+    this._controls.editor = fld;
+    if (this.options.loadTextURL)
+      this.loadExternalText();
+    this._form.appendChild(this._controls.editor);
   },
   createForm: function() {
   },
   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);
-
+    var ipe = this;
+    function addText(mode, condition) {
+      var text = ipe.options['text' + mode + 'Controls'];
+      if (!text || condition === false) return;
+      ipe._form.appendChild(document.createTextNode(text));
+    };
+    this._form = $(document.createElement('form'));
+    this._form.id = this.options.formId;
+    this._form.addClassName(this.options.formClassName);
+    this._form.onsubmit = this._boundSubmitHandler;
     this.createEditField();
     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;
-      okButton.className = 'editor_ok_button';
-      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);
-      cancelLink.className = 'editor_cancel';      
-      this.form.appendChild(cancelLink);
-    }
+    if ('textarea' == this._controls.editor.tagName.toLowerCase())
+      this._form.appendChild(document.createElement('br'));
+    if (this.options.onFormCustomization)
+      this.options.onFormCustomization(this, this._form);
+    addText('Before', this.options.okControl || this.options.cancelControl);
+    this.createControl('ok', this._boundSubmitHandler);
+    addText('Between', this.options.okControl && this.options.cancelControl);
+    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+    addText('After', this.options.okControl || this.options.cancelControl);
+  },
+  destroy: function() {
+    if (this._oldInnerHTML)
+      this.element.innerHTML = this._oldInnerHTML;
+    this.leaveEditMode();
+    this.unregisterListeners();
+  },
+  enterEditMode: function(e) {
+    if (this._saving || this._editing) return;
+    this._editing = true;
+    this.triggerCallback('onEnterEditMode');
+    if (this.options.externalControl)
+      this.options.externalControl.hide();
+    this.element.hide();
+    this.createForm();
+    this.element.parentNode.insertBefore(this._form, this.element);
+    if (!this.options.loadTextURL)
+      this.postProcessEditField();
+    if (e) Event.stop(e);
   },
   },
-  hasHTMLLineBreaks: function(string) {
-    if (!this.options.handleLineBreaks) return false;
-    return string.match(/<br/i) || string.match(/<p>/i);
+  enterHover: function(e) {
+    if (this.options.hoverClassName)
+      this.element.addClassName(this.options.hoverClassName);
+    if (this._saving) return;
+    this.triggerCallback('onEnterHover');
   },
   },
-  convertHTMLLineBreaks: function(string) {
-    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+  getText: function() {
+    return this.element.innerHTML.unescapeHTML();
   },
   },
-  createEditField: function() {
-    var text;
-    if(this.options.loadTextURL) {
-      text = this.options.loadingText;
-    } else {
-      text = this.getText();
+  handleAJAXFailure: function(transport) {
+    this.triggerCallback('onFailure', transport);
+    if (this._oldInnerHTML) {
+      this.element.innerHTML = this._oldInnerHTML;
+      this._oldInnerHTML = null;
     }
     }
-
-    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 = this.options.paramName;
-      textField.value = text;
-      textField.style.backgroundColor = this.options.highlightcolor;
-      textField.className = 'editor_field';
-      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;
+  },
+  handleFormCancellation: function(e) {
+    this.wrapUp();
+    if (e) Event.stop(e);
+  },
+  handleFormSubmission: function(e) {
+    var form = this._form;
+    var value = $F(this._controls.editor);
+    this.prepareSubmission();
+    var params = this.options.callback(form, value) || '';
+    if (Object.isString(params))
+      params = params.toQueryParams();
+    params.editorId = this.element.id;
+    if (this.options.htmlResponse) {
+      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+      Object.extend(options, {
+        parameters: params,
+        onComplete: this._boundWrapperHandler,
+        onFailure: this._boundFailureHandler
+      });
+      new Ajax.Updater({ success: this.element }, this.url, options);
     } else {
     } else {
-      this.options.textarea = true;
-      var textArea = document.createElement("textarea");
-      textArea.obj = this;
-      textArea.name = this.options.paramName;
-      textArea.value = this.convertHTMLLineBreaks(text);
-      textArea.rows = this.options.rows;
-      textArea.cols = this.options.cols || 40;
-      textArea.className = 'editor_field';      
-      if (this.options.submitOnBlur)
-        textArea.onblur = this.onSubmit.bind(this);
-      this.editField = textArea;
-    }
-    
-    if(this.options.loadTextURL) {
-      this.loadExternalText();
+      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+      Object.extend(options, {
+        parameters: params,
+        onComplete: this._boundWrapperHandler,
+        onFailure: this._boundFailureHandler
+      });
+      new Ajax.Request(this.url, options);
     }
     }
-    this.form.appendChild(this.editField);
+    if (e) Event.stop(e);
   },
   },
-  getText: function() {
-    return this.element.innerHTML;
+  leaveEditMode: function() {
+    this.element.removeClassName(this.options.savingClassName);
+    this.removeForm();
+    this.leaveHover();
+    this.element.style.backgroundColor = this._originalBackground;
+    this.element.show();
+    if (this.options.externalControl)
+      this.options.externalControl.show();
+    this._saving = false;
+    this._editing = false;
+    this._oldInnerHTML = null;
+    this.triggerCallback('onLeaveEditMode');
+  },
+  leaveHover: function(e) {
+    if (this.options.hoverClassName)
+      this.element.removeClassName(this.options.hoverClassName);
+    if (this._saving) return;
+    this.triggerCallback('onLeaveHover');
   },
   loadExternalText: function() {
   },
   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();
-    Field.scrollFreeActivate(this.editField);
-  },
-  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();
-    
-    if (this.options.evalScripts) {
-      new Ajax.Request(
-        this.url, Object.extend({
-          parameters: this.options.callback(form, value),
-          onComplete: this.onComplete.bind(this),
-          onFailure: this.onFailure.bind(this),
-          asynchronous:true, 
-          evalScripts:true
-        }, this.options.ajaxOptions));
-    } else  {
-      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._form.addClassName(this.options.loadingClassName);
+    this._controls.editor.disabled = true;
+    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+    Object.extend(options, {
+      parameters: 'editorId=' + encodeURIComponent(this.element.id),
+      onComplete: Prototype.emptyFunction,
+      onSuccess: function(transport) {
+        this._form.removeClassName(this.options.loadingClassName);
+        var text = transport.responseText;
+        if (this.options.stripLoadedTextTags)
+          text = text.stripTags();
+        this._controls.editor.value = text;
+        this._controls.editor.disabled = false;
+        this.postProcessEditField();
+      }.bind(this),
+      onFailure: this._boundFailureHandler
+    });
+    new Ajax.Request(this.options.loadTextURL, options);
+  },
+  postProcessEditField: function() {
+    var fpc = this.options.fieldPostCreation;
+    if (fpc)
+      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+  },
+  prepareOptions: function() {
+    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
+    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
+    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
+      Object.extend(this.options, defs);
+    }.bind(this));
+  },
+  prepareSubmission: function() {
+    this._saving = true;
     this.removeForm();
     this.leaveHover();
     this.showSaving();
   },
     this.removeForm();
     this.leaveHover();
     this.showSaving();
   },
+  registerListeners: function() {
+    this._listeners = { };
+    var listener;
+    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
+      listener = this[pair.value].bind(this);
+      this._listeners[pair.key] = listener;
+      if (!this.options.externalControlOnly)
+        this.element.observe(pair.key, listener);
+      if (this.options.externalControl)
+        this.options.externalControl.observe(pair.key, listener);
+    }.bind(this));
+  },
+  removeForm: function() {
+    if (!this._form) return;
+    this._form.remove();
+    this._form = null;
+    this._controls = { };
+  },
   showSaving: function() {
   showSaving: function() {
-    this.oldInnerHTML = this.element.innerHTML;
+    this._oldInnerHTML = this.element.innerHTML;
     this.element.innerHTML = this.options.savingText;
     this.element.innerHTML = this.options.savingText;
-    Element.addClassName(this.element, this.options.savingClassName);
-    this.element.style.backgroundColor = this.originalBackground;
-    Element.show(this.element);
+    this.element.addClassName(this.options.savingClassName);
+    this.element.style.backgroundColor = this._originalBackground;
+    this.element.show();
   },
   },
-  removeForm: function() {
-    if(this.form) {
-      if (this.form.parentNode) Element.remove(this.form);
-      this.form = null;
+  triggerCallback: function(cbName, arg) {
+    if ('function' == typeof this.options[cbName]) {
+      this.options[cbName](this, arg);
     }
   },
     }
   },
-  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)
+  unregisterListeners: function() {
+    $H(this._listeners).each(function(pair) {
+      if (!this.options.externalControlOnly)
+        this.element.stopObserving(pair.key, pair.value);
+      if (this.options.externalControl)
+        this.options.externalControl.stopObserving(pair.key, pair.value);
+    }.bind(this));
   },
   },
-  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
+  wrapUp: function(transport) {
+    this.leaveEditMode();
+    // Can't use triggerCallback due to backward compatibility: requires
+    // binding + direct element
+    this._boundComplete(transport, this.element);
+  }
+});
+
+Object.extend(Ajax.InPlaceEditor.prototype, {
+  dispose: Ajax.InPlaceEditor.prototype.destroy
+});
+
+Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
+  initialize: function($super, element, url, options) {
+    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
+    $super(element, url, options);
+  },
+
+  createEditField: function() {
+    var list = document.createElement('select');
+    list.name = this.options.paramName;
+    list.size = 1;
+    this._controls.editor = list;
+    this._collection = this.options.collection || [];
+    if (this.options.loadCollectionURL)
+      this.loadCollection();
+    else
+      this.checkForExternalText();
+    this._form.appendChild(this._controls.editor);
+  },
+
+  loadCollection: function() {
+    this._form.addClassName(this.options.loadingClassName);
+    this.showLoadingText(this.options.loadingCollectionText);
+    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+    Object.extend(options, {
+      parameters: 'editorId=' + encodeURIComponent(this.element.id),
+      onComplete: Prototype.emptyFunction,
+      onSuccess: function(transport) {
+        var js = transport.responseText.strip();
+        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+          throw('Server returned an invalid collection representation.');
+        this._collection = eval(js);
+        this.checkForExternalText();
+      }.bind(this),
+      onFailure: this.onFailure
     });
     });
+    new Ajax.Request(this.options.loadCollectionURL, options);
   },
   },
-  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);
+
+  showLoadingText: function(text) {
+    this._controls.editor.disabled = true;
+    var tempOption = this._controls.editor.firstChild;
+    if (!tempOption) {
+      tempOption = document.createElement('option');
+      tempOption.value = '';
+      this._controls.editor.appendChild(tempOption);
+      tempOption.selected = true;
     }
     }
-    this.editing = false;
-    this.saving = false;
-    this.oldInnerHTML = null;
-    this.onLeaveEditMode();
+    tempOption.update((text || '').stripScripts().stripTags());
   },
   },
-  onComplete: function(transport) {
-    this.leaveEditMode();
-    this.options.onComplete.bind(this)(transport, this.element);
+
+  checkForExternalText: function() {
+    this._text = this.getText();
+    if (this.options.loadTextURL)
+      this.loadExternalText();
+    else
+      this.buildOptionList();
   },
   },
-  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);
-    }
+
+  loadExternalText: function() {
+    this.showLoadingText(this.options.loadingText);
+    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+    Object.extend(options, {
+      parameters: 'editorId=' + encodeURIComponent(this.element.id),
+      onComplete: Prototype.emptyFunction,
+      onSuccess: function(transport) {
+        this._text = transport.responseText.strip();
+        this.buildOptionList();
+      }.bind(this),
+      onFailure: this.onFailure
+    });
+    new Ajax.Request(this.options.loadTextURL, options);
+  },
+
+  buildOptionList: function() {
+    this._form.removeClassName(this.options.loadingClassName);
+    this._collection = this._collection.map(function(entry) {
+      return 2 === entry.length ? entry : [entry, entry].flatten();
+    });
+    var marker = ('value' in this.options) ? this.options.value : this._text;
+    var textFound = this._collection.any(function(entry) {
+      return entry[0] == marker;
+    }.bind(this));
+    this._controls.editor.update('');
+    var option;
+    this._collection.each(function(entry, index) {
+      option = document.createElement('option');
+      option.value = entry[0];
+      option.selected = textFound ? entry[0] == marker : 0 == index;
+      option.appendChild(document.createTextNode(entry[1]));
+      this._controls.editor.appendChild(option);
+    }.bind(this));
+    this._controls.editor.disabled = false;
+    Field.scrollFreeActivate(this._controls.editor);
   }
   }
-};
+});
 
 
-Ajax.InPlaceCollectionEditor = Class.create();
-Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
-Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
-  createEditField: function() {
-    if (!this.cached_selectTag) {
-      var selectTag = document.createElement("select");
-      var collection = this.options.collection || [];
-      var optionTag;
-      collection.each(function(e,i) {
-        optionTag = document.createElement("option");
-        optionTag.value = (e instanceof Array) ? e[0] : e;
-        if((typeof this.options.value == 'undefined') && 
-          ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
-        if(this.options.value==optionTag.value) optionTag.selected = true;
-        optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
-        selectTag.appendChild(optionTag);
-      }.bind(this));
-      this.cached_selectTag = selectTag;
-    }
+//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
+//**** This only  exists for a while,  in order to  let ****
+//**** users adapt to  the new API.  Read up on the new ****
+//**** API and convert your code to it ASAP!            ****
+
+Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
+  if (!options) return;
+  function fallback(name, expr) {
+    if (name in options || expr === undefined) return;
+    options[name] = expr;
+  };
+  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
+    options.cancelLink == options.cancelButton == false ? false : undefined)));
+  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
+    options.okLink == options.okButton == false ? false : undefined)));
+  fallback('highlightColor', options.highlightcolor);
+  fallback('highlightEndColor', options.highlightendcolor);
+};
 
 
-    this.editField = this.cached_selectTag;
-    if(this.options.loadTextURL) this.loadExternalText();
-    this.form.appendChild(this.editField);
-    this.options.callback = function(form, value) {
-      return "value=" + encodeURIComponent(value);
+Object.extend(Ajax.InPlaceEditor, {
+  DefaultOptions: {
+    ajaxOptions: { },
+    autoRows: 3,                                // Use when multi-line w/ rows == 1
+    cancelControl: 'link',                      // 'link'|'button'|false
+    cancelText: 'cancel',
+    clickToEditText: 'Click to edit',
+    externalControl: null,                      // id|elt
+    externalControlOnly: false,
+    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
+    formClassName: 'inplaceeditor-form',
+    formId: null,                               // id|elt
+    highlightColor: '#ffff99',
+    highlightEndColor: '#ffffff',
+    hoverClassName: '',
+    htmlResponse: true,
+    loadingClassName: 'inplaceeditor-loading',
+    loadingText: 'Loading...',
+    okControl: 'button',                        // 'link'|'button'|false
+    okText: 'ok',
+    paramName: 'value',
+    rows: 1,                                    // If 1 and multi-line, uses autoRows
+    savingClassName: 'inplaceeditor-saving',
+    savingText: 'Saving...',
+    size: 0,
+    stripLoadedTextTags: false,
+    submitOnBlur: false,
+    textAfterControls: '',
+    textBeforeControls: '',
+    textBetweenControls: ''
+  },
+  DefaultCallbacks: {
+    callback: function(form) {
+      return Form.serialize(form);
+    },
+    onComplete: function(transport, element) {
+      // For backward compatibility, this one is bound to the IPE, and passes
+      // the element directly.  It was too often customized, so we don't break it.
+      new Effect.Highlight(element, {
+        startcolor: this.options.highlightColor, keepBackgroundImage: true });
+    },
+    onEnterEditMode: null,
+    onEnterHover: function(ipe) {
+      ipe.element.style.backgroundColor = ipe.options.highlightColor;
+      if (ipe._effect)
+        ipe._effect.cancel();
+    },
+    onFailure: function(transport, ipe) {
+      alert('Error communication with the server: ' + transport.responseText.stripTags());
+    },
+    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
+    onLeaveEditMode: null,
+    onLeaveHover: function(ipe) {
+      ipe._effect = new Effect.Highlight(ipe.element, {
+        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+        restorecolor: ipe._originalBackground, keepBackgroundImage: true
+      });
     }
     }
+  },
+  Listeners: {
+    click: 'enterEditMode',
+    keydown: 'checkForEscapeOrReturn',
+    mouseover: 'enterHover',
+    mouseout: 'leaveHover'
   }
 });
 
   }
 });
 
-// Delayed observer, like Form.Element.Observer, 
+Ajax.InPlaceCollectionEditor.DefaultOptions = {
+  loadingCollectionText: 'Loading options...'
+};
+
+// Delayed observer, like Form.Element.Observer,
 // but waits for delay after last key input
 // Ideal for live-search fields
 
 // but waits for delay after last key input
 // Ideal for live-search fields
 
-Form.Element.DelayedObserver = Class.create();
-Form.Element.DelayedObserver.prototype = {
+Form.Element.DelayedObserver = Class.create({
   initialize: function(element, delay, callback) {
     this.delay     = delay || 0.5;
     this.element   = $(element);
     this.callback  = callback;
     this.timer     = null;
   initialize: function(element, delay, callback) {
     this.delay     = delay || 0.5;
     this.element   = $(element);
     this.callback  = callback;
     this.timer     = null;
-    this.lastValue = $F(this.element); 
+    this.lastValue = $F(this.element);
     Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
   },
   delayedListener: function(event) {
     Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
   },
   delayedListener: function(event) {
@@ -830,4 +960,4 @@ Form.Element.DelayedObserver.prototype = {
     this.timer = null;
     this.callback(this.element, $F(this.element));
   }
     this.timer = null;
     this.callback(this.element, $F(this.element));
   }
-};
+});
\ No newline at end of file
index c71ddb82746d45a97317a84ee50341cef87fb2c5..07229f986f58f3b2101b7a79991d4534051e79e0 100644 (file)
@@ -1,10 +1,10 @@
-// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-//           (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
-// 
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+//
 // script.aculo.us is freely distributable under the terms of an MIT-style license.
 // For details, see the script.aculo.us web site: http://script.aculo.us/
 
 // script.aculo.us is freely distributable under the terms of an MIT-style license.
 // For details, see the script.aculo.us web site: http://script.aculo.us/
 
-if(typeof Effect == 'undefined')
+if(Object.isUndefined(Effect))
   throw("dragdrop.js requires including script.aculo.us' effects.js library");
 
 var Droppables = {
   throw("dragdrop.js requires including script.aculo.us' effects.js library");
 
 var Droppables = {
@@ -20,20 +20,19 @@ var Droppables = {
       greedy:     true,
       hoverclass: null,
       tree:       false
       greedy:     true,
       hoverclass: null,
       tree:       false
-    }, arguments[1] || {});
+    }, arguments[1] || { });
 
     // cache containers
     if(options.containment) {
       options._containers = [];
       var containment = options.containment;
 
     // cache containers
     if(options.containment) {
       options._containers = [];
       var containment = options.containment;
-      if((typeof containment == 'object') && 
-        (containment.constructor == Array)) {
+      if(Object.isArray(containment)) {
         containment.each( function(c) { options._containers.push($(c)) });
       } else {
         options._containers.push($(containment));
       }
     }
         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
     if(options.accept) options.accept = [options.accept].flatten();
 
     Element.makePositioned(element); // fix IE
@@ -41,34 +40,34 @@ var Droppables = {
 
     this.drops.push(options);
   },
 
     this.drops.push(options);
   },
-  
+
   findDeepestChild: function(drops) {
     deepest = drops[0];
   findDeepestChild: function(drops) {
     deepest = drops[0];
-      
+
     for (i = 1; i < drops.length; ++i)
       if (Element.isParent(drops[i].element, deepest.element))
         deepest = drops[i];
     for (i = 1; i < drops.length; ++i)
       if (Element.isParent(drops[i].element, deepest.element))
         deepest = drops[i];
-    
+
     return deepest;
   },
 
   isContained: function(element, drop) {
     var containmentNode;
     if(drop.tree) {
     return deepest;
   },
 
   isContained: function(element, drop) {
     var containmentNode;
     if(drop.tree) {
-      containmentNode = element.treeNode; 
+      containmentNode = element.treeNode;
     } else {
       containmentNode = element.parentNode;
     }
     return drop._containers.detect(function(c) { return containmentNode == c });
   },
     } else {
       containmentNode = element.parentNode;
     }
     return drop._containers.detect(function(c) { return containmentNode == c });
   },
-  
+
   isAffected: function(point, element, drop) {
     return (
       (drop.element!=element) &&
       ((!drop._containers) ||
         this.isContained(element, drop)) &&
       ((!drop.accept) ||
   isAffected: function(point, element, drop) {
     return (
       (drop.element!=element) &&
       ((!drop._containers) ||
         this.isContained(element, drop)) &&
       ((!drop.accept) ||
-        (Element.classNames(element).detect( 
+        (Element.classNames(element).detect(
           function(v) { return drop.accept.include(v) } ) )) &&
       Position.within(drop.element, point[0], point[1]) );
   },
           function(v) { return drop.accept.include(v) } ) )) &&
       Position.within(drop.element, point[0], point[1]) );
   },
@@ -87,21 +86,23 @@ var Droppables = {
 
   show: function(point, element) {
     if(!this.drops.length) return;
 
   show: function(point, element) {
     if(!this.drops.length) return;
-    var affected = [];
-    
-    if(this.last_active) this.deactivate(this.last_active);
+    var drop, affected = [];
+
     this.drops.each( function(drop) {
       if(Droppables.isAffected(point, element, drop))
         affected.push(drop);
     });
     this.drops.each( function(drop) {
       if(Droppables.isAffected(point, element, drop))
         affected.push(drop);
     });
-        
-    if(affected.length>0) {
+
+    if(affected.length>0)
       drop = Droppables.findDeepestChild(affected);
       drop = Droppables.findDeepestChild(affected);
+
+    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
+    if (drop) {
       Position.within(drop.element, point[0], point[1]);
       if(drop.onHover)
         drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
       Position.within(drop.element, point[0], point[1]);
       if(drop.onHover)
         drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
-      
-      Droppables.activate(drop);
+
+      if (drop != this.last_active) Droppables.activate(drop);
     }
   },
 
     }
   },
 
@@ -110,33 +111,35 @@ var Droppables = {
     Position.prepare();
 
     if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
     Position.prepare();
 
     if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
-      if (this.last_active.onDrop) 
+      if (this.last_active.onDrop) {
         this.last_active.onDrop(element, this.last_active.element, event);
         this.last_active.onDrop(element, this.last_active.element, event);
+        return true;
+      }
   },
 
   reset: function() {
     if(this.last_active)
       this.deactivate(this.last_active);
   }
   },
 
   reset: function() {
     if(this.last_active)
       this.deactivate(this.last_active);
   }
-}
+};
 
 var Draggables = {
   drags: [],
   observers: [],
 
 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);
   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);
   },
       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) {
   unregister: function(draggable) {
     this.drags = this.drags.reject(function(d) { return d==draggable });
     if(this.drags.length == 0) {
@@ -145,24 +148,24 @@ var Draggables = {
       Event.stopObserving(document, "keypress", this.eventKeypress);
     }
   },
       Event.stopObserving(document, "keypress", this.eventKeypress);
     }
   },
-  
+
   activate: function(draggable) {
   activate: function(draggable) {
-    if(draggable.options.delay) { 
-      this._timeout = setTimeout(function() { 
-        Draggables._timeout = null; 
-        window.focus(); 
-        Draggables.activeDraggable = draggable; 
-      }.bind(this), draggable.options.delay); 
+    if(draggable.options.delay) {
+      this._timeout = setTimeout(function() {
+        Draggables._timeout = null;
+        window.focus();
+        Draggables.activeDraggable = draggable;
+      }.bind(this), draggable.options.delay);
     } else {
       window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
       this.activeDraggable = draggable;
     }
   },
     } else {
       window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
       this.activeDraggable = draggable;
     }
   },
-  
+
   deactivate: function() {
     this.activeDraggable = null;
   },
   deactivate: function() {
     this.activeDraggable = null;
   },
-  
+
   updateDrag: function(event) {
     if(!this.activeDraggable) return;
     var pointer = [Event.pointerX(event), Event.pointerY(event)];
   updateDrag: function(event) {
     if(!this.activeDraggable) return;
     var pointer = [Event.pointerX(event), Event.pointerY(event)];
@@ -170,36 +173,36 @@ var Draggables = {
     // the same coordinates, prevent needless redrawing (moz bug?)
     if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
     this._lastPointer = pointer;
     // 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);
   },
     this.activeDraggable.updateDrag(event, pointer);
   },
-  
+
   endDrag: function(event) {
   endDrag: function(event) {
-    if(this._timeout) { 
-      clearTimeout(this._timeout); 
-      this._timeout = null; 
+    if(this._timeout) {
+      clearTimeout(this._timeout);
+      this._timeout = null;
     }
     if(!this.activeDraggable) return;
     this._lastPointer = null;
     this.activeDraggable.endDrag(event);
     this.activeDraggable = null;
   },
     }
     if(!this.activeDraggable) return;
     this._lastPointer = null;
     this.activeDraggable.endDrag(event);
     this.activeDraggable = null;
   },
-  
+
   keyPress: function(event) {
     if(this.activeDraggable)
       this.activeDraggable.keyPress(event);
   },
   keyPress: function(event) {
     if(this.activeDraggable)
       this.activeDraggable.keyPress(event);
   },
-  
+
   addObserver: function(observer) {
     this.observers.push(observer);
     this._cacheObserverCallbacks();
   },
   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();
   },
   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) {
   notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
     if(this[eventName+'Count'] > 0)
       this.observers.each( function(o) {
@@ -207,7 +210,7 @@ var Draggables = {
       });
     if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
   },
       });
     if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
   },
-  
+
   _cacheObserverCallbacks: function() {
     ['onStart','onEnd','onDrag'].each( function(eventName) {
       Draggables[eventName+'Count'] = Draggables.observers.select(
   _cacheObserverCallbacks: function() {
     ['onStart','onEnd','onDrag'].each( function(eventName) {
       Draggables[eventName+'Count'] = Draggables.observers.select(
@@ -215,14 +218,11 @@ var Draggables = {
       ).length;
     });
   }
       ).length;
     });
   }
-}
+};
 
 /*--------------------------------------------------------------------------*/
 
 
 /*--------------------------------------------------------------------------*/
 
-var Draggable = Class.create();
-Draggable._dragging    = {};
-
-Draggable.prototype = {
+var Draggable = Class.create({
   initialize: function(element) {
     var defaults = {
       handle: false,
   initialize: function(element) {
     var defaults = {
       handle: false,
@@ -233,106 +233,110 @@ Draggable.prototype = {
         });
       },
       endeffect: function(element) {
         });
       },
       endeffect: function(element) {
-        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
-        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
+        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
+        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
           queue: {scope:'_draggable', position:'end'},
           queue: {scope:'_draggable', position:'end'},
-          afterFinish: function(){ 
-            Draggable._dragging[element] = false 
+          afterFinish: function(){
+            Draggable._dragging[element] = false
           }
           }
-        }); 
+        });
       },
       zindex: 1000,
       revert: false,
       },
       zindex: 1000,
       revert: false,
+      quiet: false,
       scroll: false,
       scrollSensitivity: 20,
       scrollSpeed: 15,
       snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
       delay: 0
     };
       scroll: false,
       scrollSensitivity: 20,
       scrollSpeed: 15,
       snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
       delay: 0
     };
-    
-    if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
+
+    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
       Object.extend(defaults, {
         starteffect: function(element) {
           element._opacity = Element.getOpacity(element);
           Draggable._dragging[element] = true;
       Object.extend(defaults, {
         starteffect: function(element) {
           element._opacity = Element.getOpacity(element);
           Draggable._dragging[element] = true;
-          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
+          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
         }
       });
         }
       });
-    
-    var options = Object.extend(defaults, arguments[1] || {});
+
+    var options = Object.extend(defaults, arguments[1] || { });
 
     this.element = $(element);
 
     this.element = $(element);
-    
-    if(options.handle && (typeof options.handle == 'string'))
+
+    if(options.handle && Object.isString(options.handle))
       this.handle = this.element.down('.'+options.handle, 0);
       this.handle = this.element.down('.'+options.handle, 0);
-    
+
     if(!this.handle) this.handle = $(options.handle);
     if(!this.handle) this.handle = this.element;
     if(!this.handle) this.handle = $(options.handle);
     if(!this.handle) this.handle = this.element;
-    
+
     if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
       options.scroll = $(options.scroll);
       this._isScrollChild = Element.childOf(this.element, options.scroll);
     }
 
     if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
       options.scroll = $(options.scroll);
       this._isScrollChild = Element.childOf(this.element, options.scroll);
     }
 
-    Element.makePositioned(this.element); // fix IE    
+    Element.makePositioned(this.element); // fix IE
 
 
-    this.delta    = this.currentDelta();
     this.options  = options;
     this.options  = options;
-    this.dragging = false;   
+    this.dragging = false;
 
     this.eventMouseDown = this.initDrag.bindAsEventListener(this);
     Event.observe(this.handle, "mousedown", this.eventMouseDown);
 
     this.eventMouseDown = this.initDrag.bindAsEventListener(this);
     Event.observe(this.handle, "mousedown", this.eventMouseDown);
-    
+
     Draggables.register(this);
   },
     Draggables.register(this);
   },
-  
+
   destroy: function() {
     Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
     Draggables.unregister(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')]);
   },
   currentDelta: function() {
     return([
       parseInt(Element.getStyle(this.element,'left') || '0'),
       parseInt(Element.getStyle(this.element,'top') || '0')]);
   },
-  
+
   initDrag: function(event) {
   initDrag: function(event) {
-    if(typeof Draggable._dragging[this.element] != 'undefined' &&
+    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
       Draggable._dragging[this.element]) return;
       Draggable._dragging[this.element]) return;
-    if(Event.isLeftClick(event)) {    
+    if(Event.isLeftClick(event)) {
       // abort on form elements, fixes a Firefox issue
       var src = Event.element(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=='OPTION' ||
-        src.tagName=='BUTTON' ||
-        src.tagName=='TEXTAREA')) return;
-        
+      if((tag_name = src.tagName.toUpperCase()) && (
+        tag_name=='INPUT' ||
+        tag_name=='SELECT' ||
+        tag_name=='OPTION' ||
+        tag_name=='BUTTON' ||
+        tag_name=='TEXTAREA')) return;
+
       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]) });
       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);
     }
   },
       Draggables.activate(this);
       Event.stop(event);
     }
   },
-  
+
   startDrag: function(event) {
     this.dragging = true;
   startDrag: function(event) {
     this.dragging = true;
-    
+    if(!this.delta)
+      this.delta = this.currentDelta();
+
     if(this.options.zindex) {
       this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
       this.element.style.zIndex = this.options.zindex;
     }
     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);
     if(this.options.ghosting) {
       this._clone = this.element.cloneNode(true);
-      Position.absolutize(this.element);
+      this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
+      if (!this._originallyAbsolute)
+        Position.absolutize(this.element);
       this.element.parentNode.insertBefore(this._clone, this.element);
     }
       this.element.parentNode.insertBefore(this._clone, this.element);
     }
-    
+
     if(this.options.scroll) {
       if (this.options.scroll == window) {
         var where = this._getWindowScroll(this.options.scroll);
     if(this.options.scroll) {
       if (this.options.scroll == window) {
         var where = this._getWindowScroll(this.options.scroll);
@@ -343,24 +347,28 @@ Draggable.prototype = {
         this.originalScrollTop = this.options.scroll.scrollTop;
       }
     }
         this.originalScrollTop = this.options.scroll.scrollTop;
       }
     }
-    
+
     Draggables.notify('onStart', this, event);
     Draggables.notify('onStart', this, event);
-        
+
     if(this.options.starteffect) this.options.starteffect(this.element);
   },
     if(this.options.starteffect) this.options.starteffect(this.element);
   },
-  
+
   updateDrag: function(event, pointer) {
     if(!this.dragging) this.startDrag(event);
   updateDrag: function(event, pointer) {
     if(!this.dragging) this.startDrag(event);
-    Position.prepare();
-    Droppables.show(pointer, this.element);
+
+    if(!this.options.quiet){
+      Position.prepare();
+      Droppables.show(pointer, this.element);
+    }
+
     Draggables.notify('onDrag', this, event);
     Draggables.notify('onDrag', this, event);
-    
+
     this.draw(pointer);
     if(this.options.change) this.options.change(this);
     this.draw(pointer);
     if(this.options.change) this.options.change(this);
-    
+
     if(this.options.scroll) {
       this.stopScrolling();
     if(this.options.scroll) {
       this.stopScrolling();
-      
+
       var p;
       if (this.options.scroll == window) {
         with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
       var p;
       if (this.options.scroll == window) {
         with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
@@ -378,32 +386,46 @@ Draggable.prototype = {
       if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
       this.startScrolling(speed);
     }
       if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
       this.startScrolling(speed);
     }
-    
+
     // fix AppleWebKit rendering
     // fix AppleWebKit rendering
-    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
-    
+    if(Prototype.Browser.WebKit) window.scrollBy(0,0);
+
     Event.stop(event);
   },
     Event.stop(event);
   },
-  
+
   finishDrag: function(event, success) {
     this.dragging = false;
 
   finishDrag: function(event, success) {
     this.dragging = false;
 
+    if(this.options.quiet){
+      Position.prepare();
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      Droppables.show(pointer, this.element);
+    }
+
     if(this.options.ghosting) {
     if(this.options.ghosting) {
-      Position.relativize(this.element);
+      if (!this._originallyAbsolute)
+        Position.relativize(this.element);
+      delete this._originallyAbsolute;
       Element.remove(this._clone);
       this._clone = null;
     }
 
       Element.remove(this._clone);
       this._clone = null;
     }
 
-    if(success) Droppables.fire(event, this.element);
+    var dropped = false;
+    if(success) {
+      dropped = Droppables.fire(event, this.element);
+      if (!dropped) dropped = false;
+    }
+    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
     Draggables.notify('onEnd', this, event);
 
     var revert = this.options.revert;
     Draggables.notify('onEnd', this, event);
 
     var revert = this.options.revert;
-    if(revert && typeof revert == 'function') revert = revert(this.element);
-    
+    if(revert && Object.isFunction(revert)) revert = revert(this.element);
+
     var d = this.currentDelta();
     if(revert && this.options.reverteffect) {
     var d = this.currentDelta();
     if(revert && this.options.reverteffect) {
-      this.options.reverteffect(this.element, 
-        d[1]-this.delta[1], d[0]-this.delta[0]);
+      if (dropped == 0 || revert != 'failure')
+        this.options.reverteffect(this.element,
+          d[1]-this.delta[1], d[0]-this.delta[0]);
     } else {
       this.delta = d;
     }
     } else {
       this.delta = d;
     }
@@ -411,67 +433,67 @@ Draggable.prototype = {
     if(this.options.zindex)
       this.element.style.zIndex = this.originalZ;
 
     if(this.options.zindex)
       this.element.style.zIndex = this.originalZ;
 
-    if(this.options.endeffect) 
+    if(this.options.endeffect)
       this.options.endeffect(this.element);
       this.options.endeffect(this.element);
-      
+
     Draggables.deactivate(this);
     Droppables.reset();
   },
     Draggables.deactivate(this);
     Droppables.reset();
   },
-  
+
   keyPress: function(event) {
     if(event.keyCode!=Event.KEY_ESC) return;
     this.finishDrag(event, false);
     Event.stop(event);
   },
   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.stopScrolling();
     this.finishDrag(event, true);
     Event.stop(event);
   },
   endDrag: function(event) {
     if(!this.dragging) return;
     this.stopScrolling();
     this.finishDrag(event, true);
     Event.stop(event);
   },
-  
+
   draw: function(point) {
     var pos = Position.cumulativeOffset(this.element);
     if(this.options.ghosting) {
       var r   = Position.realOffset(this.element);
       pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
     }
   draw: function(point) {
     var pos = Position.cumulativeOffset(this.element);
     if(this.options.ghosting) {
       var r   = Position.realOffset(this.element);
       pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
     }
-    
+
     var d = this.currentDelta();
     pos[0] -= d[0]; pos[1] -= d[1];
     var d = this.currentDelta();
     pos[0] -= d[0]; pos[1] -= d[1];
-    
+
     if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
       pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
       pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
     }
     if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
       pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
       pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
     }
-    
-    var p = [0,1].map(function(i){ 
-      return (point[i]-pos[i]-this.offset[i]) 
+
+    var p = [0,1].map(function(i){
+      return (point[i]-pos[i]-this.offset[i])
     }.bind(this));
     }.bind(this));
-    
+
     if(this.options.snap) {
     if(this.options.snap) {
-      if(typeof this.options.snap == 'function') {
+      if(Object.isFunction(this.options.snap)) {
         p = this.options.snap(p[0],p[1],this);
       } else {
         p = this.options.snap(p[0],p[1],this);
       } else {
-      if(this.options.snap instanceof Array) {
+      if(Object.isArray(this.options.snap)) {
         p = p.map( function(v, i) {
         p = p.map( function(v, i) {
-          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
       } else {
         p = p.map( function(v) {
       } else {
         p = p.map( function(v) {
-          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+          return (v/this.options.snap).round()*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";
     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
   },
     if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
   },
-  
+
   stopScrolling: function() {
     if(this.scrollInterval) {
       clearInterval(this.scrollInterval);
   stopScrolling: function() {
     if(this.scrollInterval) {
       clearInterval(this.scrollInterval);
@@ -479,14 +501,14 @@ Draggable.prototype = {
       Draggables._lastScrollPointer = null;
     }
   },
       Draggables._lastScrollPointer = null;
     }
   },
-  
+
   startScrolling: function(speed) {
     if(!(speed[0] || speed[1])) return;
     this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
     this.lastScrolled = new Date();
     this.scrollInterval = setInterval(this.scroll.bind(this), 10);
   },
   startScrolling: function(speed) {
     if(!(speed[0] || speed[1])) return;
     this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
     this.lastScrolled = new Date();
     this.scrollInterval = setInterval(this.scroll.bind(this), 10);
   },
-  
+
   scroll: function() {
     var current = new Date();
     var delta = current - this.lastScrolled;
   scroll: function() {
     var current = new Date();
     var delta = current - this.lastScrolled;
@@ -502,7 +524,7 @@ Draggable.prototype = {
       this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
       this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
     }
       this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
       this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
     }
-    
+
     Position.prepare();
     Droppables.show(Draggables._lastPointer, this.element);
     Draggables.notify('onDrag', this);
     Position.prepare();
     Droppables.show(Draggables._lastPointer, this.element);
     Draggables.notify('onDrag', this);
@@ -516,10 +538,10 @@ Draggable.prototype = {
         Draggables._lastScrollPointer[1] = 0;
       this.draw(Draggables._lastScrollPointer);
     }
         Draggables._lastScrollPointer[1] = 0;
       this.draw(Draggables._lastScrollPointer);
     }
-    
+
     if(this.options.change) this.options.change(this);
   },
     if(this.options.change) this.options.change(this);
   },
-  
+
   _getWindowScroll: function(w) {
     var T, L, W, H;
     with (w.document) {
   _getWindowScroll: function(w) {
     var T, L, W, H;
     with (w.document) {
@@ -538,41 +560,42 @@ Draggable.prototype = {
         H = documentElement.clientHeight;
       } else {
         W = body.offsetWidth;
         H = documentElement.clientHeight;
       } else {
         W = body.offsetWidth;
-        H = body.offsetHeight
+        H = body.offsetHeight;
       }
     }
     return { top: T, left: L, width: W, height: H };
   }
       }
     }
     return { top: T, left: L, width: W, height: H };
   }
-}
+});
+
+Draggable._dragging = { };
 
 /*--------------------------------------------------------------------------*/
 
 
 /*--------------------------------------------------------------------------*/
 
-var SortableObserver = Class.create();
-SortableObserver.prototype = {
+var SortableObserver = Class.create({
   initialize: function(element, observer) {
     this.element   = $(element);
     this.observer  = observer;
     this.lastValue = Sortable.serialize(this.element);
   },
   initialize: function(element, observer) {
     this.element   = $(element);
     this.observer  = observer;
     this.lastValue = Sortable.serialize(this.element);
   },
-  
+
   onStart: function() {
     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)
   }
   onEnd: function() {
     Sortable.unmark();
     if(this.lastValue != Sortable.serialize(this.element))
       this.observer(this.element)
   }
-}
+});
 
 var Sortable = {
   SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
 
 var Sortable = {
   SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
-  
-  sortables: {},
-  
+
+  sortables: { },
+
   _findRootElement: function(element) {
   _findRootElement: function(element) {
-    while (element.tagName != "BODY") {  
+    while (element.tagName.toUpperCase() != "BODY") {
       if(element.id && Sortable.sortables[element.id]) return element;
       element = element.parentNode;
     }
       if(element.id && Sortable.sortables[element.id]) return element;
       element = element.parentNode;
     }
@@ -583,22 +606,23 @@ var Sortable = {
     if(!element) return;
     return Sortable.sortables[element.id];
   },
     if(!element) return;
     return Sortable.sortables[element.id];
   },
-  
+
   destroy: function(element){
   destroy: function(element){
-    var s = Sortable.options(element);
-    
+    element = $(element);
+    var s = Sortable.sortables[element.id];
+
     if(s) {
       Draggables.removeObserver(s.element);
       s.droppables.each(function(d){ Droppables.remove(d) });
       s.draggables.invoke('destroy');
     if(s) {
       Draggables.removeObserver(s.element);
       s.droppables.each(function(d){ Droppables.remove(d) });
       s.draggables.invoke('destroy');
-      
+
       delete Sortable.sortables[s.element.id];
     }
   },
 
   create: function(element) {
     element = $(element);
       delete Sortable.sortables[s.element.id];
     }
   },
 
   create: function(element) {
     element = $(element);
-    var options = Object.extend({ 
+    var options = Object.extend({
       element:     element,
       tag:         'li',       // assumes li children, override with tag: 'tagname'
       dropOnEmpty: false,
       element:     element,
       tag:         'li',       // assumes li children, override with tag: 'tagname'
       dropOnEmpty: false,
@@ -612,13 +636,20 @@ var Sortable = {
       delay:       0,
       hoverclass:  null,
       ghosting:    false,
       delay:       0,
       hoverclass:  null,
       ghosting:    false,
+      quiet:       false,
       scroll:      false,
       scrollSensitivity: 20,
       scrollSpeed: 15,
       format:      this.SERIALIZE_RULE,
       scroll:      false,
       scrollSensitivity: 20,
       scrollSpeed: 15,
       format:      this.SERIALIZE_RULE,
+
+      // these take arrays of elements or ids and can be
+      // used for better initialization performance
+      elements:    false,
+      handles:     false,
+
       onChange:    Prototype.emptyFunction,
       onUpdate:    Prototype.emptyFunction
       onChange:    Prototype.emptyFunction,
       onUpdate:    Prototype.emptyFunction
-    }, arguments[1] || {});
+    }, arguments[1] || { });
 
     // clear any old sortable with same element
     this.destroy(element);
 
     // clear any old sortable with same element
     this.destroy(element);
@@ -626,6 +657,7 @@ var Sortable = {
     // build options for the draggables
     var options_for_draggable = {
       revert:      true,
     // build options for the draggables
     var options_for_draggable = {
       revert:      true,
+      quiet:       options.quiet,
       scroll:      options.scroll,
       scrollSpeed: options.scrollSpeed,
       scrollSensitivity: options.scrollSensitivity,
       scroll:      options.scroll,
       scrollSpeed: options.scrollSpeed,
       scrollSensitivity: options.scrollSensitivity,
@@ -651,24 +683,24 @@ var Sortable = {
     if(options.zindex)
       options_for_draggable.zindex = options.zindex;
 
     if(options.zindex)
       options_for_draggable.zindex = options.zindex;
 
-    // build options for the droppables  
+    // build options for the droppables
     var options_for_droppable = {
       overlap:     options.overlap,
       containment: options.containment,
       tree:        options.tree,
       hoverclass:  options.hoverclass,
       onHover:     Sortable.onHover
     var options_for_droppable = {
       overlap:     options.overlap,
       containment: options.containment,
       tree:        options.tree,
       hoverclass:  options.hoverclass,
       onHover:     Sortable.onHover
-    }
-    
+    };
+
     var options_for_tree = {
       onHover:      Sortable.onEmptyHover,
       overlap:      options.overlap,
       containment:  options.containment,
       hoverclass:   options.hoverclass
     var options_for_tree = {
       onHover:      Sortable.onEmptyHover,
       overlap:      options.overlap,
       containment:  options.containment,
       hoverclass:   options.hoverclass
-    }
+    };
 
     // fix for gecko engine
 
     // fix for gecko engine
-    Element.cleanWhitespace(element); 
+    Element.cleanWhitespace(element);
 
     options.draggables = [];
     options.droppables = [];
 
     options.draggables = [];
     options.droppables = [];
@@ -679,17 +711,16 @@ var Sortable = {
       options.droppables.push(element);
     }
 
       options.droppables.push(element);
     }
 
-    (this.findElements(element, options) || []).each( function(e) {
-      // handles are per-draggable
-      var handle = options.handle ? 
-        $(e).down('.'+options.handle,0) : e;    
+    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
+      var handle = options.handles ? $(options.handles[i]) :
+        (options.handle ? $(e).select('.' + options.handle)[0] : e);
       options.draggables.push(
         new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
       Droppables.add(e, options_for_droppable);
       if(options.tree) e.treeNode = element;
       options.draggables.push(
         new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
       Droppables.add(e, options_for_droppable);
       if(options.tree) e.treeNode = element;
-      options.droppables.push(e);      
+      options.droppables.push(e);
     });
     });
-    
+
     if(options.tree) {
       (Sortable.findTreeElements(element, options) || []).each( function(e) {
         Droppables.add(e, options_for_tree);
     if(options.tree) {
       (Sortable.findTreeElements(element, options) || []).each( function(e) {
         Droppables.add(e, options_for_tree);
@@ -711,7 +742,7 @@ var Sortable = {
     return Element.findChildren(
       element, options.only, options.tree ? true : false, options.tag);
   },
     return Element.findChildren(
       element, options.only, options.tree ? true : false, options.tag);
   },
-  
+
   findTreeElements: function(element, options) {
     return Element.findChildren(
       element, options.only, options.tree ? true : false, options.treeTag);
   findTreeElements: function(element, options) {
     return Element.findChildren(
       element, options.only, options.tree ? true : false, options.treeTag);
@@ -728,7 +759,7 @@ var Sortable = {
         var oldParentNode = element.parentNode;
         element.style.visibility = "hidden"; // fix gecko rendering
         dropon.parentNode.insertBefore(element, dropon);
         var oldParentNode = element.parentNode;
         element.style.visibility = "hidden"; // fix gecko rendering
         dropon.parentNode.insertBefore(element, dropon);
-        if(dropon.parentNode!=oldParentNode) 
+        if(dropon.parentNode!=oldParentNode)
           Sortable.options(oldParentNode).onChange(element);
         Sortable.options(dropon.parentNode).onChange(element);
       }
           Sortable.options(oldParentNode).onChange(element);
         Sortable.options(dropon.parentNode).onChange(element);
       }
@@ -739,26 +770,26 @@ var Sortable = {
         var oldParentNode = element.parentNode;
         element.style.visibility = "hidden"; // fix gecko rendering
         dropon.parentNode.insertBefore(element, nextElement);
         var oldParentNode = element.parentNode;
         element.style.visibility = "hidden"; // fix gecko rendering
         dropon.parentNode.insertBefore(element, nextElement);
-        if(dropon.parentNode!=oldParentNode) 
+        if(dropon.parentNode!=oldParentNode)
           Sortable.options(oldParentNode).onChange(element);
         Sortable.options(dropon.parentNode).onChange(element);
       }
     }
   },
           Sortable.options(oldParentNode).onChange(element);
         Sortable.options(dropon.parentNode).onChange(element);
       }
     }
   },
-  
+
   onEmptyHover: function(element, dropon, overlap) {
     var oldParentNode = element.parentNode;
     var droponOptions = Sortable.options(dropon);
   onEmptyHover: function(element, dropon, overlap) {
     var oldParentNode = element.parentNode;
     var droponOptions = Sortable.options(dropon);
-        
+
     if(!Element.isParent(dropon, element)) {
       var index;
     if(!Element.isParent(dropon, element)) {
       var index;
-      
+
       var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
       var child = null;
       var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
       var child = null;
-            
+
       if(children) {
         var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
       if(children) {
         var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
-        
+
         for (index = 0; index < children.length; index += 1) {
           if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
             offset -= Element.offsetSize (children[index], droponOptions.overlap);
         for (index = 0; index < children.length; index += 1) {
           if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
             offset -= Element.offsetSize (children[index], droponOptions.overlap);
@@ -771,9 +802,9 @@ var Sortable = {
           }
         }
       }
           }
         }
       }
-      
+
       dropon.insertBefore(element, child);
       dropon.insertBefore(element, child);
-      
+
       Sortable.options(oldParentNode).onChange(element);
       droponOptions.onChange(element);
     }
       Sortable.options(oldParentNode).onChange(element);
       droponOptions.onChange(element);
     }
@@ -786,34 +817,34 @@ var Sortable = {
   mark: function(dropon, position) {
     // mark on ghosting only
     var sortable = Sortable.options(dropon.parentNode);
   mark: function(dropon, position) {
     // mark on ghosting only
     var sortable = Sortable.options(dropon.parentNode);
-    if(sortable && !sortable.ghosting) return; 
+    if(sortable && !sortable.ghosting) return;
 
     if(!Sortable._marker) {
 
     if(!Sortable._marker) {
-      Sortable._marker = 
+      Sortable._marker =
         ($('dropmarker') || Element.extend(document.createElement('DIV'))).
           hide().addClassName('dropmarker').setStyle({position:'absolute'});
       document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
         ($('dropmarker') || Element.extend(document.createElement('DIV'))).
           hide().addClassName('dropmarker').setStyle({position:'absolute'});
       document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
-    }    
+    }
     var offsets = Position.cumulativeOffset(dropon);
     Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
     var offsets = Position.cumulativeOffset(dropon);
     Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
-    
+
     if(position=='after')
     if(position=='after')
-      if(sortable.overlap == 'horizontal') 
+      if(sortable.overlap == 'horizontal')
         Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
       else
         Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
         Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
       else
         Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
-    
+
     Sortable._marker.show();
   },
     Sortable._marker.show();
   },
-  
+
   _tree: function(element, options, parent) {
     var children = Sortable.findElements(element, options) || [];
   _tree: function(element, options, parent) {
     var children = Sortable.findElements(element, options) || [];
-  
+
     for (var i = 0; i < children.length; ++i) {
       var match = children[i].id.match(options.format);
 
       if (!match) continue;
     for (var i = 0; i < children.length; ++i) {
       var match = children[i].id.match(options.format);
 
       if (!match) continue;
-      
+
       var child = {
         id: encodeURIComponent(match ? match[1] : null),
         element: element,
       var child = {
         id: encodeURIComponent(match ? match[1] : null),
         element: element,
@@ -821,16 +852,16 @@ var Sortable = {
         children: [],
         position: parent.children.length,
         container: $(children[i]).down(options.treeTag)
         children: [],
         position: parent.children.length,
         container: $(children[i]).down(options.treeTag)
-      }
-      
+      };
+
       /* Get the element containing the children and recurse over it */
       if (child.container)
       /* Get the element containing the children and recurse over it */
       if (child.container)
-        this._tree(child.container, options, child)
-      
+        this._tree(child.container, options, child);
+
       parent.children.push (child);
     }
 
       parent.children.push (child);
     }
 
-    return parent; 
+    return parent;
   },
 
   tree: function(element) {
   },
 
   tree: function(element) {
@@ -842,16 +873,16 @@ var Sortable = {
       only: sortableOptions.only,
       name: element.id,
       format: sortableOptions.format
       only: sortableOptions.only,
       name: element.id,
       format: sortableOptions.format
-    }, arguments[1] || {});
-    
+    }, arguments[1] || { });
+
     var root = {
       id: null,
       parent: null,
       children: [],
       container: element,
       position: 0
     var root = {
       id: null,
       parent: null,
       children: [],
       container: element,
       position: 0
-    }
-    
+    };
+
     return Sortable._tree(element, options, root);
   },
 
     return Sortable._tree(element, options, root);
   },
 
@@ -866,8 +897,8 @@ var Sortable = {
 
   sequence: function(element) {
     element = $(element);
 
   sequence: function(element) {
     element = $(element);
-    var options = Object.extend(this.options(element), arguments[1] || {});
-    
+    var options = Object.extend(this.options(element), arguments[1] || { });
+
     return $(this.findElements(element, options) || []).map( function(item) {
       return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
     });
     return $(this.findElements(element, options) || []).map( function(item) {
       return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
     });
@@ -875,15 +906,15 @@ var Sortable = {
 
   setSequence: function(element, new_sequence) {
     element = $(element);
 
   setSequence: function(element, new_sequence) {
     element = $(element);
-    var options = Object.extend(this.options(element), arguments[2] || {});
-    
-    var nodeMap = {};
+    var options = Object.extend(this.options(element), arguments[2] || { });
+
+    var nodeMap = { };
     this.findElements(element, options).each( function(n) {
         if (n.id.match(options.format))
             nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
         n.parentNode.removeChild(n);
     });
     this.findElements(element, options).each( function(n) {
         if (n.id.match(options.format))
             nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
         n.parentNode.removeChild(n);
     });
-   
+
     new_sequence.each(function(ident) {
       var n = nodeMap[ident];
       if (n) {
     new_sequence.each(function(ident) {
       var n = nodeMap[ident];
       if (n) {
@@ -892,16 +923,16 @@ var Sortable = {
       }
     });
   },
       }
     });
   },
-  
+
   serialize: function(element) {
     element = $(element);
   serialize: function(element) {
     element = $(element);
-    var options = Object.extend(Sortable.options(element), arguments[1] || {});
+    var options = Object.extend(Sortable.options(element), arguments[1] || { });
     var name = encodeURIComponent(
       (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
     var name = encodeURIComponent(
       (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
-    
+
     if (options.tree) {
       return Sortable.tree(element, arguments[1]).children.map( function (item) {
     if (options.tree) {
       return Sortable.tree(element, arguments[1]).children.map( function (item) {
-        return [name + Sortable._constructIndex(item) + "[id]=" + 
+        return [name + Sortable._constructIndex(item) + "[id]=" +
                 encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
       }).flatten().join('&');
     } else {
                 encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
       }).flatten().join('&');
     } else {
@@ -910,16 +941,16 @@ var Sortable = {
       }).join('&');
     }
   }
       }).join('&');
     }
   }
-}
+};
 
 // Returns true if child is contained within element
 Element.isParent = function(child, element) {
   if (!child.parentNode || child == element) return false;
   if (child.parentNode == element) return true;
   return Element.isParent(child.parentNode, element);
 
 // Returns true if child is contained within element
 Element.isParent = function(child, element) {
   if (!child.parentNode || child == element) return false;
   if (child.parentNode == element) return true;
   return Element.isParent(child.parentNode, element);
-}
+};
 
 
-Element.findChildren = function(element, only, recursive, tagName) {    
+Element.findChildren = function(element, only, recursive, tagName) {
   if(!element.hasChildNodes()) return null;
   tagName = tagName.toUpperCase();
   if(only) only = [only].flatten();
   if(!element.hasChildNodes()) return null;
   tagName = tagName.toUpperCase();
   if(only) only = [only].flatten();
@@ -935,8 +966,8 @@ Element.findChildren = function(element, only, recursive, tagName) {
   });
 
   return (elements.length>0 ? elements.flatten() : []);
   });
 
   return (elements.length>0 ? elements.flatten() : []);
-}
+};
 
 Element.offsetSize = function (element, type) {
   return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
 
 Element.offsetSize = function (element, type) {
   return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
-}
+};
\ No newline at end of file
index 3b02eda2b29ffd4d55fff72caa72d91b18a92551..5a639d2dea94463ceebfeffec4e26e7738d0fdc7 100644 (file)
@@ -1,84 +1,55 @@
-// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
 // Contributors:
 //  Justin Palmer (http://encytemedia.com/)
 //  Mark Pilgrim (http://diveintomark.org/)
 //  Martin Bialasinki
 // Contributors:
 //  Justin Palmer (http://encytemedia.com/)
 //  Mark Pilgrim (http://diveintomark.org/)
 //  Martin Bialasinki
-// 
+//
 // script.aculo.us is freely distributable under the terms of an MIT-style license.
 // script.aculo.us is freely distributable under the terms of an MIT-style license.
-// For details, see the script.aculo.us web site: http://script.aculo.us/ 
+// For details, see the script.aculo.us web site: http://script.aculo.us/
 
 
-// converts rgb() and #xxx to #xxxxxx format,  
-// returns self (or first argument) if not convertable  
-String.prototype.parseColor = function() {  
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
   var color = '#';
   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));  
-}
+  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) {  
+Element.collectTextNodes = function(element) {
   return $A($(element).childNodes).collect( function(node) {
   return $A($(element).childNodes).collect( function(node) {
-    return (node.nodeType==3 ? node.nodeValue : 
+    return (node.nodeType==3 ? node.nodeValue :
       (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
   }).flatten().join('');
       (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
   }).flatten().join('');
-}
+};
 
 
-Element.collectTextNodesIgnoreClass = function(element, className) {  
+Element.collectTextNodesIgnoreClass = function(element, className) {
   return $A($(element).childNodes).collect( function(node) {
   return $A($(element).childNodes).collect( function(node) {
-    return (node.nodeType==3 ? node.nodeValue : 
-      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 
+    return (node.nodeType==3 ? node.nodeValue :
+      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
         Element.collectTextNodesIgnoreClass(node, className) : ''));
   }).flatten().join('');
         Element.collectTextNodesIgnoreClass(node, className) : ''));
   }).flatten().join('');
-}
+};
 
 Element.setContentZoom = function(element, percent) {
 
 Element.setContentZoom = function(element, percent) {
-  element = $(element);  
-  element.setStyle({fontSize: (percent/100) + 'em'});   
-  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
-  return element;
-}
-
-Element.getOpacity = function(element){
   element = $(element);
   element = $(element);
-  var opacity;
-  if (opacity = element.getStyle('opacity'))  
-    return parseFloat(opacity);  
-  if (opacity = (element.getStyle('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({ opacity: 
-      (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
-      0.999999 : 1.0 });
-    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
-      element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
-  } else {  
-    if(value < 0.00001) value = 0;  
-    element.setStyle({opacity: value});
-    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
-      element.setStyle(
-        { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
-            'alpha(opacity='+value*100+')' });  
-  }
+  element.setStyle({fontSize: (percent/100) + 'em'});
+  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
   return element;
   return element;
-}  
-Element.getInlineOpacity = function(element){  
+};
+
+Element.getInlineOpacity = function(element){
   return $(element).style.opacity || '';
   return $(element).style.opacity || '';
-}  
+};
 
 Element.forceRerendering = function(element) {
   try {
 
 Element.forceRerendering = function(element) {
   try {
@@ -91,32 +62,59 @@ Element.forceRerendering = function(element) {
 
 /*--------------------------------------------------------------------------*/
 
 
 /*--------------------------------------------------------------------------*/
 
-Array.prototype.call = function() {
-  var args = arguments;
-  this.each(function(f){ f.apply(this, args) });
-}
-
-/*--------------------------------------------------------------------------*/
-
 var Effect = {
   _elementDoesNotExistError: {
     name: 'ElementDoesNotExistError',
     message: 'The specified DOM element does not exist, but is required for this effect to operate'
   },
 var Effect = {
   _elementDoesNotExistError: {
     name: 'ElementDoesNotExistError',
     message: 'The specified DOM element does not exist, but is required for this effect to operate'
   },
+  Transitions: {
+    linear: Prototype.K,
+    sinoidal: function(pos) {
+      return (-Math.cos(pos*Math.PI)/2) + .5;
+    },
+    reverse: function(pos) {
+      return 1-pos;
+    },
+    flicker: function(pos) {
+      var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
+      return pos > 1 ? 1 : pos;
+    },
+    wobble: function(pos) {
+      return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
+    },
+    pulse: function(pos, pulses) {
+      return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
+    },
+    spring: function(pos) {
+      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
+    },
+    none: function(pos) {
+      return 0;
+    },
+    full: function(pos) {
+      return 1;
+    }
+  },
+  DefaultOptions: {
+    duration:   1.0,   // seconds
+    fps:        100,   // 100= assume 66fps max.
+    sync:       false, // true for combining
+    from:       0.0,
+    to:         1.0,
+    delay:      0.0,
+    queue:      'parallel'
+  },
   tagifyText: function(element) {
   tagifyText: function(element) {
-    if(typeof Builder == 'undefined')
-      throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
-      
     var tagifyStyle = 'position:relative';
     var tagifyStyle = 'position:relative';
-    if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
-    
+    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
+
     element = $(element);
     $A(element.childNodes).each( function(child) {
     element = $(element);
     $A(element.childNodes).each( function(child) {
-      if(child.nodeType==3) {
+      if (child.nodeType==3) {
         child.nodeValue.toArray().each( function(character) {
           element.insertBefore(
         child.nodeValue.toArray().each( function(character) {
           element.insertBefore(
-            Builder.node('span',{style: tagifyStyle},
-              character == ' ' ? String.fromCharCode(160) : character), 
+            new Element('span', {style: tagifyStyle}).update(
+              character == ' ' ? String.fromCharCode(160) : character),
               child);
         });
         Element.remove(child);
               child);
         });
         Element.remove(child);
@@ -125,17 +123,17 @@ var Effect = {
   },
   multiple: function(element, effect) {
     var elements;
   },
   multiple: function(element, effect) {
     var elements;
-    if(((typeof element == 'object') || 
-        (typeof element == 'function')) && 
+    if (((typeof element == 'object') ||
+        Object.isFunction(element)) &&
        (element.length))
       elements = element;
     else
       elements = $(element).childNodes;
        (element.length))
       elements = element;
     else
       elements = $(element).childNodes;
-      
+
     var options = Object.extend({
       speed: 0.1,
       delay: 0.0
     var options = Object.extend({
       speed: 0.1,
       delay: 0.0
-    }, arguments[2] || {});
+    }, arguments[2] || { });
     var masterDelay = options.delay;
 
     $A(elements).each( function(element, index) {
     var masterDelay = options.delay;
 
     $A(elements).each( function(element, index) {
@@ -152,50 +150,17 @@ var Effect = {
     effect = (effect || 'appear').toLowerCase();
     var options = Object.extend({
       queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
     effect = (effect || 'appear').toLowerCase();
     var options = Object.extend({
       queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
-    }, arguments[2] || {});
-    Effect[element.visible() ? 
+    }, arguments[2] || { });
+    Effect[element.visible() ?
       Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
   }
 };
 
       Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
   }
 };
 
-var Effect2 = Effect; // deprecated
-
-/* ------------- transitions ------------- */
-
-Effect.Transitions = {
-  linear: Prototype.K,
-  sinoidal: function(pos) {
-    return (-Math.cos(pos*Math.PI)/2) + 0.5;
-  },
-  reverse: function(pos) {
-    return 1-pos;
-  },
-  flicker: function(pos) {
-    return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
-  },
-  wobble: function(pos) {
-    return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
-  },
-  pulse: function(pos, pulses) { 
-    pulses = pulses || 5; 
-    return (
-      Math.round((pos % (1/pulses)) * pulses) == 0 ? 
-            ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : 
-        1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
-      );
-  },
-  none: function(pos) {
-    return 0;
-  },
-  full: function(pos) {
-    return 1;
-  }
-};
+Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
 
 /* ------------- core effects ------------- */
 
 
 /* ------------- core effects ------------- */
 
-Effect.ScopedQueue = Class.create();
-Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+Effect.ScopedQueue = Class.create(Enumerable, {
   initialize: function() {
     this.effects  = [];
     this.interval = null;
   initialize: function() {
     this.effects  = [];
     this.interval = null;
@@ -205,13 +170,13 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
   },
   add: function(effect) {
     var timestamp = new Date().getTime();
   },
   add: function(effect) {
     var timestamp = new Date().getTime();
-    
-    var position = (typeof effect.options.queue == 'string') ? 
+
+    var position = Object.isString(effect.options.queue) ?
       effect.options.queue : effect.options.queue.position;
       effect.options.queue : effect.options.queue.position;
-    
+
     switch(position) {
       case 'front':
     switch(position) {
       case 'front':
-        // move unstarted effects after this effect  
+        // 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;
         this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
             e.startOn  += effect.finishOn;
             e.finishOn += effect.finishOn;
@@ -225,119 +190,127 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
         timestamp = this.effects.pluck('finishOn').max() || timestamp;
         break;
     }
         timestamp = this.effects.pluck('finishOn').max() || timestamp;
         break;
     }
-    
+
     effect.startOn  += timestamp;
     effect.finishOn += timestamp;
 
     effect.startOn  += timestamp;
     effect.finishOn += timestamp;
 
-    if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
       this.effects.push(effect);
       this.effects.push(effect);
-    
-    if(!this.interval) 
-      this.interval = setInterval(this.loop.bind(this), 40);
+
+    if (!this.interval)
+      this.interval = setInterval(this.loop.bind(this), 15);
   },
   remove: function(effect) {
     this.effects = this.effects.reject(function(e) { return e==effect });
   },
   remove: function(effect) {
     this.effects = this.effects.reject(function(e) { return e==effect });
-    if(this.effects.length == 0) {
+    if (this.effects.length == 0) {
       clearInterval(this.interval);
       this.interval = null;
     }
   },
   loop: function() {
     var timePos = new Date().getTime();
       clearInterval(this.interval);
       this.interval = null;
     }
   },
   loop: function() {
     var timePos = new Date().getTime();
-    this.effects.invoke('loop', timePos);
+    for(var i=0, len=this.effects.length;i<len;i++)
+      this.effects[i] && this.effects[i].loop(timePos);
   }
 });
 
 Effect.Queues = {
   instances: $H(),
   get: function(queueName) {
   }
 });
 
 Effect.Queues = {
   instances: $H(),
   get: function(queueName) {
-    if(typeof queueName != 'string') return queueName;
-    
-    if(!this.instances[queueName])
-      this.instances[queueName] = new Effect.ScopedQueue();
-      
-    return this.instances[queueName];
+    if (!Object.isString(queueName)) return queueName;
+
+    return this.instances.get(queueName) ||
+      this.instances.set(queueName, new Effect.ScopedQueue());
   }
   }
-}
+};
 Effect.Queue = Effect.Queues.get('global');
 
 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 = {
+Effect.Base = Class.create({
   position: null,
   start: function(options) {
   position: null,
   start: function(options) {
-    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+    function codeForEvent(options,eventName){
+      return (
+        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
+        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
+      );
+    }
+    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
+    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
     this.currentFrame = 0;
     this.state        = 'idle';
     this.startOn      = this.options.delay*1000;
     this.currentFrame = 0;
     this.state        = 'idle';
     this.startOn      = this.options.delay*1000;
-    this.finishOn     = this.startOn + (this.options.duration*1000);
+    this.finishOn     = this.startOn+(this.options.duration*1000);
+    this.fromToDelta  = this.options.to-this.options.from;
+    this.totalTime    = this.finishOn-this.startOn;
+    this.totalFrames  = this.options.fps*this.options.duration;
+
+    this.render = (function() {
+      function dispatch(effect, eventName) {
+        if (effect.options[eventName + 'Internal'])
+          effect.options[eventName + 'Internal'](effect);
+        if (effect.options[eventName])
+          effect.options[eventName](effect);
+      }
+
+      return function(pos) {
+        if (this.state === "idle") {
+          this.state = "running";
+          dispatch(this, 'beforeSetup');
+          if (this.setup) this.setup();
+          dispatch(this, 'afterSetup');
+        }
+        if (this.state === "running") {
+          pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
+          this.position = pos;
+          dispatch(this, 'beforeUpdate');
+          if (this.update) this.update(pos);
+          dispatch(this, 'afterUpdate');
+        }
+      };
+    })();
+
     this.event('beforeStart');
     this.event('beforeStart');
-    if(!this.options.sync)
-      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+    if (!this.options.sync)
+      Effect.Queues.get(Object.isString(this.options.queue) ?
         'global' : this.options.queue.scope).add(this);
   },
   loop: function(timePos) {
         'global' : this.options.queue.scope).add(this);
   },
   loop: function(timePos) {
-    if(timePos >= this.startOn) {
-      if(timePos >= this.finishOn) {
+    if (timePos >= this.startOn) {
+      if (timePos >= this.finishOn) {
         this.render(1.0);
         this.cancel();
         this.event('beforeFinish');
         this.render(1.0);
         this.cancel();
         this.event('beforeFinish');
-        if(this.finish) this.finish(); 
+        if (this.finish) this.finish();
         this.event('afterFinish');
         this.event('afterFinish');
-        return;  
+        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) {
+      var pos   = (timePos - this.startOn) / this.totalTime,
+          frame = (pos * this.totalFrames).round();
+      if (frame > this.currentFrame) {
         this.render(pos);
         this.currentFrame = frame;
       }
     }
   },
         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() {
   cancel: function() {
-    if(!this.options.sync)
-      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+    if (!this.options.sync)
+      Effect.Queues.get(Object.isString(this.options.queue) ?
         'global' : this.options.queue.scope).remove(this);
     this.state = 'finished';
   },
   event: function(eventName) {
         '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);
+    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+    if (this.options[eventName]) this.options[eventName](this);
   },
   inspect: function() {
   },
   inspect: function() {
-    return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
+    var data = $H();
+    for(property in this)
+      if (!Object.isFunction(this[property])) data.set(property, this[property]);
+    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
   }
   }
-}
+});
 
 
-Effect.Parallel = Class.create();
-Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+Effect.Parallel = Class.create(Effect.Base, {
   initialize: function(effects) {
     this.effects = effects || [];
     this.start(arguments[1]);
   initialize: function(effects) {
     this.effects = effects || [];
     this.start(arguments[1]);
@@ -350,35 +323,45 @@ Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
       effect.render(1.0);
       effect.cancel();
       effect.event('beforeFinish');
       effect.render(1.0);
       effect.cancel();
       effect.event('beforeFinish');
-      if(effect.finish) effect.finish(position);
+      if (effect.finish) effect.finish(position);
       effect.event('afterFinish');
     });
   }
 });
 
       effect.event('afterFinish');
     });
   }
 });
 
-Effect.Event = Class.create();
-Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
+Effect.Tween = Class.create(Effect.Base, {
+  initialize: function(object, from, to) {
+    object = Object.isString(object) ? $(object) : object;
+    var args = $A(arguments), method = args.last(),
+      options = args.length == 5 ? args[3] : null;
+    this.method = Object.isFunction(method) ? method.bind(object) :
+      Object.isFunction(object[method]) ? object[method].bind(object) :
+      function(value) { object[method] = value };
+    this.start(Object.extend({ from: from, to: to }, options || { }));
+  },
+  update: function(position) {
+    this.method(position);
+  }
+});
+
+Effect.Event = Class.create(Effect.Base, {
   initialize: function() {
   initialize: function() {
-    var options = Object.extend({
-      duration: 0
-    }, arguments[0] || {});
-    this.start(options);
+    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
   },
   update: Prototype.emptyFunction
 });
 
   },
   update: Prototype.emptyFunction
 });
 
-Effect.Opacity = Class.create();
-Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+Effect.Opacity = Class.create(Effect.Base, {
   initialize: function(element) {
     this.element = $(element);
   initialize: function(element) {
     this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
+    if (!this.element) throw(Effect._elementDoesNotExistError);
     // make this work on IE on elements without 'layout'
     // make this work on IE on elements without 'layout'
-    if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
+    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
       this.element.setStyle({zoom: 1});
     var options = Object.extend({
       from: this.element.getOpacity() || 0.0,
       to:   1.0
       this.element.setStyle({zoom: 1});
     var options = Object.extend({
       from: this.element.getOpacity() || 0.0,
       to:   1.0
-    }, arguments[1] || {});
+    }, arguments[1] || { });
     this.start(options);
   },
   update: function(position) {
     this.start(options);
   },
   update: function(position) {
@@ -386,139 +369,133 @@ Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
   }
 });
 
   }
 });
 
-Effect.Move = Class.create();
-Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+Effect.Move = Class.create(Effect.Base, {
   initialize: function(element) {
     this.element = $(element);
   initialize: function(element) {
     this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
+    if (!this.element) throw(Effect._elementDoesNotExistError);
     var options = Object.extend({
       x:    0,
       y:    0,
       mode: 'relative'
     var options = Object.extend({
       x:    0,
       y:    0,
       mode: 'relative'
-    }, arguments[1] || {});
+    }, arguments[1] || { });
     this.start(options);
   },
   setup: function() {
     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) 
     this.element.makePositioned();
     this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
     this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
     this.element.makePositioned();
     this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
     this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
-    if(this.options.mode == 'absolute') {
-      // absolute movement, so we need to calc deltaX and deltaY
+    if (this.options.mode == 'absolute') {
       this.options.x = this.options.x - this.originalLeft;
       this.options.y = this.options.y - this.originalTop;
     }
   },
   update: function(position) {
     this.element.setStyle({
       this.options.x = this.options.x - this.originalLeft;
       this.options.y = this.options.y - this.originalTop;
     }
   },
   update: function(position) {
     this.element.setStyle({
-      left: Math.round(this.options.x  * position + this.originalLeft) + 'px',
-      top:  Math.round(this.options.y  * position + this.originalTop)  + 'px'
+      left: (this.options.x  * position + this.originalLeft).round() + 'px',
+      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
     });
   }
 });
 
 // for backwards compatibility
 Effect.MoveBy = function(element, toTop, toLeft) {
     });
   }
 });
 
 // for backwards compatibility
 Effect.MoveBy = function(element, toTop, toLeft) {
-  return new Effect.Move(element, 
-    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+  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), {
+Effect.Scale = Class.create(Effect.Base, {
   initialize: function(element, percent) {
     this.element = $(element);
   initialize: function(element, percent) {
     this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
+    if (!this.element) throw(Effect._elementDoesNotExistError);
     var options = Object.extend({
       scaleX: true,
       scaleY: true,
       scaleContent: true,
       scaleFromCenter: false,
     var options = Object.extend({
       scaleX: true,
       scaleY: true,
       scaleContent: true,
       scaleFromCenter: false,
-      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
+      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
       scaleFrom: 100.0,
       scaleTo:   percent
       scaleFrom: 100.0,
       scaleTo:   percent
-    }, arguments[2] || {});
+    }, arguments[2] || { });
     this.start(options);
   },
   setup: function() {
     this.restoreAfterFinish = this.options.restoreAfterFinish || false;
     this.elementPositioning = this.element.getStyle('position');
     this.start(options);
   },
   setup: function() {
     this.restoreAfterFinish = this.options.restoreAfterFinish || false;
     this.elementPositioning = this.element.getStyle('position');
-    
-    this.originalStyle = {};
+
+    this.originalStyle = { };
     ['top','left','width','height','fontSize'].each( function(k) {
       this.originalStyle[k] = this.element.style[k];
     }.bind(this));
     ['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;
     this.originalTop  = this.element.offsetTop;
     this.originalLeft = this.element.offsetLeft;
-    
+
     var fontSize = this.element.getStyle('font-size') || '100%';
     ['em','px','%','pt'].each( function(fontSizeType) {
     var fontSize = this.element.getStyle('font-size') || '100%';
     ['em','px','%','pt'].each( function(fontSizeType) {
-      if(fontSize.indexOf(fontSizeType)>0) {
+      if (fontSize.indexOf(fontSizeType)>0) {
         this.fontSize     = parseFloat(fontSize);
         this.fontSizeType = fontSizeType;
       }
     }.bind(this));
         this.fontSize     = parseFloat(fontSize);
         this.fontSizeType = fontSizeType;
       }
     }.bind(this));
-    
+
     this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
     this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
-    
+
     this.dims = null;
     this.dims = null;
-    if(this.options.scaleMode=='box')
+    if (this.options.scaleMode=='box')
       this.dims = [this.element.offsetHeight, this.element.offsetWidth];
       this.dims = [this.element.offsetHeight, this.element.offsetWidth];
-    if(/^content/.test(this.options.scaleMode))
+    if (/^content/.test(this.options.scaleMode))
       this.dims = [this.element.scrollHeight, this.element.scrollWidth];
       this.dims = [this.element.scrollHeight, this.element.scrollWidth];
-    if(!this.dims)
+    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);
       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)
+    if (this.options.scaleContent && this.fontSize)
       this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
     this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
   },
   finish: function(position) {
       this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
     this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
   },
   finish: function(position) {
-    if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
   },
   setDimensions: function(height, width) {
   },
   setDimensions: function(height, width) {
-    var d = {};
-    if(this.options.scaleX) d.width = Math.round(width) + 'px';
-    if(this.options.scaleY) d.height = Math.round(height) + 'px';
-    if(this.options.scaleFromCenter) {
+    var d = { };
+    if (this.options.scaleX) d.width = width.round() + 'px';
+    if (this.options.scaleY) d.height = height.round() + 'px';
+    if (this.options.scaleFromCenter) {
       var topd  = (height - this.dims[0])/2;
       var leftd = (width  - this.dims[1])/2;
       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';
+      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 {
       } else {
-        if(this.options.scaleY) d.top = -topd + 'px';
-        if(this.options.scaleX) d.left = -leftd + 'px';
+        if (this.options.scaleY) d.top = -topd + 'px';
+        if (this.options.scaleX) d.left = -leftd + 'px';
       }
     }
     this.element.setStyle(d);
   }
 });
 
       }
     }
     this.element.setStyle(d);
   }
 });
 
-Effect.Highlight = Class.create();
-Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+Effect.Highlight = Class.create(Effect.Base, {
   initialize: function(element) {
     this.element = $(element);
   initialize: function(element) {
     this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
-    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+    if (!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
     this.start(options);
   },
   setup: function() {
     // Prevent executing on elements not in the layout flow
     this.start(options);
   },
   setup: function() {
     // Prevent executing on elements not in the layout flow
-    if(this.element.getStyle('display')=='none') { this.cancel(); return; }
+    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
     // Disable background image during the effect
     // Disable background image during the effect
-    this.oldStyle = {
-      backgroundImage: this.element.getStyle('background-image') };
-    this.element.setStyle({backgroundImage: 'none'});
-    if(!this.options.endcolor)
+    this.oldStyle = { };
+    if (!this.options.keepBackgroundImage) {
+      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
+      this.element.setStyle({backgroundImage: 'none'});
+    }
+    if (!this.options.endcolor)
       this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
       this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
-    if(!this.options.restorecolor)
+    if (!this.options.restorecolor)
       this.options.restorecolor = this.element.getStyle('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.options.restorecolor = this.element.getStyle('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));
@@ -526,7 +503,7 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype),
   },
   update: function(position) {
     this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
   },
   update: function(position) {
     this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
-      return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
   },
   finish: function() {
     this.element.setStyle(Object.extend(this.oldStyle, {
   },
   finish: function() {
     this.element.setStyle(Object.extend(this.oldStyle, {
@@ -535,30 +512,20 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype),
   }
 });
 
   }
 });
 
-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));
-  }
-});
+Effect.ScrollTo = function(element) {
+  var options = arguments[1] || { },
+  scrollOffsets = document.viewport.getScrollOffsets(),
+  elementOffsets = $(element).cumulativeOffset();
+
+  if (options.offset) elementOffsets[1] += options.offset;
+
+  return new Effect.Tween(null,
+    scrollOffsets.top,
+    elementOffsets[1],
+    options,
+    function(p){ scrollTo(scrollOffsets.left, p.round()); }
+  );
+};
 
 /* ------------- combination effects ------------- */
 
 
 /* ------------- combination effects ------------- */
 
@@ -566,14 +533,15 @@ Effect.Fade = function(element) {
   element = $(element);
   var oldOpacity = element.getInlineOpacity();
   var options = Object.extend({
   element = $(element);
   var oldOpacity = element.getInlineOpacity();
   var options = Object.extend({
-  from: element.getOpacity() || 1.0,
-  to:   0.0,
-  afterFinishInternal: function(effect) { 
-    if(effect.options.to!=0) return;
-    effect.element.hide().setStyle({opacity: oldOpacity}); 
-  }}, arguments[1] || {});
+    from: element.getOpacity() || 1.0,
+    to:   0.0,
+    afterFinishInternal: function(effect) {
+      if (effect.options.to!=0) return;
+      effect.element.hide().setStyle({opacity: oldOpacity});
+    }
+  }, arguments[1] || { });
   return new Effect.Opacity(element,options);
   return new Effect.Opacity(element,options);
-}
+};
 
 Effect.Appear = function(element) {
   element = $(element);
 
 Effect.Appear = function(element) {
   element = $(element);
@@ -585,15 +553,15 @@ Effect.Appear = function(element) {
     effect.element.forceRerendering();
   },
   beforeSetup: function(effect) {
     effect.element.forceRerendering();
   },
   beforeSetup: function(effect) {
-    effect.element.setOpacity(effect.options.from).show(); 
-  }}, arguments[1] || {});
+    effect.element.setOpacity(effect.options.from).show();
+  }}, arguments[1] || { });
   return new Effect.Opacity(element,options);
   return new Effect.Opacity(element,options);
-}
+};
 
 Effect.Puff = function(element) {
   element = $(element);
 
 Effect.Puff = function(element) {
   element = $(element);
-  var oldStyle = { 
-    opacity: element.getInlineOpacity(), 
+  var oldStyle = {
+    opacity: element.getInlineOpacity(),
     position: element.getStyle('position'),
     top:  element.style.top,
     left: element.style.left,
     position: element.getStyle('position'),
     top:  element.style.top,
     left: element.style.left,
@@ -601,50 +569,50 @@ Effect.Puff = function(element) {
     height: element.style.height
   };
   return new Effect.Parallel(
     height: element.style.height
   };
   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, 
+   [ 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) {
       beforeSetupInternal: function(effect) {
-        Position.absolutize(effect.effects[0].element)
+        Position.absolutize(effect.effects[0].element);
       },
       afterFinishInternal: function(effect) {
          effect.effects[0].element.hide().setStyle(oldStyle); }
       },
       afterFinishInternal: function(effect) {
          effect.effects[0].element.hide().setStyle(oldStyle); }
-     }, arguments[1] || {})
+     }, arguments[1] || { })
    );
    );
-}
+};
 
 Effect.BlindUp = function(element) {
   element = $(element);
   element.makeClipping();
   return new Effect.Scale(element, 0,
 
 Effect.BlindUp = function(element) {
   element = $(element);
   element.makeClipping();
   return new Effect.Scale(element, 0,
-    Object.extend({ scaleContent: false, 
-      scaleX: false, 
+    Object.extend({ scaleContent: false,
+      scaleX: false,
       restoreAfterFinish: true,
       afterFinishInternal: function(effect) {
         effect.element.hide().undoClipping();
       restoreAfterFinish: true,
       afterFinishInternal: function(effect) {
         effect.element.hide().undoClipping();
-      } 
-    }, arguments[1] || {})
+      }
+    }, arguments[1] || { })
   );
   );
-}
+};
 
 Effect.BlindDown = function(element) {
   element = $(element);
   var elementDimensions = element.getDimensions();
 
 Effect.BlindDown = function(element) {
   element = $(element);
   var elementDimensions = element.getDimensions();
-  return new Effect.Scale(element, 100, Object.extend({ 
-    scaleContent: false, 
+  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) {
     scaleX: false,
     scaleFrom: 0,
     scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
     restoreAfterFinish: true,
     afterSetup: function(effect) {
-      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
-    },  
+      effect.element.makeClipping().setStyle({height: '0px'}).show();
+    },
     afterFinishInternal: function(effect) {
       effect.element.undoClipping();
     }
     afterFinishInternal: function(effect) {
       effect.element.undoClipping();
     }
-  }, arguments[1] || {}));
-}
+  }, arguments[1] || { }));
+};
 
 Effect.SwitchOff = function(element) {
   element = $(element);
 
 Effect.SwitchOff = function(element) {
   element = $(element);
@@ -654,19 +622,19 @@ Effect.SwitchOff = function(element) {
     from: 0,
     transition: Effect.Transitions.flicker,
     afterFinishInternal: function(effect) {
     from: 0,
     transition: Effect.Transitions.flicker,
     afterFinishInternal: function(effect) {
-      new Effect.Scale(effect.element, 1, { 
+      new Effect.Scale(effect.element, 1, {
         duration: 0.3, scaleFromCenter: true,
         scaleX: false, scaleContent: false, restoreAfterFinish: true,
         duration: 0.3, scaleFromCenter: true,
         scaleX: false, scaleContent: false, restoreAfterFinish: true,
-        beforeSetup: function(effect) { 
+        beforeSetup: function(effect) {
           effect.element.makePositioned().makeClipping();
         },
         afterFinishInternal: function(effect) {
           effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
         }
           effect.element.makePositioned().makeClipping();
         },
         afterFinishInternal: function(effect) {
           effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
         }
-      })
+      });
     }
     }
-  }, arguments[1] || {}));
-}
+  }, arguments[1] || { }));
+};
 
 Effect.DropOut = function(element) {
   element = $(element);
 
 Effect.DropOut = function(element) {
   element = $(element);
@@ -675,107 +643,115 @@ Effect.DropOut = function(element) {
     left: element.getStyle('left'),
     opacity: element.getInlineOpacity() };
   return new Effect.Parallel(
     left: element.getStyle('left'),
     opacity: element.getInlineOpacity() };
   return new Effect.Parallel(
-    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
+    [ 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) {
       new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
     Object.extend(
       { duration: 0.5,
         beforeSetup: function(effect) {
-          effect.effects[0].element.makePositioned(); 
+          effect.effects[0].element.makePositioned();
         },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
         },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
-        } 
-      }, arguments[1] || {}));
-}
+        }
+      }, arguments[1] || { }));
+};
 
 Effect.Shake = function(element) {
   element = $(element);
 
 Effect.Shake = function(element) {
   element = $(element);
+  var options = Object.extend({
+    distance: 20,
+    duration: 0.5
+  }, arguments[1] || {});
+  var distance = parseFloat(options.distance);
+  var split = parseFloat(options.duration) / 10.0;
   var oldStyle = {
     top: element.getStyle('top'),
     left: element.getStyle('left') };
   var oldStyle = {
     top: element.getStyle('top'),
     left: element.getStyle('left') };
-    return new Effect.Move(element, 
-      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+    return new Effect.Move(element,
+      { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
     new Effect.Move(effect.element,
     new Effect.Move(effect.element,
-      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
     new Effect.Move(effect.element,
     new Effect.Move(effect.element,
-      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
     new Effect.Move(effect.element,
     new Effect.Move(effect.element,
-      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
     new Effect.Move(effect.element,
     new Effect.Move(effect.element,
-      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
     new Effect.Move(effect.element,
     new Effect.Move(effect.element,
-      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+      { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
         effect.element.undoPositioned().setStyle(oldStyle);
         effect.element.undoPositioned().setStyle(oldStyle);
-  }}) }}) }}) }}) }}) }});
-}
+  }}); }}); }}); }}); }}); }});
+};
 
 Effect.SlideDown = function(element) {
   element = $(element).cleanWhitespace();
   // SlideDown need to have the content of the element wrapped in a container element with fixed height!
   var oldInnerBottom = element.down().getStyle('bottom');
   var elementDimensions = element.getDimensions();
 
 Effect.SlideDown = function(element) {
   element = $(element).cleanWhitespace();
   // SlideDown need to have the content of the element wrapped in a container element with fixed height!
   var oldInnerBottom = element.down().getStyle('bottom');
   var elementDimensions = element.getDimensions();
-  return new Effect.Scale(element, 100, Object.extend({ 
-    scaleContent: false, 
-    scaleX: false, 
+  return new Effect.Scale(element, 100, Object.extend({
+    scaleContent: false,
+    scaleX: false,
     scaleFrom: window.opera ? 0 : 1,
     scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
     restoreAfterFinish: true,
     afterSetup: function(effect) {
       effect.element.makePositioned();
       effect.element.down().makePositioned();
     scaleFrom: window.opera ? 0 : 1,
     scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
     restoreAfterFinish: true,
     afterSetup: function(effect) {
       effect.element.makePositioned();
       effect.element.down().makePositioned();
-      if(window.opera) effect.element.setStyle({top: ''});
-      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
+      if (window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping().setStyle({height: '0px'}).show();
     },
     afterUpdateInternal: function(effect) {
       effect.element.down().setStyle({bottom:
     },
     afterUpdateInternal: function(effect) {
       effect.element.down().setStyle({bottom:
-        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
+        (effect.dims[0] - effect.element.clientHeight) + 'px' });
     },
     afterFinishInternal: function(effect) {
       effect.element.undoClipping().undoPositioned();
       effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
     },
     afterFinishInternal: function(effect) {
       effect.element.undoClipping().undoPositioned();
       effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
-    }, arguments[1] || {})
+    }, arguments[1] || { })
   );
   );
-}
+};
 
 Effect.SlideUp = function(element) {
   element = $(element).cleanWhitespace();
   var oldInnerBottom = element.down().getStyle('bottom');
 
 Effect.SlideUp = function(element) {
   element = $(element).cleanWhitespace();
   var oldInnerBottom = element.down().getStyle('bottom');
+  var elementDimensions = element.getDimensions();
   return new Effect.Scale(element, window.opera ? 0 : 1,
   return new Effect.Scale(element, window.opera ? 0 : 1,
-   Object.extend({ scaleContent: false, 
-    scaleX: false, 
+   Object.extend({ scaleContent: false,
+    scaleX: false,
     scaleMode: 'box',
     scaleFrom: 100,
     scaleMode: 'box',
     scaleFrom: 100,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
     restoreAfterFinish: true,
     restoreAfterFinish: true,
-    beforeStartInternal: function(effect) {
+    afterSetup: function(effect) {
       effect.element.makePositioned();
       effect.element.down().makePositioned();
       effect.element.makePositioned();
       effect.element.down().makePositioned();
-      if(window.opera) effect.element.setStyle({top: ''});
+      if (window.opera) effect.element.setStyle({top: ''});
       effect.element.makeClipping().show();
       effect.element.makeClipping().show();
-    },  
+    },
     afterUpdateInternal: function(effect) {
       effect.element.down().setStyle({bottom:
         (effect.dims[0] - effect.element.clientHeight) + 'px' });
     },
     afterFinishInternal: function(effect) {
     afterUpdateInternal: function(effect) {
       effect.element.down().setStyle({bottom:
         (effect.dims[0] - effect.element.clientHeight) + 'px' });
     },
     afterFinishInternal: function(effect) {
-      effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
-      effect.element.down().undoPositioned();
+      effect.element.hide().undoClipping().undoPositioned();
+      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
     }
     }
-   }, arguments[1] || {})
+   }, arguments[1] || { })
   );
   );
-}
+};
 
 
-// Bug in opera makes the TD containing this element expand for a instance after finish 
+// Bug in opera makes the TD containing this element expand for a instance after finish
 Effect.Squish = function(element) {
 Effect.Squish = function(element) {
-  return new Effect.Scale(element, window.opera ? 1 : 0, { 
+  return new Effect.Scale(element, window.opera ? 1 : 0, {
     restoreAfterFinish: true,
     beforeSetup: function(effect) {
     restoreAfterFinish: true,
     beforeSetup: function(effect) {
-      effect.element.makeClipping(); 
-    },  
+      effect.element.makeClipping();
+    },
     afterFinishInternal: function(effect) {
     afterFinishInternal: function(effect) {
-      effect.element.hide().undoClipping(); 
+      effect.element.hide().undoClipping();
     }
   });
     }
   });
-}
+};
 
 Effect.Grow = function(element) {
   element = $(element);
 
 Effect.Grow = function(element) {
   element = $(element);
@@ -784,7 +760,7 @@ Effect.Grow = function(element) {
     moveTransition: Effect.Transitions.sinoidal,
     scaleTransition: Effect.Transitions.sinoidal,
     opacityTransition: Effect.Transitions.full
     moveTransition: Effect.Transitions.sinoidal,
     scaleTransition: Effect.Transitions.sinoidal,
     opacityTransition: Effect.Transitions.full
-  }, arguments[1] || {});
+  }, arguments[1] || { });
   var oldStyle = {
     top: element.style.top,
     left: element.style.left,
   var oldStyle = {
     top: element.style.top,
     left: element.style.left,
@@ -792,13 +768,13 @@ Effect.Grow = function(element) {
     width: element.style.width,
     opacity: element.getInlineOpacity() };
 
     width: element.style.width,
     opacity: element.getInlineOpacity() };
 
-  var dims = element.getDimensions();    
+  var dims = element.getDimensions();
   var initialMoveX, initialMoveY;
   var moveX, moveY;
   var initialMoveX, initialMoveY;
   var moveX, moveY;
-  
+
   switch (options.direction) {
     case 'top-left':
   switch (options.direction) {
     case 'top-left':
-      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      initialMoveX = initialMoveY = moveX = moveY = 0;
       break;
     case 'top-right':
       initialMoveX = dims.width;
       break;
     case 'top-right':
       initialMoveX = dims.width;
@@ -823,11 +799,11 @@ Effect.Grow = function(element) {
       moveY = -dims.height / 2;
       break;
   }
       moveY = -dims.height / 2;
       break;
   }
-  
+
   return new Effect.Move(element, {
     x: initialMoveX,
     y: initialMoveY,
   return new Effect.Move(element, {
     x: initialMoveX,
     y: initialMoveY,
-    duration: 0.01, 
+    duration: 0.01,
     beforeSetup: function(effect) {
       effect.element.hide().makeClipping().makePositioned();
     },
     beforeSetup: function(effect) {
       effect.element.hide().makeClipping().makePositioned();
     },
@@ -836,20 +812,20 @@ Effect.Grow = function(element) {
         [ 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, {
         [ 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 }, 
+            scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
             sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
         ], Object.extend({
              beforeSetup: function(effect) {
             sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
         ], Object.extend({
              beforeSetup: function(effect) {
-               effect.effects[0].element.setStyle({height: '0px'}).show(); 
+               effect.effects[0].element.setStyle({height: '0px'}).show();
              },
              afterFinishInternal: function(effect) {
              },
              afterFinishInternal: function(effect) {
-               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 
+               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
              }
            }, options)
              }
            }, options)
-      )
+      );
     }
   });
     }
   });
-}
+};
 
 Effect.Shrink = function(element) {
   element = $(element);
 
 Effect.Shrink = function(element) {
   element = $(element);
@@ -858,7 +834,7 @@ Effect.Shrink = function(element) {
     moveTransition: Effect.Transitions.sinoidal,
     scaleTransition: Effect.Transitions.sinoidal,
     opacityTransition: Effect.Transitions.none
     moveTransition: Effect.Transitions.sinoidal,
     scaleTransition: Effect.Transitions.sinoidal,
     opacityTransition: Effect.Transitions.none
-  }, arguments[1] || {});
+  }, arguments[1] || { });
   var oldStyle = {
     top: element.style.top,
     left: element.style.left,
   var oldStyle = {
     top: element.style.top,
     left: element.style.left,
@@ -868,7 +844,7 @@ Effect.Shrink = function(element) {
 
   var dims = element.getDimensions();
   var moveX, moveY;
 
   var dims = element.getDimensions();
   var moveX, moveY;
-  
+
   switch (options.direction) {
     case 'top-left':
       moveX = moveY = 0;
   switch (options.direction) {
     case 'top-left':
       moveX = moveY = 0;
@@ -885,38 +861,40 @@ Effect.Shrink = function(element) {
       moveX = dims.width;
       moveY = dims.height;
       break;
       moveX = dims.width;
       moveY = dims.height;
       break;
-    case 'center':  
+    case 'center':
       moveX = dims.width / 2;
       moveY = dims.height / 2;
       break;
   }
       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 })
   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({            
+    ], Object.extend({
          beforeStartInternal: function(effect) {
          beforeStartInternal: function(effect) {
-           effect.effects[0].element.makePositioned().makeClipping(); 
+           effect.effects[0].element.makePositioned().makeClipping();
          },
          afterFinishInternal: function(effect) {
            effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
        }, options)
   );
          },
          afterFinishInternal: function(effect) {
            effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
        }, options)
   );
-}
+};
 
 Effect.Pulsate = function(element) {
   element = $(element);
 
 Effect.Pulsate = function(element) {
   element = $(element);
-  var options    = arguments[1] || {};
-  var oldOpacity = element.getInlineOpacity();
-  var transition = options.transition || Effect.Transitions.sinoidal;
-  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
-  reverser.bind(transition);
-  return new Effect.Opacity(element, 
+  var options    = arguments[1] || { },
+    oldOpacity = element.getInlineOpacity(),
+    transition = options.transition || Effect.Transitions.linear,
+    reverser   = function(pos){
+      return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
+    };
+
+  return new Effect.Opacity(element,
     Object.extend(Object.extend({  duration: 2.0, from: 0,
       afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
     }, options), {transition: reverser}));
     Object.extend(Object.extend({  duration: 2.0, from: 0,
       afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
     }, options), {transition: reverser}));
-}
+};
 
 Effect.Fold = function(element) {
   element = $(element);
 
 Effect.Fold = function(element) {
   element = $(element);
@@ -926,47 +904,81 @@ Effect.Fold = function(element) {
     width: element.style.width,
     height: element.style.height };
   element.makeClipping();
     width: element.style.width,
     height: element.style.height };
   element.makeClipping();
-  return new Effect.Scale(element, 5, Object.extend({   
+  return new Effect.Scale(element, 5, Object.extend({
     scaleContent: false,
     scaleX: false,
     afterFinishInternal: function(effect) {
     scaleContent: false,
     scaleX: false,
     afterFinishInternal: function(effect) {
-    new Effect.Scale(element, 1, { 
-      scaleContent: false, 
+    new Effect.Scale(element, 1, {
+      scaleContent: false,
       scaleY: false,
       afterFinishInternal: function(effect) {
         effect.element.hide().undoClipping().setStyle(oldStyle);
       } });
       scaleY: false,
       afterFinishInternal: function(effect) {
         effect.element.hide().undoClipping().setStyle(oldStyle);
       } });
-  }}, arguments[1] || {}));
+  }}, arguments[1] || { }));
 };
 
 };
 
-Effect.Morph = Class.create();
-Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
+Effect.Morph = Class.create(Effect.Base, {
   initialize: function(element) {
     this.element = $(element);
   initialize: function(element) {
     this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
+    if (!this.element) throw(Effect._elementDoesNotExistError);
     var options = Object.extend({
     var options = Object.extend({
-      style: ''
-    }, arguments[1] || {});
+      style: { }
+    }, arguments[1] || { });
+
+    if (!Object.isString(options.style)) this.style = $H(options.style);
+    else {
+      if (options.style.include(':'))
+        this.style = options.style.parseStyle();
+      else {
+        this.element.addClassName(options.style);
+        this.style = $H(this.element.getStyles());
+        this.element.removeClassName(options.style);
+        var css = this.element.getStyles();
+        this.style = this.style.reject(function(style) {
+          return style.value == css[style.key];
+        });
+        options.afterFinishInternal = function(effect) {
+          effect.element.addClassName(effect.options.style);
+          effect.transforms.each(function(transform) {
+            effect.element.style[transform.style] = '';
+          });
+        };
+      }
+    }
     this.start(options);
   },
     this.start(options);
   },
+
   setup: function(){
     function parseColor(color){
   setup: function(){
     function parseColor(color){
-      if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
       color = color.parseColor();
       return $R(0,2).map(function(i){
       color = color.parseColor();
       return $R(0,2).map(function(i){
-        return parseInt( color.slice(i*2+1,i*2+3), 16 ) 
+        return parseInt( color.slice(i*2+1,i*2+3), 16 );
       });
     }
       });
     }
-    this.transforms = this.options.style.parseStyle().map(function(property){
-      var originalValue = this.element.getStyle(property[0]);
-      return $H({ 
-        style: property[0], 
-        originalValue: property[1].unit=='color' ? 
-          parseColor(originalValue) : parseFloat(originalValue || 0), 
-        targetValue: property[1].unit=='color' ? 
-          parseColor(property[1].value) : property[1].value,
-        unit: property[1].unit
-      });
+    this.transforms = this.style.map(function(pair){
+      var property = pair[0], value = pair[1], unit = null;
+
+      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
+        value = value.parseColor();
+        unit  = 'color';
+      } else if (property == 'opacity') {
+        value = parseFloat(value);
+        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+          this.element.setStyle({zoom: 1});
+      } else if (Element.CSS_LENGTH.test(value)) {
+          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
+          value = parseFloat(components[1]);
+          unit = (components.length == 3) ? components[2] : null;
+      }
+
+      var originalValue = this.element.getStyle(property);
+      return {
+        style: property.camelize(),
+        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+        targetValue: unit=='color' ? parseColor(value) : value,
+        unit: unit
+      };
     }.bind(this)).reject(function(transform){
       return (
         (transform.originalValue == transform.targetValue) ||
     }.bind(this)).reject(function(transform){
       return (
         (transform.originalValue == transform.targetValue) ||
@@ -974,36 +986,39 @@ Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
           transform.unit != 'color' &&
           (isNaN(transform.originalValue) || isNaN(transform.targetValue))
         )
           transform.unit != 'color' &&
           (isNaN(transform.originalValue) || isNaN(transform.targetValue))
         )
-      )
+      );
     });
   },
   update: function(position) {
     });
   },
   update: function(position) {
-    var style = $H(), value = null;
-    this.transforms.each(function(transform){
-      value = transform.unit=='color' ?
-        $R(0,2).inject('#',function(m,v,i){
-          return m+(Math.round(transform.originalValue[i]+
-            (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) : 
-        transform.originalValue + Math.round(
-          ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
-      style[transform.style] = value;
-    });
-    this.element.setStyle(style);
+    var style = { }, transform, i = this.transforms.length;
+    while(i--)
+      style[(transform = this.transforms[i]).style] =
+        transform.unit=='color' ? '#'+
+          (Math.round(transform.originalValue[0]+
+            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
+          (Math.round(transform.originalValue[1]+
+            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
+          (Math.round(transform.originalValue[2]+
+            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
+        (transform.originalValue +
+          (transform.targetValue - transform.originalValue) * position).toFixed(3) +
+            (transform.unit === null ? '' : transform.unit);
+    this.element.setStyle(style, true);
   }
 });
 
   }
 });
 
-Effect.Transform = Class.create();
-Object.extend(Effect.Transform.prototype, {
+Effect.Transform = Class.create({
   initialize: function(tracks){
     this.tracks  = [];
   initialize: function(tracks){
     this.tracks  = [];
-    this.options = arguments[1] || {};
+    this.options = arguments[1] || { };
     this.addTracks(tracks);
   },
   addTracks: function(tracks){
     tracks.each(function(track){
     this.addTracks(tracks);
   },
   addTracks: function(tracks){
     tracks.each(function(track){
-      var data = $H(track).values().first();
+      track = $H(track);
+      var data = track.values().first();
       this.tracks.push($H({
       this.tracks.push($H({
-        ids:     $H(track).keys().first(),
+        ids:     track.keys().first(),
         effect:  Effect.Morph,
         options: { style: data }
       }));
         effect:  Effect.Morph,
         options: { style: data }
       }));
@@ -1013,76 +1028,101 @@ Object.extend(Effect.Transform.prototype, {
   play: function(){
     return new Effect.Parallel(
       this.tracks.map(function(track){
   play: function(){
     return new Effect.Parallel(
       this.tracks.map(function(track){
-        var elements = [$(track.ids) || $$(track.ids)].flatten();
-        return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
+        var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
+        var elements = [$(ids) || $$(ids)].flatten();
+        return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
       }).flatten(),
       this.options
     );
   }
 });
 
       }).flatten(),
       this.options
     );
   }
 });
 
-Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage', 
-  'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle', 
-  'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth',
-  'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor',
-  'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content',
-  'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction',
-  'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch',
-  'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight',
-  'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight',
-  'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity',
-  'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY',
-  'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore',
-  'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes',
-  'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress',
-  'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top',
-  'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows',
-  'width', 'wordSpacing', 'zIndex'];
-  
+Element.CSS_PROPERTIES = $w(
+  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+  'fontSize fontWeight height left letterSpacing lineHeight ' +
+  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+  'right textIndent top width wordSpacing zIndex');
+
 Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
 
 Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
 
+String.__parseStyleElement = document.createElement('div');
 String.prototype.parseStyle = function(){
 String.prototype.parseStyle = function(){
-  var element = Element.extend(document.createElement('div'));
-  element.innerHTML = '<div style="' + this + '"></div>';
-  var style = element.down().style, styleRules = $H();
-  
+  var style, styleRules = $H();
+  if (Prototype.Browser.WebKit)
+    style = new Element('div',{style:this}).style;
+  else {
+    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
+    style = String.__parseStyleElement.childNodes[0].style;
+  }
+
   Element.CSS_PROPERTIES.each(function(property){
   Element.CSS_PROPERTIES.each(function(property){
-   if(style[property]) styleRules[property] = style[property]; 
+    if (style[property]) styleRules.set(property, style[property]);
   });
   });
-  
-  var result = $H();
-  
-  styleRules.each(function(pair){
-    var property = pair[0], value = pair[1], unit = null;
-    
-    if(value.parseColor('#zzzzzz') != '#zzzzzz') {
-      value = value.parseColor();
-      unit  = 'color';
-    } else if(Element.CSS_LENGTH.test(value)) 
-      var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/),
-          value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null;
-    
-    result[property.underscore().dasherize()] = $H({ value:value, unit:unit });
-  }.bind(this));
-  
-  return result;
+
+  if (Prototype.Browser.IE && this.include('opacity'))
+    styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
+
+  return styleRules;
 };
 
 };
 
-Element.morph = function(element, style) {
-  new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
-  return element;
+if (document.defaultView && document.defaultView.getComputedStyle) {
+  Element.getStyles = function(element) {
+    var css = document.defaultView.getComputedStyle($(element), null);
+    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
+      styles[property] = css[property];
+      return styles;
+    });
+  };
+} else {
+  Element.getStyles = function(element) {
+    element = $(element);
+    var css = element.currentStyle, styles;
+    styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
+      results[property] = css[property];
+      return results;
+    });
+    if (!styles.opacity) styles.opacity = element.getOpacity();
+    return styles;
+  };
+}
+
+Effect.Methods = {
+  morph: function(element, style) {
+    element = $(element);
+    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
+    return element;
+  },
+  visualEffect: function(element, effect, options) {
+    element = $(element);
+    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
+    new Effect[klass](element, options);
+    return element;
+  },
+  highlight: function(element, options) {
+    element = $(element);
+    new Effect.Highlight(element, options);
+    return element;
+  }
 };
 
 };
 
-['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
- 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( 
-  function(f) { Element.Methods[f] = Element[f]; }
+$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
+  'pulsate shake puff squish switchOff dropOut').each(
+  function(effect) {
+    Effect.Methods[effect] = function(element, options){
+      element = $(element);
+      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
+      return element;
+    };
+  }
 );
 
 );
 
-Element.Methods.visualEffect = function(element, effect, options) {
-  s = effect.gsub(/_/, '-').camelize();
-  effect_class = s.charAt(0).toUpperCase() + s.substring(1);
-  new Effect[effect_class](element, options);
-  return $(element);
-};
+$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
+  function(f) { Effect.Methods[f] = Element[f]; }
+);
 
 
-Element.addMethods();
\ No newline at end of file
+Element.addMethods(Effect.Methods);
\ No newline at end of file
index 505822177b81910ba7426835e5014c1f3de374de..dfe8ab4e13652153903d481a1b006878df6a1f3b 100644 (file)
-/*  Prototype JavaScript framework, version 1.5.0
- *  (c) 2005-2007 Sam Stephenson
+/*  Prototype JavaScript framework, version 1.6.0.3
+ *  (c) 2005-2008 Sam Stephenson
  *
  *  Prototype is freely distributable under the terms of an MIT-style license.
  *
  *  Prototype is freely distributable under the terms of an MIT-style license.
- *  For details, see the Prototype web site: http://prototype.conio.net/
+ *  For details, see the Prototype web site: http://www.prototypejs.org/
  *
  *
-/*--------------------------------------------------------------------------*/
+ *--------------------------------------------------------------------------*/
 
 var Prototype = {
 
 var Prototype = {
-  Version: '1.5.0',
+  Version: '1.6.0.3',
+
+  Browser: {
+    IE:     !!(window.attachEvent &&
+      navigator.userAgent.indexOf('Opera') === -1),
+    Opera:  navigator.userAgent.indexOf('Opera') > -1,
+    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: {
   BrowserFeatures: {
-    XPath: !!document.evaluate
+    XPath: !!document.evaluate,
+    SelectorsAPI: !!document.querySelector,
+    ElementExtensions: !!window.HTMLElement,
+    SpecificElementExtensions:
+      document.createElement('div')['__proto__'] &&
+      document.createElement('div')['__proto__'] !==
+        document.createElement('form')['__proto__']
   },
 
   },
 
-  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
-  emptyFunction: function() {},
+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  emptyFunction: function() { },
   K: function(x) { return x }
   K: function(x) { return x }
-}
+};
+
+if (Prototype.Browser.MobileSafari)
+  Prototype.BrowserFeatures.SpecificElementExtensions = false;
 
 
+
+/* Based on Alex Arnell's inheritance implementation. */
 var Class = {
   create: function() {
 var Class = {
   create: function() {
-    return function() {
+    var parent = null, properties = $A(arguments);
+    if (Object.isFunction(properties[0]))
+      parent = properties.shift();
+
+    function klass() {
       this.initialize.apply(this, arguments);
     }
       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 = (function(m) {
+          return function() { return ancestor[m].apply(this, arguments) };
+        })(property).wrap(method);
+
+        value.valueOf = method.valueOf.bind(method);
+        value.toString = method.toString.bind(method);
+      }
+      this.prototype[property] = value;
+    }
+
+    return this;
   }
   }
-}
+};
 
 
-var Abstract = new Object();
+var Abstract = { };
 
 Object.extend = function(destination, source) {
 
 Object.extend = function(destination, source) {
-  for (var property in source) {
+  for (var property in source)
     destination[property] = source[property];
     destination[property] = source[property];
-  }
   return destination;
   return destination;
-}
+};
 
 Object.extend(Object, {
   inspect: function(object) {
     try {
 
 Object.extend(Object, {
   inspect: function(object) {
     try {
-      if (object === undefined) return 'undefined';
+      if (Object.isUndefined(object)) return 'undefined';
       if (object === null) return 'null';
       if (object === null) return 'null';
-      return object.inspect ? object.inspect() : object.toString();
+      return object.inspect ? object.inspect() : String(object);
     } catch (e) {
       if (e instanceof RangeError) return '...';
       throw e;
     }
   },
 
     } 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 (!Object.isUndefined(value))
+        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: function(object) {
     var keys = [];
     for (var property in object)
@@ -61,41 +167,106 @@ Object.extend(Object, {
   },
 
   clone: function(object) {
   },
 
   clone: function(object) {
-    return Object.extend({}, object);
+    return Object.extend({ }, object);
+  },
+
+  isElement: function(object) {
+    return !!(object && object.nodeType == 1);
+  },
+
+  isArray: function(object) {
+    return object != null && typeof object == "object" &&
+      'splice' in object && 'join' in object;
+  },
+
+  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";
   }
 });
 
   }
 });
 
-Function.prototype.bind = function() {
-  var __method = this, args = $A(arguments), object = args.shift();
-  return function() {
-    return __method.apply(object, args.concat($A(arguments)));
-  }
-}
+Object.extend(Function.prototype, {
+  argumentNames: function() {
+    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
+      .replace(/\s+/g, '').split(',');
+    return names.length == 1 && !names[0] ? [] : names;
+  },
 
 
-Function.prototype.bindAsEventListener = function(object) {
-  var __method = this, args = $A(arguments), object = args.shift();
-  return function(event) {
-    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
-  }
-}
+  bind: function() {
+    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function() {
+      return __method.apply(object, args.concat($A(arguments)));
+    }
+  },
 
 
-Object.extend(Number.prototype, {
-  toColorPart: function() {
-    var digits = this.toString(16);
-    if (this < 16) return '0' + digits;
-    return digits;
+  bindAsEventListener: function() {
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function(event) {
+      return __method.apply(object, [event || window.event].concat(args));
+    }
   },
 
   },
 
-  succ: function() {
-    return this + 1;
+  curry: function() {
+    if (!arguments.length) return this;
+    var __method = this, args = $A(arguments);
+    return function() {
+      return __method.apply(this, args.concat($A(arguments)));
+    }
   },
 
   },
 
-  times: function(iterator) {
-    $R(0, this, true).each(iterator);
-    return this;
+  delay: function() {
+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+    return window.setTimeout(function() {
+      return __method.apply(__method, args);
+    }, timeout);
+  },
+
+  defer: function() {
+    var args = [0.01].concat($A(arguments));
+    return this.delay.apply(this, args);
+  },
+
+  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)));
+    };
   }
 });
 
   }
 });
 
+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;
 var Try = {
   these: function() {
     var returnValue;
@@ -105,17 +276,22 @@ var Try = {
       try {
         returnValue = lambda();
         break;
       try {
         returnValue = lambda();
         break;
-      } catch (e) {}
+      } catch (e) { }
     }
 
     return returnValue;
   }
     }
 
     return returnValue;
   }
-}
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
 
 /*--------------------------------------------------------------------------*/
 
 
 /*--------------------------------------------------------------------------*/
 
-var PeriodicalExecuter = Class.create();
-PeriodicalExecuter.prototype = {
+var PeriodicalExecuter = Class.create({
   initialize: function(callback, frequency) {
     this.callback = callback;
     this.frequency = frequency;
   initialize: function(callback, frequency) {
     this.callback = callback;
     this.frequency = frequency;
@@ -128,6 +304,10 @@ PeriodicalExecuter.prototype = {
     this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
   },
 
     this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
   },
 
+  execute: function() {
+    this.callback(this);
+  },
+
   stop: function() {
     if (!this.timer) return;
     clearInterval(this.timer);
   stop: function() {
     if (!this.timer) return;
     clearInterval(this.timer);
@@ -138,16 +318,26 @@ PeriodicalExecuter.prototype = {
     if (!this.currentlyExecuting) {
       try {
         this.currentlyExecuting = true;
     if (!this.currentlyExecuting) {
       try {
         this.currentlyExecuting = true;
-        this.callback(this);
+        this.execute();
       } finally {
         this.currentlyExecuting = false;
       }
     }
   }
       } finally {
         this.currentlyExecuting = false;
       }
     }
   }
-}
-String.interpret = function(value){
-  return value == null ? '' : String(value);
-}
+});
+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) {
 
 Object.extend(String.prototype, {
   gsub: function(pattern, replacement) {
@@ -168,7 +358,7 @@ Object.extend(String.prototype, {
 
   sub: function(pattern, replacement, count) {
     replacement = this.gsub.prepareReplacement(replacement);
 
   sub: function(pattern, replacement, count) {
     replacement = this.gsub.prepareReplacement(replacement);
-    count = count === undefined ? 1 : count;
+    count = Object.isUndefined(count) ? 1 : count;
 
     return this.gsub(pattern, function(match) {
       if (--count < 0) return match[0];
 
     return this.gsub(pattern, function(match) {
       if (--count < 0) return match[0];
@@ -178,14 +368,14 @@ Object.extend(String.prototype, {
 
   scan: function(pattern, iterator) {
     this.gsub(pattern, iterator);
 
   scan: function(pattern, iterator) {
     this.gsub(pattern, iterator);
-    return this;
+    return String(this);
   },
 
   truncate: function(length, truncation) {
     length = length || 30;
   },
 
   truncate: function(length, truncation) {
     length = length || 30;
-    truncation = truncation === undefined ? '...' : truncation;
+    truncation = Object.isUndefined(truncation) ? '...' : truncation;
     return this.length > length ?
     return this.length > length ?
-      this.slice(0, length - truncation.length) + truncation : this;
+      this.slice(0, length - truncation.length) + truncation : String(this);
   },
 
   strip: function() {
   },
 
   strip: function() {
@@ -213,35 +403,34 @@ Object.extend(String.prototype, {
   },
 
   escapeHTML: function() {
   },
 
   escapeHTML: function() {
-    var div = document.createElement('div');
-    var text = document.createTextNode(this);
-    div.appendChild(text);
-    return div.innerHTML;
+    var self = arguments.callee;
+    self.text.data = this;
+    return self.div.innerHTML;
   },
 
   unescapeHTML: function() {
   },
 
   unescapeHTML: function() {
-    var div = document.createElement('div');
+    var div = new Element('div');
     div.innerHTML = this.stripTags();
     return div.childNodes[0] ? (div.childNodes.length > 1 ?
     div.innerHTML = this.stripTags();
     return div.childNodes[0] ? (div.childNodes.length > 1 ?
-      $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
       div.childNodes[0].nodeValue) : '';
   },
 
   toQueryParams: function(separator) {
     var match = this.strip().match(/([^?#]*)(#.*)?$/);
       div.childNodes[0].nodeValue) : '';
   },
 
   toQueryParams: function(separator) {
     var match = this.strip().match(/([^?#]*)(#.*)?$/);
-    if (!match) return {};
+    if (!match) return { };
 
 
-    return match[1].split(separator || '&').inject({}, function(hash, pair) {
+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
       if ((pair = pair.split('='))[0]) {
       if ((pair = pair.split('='))[0]) {
-        var name = decodeURIComponent(pair[0]);
-        var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
+        var key = decodeURIComponent(pair.shift());
+        var value = pair.length > 1 ? pair.join('=') : pair[0];
+        if (value != undefined) value = decodeURIComponent(value);
 
 
-        if (hash[name] !== undefined) {
-          if (hash[name].constructor != Array)
-            hash[name] = [hash[name]];
-          if (value) hash[name].push(value);
+        if (key in hash) {
+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+          hash[key].push(value);
         }
         }
-        else hash[name] = value;
+        else hash[key] = value;
       }
       return hash;
     });
       }
       return hash;
     });
@@ -256,6 +445,10 @@ Object.extend(String.prototype, {
       String.fromCharCode(this.charCodeAt(this.length - 1) + 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];
   camelize: function() {
     var parts = this.split('-'), len = parts.length;
     if (len == 1) return parts[0];
@@ -270,7 +463,7 @@ Object.extend(String.prototype, {
     return camelized;
   },
 
     return camelized;
   },
 
-  capitalize: function(){
+  capitalize: function() {
     return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
   },
 
     return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
   },
 
@@ -283,52 +476,130 @@ Object.extend(String.prototype, {
   },
 
   inspect: function(useDoubleQuotes) {
   },
 
   inspect: function(useDoubleQuotes) {
-    var escapedString = this.replace(/\\/g, '\\\\');
-    if (useDoubleQuotes)
-      return '"' + escapedString.replace(/"/g, '\\"') + '"';
-    else
-      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+    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;
+    if (str.blank()) return false;
+    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.stripTags().replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
   }
 });
 
 String.prototype.gsub.prepareReplacement = function(replacement) {
   }
 });
 
 String.prototype.gsub.prepareReplacement = function(replacement) {
-  if (typeof replacement == 'function') return replacement;
+  if (Object.isFunction(replacement)) return replacement;
   var template = new Template(replacement);
   return function(match) { return template.evaluate(match) };
   var template = new Template(replacement);
   return function(match) { return template.evaluate(match) };
-}
+};
 
 String.prototype.parseQuery = String.prototype.toQueryParams;
 
 
 String.prototype.parseQuery = String.prototype.toQueryParams;
 
-var Template = Class.create();
-Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
-Template.prototype = {
+Object.extend(String.prototype.escapeHTML, {
+  div:  document.createElement('div'),
+  text: document.createTextNode('')
+});
+
+String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);
+
+var Template = Class.create({
   initialize: function(template, pattern) {
     this.template = template.toString();
   initialize: function(template, pattern) {
     this.template = template.toString();
-    this.pattern  = pattern || Template.Pattern;
+    this.pattern = pattern || Template.Pattern;
   },
 
   evaluate: function(object) {
   },
 
   evaluate: function(object) {
+    if (Object.isFunction(object.toTemplateReplacements))
+      object = object.toTemplateReplacements();
+
     return this.template.gsub(this.pattern, function(match) {
     return this.template.gsub(this.pattern, function(match) {
-      var before = match[1];
+      if (object == null) return '';
+
+      var before = match[1] || '';
       if (before == '\\') return match[2];
       if (before == '\\') return match[2];
-      return before + String.interpret(object[match[3]]);
+
+      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);
     });
   }
     });
   }
-}
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
 
 
-var $break    = new Object();
-var $continue = new Object();
+var $break = { };
 
 var Enumerable = {
 
 var Enumerable = {
-  each: function(iterator) {
+  each: function(iterator, context) {
     var index = 0;
     try {
       this._each(function(value) {
     var index = 0;
     try {
       this._each(function(value) {
-        try {
-          iterator(value, index++);
-        } catch (e) {
-          if (e != $continue) throw e;
-        }
+        iterator.call(context, value, index++);
       });
     } catch (e) {
       if (e != $break) throw e;
       });
     } catch (e) {
       if (e != $break) throw e;
@@ -336,43 +607,47 @@ var Enumerable = {
     return this;
   },
 
     return this;
   },
 
-  eachSlice: function(number, iterator) {
+  eachSlice: function(number, iterator, context) {
     var index = -number, slices = [], array = this.toArray();
     var index = -number, slices = [], array = this.toArray();
+    if (number < 1) return array;
     while ((index += number) < array.length)
       slices.push(array.slice(index, index+number));
     while ((index += number) < array.length)
       slices.push(array.slice(index, index+number));
-    return slices.map(iterator);
+    return slices.collect(iterator, context);
   },
 
   },
 
-  all: function(iterator) {
+  all: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result = true;
     this.each(function(value, index) {
     var result = true;
     this.each(function(value, index) {
-      result = result && !!(iterator || Prototype.K)(value, index);
+      result = result && !!iterator.call(context, value, index);
       if (!result) throw $break;
     });
     return result;
   },
 
       if (!result) throw $break;
     });
     return result;
   },
 
-  any: function(iterator) {
+  any: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result = false;
     this.each(function(value, index) {
     var result = false;
     this.each(function(value, index) {
-      if (result = !!(iterator || Prototype.K)(value, index))
+      if (result = !!iterator.call(context, value, index))
         throw $break;
     });
     return result;
   },
 
         throw $break;
     });
     return result;
   },
 
-  collect: function(iterator) {
+  collect: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var results = [];
     this.each(function(value, index) {
     var results = [];
     this.each(function(value, index) {
-      results.push((iterator || Prototype.K)(value, index));
+      results.push(iterator.call(context, value, index));
     });
     return results;
   },
 
     });
     return results;
   },
 
-  detect: function(iterator) {
+  detect: function(iterator, context) {
     var result;
     this.each(function(value, index) {
     var result;
     this.each(function(value, index) {
-      if (iterator(value, index)) {
+      if (iterator.call(context, value, index)) {
         result = value;
         throw $break;
       }
         result = value;
         throw $break;
       }
@@ -380,26 +655,33 @@ var Enumerable = {
     return result;
   },
 
     return result;
   },
 
-  findAll: function(iterator) {
+  findAll: function(iterator, context) {
     var results = [];
     this.each(function(value, index) {
     var results = [];
     this.each(function(value, index) {
-      if (iterator(value, index))
+      if (iterator.call(context, value, index))
         results.push(value);
     });
     return results;
   },
 
         results.push(value);
     });
     return results;
   },
 
-  grep: function(pattern, iterator) {
+  grep: function(filter, iterator, context) {
+    iterator = iterator || Prototype.K;
     var results = [];
     var results = [];
+
+    if (Object.isString(filter))
+      filter = new RegExp(filter);
+
     this.each(function(value, index) {
     this.each(function(value, index) {
-      var stringValue = value.toString();
-      if (stringValue.match(pattern))
-        results.push((iterator || Prototype.K)(value, index));
-    })
+      if (filter.match(value))
+        results.push(iterator.call(context, value, index));
+    });
     return results;
   },
 
   include: function(object) {
     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) {
     var found = false;
     this.each(function(value) {
       if (value == object) {
@@ -411,16 +693,16 @@ var Enumerable = {
   },
 
   inGroupsOf: function(number, fillWith) {
   },
 
   inGroupsOf: function(number, fillWith) {
-    fillWith = fillWith === undefined ? null : fillWith;
+    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
     return this.eachSlice(number, function(slice) {
       while(slice.length < number) slice.push(fillWith);
       return slice;
     });
   },
 
     return this.eachSlice(number, function(slice) {
       while(slice.length < number) slice.push(fillWith);
       return slice;
     });
   },
 
-  inject: function(memo, iterator) {
+  inject: function(memo, iterator, context) {
     this.each(function(value, index) {
     this.each(function(value, index) {
-      memo = iterator(memo, value, index);
+      memo = iterator.call(context, memo, value, index);
     });
     return memo;
   },
     });
     return memo;
   },
@@ -432,30 +714,33 @@ var Enumerable = {
     });
   },
 
     });
   },
 
-  max: function(iterator) {
+  max: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result;
     this.each(function(value, index) {
     var result;
     this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (result == undefined || value >= result)
+      value = iterator.call(context, value, index);
+      if (result == null || value >= result)
         result = value;
     });
     return result;
   },
 
         result = value;
     });
     return result;
   },
 
-  min: function(iterator) {
+  min: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var result;
     this.each(function(value, index) {
     var result;
     this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (result == undefined || value < result)
+      value = iterator.call(context, value, index);
+      if (result == null || value < result)
         result = value;
     });
     return result;
   },
 
         result = value;
     });
     return result;
   },
 
-  partition: function(iterator) {
+  partition: function(iterator, context) {
+    iterator = iterator || Prototype.K;
     var trues = [], falses = [];
     this.each(function(value, index) {
     var trues = [], falses = [];
     this.each(function(value, index) {
-      ((iterator || Prototype.K)(value, index) ?
+      (iterator.call(context, value, index) ?
         trues : falses).push(value);
     });
     return [trues, falses];
         trues : falses).push(value);
     });
     return [trues, falses];
@@ -463,24 +748,27 @@ var Enumerable = {
 
   pluck: function(property) {
     var results = [];
 
   pluck: function(property) {
     var results = [];
-    this.each(function(value, index) {
+    this.each(function(value) {
       results.push(value[property]);
     });
     return results;
   },
 
       results.push(value[property]);
     });
     return results;
   },
 
-  reject: function(iterator) {
+  reject: function(iterator, context) {
     var results = [];
     this.each(function(value, index) {
     var results = [];
     this.each(function(value, index) {
-      if (!iterator(value, index))
+      if (!iterator.call(context, value, index))
         results.push(value);
     });
     return results;
   },
 
         results.push(value);
     });
     return results;
   },
 
-  sortBy: function(iterator) {
+  sortBy: function(iterator, context) {
     return this.map(function(value, index) {
     return this.map(function(value, index) {
-      return {value: value, criteria: iterator(value, index)};
+      return {
+        value: value,
+        criteria: iterator.call(context, value, index)
+      };
     }).sort(function(left, right) {
       var a = left.criteria, b = right.criteria;
       return a < b ? -1 : a > b ? 1 : 0;
     }).sort(function(left, right) {
       var a = left.criteria, b = right.criteria;
       return a < b ? -1 : a > b ? 1 : 0;
@@ -493,7 +781,7 @@ var Enumerable = {
 
   zip: function() {
     var iterator = Prototype.K, args = $A(arguments);
 
   zip: function() {
     var iterator = Prototype.K, args = $A(arguments);
-    if (typeof args.last() == 'function')
+    if (Object.isFunction(args.last()))
       iterator = args.pop();
 
     var collections = [this].concat(args).map($A);
       iterator = args.pop();
 
     var collections = [this].concat(args).map($A);
@@ -509,31 +797,46 @@ var Enumerable = {
   inspect: function() {
     return '#<Enumerable:' + this.toArray().inspect() + '>';
   }
   inspect: function() {
     return '#<Enumerable:' + this.toArray().inspect() + '>';
   }
-}
+};
 
 Object.extend(Enumerable, {
   map:     Enumerable.collect,
   find:    Enumerable.detect,
   select:  Enumerable.findAll,
 
 Object.extend(Enumerable, {
   map:     Enumerable.collect,
   find:    Enumerable.detect,
   select:  Enumerable.findAll,
+  filter:  Enumerable.findAll,
   member:  Enumerable.include,
   member:  Enumerable.include,
-  entries: Enumerable.toArray
+  entries: Enumerable.toArray,
+  every:   Enumerable.all,
+  some:    Enumerable.any
 });
 });
-var $A = Array.from = function(iterable) {
+function $A(iterable) {
   if (!iterable) return [];
   if (!iterable) return [];
-  if (iterable.toArray) {
-    return iterable.toArray();
-  } else {
-    var results = [];
-    for (var i = 0, length = iterable.length; i < length; i++)
-      results.push(iterable[i]);
+  if (iterable.toArray) return iterable.toArray();
+  var length = iterable.length || 0, results = new Array(length);
+  while (length--) results[length] = iterable[length];
+  return results;
+}
+
+if (Prototype.Browser.WebKit) {
+  $A = function(iterable) {
+    if (!iterable) return [];
+    // In Safari, only use the `toArray` method if it's not a NodeList.
+    // A NodeList is a function, has an function `item` property, and a numeric
+    // `length` property. Adapted from Google Doctype.
+    if (!(typeof iterable === 'function' && typeof iterable.length ===
+        'number' && typeof iterable.item === 'function') && iterable.toArray)
+      return iterable.toArray();
+    var length = iterable.length || 0, results = new Array(length);
+    while (length--) results[length] = iterable[length];
     return results;
     return results;
-  }
+  };
 }
 
 }
 
+Array.from = $A;
+
 Object.extend(Array.prototype, Enumerable);
 
 Object.extend(Array.prototype, Enumerable);
 
-if (!Array.prototype._reverse)
-  Array.prototype._reverse = Array.prototype.reverse;
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
 
 Object.extend(Array.prototype, {
   _each: function(iterator) {
 
 Object.extend(Array.prototype, {
   _each: function(iterator) {
@@ -562,7 +865,7 @@ Object.extend(Array.prototype, {
 
   flatten: function() {
     return this.inject([], function(array, value) {
 
   flatten: function() {
     return this.inject([], function(array, value) {
-      return array.concat(value && value.constructor == Array ?
+      return array.concat(Object.isArray(value) ?
         value.flatten() : [value]);
     });
   },
         value.flatten() : [value]);
     });
   },
@@ -574,12 +877,6 @@ Object.extend(Array.prototype, {
     });
   },
 
     });
   },
 
-  indexOf: function(object) {
-    for (var i = 0, length = this.length; i < length; i++)
-      if (this[i] == object) return i;
-    return -1;
-  },
-
   reverse: function(inline) {
     return (inline !== false ? this : this.toArray())._reverse();
   },
   reverse: function(inline) {
     return (inline !== false ? this : this.toArray())._reverse();
   },
@@ -588,9 +885,17 @@ Object.extend(Array.prototype, {
     return this.length > 1 ? this : this[0];
   },
 
     return this.length > 1 ? this : this[0];
   },
 
-  uniq: function() {
-    return this.inject([], function(array, value) {
-      return array.include(value) ? array : array.concat([value]);
+  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 });
     });
   },
 
     });
   },
 
@@ -604,125 +909,189 @@ Object.extend(Array.prototype, {
 
   inspect: function() {
     return '[' + this.map(Object.inspect).join(', ') + ']';
 
   inspect: function() {
     return '[' + this.map(Object.inspect).join(', ') + ']';
+  },
+
+  toJSON: function() {
+    var results = [];
+    this.each(function(object) {
+      var value = Object.toJSON(object);
+      if (!Object.isUndefined(value)) 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;
 
 Array.prototype.toArray = Array.prototype.clone;
 
-function $w(string){
+function $w(string) {
+  if (!Object.isString(string)) return [];
   string = string.strip();
   return string ? string.split(/\s+/) : [];
 }
 
   string = string.strip();
   return string ? string.split(/\s+/) : [];
 }
 
-if(window.opera){
-  Array.prototype.concat = function(){
+if (Prototype.Browser.Opera){
+  Array.prototype.concat = function() {
     var array = [];
     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(arguments[i].constructor == Array) {
-        for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+    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;
           array.push(arguments[i][j]);
       } else {
         array.push(arguments[i]);
       }
     }
     return array;
-  }
+  };
 }
 }
-var Hash = function(obj) {
-  Object.extend(this, obj || {});
-};
-
-Object.extend(Hash, {
-  toQueryString: function(obj) {
-    var parts = [];
-
-         this.prototype._each.call(obj, function(pair) {
-      if (!pair.key) return;
-
-      if (pair.value && pair.value.constructor == Array) {
-        var values = pair.value.compact();
-        if (values.length < 2) pair.value = values.reduce();
-        else {
-               key = encodeURIComponent(pair.key);
-          values.each(function(value) {
-            value = value != undefined ? encodeURIComponent(value) : '';
-            parts.push(key + '=' + encodeURIComponent(value));
-          });
-          return;
-        }
-      }
-      if (pair.value == undefined) pair[1] = '';
-      parts.push(pair.map(encodeURIComponent).join('='));
-         });
-
-    return parts.join('&');
-  }
-});
-
-Object.extend(Hash.prototype, Enumerable);
-Object.extend(Hash.prototype, {
-  _each: function(iterator) {
-    for (var key in this) {
-      var value = this[key];
-      if (value && value == Hash.prototype[key]) continue;
-
-      var pair = [key, value];
-      pair.key = key;
-      pair.value = value;
-      iterator(pair);
-    }
-  },
-
-  keys: function() {
-    return this.pluck('key');
-  },
-
-  values: function() {
-    return this.pluck('value');
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
   },
 
   },
 
-  merge: function(hash) {
-    return $H(hash).inject(this, function(mergedHash, pair) {
-      mergedHash[pair.key] = pair.value;
-      return mergedHash;
-    });
+  succ: function() {
+    return this + 1;
   },
 
   },
 
-  remove: function() {
-    var result;
-    for(var i = 0, length = arguments.length; i < length; i++) {
-      var value = this[arguments[i]];
-      if (value !== undefined){
-        if (result === undefined) result = value;
-        else {
-          if (result.constructor != Array) result = [result];
-          result.push(value)
-        }
-      }
-      delete this[arguments[i]];
-    }
-    return result;
+  times: function(iterator, context) {
+    $R(0, this, true).each(iterator, context);
+    return this;
   },
 
   },
 
-  toQueryString: function() {
-    return Hash.toQueryString(this);
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
   },
 
   },
 
-  inspect: function() {
-    return '#<Hash:{' + this.map(function(pair) {
-      return pair.map(Object.inspect).join(': ');
-    }).join(', ') + '}>';
+  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) {
 function $H(object) {
-  if (object && object.constructor == Hash) return object;
   return new Hash(object);
 };
   return new Hash(object);
 };
-ObjectRange = Class.create();
-Object.extend(ObjectRange.prototype, Enumerable);
-Object.extend(ObjectRange.prototype, {
+
+var Hash = Class.create(Enumerable, (function() {
+
+  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: function(iterator) {
+      for (var key in this._object) {
+        var value = this._object[key], pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    },
+
+    set: function(key, value) {
+      return this._object[key] = value;
+    },
+
+    get: function(key) {
+      // simulating poorly supported hasOwnProperty
+      if (this._object[key] !== Object.prototype[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.inject([], function(results, pair) {
+        var key = encodeURIComponent(pair.key), values = pair.value;
+
+        if (values && typeof values == 'object') {
+          if (Object.isArray(values))
+            return results.concat(values.map(toQueryPair.curry(key)));
+        } else results.push(toQueryPair(key, values));
+        return results;
+      }).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;
   initialize: function(start, end, exclusive) {
     this.start = start;
     this.end = end;
@@ -748,7 +1117,7 @@ Object.extend(ObjectRange.prototype, {
 
 var $R = function(start, end, exclusive) {
   return new ObjectRange(start, end, exclusive);
 
 var $R = function(start, end, exclusive) {
   return new ObjectRange(start, end, exclusive);
-}
+};
 
 var Ajax = {
   getTransport: function() {
 
 var Ajax = {
   getTransport: function() {
@@ -760,7 +1129,7 @@ var Ajax = {
   },
 
   activeRequestCount: 0
   },
 
   activeRequestCount: 0
-}
+};
 
 Ajax.Responders = {
   responders: [],
 
 Ajax.Responders = {
   responders: [],
@@ -780,10 +1149,10 @@ Ajax.Responders = {
 
   dispatch: function(callback, request, transport, json) {
     this.each(function(responder) {
 
   dispatch: function(callback, request, transport, json) {
     this.each(function(responder) {
-      if (typeof responder[callback] == 'function') {
+      if (Object.isFunction(responder[callback])) {
         try {
           responder[callback].apply(responder, [request, transport, json]);
         try {
           responder[callback].apply(responder, [request, transport, json]);
-        } catch (e) {}
+        } catch (e) { }
       }
     });
   }
       }
     });
   }
@@ -792,49 +1161,45 @@ Ajax.Responders = {
 Object.extend(Ajax.Responders, Enumerable);
 
 Ajax.Responders.register({
 Object.extend(Ajax.Responders, Enumerable);
 
 Ajax.Responders.register({
-  onCreate: function() {
-    Ajax.activeRequestCount++;
-  },
-  onComplete: function() {
-    Ajax.activeRequestCount--;
-  }
+  onCreate:   function() { Ajax.activeRequestCount++ },
+  onComplete: function() { Ajax.activeRequestCount-- }
 });
 
 });
 
-Ajax.Base = function() {};
-Ajax.Base.prototype = {
-  setOptions: function(options) {
+Ajax.Base = Class.create({
+  initialize: function(options) {
     this.options = {
       method:       'post',
       asynchronous: true,
       contentType:  'application/x-www-form-urlencoded',
       encoding:     'UTF-8',
     this.options = {
       method:       'post',
       asynchronous: true,
       contentType:  'application/x-www-form-urlencoded',
       encoding:     'UTF-8',
-      parameters:   ''
-    }
-    Object.extend(this.options, options || {});
+      parameters:   '',
+      evalJSON:     true,
+      evalJS:       true
+    };
+    Object.extend(this.options, options || { });
 
     this.options.method = this.options.method.toLowerCase();
 
     this.options.method = this.options.method.toLowerCase();
-    if (typeof this.options.parameters == 'string')
+
+    if (Object.isString(this.options.parameters))
       this.options.parameters = this.options.parameters.toQueryParams();
       this.options.parameters = this.options.parameters.toQueryParams();
+    else if (Object.isHash(this.options.parameters))
+      this.options.parameters = this.options.parameters.toObject();
   }
   }
-}
-
-Ajax.Request = Class.create();
-Ajax.Request.Events =
-  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+});
 
 
-Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+Ajax.Request = Class.create(Ajax.Base, {
   _complete: false,
 
   _complete: false,
 
-  initialize: function(url, options) {
+  initialize: function($super, url, options) {
+    $super(options);
     this.transport = Ajax.getTransport();
     this.transport = Ajax.getTransport();
-    this.setOptions(options);
     this.request(url);
   },
 
   request: function(url) {
     this.url = url;
     this.method = this.options.method;
     this.request(url);
   },
 
   request: function(url) {
     this.url = url;
     this.method = this.options.method;
-    var params = this.options.parameters;
+    var params = Object.clone(this.options.parameters);
 
     if (!['get', 'post'].include(this.method)) {
       // simulate other verbs over post
 
     if (!['get', 'post'].include(this.method)) {
       // simulate other verbs over post
@@ -842,28 +1207,31 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
       this.method = 'post';
     }
 
       this.method = 'post';
     }
 
-    params = Hash.toQueryString(params);
-    if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='
+    this.parameters = params;
 
 
-    // when GET, append parameters to URL
-    if (this.method == 'get' && params)
-      this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + 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 {
 
     try {
-      Ajax.Responders.dispatch('onCreate', this, this.transport);
+      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);
 
 
       this.transport.open(this.method.toUpperCase(), this.url,
         this.options.asynchronous);
 
-      if (this.options.asynchronous)
-        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
 
       this.transport.onreadystatechange = this.onStateChange.bind(this);
       this.setRequestHeaders();
 
 
       this.transport.onreadystatechange = this.onStateChange.bind(this);
       this.setRequestHeaders();
 
-      var body = this.method == 'post' ? (this.options.postBody || params) : null;
-
-      this.transport.send(body);
+      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)
 
       /* Force Firefox to handle ready state 4 for synchronous requests */
       if (!this.options.asynchronous && this.transport.overrideMimeType)
@@ -905,7 +1273,7 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
     if (typeof this.options.requestHeaders == 'object') {
       var extras = this.options.requestHeaders;
 
     if (typeof this.options.requestHeaders == 'object') {
       var extras = this.options.requestHeaders;
 
-      if (typeof extras.push == 'function')
+      if (Object.isFunction(extras.push))
         for (var i = 0, length = extras.length; i < length; i += 2)
           headers[extras[i]] = extras[i+1];
       else
         for (var i = 0, length = extras.length; i < length; i += 2)
           headers[extras[i]] = extras[i+1];
       else
@@ -917,32 +1285,39 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
   },
 
   success: function() {
   },
 
   success: function() {
-    return !this.transport.status
-        || (this.transport.status >= 200 && this.transport.status < 300);
+    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) {
   },
 
   respondToReadyState: function(readyState) {
-    var state = Ajax.Request.Events[readyState];
-    var transport = this.transport, json = this.evalJSON();
+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
 
     if (state == 'Complete') {
       try {
         this._complete = true;
 
     if (state == 'Complete') {
       try {
         this._complete = true;
-        (this.options['on' + this.transport.status]
+        (this.options['on' + response.status]
          || this.options['on' + (this.success() ? 'Success' : 'Failure')]
          || this.options['on' + (this.success() ? 'Success' : 'Failure')]
-         || Prototype.emptyFunction)(transport, json);
+         || Prototype.emptyFunction)(response, response.headerJSON);
       } catch (e) {
         this.dispatchException(e);
       }
 
       } catch (e) {
         this.dispatchException(e);
       }
 
-      if ((this.getHeader('Content-type') || 'text/javascript').strip().
-        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
-          this.evalResponse();
+      var contentType = response.getHeader('Content-type');
+      if (this.options.evalJS == 'force'
+          || (this.options.evalJS && this.isSameOrigin() && contentType
+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+        this.evalResponse();
     }
 
     try {
     }
 
     try {
-      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
-      Ajax.Responders.dispatch('on' + state, this, transport, json);
+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
     } catch (e) {
       this.dispatchException(e);
     }
     } catch (e) {
       this.dispatchException(e);
     }
@@ -953,22 +1328,24 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
     }
   },
 
     }
   },
 
-  getHeader: function(name) {
-    try {
-      return this.transport.getResponseHeader(name);
-    } catch (e) { return null }
+  isSameOrigin: function() {
+    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+      protocol: location.protocol,
+      domain: document.domain,
+      port: location.port ? ':' + location.port : ''
+    }));
   },
 
   },
 
-  evalJSON: function() {
+  getHeader: function(name) {
     try {
     try {
-      var json = this.getHeader('X-JSON');
-      return json ? eval('(' + json + ')') : null;
+      return this.transport.getResponseHeader(name) || null;
     } catch (e) { return null }
   },
 
   evalResponse: function() {
     try {
     } catch (e) { return null }
   },
 
   evalResponse: function() {
     try {
-      return eval(this.transport.responseText);
+      return eval((this.transport.responseText || '').unfilterJSON());
     } catch (e) {
       this.dispatchException(e);
     }
     } catch (e) {
       this.dispatchException(e);
     }
@@ -980,57 +1357,128 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
   }
 });
 
   }
 });
 
-Ajax.Updater = Class.create();
+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();
+    }
 
 
-Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
-  initialize: function(container, url, options) {
-    this.container = {
-      success: (container.success || container),
-      failure: (container.failure || (container.success ? null : container))
+    if(readyState == 4) {
+      var xml = transport.responseXML;
+      this.responseXML  = Object.isUndefined(xml) ? null : xml;
+      this.responseJSON = this._getResponseJSON();
     }
     }
+  },
 
 
-    this.transport = Ajax.getTransport();
-    this.setOptions(options);
+  status:      0,
+  statusText: '',
 
 
-    var onComplete = this.options.onComplete || Prototype.emptyFunction;
-    this.options.onComplete = (function(transport, param) {
-      this.updateContent();
-      onComplete(transport, param);
-    }).bind(this);
+  getStatus: Ajax.Request.prototype.getStatus,
 
 
-    this.request(url);
+  getStatusText: function() {
+    try {
+      return this.transport.statusText || '';
+    } catch (e) { return '' }
   },
 
   },
 
-  updateContent: function() {
-    var receiver = this.container[this.success() ? 'success' : 'failure'];
-    var response = this.transport.responseText;
+  getHeader: Ajax.Request.prototype.getHeader,
 
 
-    if (!this.options.evalScripts) response = response.stripScripts();
+  getAllHeaders: function() {
+    try {
+      return this.getAllResponseHeaders();
+    } catch (e) { return null }
+  },
 
 
-    if (receiver = $(receiver)) {
-      if (this.options.insertion)
-        new this.options.insertion(receiver, response);
-      else
-        receiver.update(response);
+  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 ||
+        !this.request.isSameOrigin());
+    } 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')) ||
+        this.responseText.blank())
+          return null;
+    try {
+      return this.responseText.evalJSON(options.sanitizeJSON ||
+        !this.request.isSameOrigin());
+    } 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 = Object.clone(options);
+    var onComplete = options.onComplete;
+    options.onComplete = (function(response, json) {
+      this.updateContent(response.responseText);
+      if (Object.isFunction(onComplete)) onComplete(response, json);
+    }).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 (this.success()) {
-      if (this.onComplete)
-        setTimeout(this.onComplete.bind(this), 10);
+    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);
     }
   }
 });
 
     }
   }
 });
 
-Ajax.PeriodicalUpdater = Class.create();
-Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
-  initialize: function(container, url, options) {
-    this.setOptions(options);
+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.onComplete = this.options.onComplete;
 
     this.frequency = (this.options.frequency || 2);
     this.decay = (this.options.decay || 1);
 
-    this.updater = {};
+    this.updater = { };
     this.container = container;
     this.url = url;
 
     this.container = container;
     this.url = url;
 
@@ -1048,15 +1496,14 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
     (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
   },
 
     (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
   },
 
-  updateComplete: function(request) {
+  updateComplete: function(response) {
     if (this.options.decay) {
     if (this.options.decay) {
-      this.decay = (request.responseText == this.lastText ?
+      this.decay = (response.responseText == this.lastText ?
         this.decay * this.options.decay : 1);
 
         this.decay * this.options.decay : 1);
 
-      this.lastText = request.responseText;
+      this.lastText = response.responseText;
     }
     }
-    this.timer = setTimeout(this.onTimerEvent.bind(this),
-      this.decay * this.frequency * 1000);
+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
   },
 
   onTimerEvent: function() {
   },
 
   onTimerEvent: function() {
@@ -1069,7 +1516,7 @@ function $(element) {
       elements.push($(arguments[i]));
     return elements;
   }
       elements.push($(arguments[i]));
     return elements;
   }
-  if (typeof element == 'string')
+  if (Object.isString(element))
     element = document.getElementById(element);
   return Element.extend(element);
 }
     element = document.getElementById(element);
   return Element.extend(element);
 }
@@ -1080,63 +1527,52 @@ if (Prototype.BrowserFeatures.XPath) {
     var query = document.evaluate(expression, $(parentElement) || document,
       null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
     for (var i = 0, length = query.snapshotLength; i < length; i++)
     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(query.snapshotItem(i));
+      results.push(Element.extend(query.snapshotItem(i)));
     return results;
   };
 }
 
     return results;
   };
 }
 
-document.getElementsByClassName = function(className, parentElement) {
-  if (Prototype.BrowserFeatures.XPath) {
-    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
-    return document._getElementsByXPath(q, parentElement);
-  } else {
-    var children = ($(parentElement) || document.body).getElementsByTagName('*');
-    var elements = [], child;
-    for (var i = 0, length = children.length; i < length; i++) {
-      child = children[i];
-      if (Element.hasClassName(child, className))
-        elements.push(Element.extend(child));
-    }
-    return elements;
-  }
-};
-
 /*--------------------------------------------------------------------------*/
 
 /*--------------------------------------------------------------------------*/
 
-if (!window.Element)
-  var Element = new Object();
-
-Element.extend = function(element) {
-  if (!element || _nativeExtensions || element.nodeType == 3) return element;
-
-  if (!element._extended && element.tagName && element != window) {
-    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;
-
-    if (element.tagName == 'FORM')
-      Object.extend(methods, Form.Methods);
-    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
-      Object.extend(methods, Form.Element.Methods);
-
-    Object.extend(methods, Element.Methods.Simulated);
+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
+  });
+}
 
 
-    for (var property in methods) {
-      var value = methods[property];
-      if (typeof value == 'function' && !(property in element))
-        element[property] = cache.findOrStore(value);
+(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);
     }
     }
-  }
-
-  element._extended = true;
-  return element;
-};
+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+  };
+  Object.extend(this.Element, element || { });
+  if (element) this.Element.prototype = element.prototype;
+}).call(window);
 
 
-Element.extend.cache = {
-  findOrStore: function(value) {
-    return this[value] = this[value] || function() {
-      return value.apply(null, [this].concat($A(arguments)));
-    }
-  }
-};
+Element.cache = { };
 
 Element.Methods = {
   visible: function(element) {
 
 Element.Methods = {
   visible: function(element) {
@@ -1150,12 +1586,14 @@ Element.Methods = {
   },
 
   hide: function(element) {
   },
 
   hide: function(element) {
-    $(element).style.display = 'none';
+    element = $(element);
+    element.style.display = 'none';
     return element;
   },
 
   show: function(element) {
     return element;
   },
 
   show: function(element) {
-    $(element).style.display = '';
+    element = $(element);
+    element.style.display = '';
     return element;
   },
 
     return element;
   },
 
@@ -1165,28 +1603,78 @@ Element.Methods = {
     return element;
   },
 
     return element;
   },
 
-  update: function(element, html) {
-    html = typeof html == 'undefined' ? '' : html.toString();
-    $(element).innerHTML = html.stripScripts();
-    setTimeout(function() {html.evalScripts()}, 10);
+  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;
   },
 
     return element;
   },
 
-  replace: function(element, html) {
+  replace: function(element, content) {
     element = $(element);
     element = $(element);
-    html = typeof html == 'undefined' ? '' : html.toString();
-    if (element.outerHTML) {
-      element.outerHTML = html.stripScripts();
-    } else {
+    if (content && content.toElement) content = content.toElement();
+    else if (!Object.isElement(content)) {
+      content = Object.toHTML(content);
       var range = element.ownerDocument.createRange();
       var range = element.ownerDocument.createRange();
-      range.selectNodeContents(element);
-      element.parentNode.replaceChild(
-        range.createContextualFragment(html.stripScripts()), element);
+      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, insert, tagName, childNodes;
+
+    for (var position in insertions) {
+      content  = insertions[position];
+      position = position.toLowerCase();
+      insert = Element._insertionTranslations[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+
+      tagName = ((position == 'before' || position == 'after')
+        ? element.parentNode : element).tagName.toUpperCase();
+
+      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+      if (position == 'top' || position == 'after') childNodes.reverse();
+      childNodes.each(insert.curry(element));
+
+      content.evalScripts.bind(content).defer();
     }
     }
-    setTimeout(function() {html.evalScripts()}, 10);
+
     return element;
   },
 
     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();
   inspect: function(element) {
     element = $(element);
     var result = '<' + element.tagName.toLowerCase();
@@ -1212,7 +1700,13 @@ Element.Methods = {
   },
 
   descendants: function(element) {
   },
 
   descendants: function(element) {
-    return $A($(element).getElementsByTagName('*'));
+    return $(element).select("*");
+  },
+
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
   },
 
   immediateDescendants: function(element) {
   },
 
   immediateDescendants: function(element) {
@@ -1236,48 +1730,95 @@ Element.Methods = {
   },
 
   match: function(element, selector) {
   },
 
   match: function(element, selector) {
-    if (typeof selector == 'string')
+    if (Object.isString(selector))
       selector = new Selector(selector);
     return selector.match($(element));
   },
 
   up: function(element, expression, index) {
       selector = new Selector(selector);
     return selector.match($(element));
   },
 
   up: function(element, expression, index) {
-    return Selector.findElement($(element).ancestors(), expression, index);
+    element = $(element);
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return Object.isNumber(expression) ? ancestors[expression] :
+      Selector.findElement(ancestors, expression, index);
   },
 
   down: function(element, expression, index) {
   },
 
   down: function(element, expression, index) {
-    return Selector.findElement($(element).descendants(), expression, index);
+    element = $(element);
+    if (arguments.length == 1) return element.firstDescendant();
+    return Object.isNumber(expression) ? element.descendants()[expression] :
+      Element.select(element, expression)[index || 0];
   },
 
   previous: function(element, expression, index) {
   },
 
   previous: function(element, expression, index) {
-    return Selector.findElement($(element).previousSiblings(), expression, index);
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return Object.isNumber(expression) ? previousSiblings[expression] :
+      Selector.findElement(previousSiblings, expression, index);
   },
 
   next: function(element, expression, index) {
   },
 
   next: function(element, expression, index) {
-    return Selector.findElement($(element).nextSiblings(), expression, index);
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return Object.isNumber(expression) ? nextSiblings[expression] :
+      Selector.findElement(nextSiblings, expression, index);
   },
 
   },
 
-  getElementsBySelector: function() {
+  select: function() {
     var args = $A(arguments), element = $(args.shift());
     return Selector.findChildElements(element, args);
   },
 
     var args = $A(arguments), element = $(args.shift());
     return Selector.findChildElements(element, args);
   },
 
-  getElementsByClassName: function(element, className) {
-    return document.getElementsByClassName(className, element);
+  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);
   },
 
   readAttribute: function(element, name) {
     element = $(element);
-    if (document.all && !window.opera) {
-      var t = Element._attributeTranslations;
+    if (Prototype.Browser.IE) {
+      var t = Element._attributeTranslations.read;
       if (t.values[name]) return t.values[name](element, name);
       if (t.values[name]) return t.values[name](element, name);
-      if (t.names[name])  name = t.names[name];
-      var attribute = element.attributes[name];
-      if(attribute) return attribute.nodeValue;
+      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);
   },
 
     }
     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] = Object.isUndefined(value) ? true : value;
+
+    for (var attr in attributes) {
+      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;
   },
   getHeight: function(element) {
     return $(element).getDimensions().height;
   },
@@ -1293,39 +1834,28 @@ Element.Methods = {
   hasClassName: function(element, className) {
     if (!(element = $(element))) return;
     var elementClassName = element.className;
   hasClassName: function(element, className) {
     if (!(element = $(element))) return;
     var elementClassName = element.className;
-    if (elementClassName.length == 0) return false;
-    if (elementClassName == className ||
-        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
-      return true;
-    return false;
+    return (elementClassName.length > 0 && (elementClassName == className ||
+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
   },
 
   addClassName: function(element, className) {
     if (!(element = $(element))) return;
   },
 
   addClassName: function(element, className) {
     if (!(element = $(element))) return;
-    Element.classNames(element).add(className);
+    if (!element.hasClassName(className))
+      element.className += (element.className ? ' ' : '') + className;
     return element;
   },
 
   removeClassName: function(element, className) {
     if (!(element = $(element))) return;
     return element;
   },
 
   removeClassName: function(element, className) {
     if (!(element = $(element))) return;
-    Element.classNames(element).remove(className);
+    element.className = element.className.replace(
+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
     return element;
   },
 
   toggleClassName: function(element, className) {
     if (!(element = $(element))) return;
     return element;
   },
 
   toggleClassName: function(element, className) {
     if (!(element = $(element))) return;
-    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
-    return element;
-  },
-
-  observe: function() {
-    Event.observe.apply(Event, arguments);
-    return $A(arguments).first();
-  },
-
-  stopObserving: function() {
-    Event.stopObserving.apply(Event, arguments);
-    return $A(arguments).first();
+    return element[element.hasClassName(className) ?
+      'removeClassName' : 'addClassName'](className);
   },
 
   // removes whitespace-only text node children
   },
 
   // removes whitespace-only text node children
@@ -1342,80 +1872,75 @@ Element.Methods = {
   },
 
   empty: function(element) {
   },
 
   empty: function(element) {
-    return $(element).innerHTML.match(/^\s*$/);
+    return $(element).innerHTML.blank();
   },
 
   descendantOf: function(element, ancestor) {
     element = $(element), ancestor = $(ancestor);
   },
 
   descendantOf: function(element, ancestor) {
     element = $(element), ancestor = $(ancestor);
+
+    if (element.compareDocumentPosition)
+      return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+    if (ancestor.contains)
+      return ancestor.contains(element) && ancestor !== element;
+
     while (element = element.parentNode)
       if (element == ancestor) return true;
     while (element = element.parentNode)
       if (element == ancestor) return true;
+
     return false;
   },
 
   scrollTo: function(element) {
     element = $(element);
     return false;
   },
 
   scrollTo: function(element) {
     element = $(element);
-    var pos = Position.cumulativeOffset(element);
+    var pos = element.cumulativeOffset();
     window.scrollTo(pos[0], pos[1]);
     return element;
   },
 
   getStyle: function(element, style) {
     element = $(element);
     window.scrollTo(pos[0], pos[1]);
     return element;
   },
 
   getStyle: function(element, style) {
     element = $(element);
-    if (['float','cssFloat'].include(style))
-      style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat');
-    style = style.camelize();
+    style = style == 'float' ? 'cssFloat' : style.camelize();
     var value = element.style[style];
     var value = element.style[style];
-    if (!value) {
-      if (document.defaultView && document.defaultView.getComputedStyle) {
-        var css = document.defaultView.getComputedStyle(element, null);
-        value = css ? css[style] : null;
-      } else if (element.currentStyle) {
-        value = element.currentStyle[style];
-      }
+    if (!value || value == 'auto') {
+      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;
+  },
 
 
-    if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none'))
-      value = element['offset'+style.capitalize()] + 'px';
+  getOpacity: function(element) {
+    return $(element).getStyle('opacity');
+  },
 
 
-    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
-      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
-    if(style == 'opacity') {
-      if(value) return parseFloat(value);
-      if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
-        if(value[1]) return parseFloat(value[1]) / 100;
-      return 1.0;
+  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;
     }
     }
-    return value == 'auto' ? null : value;
+    for (var property in styles)
+      if (property == 'opacity') element.setOpacity(styles[property]);
+      else
+        elementStyle[(property == 'float' || property == 'cssFloat') ?
+          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+            property] = styles[property];
+
+    return element;
   },
 
   },
 
-  setStyle: function(element, style) {
+  setOpacity: function(element, value) {
     element = $(element);
     element = $(element);
-    for (var name in style) {
-      var value = style[name];
-      if(name == 'opacity') {
-        if (value == 1) {
-          value = (/Gecko/.test(navigator.userAgent) &&
-            !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
-          if(/MSIE/.test(navigator.userAgent) && !window.opera)
-            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
-        } else if(value == '') {
-          if(/MSIE/.test(navigator.userAgent) && !window.opera)
-            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
-        } else {
-          if(value < 0.00001) value = 0;
-          if(/MSIE/.test(navigator.userAgent) && !window.opera)
-            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
-              'alpha(opacity='+value*100+')';
-        }
-      } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
-      element.style[name.camelize()] = value;
-    }
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
     return element;
   },
 
   getDimensions: function(element) {
     element = $(element);
     return element;
   },
 
   getDimensions: function(element) {
     element = $(element);
-    var display = $(element).getStyle('display');
+    var display = element.getStyle('display');
     if (display != 'none' && display != null) // Safari bug
       return {width: element.offsetWidth, height: element.offsetHeight};
 
     if (display != 'none' && display != null) // Safari bug
       return {width: element.offsetWidth, height: element.offsetHeight};
 
@@ -1444,7 +1969,7 @@ Element.Methods = {
       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
       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) {
+      if (Prototype.Browser.Opera) {
         element.style.top = 0;
         element.style.left = 0;
       }
         element.style.top = 0;
         element.style.left = 0;
       }
@@ -1468,8 +1993,8 @@ Element.Methods = {
   makeClipping: function(element) {
     element = $(element);
     if (element._overflow) return element;
   makeClipping: function(element) {
     element = $(element);
     if (element._overflow) return element;
-    element._overflow = element.style.overflow || 'auto';
-    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+    if (element._overflow !== 'hidden')
       element.style.overflow = 'hidden';
     return element;
   },
       element.style.overflow = 'hidden';
     return element;
   },
@@ -1480,393 +2005,1441 @@ Element.Methods = {
     element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
     element._overflow = null;
     return element;
     element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
     element._overflow = null;
     return element;
-  }
-};
-
-Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf});
-
-Element._attributeTranslations = {};
-
-Element._attributeTranslations.names = {
-  colspan:   "colSpan",
-  rowspan:   "rowSpan",
-  valign:    "vAlign",
-  datetime:  "dateTime",
-  accesskey: "accessKey",
-  tabindex:  "tabIndex",
-  enctype:   "encType",
-  maxlength: "maxLength",
-  readonly:  "readOnly",
-  longdesc:  "longDesc"
-};
-
-Element._attributeTranslations.values = {
-  _getAttr: function(element, attribute) {
-    return element.getAttribute(attribute, 2);
   },
 
   },
 
-  _flag: function(element, attribute) {
-    return $(element).hasAttribute(attribute) ? attribute : null;
+  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);
   },
 
   },
 
-  style: function(element) {
-    return element.style.cssText.toLowerCase();
+  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.toUpperCase() == 'BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p !== 'static') break;
+      }
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
   },
 
   },
 
-  title: function(element) {
-    var node = element.getAttributeNode('title');
-    return node.specified ? node.nodeValue : null;
-  }
-};
-
-Object.extend(Element._attributeTranslations.values, {
-  href: Element._attributeTranslations.values._getAttr,
-  src:  Element._attributeTranslations.values._getAttr,
-  disabled: Element._attributeTranslations.values._flag,
-  checked:  Element._attributeTranslations.values._flag,
-  readonly: Element._attributeTranslations.values._flag,
-  multiple: Element._attributeTranslations.values._flag
-});
+  absolutize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'absolute') return element;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
 
 
-Element.Methods.Simulated = {
-  hasAttribute: function(element, attribute) {
-    var t = Element._attributeTranslations;
-    attribute = t.names[attribute] || attribute;
-    return $(element).getAttributeNode(attribute).specified;
-  }
-};
+    var offsets = element.positionedOffset();
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
 
 
-// IE is missing .innerHTML support for TABLE-related elements
-if (document.all && !window.opera){
-  Element.Methods.update = function(element, html) {
-    element = $(element);
-    html = typeof html == 'undefined' ? '' : html.toString();
-    var tagName = element.tagName.toUpperCase();
-    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
-      var div = document.createElement('div');
-      switch (tagName) {
-        case 'THEAD':
-        case 'TBODY':
-          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
-          depth = 2;
-          break;
-        case 'TR':
-          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
-          depth = 3;
-          break;
-        case 'TD':
-          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
-          depth = 4;
-      }
-      $A(element.childNodes).each(function(node){
-        element.removeChild(node)
-      });
-      depth.times(function(){ div = div.firstChild });
+    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;
 
 
-      $A(div.childNodes).each(
-        function(node){ element.appendChild(node) });
-    } else {
-      element.innerHTML = html.stripScripts();
-    }
-    setTimeout(function() {html.evalScripts()}, 10);
+    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;
     return element;
-  }
-};
+  },
 
 
-Object.extend(Element, Element.Methods);
+  relativize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'relative') return element;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
 
 
-var _nativeExtensions = false;
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
 
 
-if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
-  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
-    var className = 'HTML' + tag + 'Element';
-    if(window[className]) return;
-    var klass = window[className] = {};
-    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
-  });
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+    return element;
+  },
 
 
-Element.addMethods = function(methods) {
-  Object.extend(Element.Methods, methods || {});
+  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);
+  },
 
 
-  function copy(methods, destination, onlyIfAbsent) {
-    onlyIfAbsent = onlyIfAbsent || false;
-    var cache = Element.extend.cache;
-    for (var property in methods) {
-      var value = methods[property];
-      if (!onlyIfAbsent || !(property in destination))
-        destination[property] = cache.findOrStore(value);
-    }
-  }
+  getOffsetParent: function(element) {
+    if (element.offsetParent) return $(element.offsetParent);
+    if (element == document.body) return $(element);
 
 
-  if (typeof HTMLElement != 'undefined') {
-    copy(Element.Methods, HTMLElement.prototype);
-    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
-    copy(Form.Methods, HTMLFormElement.prototype);
-    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
-      copy(Form.Element.Methods, klass.prototype);
-    });
-    _nativeExtensions = true;
-  }
-}
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return $(element);
 
 
-var Toggle = new Object();
-Toggle.display = Element.toggle;
+    return $(document.body);
+  },
 
 
-/*--------------------------------------------------------------------------*/
+  viewportOffset: function(forElement) {
+    var valueT = 0, valueL = 0;
 
 
-Abstract.Insertion = function(adjacency) {
-  this.adjacency = adjacency;
-}
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
 
 
-Abstract.Insertion.prototype = {
-  initialize: function(element, content) {
-    this.element = $(element);
-    this.content = content.stripScripts();
+      // Safari fix
+      if (element.offsetParent == document.body &&
+        Element.getStyle(element, 'position') == 'absolute') break;
 
 
-    if (this.adjacency && this.element.insertAdjacentHTML) {
-      try {
-        this.element.insertAdjacentHTML(this.adjacency, this.content);
-      } catch (e) {
-        var tagName = this.element.tagName.toUpperCase();
-        if (['TBODY', 'TR'].include(tagName)) {
-          this.insertContent(this.contentFromAnonymousTable());
-        } else {
-          throw e;
-        }
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
       }
       }
-    } else {
-      this.range = this.element.ownerDocument.createRange();
-      if (this.initializeRange) this.initializeRange();
-      this.insertContent([this.range.createContextualFragment(this.content)]);
-    }
+    } while (element = element.parentNode);
 
 
-    setTimeout(function() {content.evalScripts()}, 10);
+    return Element._returnOffset(valueL, valueT);
   },
 
   },
 
-  contentFromAnonymousTable: function() {
-    var div = document.createElement('div');
-    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
-    return $A(div.childNodes[0].childNodes[0].childNodes);
-  }
-}
+  clonePosition: function(element, source) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || { });
 
 
-var Insertion = new Object();
+    // find page position of source
+    source = $(source);
+    var p = source.viewportOffset();
 
 
-Insertion.Before = Class.create();
-Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
-  initializeRange: function() {
-    this.range.setStartBefore(this.element);
-  },
+    // 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;
+    }
 
 
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment, this.element);
-    }).bind(this));
+    // 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;
   }
   }
-});
+};
 
 
-Insertion.Top = Class.create();
-Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(true);
-  },
+Element.Methods.identify.counter = 1;
 
 
-  insertContent: function(fragments) {
-    fragments.reverse(false).each((function(fragment) {
-      this.element.insertBefore(fragment, this.element.firstChild);
-    }).bind(this));
-  }
+Object.extend(Element.Methods, {
+  getElementsBySelector: Element.Methods.select,
+  childElements: Element.Methods.immediateDescendants
 });
 
 });
 
-Insertion.Bottom = Class.create();
-Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(this.element);
+Element._attributeTranslations = {
+  write: {
+    names: {
+      className: 'class',
+      htmlFor:   'for'
+    },
+    values: { }
+  }
+};
+
+if (Prototype.Browser.Opera) {
+  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+    function(proceed, element, style) {
+      switch (style) {
+        case 'left': case 'top': case 'right': case 'bottom':
+          if (proceed(element, 'position') === 'static') return null;
+        case 'height': case 'width':
+          // returns '0px' for hidden elements; we want it to return null
+          if (!Element.visible(element)) return null;
+
+          // returns the border-box dimensions rather than the content-box
+          // dimensions, so we subtract padding and borders from the value
+          var dim = parseInt(proceed(element, style), 10);
+
+          if (dim !== element['offset' + style.capitalize()])
+            return dim + 'px';
+
+          var properties;
+          if (style === 'height') {
+            properties = ['border-top-width', 'padding-top',
+             'padding-bottom', 'border-bottom-width'];
+          }
+          else {
+            properties = ['border-left-width', 'padding-left',
+             'padding-right', 'border-right-width'];
+          }
+          return properties.inject(dim, function(memo, property) {
+            var val = proceed(element, property);
+            return val === null ? memo : memo - parseInt(val, 10);
+          }) + 'px';
+        default: return proceed(element, style);
+      }
+    }
+  );
+
+  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+    function(proceed, element, attribute) {
+      if (attribute === 'title') return element.title;
+      return proceed(element, attribute);
+    }
+  );
+}
+
+else if (Prototype.Browser.IE) {
+  // IE doesn't report offsets correctly for static elements, so we change them
+  // to "relative" to get the values, then change them back.
+  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+    function(proceed, element) {
+      element = $(element);
+      // IE throws an error if element is not in document
+      try { element.offsetParent }
+      catch(e) { return $(document.body) }
+      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;
+    }
+  );
+
+  $w('positionedOffset viewportOffset').each(function(method) {
+    Element.Methods[method] = Element.Methods[method].wrap(
+      function(proceed, element) {
+        element = $(element);
+        try { element.offsetParent }
+        catch(e) { return Element._returnOffset(0,0) }
+        var position = element.getStyle('position');
+        if (position !== 'static') return proceed(element);
+        // Trigger hasLayout on the offset parent so that IE6 reports
+        // accurate offsetTop and offsetLeft values for position: fixed.
+        var offsetParent = element.getOffsetParent();
+        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+          offsetParent.setStyle({ zoom: 1 });
+        element.setStyle({ position: 'relative' });
+        var value = proceed(element);
+        element.setStyle({ position: position });
+        return value;
+      }
+    );
+  });
+
+  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
+    function(proceed, element) {
+      try { element.offsetParent }
+      catch(e) { return Element._returnOffset(0,0) }
+      return proceed(element);
+    }
+  );
+
+  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) {
+          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.extend({
+      cellpadding: 'cellPadding',
+      cellspacing: 'cellSpacing'
+    }, 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 frameBorder').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.toUpperCase() == '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 Element#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 ('outerHTML' in document.createElement('div')) {
+  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];
+  if (t) {
+    div.innerHTML = t[0] + html + t[1];
+    t[2].times(function() { div = div.firstChild });
+  } else div.innerHTML = html;
+  return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+  before: function(element, node) {
+    element.parentNode.insertBefore(node, element);
+  },
+  top: function(element, node) {
+    element.insertBefore(node, element.firstChild);
   },
   },
+  bottom: function(element, node) {
+    element.appendChild(node);
+  },
+  after: function(element, node) {
+    element.parentNode.insertBefore(node, element.nextSibling);
+  },
+  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() {
+  Object.extend(this.tags, {
+    THEAD: this.tags.TBODY,
+    TFOOT: this.tags.TBODY,
+    TH:    this.tags.TD
+  });
+}).call(Element._insertionTranslations);
 
 
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.appendChild(fragment);
-    }).bind(this));
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    attribute = Element._attributeTranslations.has[attribute] || attribute;
+    var node = $(element).getAttributeNode(attribute);
+    return !!(node && node.specified);
   }
   }
-});
+};
 
 
-Insertion.After = Class.create();
-Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
-  initializeRange: function() {
-    this.range.setStartAfter(this.element);
-  },
+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;
 
 
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment,
-        this.element.nextSibling);
-    }).bind(this));
+  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.toUpperCase(), 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];
+  }
 
 
-Element.ClassNames = Class.create();
-Element.ClassNames.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-  },
+  if (!tagName) Object.extend(Element.Methods, methods || { });
+  else {
+    if (Object.isArray(tagName)) tagName.each(extend);
+    else extend(tagName);
+  }
 
 
-  _each: function(iterator) {
-    this.element.className.split(/\s+/).select(function(name) {
-      return name.length > 0;
-    })._each(iterator);
-  },
+  function extend(tagName) {
+    tagName = tagName.toUpperCase();
+    if (!Element.Methods.ByTag[tagName])
+      Element.Methods.ByTag[tagName] = { };
+    Object.extend(Element.Methods.ByTag[tagName], methods);
+  }
 
 
-  set: function(className) {
-    this.element.className = className;
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!Object.isFunction(value)) 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 = { }, B = Prototype.Browser;
+    $w('width height').each(function(d) {
+      var D = d.capitalize();
+      if (B.WebKit && !document.evaluate) {
+        // Safari <3.0 needs self.innerWidth/Height
+        dimensions[d] = self['inner' + D];
+      } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
+        // Opera <9.5 needs document.body.clientWidth/Height
+        dimensions[d] = document.body['client' + D]
+      } else {
+        dimensions[d] = document.documentElement['client' + D];
+      }
+    });
+    return dimensions;
   },
 
   },
 
-  add: function(classNameToAdd) {
-    if (this.include(classNameToAdd)) return;
-    this.set($A(this).concat(classNameToAdd).join(' '));
+  getWidth: function() {
+    return this.getDimensions().width;
   },
 
   },
 
-  remove: function(classNameToRemove) {
-    if (!this.include(classNameToRemove)) return;
-    this.set($A(this).without(classNameToRemove).join(' '));
+  getHeight: function() {
+    return this.getDimensions().height;
   },
 
   },
 
-  toString: function() {
-    return $A(this).join(' ');
+  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. */
 
 
-Object.extend(Element.ClassNames.prototype, Enumerable);
-var Selector = Class.create();
-Selector.prototype = {
+var Selector = Class.create({
   initialize: function(expression) {
   initialize: function(expression) {
-    this.params = {classNames: []};
-    this.expression = expression.toString().strip();
-    this.parseExpression();
-    this.compileMatcher();
+    this.expression = expression.strip();
+
+    if (this.shouldUseSelectorsAPI()) {
+      this.mode = 'selectorsAPI';
+    } else if (this.shouldUseXPath()) {
+      this.mode = 'xpath';
+      this.compileXPathMatcher();
+    } else {
+      this.mode = "normal";
+      this.compileMatcher();
+    }
+
   },
 
   },
 
-  parseExpression: function() {
-    function abort(message) { throw 'Parse error in selector: ' + message; }
+  shouldUseXPath: function() {
+    if (!Prototype.BrowserFeatures.XPath) return false;
+
+    var e = this.expression;
 
 
-    if (this.expression == '')  abort('empty expression');
+    // Safari 3 chokes on :*-of-type and :empty
+    if (Prototype.Browser.WebKit &&
+     (e.include("-of-type") || e.include(":empty")))
+      return false;
 
 
-    var params = this.params, expr = this.expression, match, modifier, clause, rest;
-    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
-      params.attributes = params.attributes || [];
-      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
-      expr = match[1];
+    // XPath can't do namespaced attributes, nor can it read
+    // the "checked" property from DOM nodes
+    if ((/(\[[\w-]*?:|:checked)/).test(e))
+      return false;
+
+    return true;
+  },
+
+  shouldUseSelectorsAPI: function() {
+    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;
+
+    if (!Selector._div) Selector._div = new Element('div');
+
+    // Make sure the browser treats the selector as valid. Test on an
+    // isolated element to minimize cost of this check.
+    try {
+      Selector._div.querySelector(this.expression);
+    } catch(e) {
+      return false;
     }
 
     }
 
-    if (expr == '*') return this.params.wildcard = true;
+    return true;
+  },
+
+  compileMatcher: function() {
+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+        c = Selector.criteria, le, p, m;
 
 
-    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
-      modifier = match[1], clause = match[2], rest = match[3];
-      switch (modifier) {
-        case '#':       params.id = clause; break;
-        case '.':       params.classNames.push(clause); break;
-        case '':
-        case undefined: params.tagName = clause.toUpperCase(); break;
-        default:        abort(expr.inspect());
+    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;
+        }
       }
       }
-      expr = rest;
     }
 
     }
 
-    if (expr.length > 0) abort(expr.inspect());
+    this.matcher.push("return h.unique(n);\n}");
+    eval(this.matcher.join('\n'));
+    Selector._cache[this.expression] = this.matcher;
   },
 
   },
 
-  buildMatchExpression: function() {
-    var params = this.params, conditions = [], clause;
+  compileXPathMatcher: function() {
+    var e = this.expression, ps = Selector.patterns,
+        x = Selector.xpath, le, m;
 
 
-    if (params.wildcard)
-      conditions.push('true');
-    if (clause = params.id)
-      conditions.push('element.readAttribute("id") == ' + clause.inspect());
-    if (clause = params.tagName)
-      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
-    if ((clause = params.classNames).length > 0)
-      for (var i = 0, length = clause.length; i < length; i++)
-        conditions.push('element.hasClassName(' + clause[i].inspect() + ')');
-    if (clause = params.attributes) {
-      clause.each(function(attribute) {
-        var value = 'element.readAttribute(' + attribute.name.inspect() + ')';
-        var splitValueBy = function(delimiter) {
-          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
-        }
+    if (Selector._cache[e]) {
+      this.xpath = Selector._cache[e]; return;
+    }
 
 
-        switch (attribute.operator) {
-          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
-          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
-          case '|=':      conditions.push(
-                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
-                          ); break;
-          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
-          case '':
-          case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break;
-          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
+    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;
         }
         }
-      });
+      }
     }
 
     }
 
-    return conditions.join(' && ');
+    this.xpath = this.matcher.join('');
+    Selector._cache[this.expression] = this.xpath;
   },
 
   },
 
-  compileMatcher: function() {
-    this.match = new Function('element', 'if (!element.tagName) return false; \
-      element = $(element); \
-      return ' + this.buildMatchExpression());
-  },
+  findElements: function(root) {
+    root = root || document;
+    var e = this.expression, results;
 
 
-  findElements: function(scope) {
-    var element;
+    switch (this.mode) {
+      case 'selectorsAPI':
+        // querySelectorAll queries document-wide, then filters to descendants
+        // of the context element. That's not what we want.
+        // Add an explicit context to the selector if necessary.
+        if (root !== document) {
+          var oldId = root.id, id = $(root).identify();
+          e = "#" + id + " " + e;
+        }
 
 
-    if (element = $(this.params.id))
-      if (this.match(element))
-        if (!scope || Element.childOf(element, scope))
-          return [element];
+        results = $A(root.querySelectorAll(e)).map(Element.extend);
+        root.id = oldId;
 
 
-    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+        return results;
+      case 'xpath':
+        return document._getElementsByXPath(this.xpath, root);
+      default:
+       return this.matcher(root);
+    }
+  },
 
 
-    var results = [];
-    for (var i = 0, length = scope.length; i < length; i++)
-      if (this.match(element = scope[i]))
-        results.push(Element.extend(element));
+  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);
+          }
+        }
+      }
+    }
 
 
-    return results;
+    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;
   },
 
   toString: function() {
     return this.expression;
+  },
+
+  inspect: function() {
+    return "#<Selector:" + this.expression.inspect() + ">";
   }
   }
-}
+});
 
 Object.extend(Selector, {
 
 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: function(m) {
+      m[1] = m[1].toLowerCase();
+      return new Template("[@#{1}]").evaluate(m);
+    },
+    attr: function(m) {
+      m[1] = m[1].toLowerCase();
+      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)]",
+      'checked':     "[@checked]",
+      'disabled':    "[(@disabled) and (@type!='hidden')]",
+      'enabled':     "[not(@disabled) and (@type!='hidden')]",
+      'not': function(m) {
+        var e = m[6], p = Selector.patterns,
+            x = Selector.xpath, le, 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); c = false;',
+    attr: function(m) {
+      m[3] = (m[5] || m[6]);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); 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]+:)?[\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 nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
+    }
+  },
+
+  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) {
+      var _true = Prototype.emptyFunction;
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._countedByPrototype = _true;
+      return nodes;
+    },
+
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._countedByPrototype = 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._countedByPrototype = Prototype.emptyFunction;
+      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._countedByPrototype)) node.nodeIndex = j++;
+        }
+      } else {
+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) 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])._countedByPrototype) {
+          n._countedByPrototype = Prototype.emptyFunction;
+          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, 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) {
+      var uTagName = 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() === uTagName) 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, combinator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      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, combinator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      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._countedByPrototype) {
+          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) 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._countedByPrototype) 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 && (!node.type || node.type !== 'hidden'))
+          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 == v || nv && nv.startsWith(v); },
+    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
+    '*=': function(nv, v) { return nv == v || nv && nv.include(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() + '-'); }
+  },
+
+  split: function(expression) {
+    var expressions = [];
+    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    return expressions;
+  },
+
   matchElements: function(elements, expression) {
   matchElements: function(elements, expression) {
-    var selector = new Selector(expression);
-    return elements.select(selector.match.bind(selector)).map(Element.extend);
+    var matches = $$(expression), h = Selector.handlers;
+    h.mark(matches);
+    for (var i = 0, results = [], element; element = elements[i]; i++)
+      if (element._countedByPrototype) results.push(element);
+    h.unmark(matches);
+    return results;
   },
 
   findElement: function(elements, expression, index) {
   },
 
   findElement: function(elements, expression, index) {
-    if (typeof expression == 'number') index = expression, expression = false;
+    if (Object.isNumber(expression)) {
+      index = expression; expression = false;
+    }
     return Selector.matchElements(elements, expression || '*')[index || 0];
   },
 
   findChildElements: function(element, expressions) {
     return Selector.matchElements(elements, expression || '*')[index || 0];
   },
 
   findChildElements: function(element, expressions) {
-    return expressions.map(function(expression) {
-      return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) {
-        var selector = new Selector(expr);
-        return results.inject([], function(elements, result) {
-          return elements.concat(selector.findElements(result || element));
-        });
-      });
-    }).flatten();
+    expressions = Selector.split(expressions.join(','));
+    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;
   }
 });
 
   }
 });
 
+if (Prototype.Browser.IE) {
+  Object.extend(Selector.handlers, {
+    // IE returns comment nodes on getElementsByTagName("*").
+    // Filter them out.
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        if (node.tagName !== "!") a.push(node);
+      return a;
+    },
+
+    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node.removeAttribute('_countedByPrototype');
+      return nodes;
+    }
+  });
+}
+
 function $$() {
   return Selector.findChildElements(document, $A(arguments));
 }
 function $$() {
   return Selector.findChildElements(document, $A(arguments));
 }
@@ -1876,13 +3449,19 @@ var Form = {
     return form;
   },
 
     return form;
   },
 
-  serializeElements: function(elements, getHash) {
-    var data = elements.inject({}, function(result, element) {
+  serializeElements: function(elements, options) {
+    if (typeof options != 'object') options = { hash: !!options };
+    else if (Object.isUndefined(options.hash)) options.hash = true;
+    var key, value, submitted = false, submit = options.submit;
+
+    var data = elements.inject({ }, function(result, element) {
       if (!element.disabled && element.name) {
       if (!element.disabled && element.name) {
-        var key = element.name, value = $(element).getValue();
-        if (value != undefined) {
-          if (result[key]) {
-            if (result[key].constructor != Array) result[key] = [result[key]];
+        key = element.name; value = $(element).getValue();
+        if (value != null && element.type != 'file' && (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;
             result[key].push(value);
           }
           else result[key] = value;
@@ -1891,13 +3470,13 @@ var Form = {
       return result;
     });
 
       return result;
     });
 
-    return getHash ? data : Hash.toQueryString(data);
+    return options.hash ? data : Object.toQueryString(data);
   }
 };
 
 Form.Methods = {
   }
 };
 
 Form.Methods = {
-  serialize: function(form, getHash) {
-    return Form.serializeElements(Form.getElements(form), getHash);
+  serialize: function(form, options) {
+    return Form.serializeElements(Form.getElements(form), options);
   },
 
   getElements: function(form) {
   },
 
   getElements: function(form) {
@@ -1928,25 +3507,26 @@ Form.Methods = {
 
   disable: function(form) {
     form = $(form);
 
   disable: function(form) {
     form = $(form);
-    form.getElements().each(function(element) {
-      element.blur();
-      element.disabled = 'true';
-    });
+    Form.getElements(form).invoke('disable');
     return form;
   },
 
   enable: function(form) {
     form = $(form);
     return form;
   },
 
   enable: function(form) {
     form = $(form);
-    form.getElements().each(function(element) {
-      element.disabled = '';
-    });
+    Form.getElements(form).invoke('enable');
     return form;
   },
 
   findFirstElement: function(form) {
     return form;
   },
 
   findFirstElement: function(form) {
-    return $(form).getElements().find(function(element) {
-      return element.type != 'hidden' && !element.disabled &&
-        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    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());
     });
   },
 
     });
   },
 
@@ -1954,10 +3534,26 @@ Form.Methods = {
     form = $(form);
     form.findFirstElement().activate();
     return 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;
 
 
-Object.extend(Form, Form.Methods);
+    return new Ajax.Request(action, options);
+  }
+};
 
 /*--------------------------------------------------------------------------*/
 
 
 /*--------------------------------------------------------------------------*/
 
@@ -1971,7 +3567,7 @@ Form.Element = {
     $(element).select();
     return element;
   }
     $(element).select();
     return element;
   }
-}
+};
 
 Form.Element.Methods = {
   serialize: function(element) {
 
 Form.Element.Methods = {
   serialize: function(element) {
@@ -1979,9 +3575,9 @@ Form.Element.Methods = {
     if (!element.disabled && element.name) {
       var value = element.getValue();
       if (value != undefined) {
     if (!element.disabled && element.name) {
       var value = element.getValue();
       if (value != undefined) {
-        var pair = {};
+        var pair = { };
         pair[element.name] = value;
         pair[element.name] = value;
-        return Hash.toQueryString(pair);
+        return Object.toQueryString(pair);
       }
     }
     return '';
       }
     }
     return '';
@@ -1993,6 +3589,13 @@ Form.Element.Methods = {
     return Form.Element.Serializers[method](element);
   },
 
     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;
   clear: function(element) {
     $(element).value = '';
     return element;
@@ -2004,10 +3607,12 @@ Form.Element.Methods = {
 
   activate: function(element) {
     element = $(element);
 
   activate: function(element) {
     element = $(element);
-    element.focus();
-    if (element.select && ( element.tagName.toLowerCase() != 'input' ||
-      !['button', 'reset', 'submit'].include(element.type) ) )
-      element.select();
+    try {
+      element.focus();
+      if (element.select && (element.tagName.toLowerCase() != 'input' ||
+          !['button', 'reset', 'submit'].include(element.type)))
+        element.select();
+    } catch (e) { }
     return element;
   },
 
     return element;
   },
 
@@ -2019,40 +3624,57 @@ Form.Element.Methods = {
 
   enable: function(element) {
     element = $(element);
 
   enable: function(element) {
     element = $(element);
-    element.blur();
     element.disabled = false;
     return element;
   }
     element.disabled = false;
     return element;
   }
-}
+};
+
+/*--------------------------------------------------------------------------*/
 
 
-Object.extend(Form.Element, Form.Element.Methods);
 var Field = Form.Element;
 var Field = Form.Element;
-var $F = Form.Element.getValue;
+var $F = Form.Element.Methods.getValue;
 
 /*--------------------------------------------------------------------------*/
 
 Form.Element.Serializers = {
 
 /*--------------------------------------------------------------------------*/
 
 Form.Element.Serializers = {
-  input: function(element) {
+  input: function(element, value) {
     switch (element.type.toLowerCase()) {
       case 'checkbox':
       case 'radio':
     switch (element.type.toLowerCase()) {
       case 'checkbox':
       case 'radio':
-        return Form.Element.Serializers.inputSelector(element);
+        return Form.Element.Serializers.inputSelector(element, value);
       default:
       default:
-        return Form.Element.Serializers.textarea(element);
+        return Form.Element.Serializers.textarea(element, value);
     }
   },
 
     }
   },
 
-  inputSelector: function(element) {
-    return element.checked ? element.value : null;
+  inputSelector: function(element, value) {
+    if (Object.isUndefined(value)) return element.checked ? element.value : null;
+    else element.checked = !!value;
   },
 
   },
 
-  textarea: function(element) {
-    return element.value;
+  textarea: function(element, value) {
+    if (Object.isUndefined(value)) return element.value;
+    else element.value = value;
   },
 
   },
 
-  select: function(element) {
-    return this[element.type == 'select-one' ?
-      'selectOne' : 'selectMany'](element);
+  select: function(element, value) {
+    if (Object.isUndefined(value))
+      return this[element.type == 'select-one' ?
+        'selectOne' : 'selectMany'](element);
+    else {
+      var opt, currentValue, single = !Object.isArray(value);
+      for (var i = 0, length = element.length; i < length; i++) {
+        opt = element.options[i];
+        currentValue = this.optionValue(opt);
+        if (single) {
+          if (currentValue == value) {
+            opt.selected = true;
+            return;
+          }
+        }
+        else opt.selected = value.include(currentValue);
+      }
+    }
   },
 
   selectOne: function(element) {
   },
 
   selectOne: function(element) {
@@ -2075,219 +3697,469 @@ Form.Element.Serializers = {
     // extend element because hasAttribute may not be native
     return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
   }
     // extend element because hasAttribute may not be native
     return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
   }
-}
+};
 
 /*--------------------------------------------------------------------------*/
 
 
 /*--------------------------------------------------------------------------*/
 
-Abstract.TimedObserver = function() {}
-Abstract.TimedObserver.prototype = {
-  initialize: function(element, frequency, callback) {
-    this.frequency = frequency;
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+  initialize: function($super, element, frequency, callback) {
+    $super(callback, frequency);
     this.element   = $(element);
     this.element   = $(element);
-    this.callback  = callback;
-
     this.lastValue = this.getValue();
     this.lastValue = this.getValue();
-    this.registerCallback();
   },
 
   },
 
-  registerCallback: function() {
-    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  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);
   },
 
   },
 
-  onTimerEvent: function() {
+  onElementEvent: function() {
     var value = this.getValue();
     var value = this.getValue();
-    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
-      ? this.lastValue != value : String(this.lastValue) != String(value));
-    if (changed) {
+    if (this.lastValue != value) {
       this.callback(this.element, value);
       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) {
+      event = Event.extend(event);
+
+      var node          = event.target,
+          type          = event.type,
+          currentTarget = event.currentTarget;
+
+      if (currentTarget && currentTarget.tagName) {
+        // Firefox screws up the "click" event when moving between radio buttons
+        // via arrow keys. It also screws up the "load" and "error" events on images,
+        // reporting the document as the target instead of the original image.
+        if (type === 'load' || type === 'error' ||
+          (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
+            && currentTarget.type === 'radio'))
+              node = currentTarget;
+      }
+      if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
+      return Element.extend(node);
+    },
+
+    findElement: function(event, expression) {
+      var element = Event.element(event);
+      if (!expression) return element;
+      var elements = [element].concat(element.ancestors());
+      return Selector.findElement(elements, expression, 0);
+    },
+
+    pointer: function(event) {
+      var docElement = document.documentElement,
+      body = document.body || { scrollLeft: 0, scrollTop: 0 };
+      return {
+        x: event.pageX || (event.clientX +
+          (docElement.scrollLeft || body.scrollLeft) -
+          (docElement.clientLeft || 0)),
+        y: event.pageY || (event.clientY +
+          (docElement.scrollTop || body.scrollTop) -
+          (docElement.clientTop || 0))
+      };
+    },
+
+    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._prototypeEventID) return element._prototypeEventID[0];
+    arguments.callee.id = arguments.callee.id || 1;
+    return element._prototypeEventID = [++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;
+  }
+
+
+  // Internet Explorer needs to remove event handlers on page unload
+  // in order to avoid memory leaks.
+  if (window.attachEvent) {
+    window.attachEvent("onunload", destroyCache);
+  }
+
+  // Safari has a dummy event handler on page unload so that it won't
+  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
+  // object when page is returned to via the back button using its bfcache.
+  if (Prototype.Browser.WebKit) {
+    window.addEventListener('unload', Prototype.emptyFunction, false);
+  }
+
+  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);
 
 
-Form.Element.Observer = Class.create();
-Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
-  getValue: function() {
-    return Form.Element.getValue(this.element);
-  }
-});
+      if (!handler && eventName) {
+        getWrappersForEventName(id, eventName).each(function(wrapper) {
+          element.stopObserving(eventName, wrapper.handler);
+        });
+        return element;
 
 
-Form.Observer = Class.create();
-Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
-  getValue: function() {
-    return Form.serialize(this.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;
 
 
-Abstract.EventObserver = function() {}
-Abstract.EventObserver.prototype = {
-  initialize: function(element, callback) {
-    this.element  = $(element);
-    this.callback = callback;
+      if (element.removeEventListener) {
+        element.removeEventListener(name, wrapper, false);
+      } else {
+        element.detachEvent("on" + name, wrapper);
+      }
 
 
-    this.lastValue = this.getValue();
-    if (this.element.tagName.toLowerCase() == 'form')
-      this.registerFormCallbacks();
-    else
-      this.registerCallback(this.element);
-  },
+      destroyWrapper(id, eventName, handler);
 
 
-  onElementEvent: function() {
-    var value = this.getValue();
-    if (this.lastValue != value) {
-      this.callback(this.element, value);
-      this.lastValue = value;
-    }
-  },
+      return element;
+    },
 
 
-  registerFormCallbacks: function() {
-    Form.getElements(this.element).each(this.registerCallback.bind(this));
-  },
+    fire: function(element, eventName, memo) {
+      element = $(element);
+      if (element == document && document.createEvent && !element.dispatchEvent)
+        element = document.documentElement;
 
 
-  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;
+      var event;
+      if (document.createEvent) {
+        event = document.createEvent("HTMLEvents");
+        event.initEvent("dataavailable", true, true);
+      } else {
+        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.extend(event);
     }
     }
-  }
-}
+  };
+})());
 
 
-Form.Element.EventObserver = Class.create();
-Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
-  getValue: function() {
-    return Form.Element.getValue(this.element);
-  }
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+  fire:          Event.fire,
+  observe:       Event.observe,
+  stopObserving: Event.stopObserving
 });
 
 });
 
-Form.EventObserver = Class.create();
-Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
-  getValue: function() {
-    return Form.serialize(this.element);
-  }
+Object.extend(document, {
+  fire:          Element.Methods.fire.methodize(),
+  observe:       Element.Methods.observe.methodize(),
+  stopObserving: Element.Methods.stopObserving.methodize(),
+  loaded:        false
 });
 });
-if (!window.Event) {
-  var Event = new Object();
-}
 
 
-Object.extend(Event, {
-  KEY_BACKSPACE: 8,
-  KEY_TAB:       9,
-  KEY_RETURN:   13,
-  KEY_ESC:      27,
-  KEY_LEFT:     37,
-  KEY_UP:       38,
-  KEY_RIGHT:    39,
-  KEY_DOWN:     40,
-  KEY_DELETE:   46,
-  KEY_HOME:     36,
-  KEY_END:      35,
-  KEY_PAGEUP:   33,
-  KEY_PAGEDOWN: 34,
+(function() {
+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+     Matthias Miller, Dean Edwards and John Resig. */
 
 
-  element: function(event) {
-    return event.target || event.srcElement;
-  },
+  var timer;
 
 
-  isLeftClick: function(event) {
-    return (((event.which) && (event.which == 1)) ||
-            ((event.button) && (event.button == 1)));
-  },
+  function fireContentLoadedEvent() {
+    if (document.loaded) return;
+    if (timer) window.clearInterval(timer);
+    document.fire("dom:loaded");
+    document.loaded = true;
+  }
 
 
-  pointerX: function(event) {
-    return event.pageX || (event.clientX +
-      (document.documentElement.scrollLeft || document.body.scrollLeft));
-  },
+  if (document.addEventListener) {
+    if (Prototype.Browser.WebKit) {
+      timer = window.setInterval(function() {
+        if (/loaded|complete/.test(document.readyState))
+          fireContentLoadedEvent();
+      }, 0);
 
 
-  pointerY: function(event) {
-    return event.pageY || (event.clientY +
-      (document.documentElement.scrollTop || document.body.scrollTop));
-  },
+      Event.observe(window, "load", fireContentLoadedEvent);
 
 
-  stop: function(event) {
-    if (event.preventDefault) {
-      event.preventDefault();
-      event.stopPropagation();
     } else {
     } else {
-      event.returnValue = false;
-      event.cancelBubble = true;
+      document.addEventListener("DOMContentLoaded",
+        fireContentLoadedEvent, false);
     }
     }
-  },
-
-  // find the first node with the given tagName, starting from the
-  // node the event was triggered on; traverses the DOM upwards
-  findElement: function(event, tagName) {
-    var element = Event.element(event);
-    while (element.parentNode && (!element.tagName ||
-        (element.tagName.toUpperCase() != tagName.toUpperCase())))
-      element = element.parentNode;
-    return element;
-  },
-
-  observers: false,
 
 
-  _observeAndCache: function(element, name, observer, useCapture) {
-    if (!this.observers) this.observers = [];
-    if (element.addEventListener) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.addEventListener(name, observer, useCapture);
-    } else if (element.attachEvent) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.attachEvent('on' + name, observer);
-    }
-  },
+  } else {
+    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+    $("__onDOMContentLoaded").onreadystatechange = function() {
+      if (this.readyState == "complete") {
+        this.onreadystatechange = null;
+        fireContentLoadedEvent();
+      }
+    };
+  }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
 
 
-  unloadCache: function() {
-    if (!Event.observers) return;
-    for (var i = 0, length = Event.observers.length; i < length; i++) {
-      Event.stopObserving.apply(this, Event.observers[i]);
-      Event.observers[i][0] = null;
-    }
-    Event.observers = false;
-  },
+Hash.toQueryString = Object.toQueryString;
 
 
-  observe: function(element, name, observer, useCapture) {
-    element = $(element);
-    useCapture = useCapture || false;
+var Toggle = { display: Element.toggle };
 
 
-    if (name == 'keypress' &&
-        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
-        || element.attachEvent))
-      name = 'keydown';
+Element.Methods.childOf = Element.Methods.descendantOf;
 
 
-    Event._observeAndCache(element, name, observer, useCapture);
+var Insertion = {
+  Before: function(element, content) {
+    return Element.insert(element, {before:content});
   },
 
   },
 
-  stopObserving: function(element, name, observer, useCapture) {
-    element = $(element);
-    useCapture = useCapture || false;
+  Top: function(element, content) {
+    return Element.insert(element, {top:content});
+  },
 
 
-    if (name == 'keypress' &&
-        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
-        || element.detachEvent))
-      name = 'keydown';
+  Bottom: function(element, content) {
+    return Element.insert(element, {bottom:content});
+  },
 
 
-    if (element.removeEventListener) {
-      element.removeEventListener(name, observer, useCapture);
-    } else if (element.detachEvent) {
-      try {
-        element.detachEvent('on' + name, observer);
-      } catch (e) {}
-    }
+  After: function(element, content) {
+    return Element.insert(element, {after:content});
   }
   }
-});
+};
 
 
-/* prevent memory leaks in IE */
-if (navigator.appVersion.match(/\bMSIE\b/))
-  Event.observe(window, 'unload', Event.unloadCache, false);
+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
 var Position = {
   // set to true if needed, warning: firefox performance problems
   // NOT neeeded for page scrolling, only if draggable contained in
@@ -2307,59 +4179,13 @@ var Position = {
                 || 0;
   },
 
                 || 0;
   },
 
-  realOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.scrollTop  || 0;
-      valueL += element.scrollLeft || 0;
-      element = element.parentNode;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  cumulativeOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  positionedOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-      if (element) {
-        if(element.tagName=='BODY') break;
-        var p = Element.getStyle(element, 'position');
-        if (p == 'relative' || p == 'absolute') break;
-      }
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  offsetParent: function(element) {
-    if (element.offsetParent) return element.offsetParent;
-    if (element == document.body) return element;
-
-    while ((element = element.parentNode) && element != document.body)
-      if (Element.getStyle(element, 'position') != 'static')
-        return element;
-
-    return document.body;
-  },
-
   // caches x/y coordinate pair to use with overlap
   within: function(element, x, y) {
     if (this.includeScrollOffsets)
       return this.withinIncludingScrolloffsets(element, x, y);
     this.xcomp = x;
     this.ycomp = y;
   // caches x/y coordinate pair to use with overlap
   within: function(element, x, y) {
     if (this.includeScrollOffsets)
       return this.withinIncludingScrolloffsets(element, x, y);
     this.xcomp = x;
     this.ycomp = y;
-    this.offset = this.cumulativeOffset(element);
+    this.offset = Element.cumulativeOffset(element);
 
     return (y >= this.offset[1] &&
             y <  this.offset[1] + element.offsetHeight &&
 
     return (y >= this.offset[1] &&
             y <  this.offset[1] + element.offsetHeight &&
@@ -2368,11 +4194,11 @@ var Position = {
   },
 
   withinIncludingScrolloffsets: function(element, x, y) {
   },
 
   withinIncludingScrolloffsets: function(element, x, y) {
-    var offsetcache = this.realOffset(element);
+    var offsetcache = Element.cumulativeScrollOffset(element);
 
     this.xcomp = x + offsetcache[0] - this.deltaX;
     this.ycomp = y + offsetcache[1] - this.deltaY;
 
     this.xcomp = x + offsetcache[0] - this.deltaX;
     this.ycomp = y + offsetcache[1] - this.deltaY;
-    this.offset = this.cumulativeOffset(element);
+    this.offset = Element.cumulativeOffset(element);
 
     return (this.ycomp >= this.offset[1] &&
             this.ycomp <  this.offset[1] + element.offsetHeight &&
 
     return (this.ycomp >= this.offset[1] &&
             this.ycomp <  this.offset[1] + element.offsetHeight &&
@@ -2391,125 +4217,104 @@ var Position = {
         element.offsetWidth;
   },
 
         element.offsetWidth;
   },
 
-  page: function(forElement) {
-    var valueT = 0, valueL = 0;
-
-    var element = forElement;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
+  // Deprecation layer -- use newer Element methods now (1.5.2).
 
 
-      // Safari fix
-      if (element.offsetParent==document.body)
-        if (Element.getStyle(element,'position')=='absolute') break;
+  cumulativeOffset: Element.Methods.cumulativeOffset,
 
 
-    } while (element = element.offsetParent);
+  positionedOffset: Element.Methods.positionedOffset,
 
 
-    element = forElement;
-    do {
-      if (!window.opera || element.tagName=='BODY') {
-        valueT -= element.scrollTop  || 0;
-        valueL -= element.scrollLeft || 0;
-      }
-    } while (element = element.parentNode);
+  absolutize: function(element) {
+    Position.prepare();
+    return Element.absolutize(element);
+  },
 
 
-    return [valueL, valueT];
+  relativize: function(element) {
+    Position.prepare();
+    return Element.relativize(element);
   },
 
   },
 
-  clone: function(source, target) {
-    var options = Object.extend({
-      setLeft:    true,
-      setTop:     true,
-      setWidth:   true,
-      setHeight:  true,
-      offsetTop:  0,
-      offsetLeft: 0
-    }, arguments[2] || {})
+  realOffset: Element.Methods.cumulativeScrollOffset,
 
 
-    // find page position of source
-    source = $(source);
-    var p = Position.page(source);
+  offsetParent: Element.Methods.getOffsetParent,
 
 
-    // find coordinate system to use
-    target = $(target);
-    var delta = [0, 0];
-    var parent = null;
-    // delta [0,0] will do fine with position: fixed elements,
-    // position:absolute needs offsetParent deltas
-    if (Element.getStyle(target,'position') == 'absolute') {
-      parent = Position.offsetParent(target);
-      delta = Position.page(parent);
-    }
+  page: Element.Methods.viewportOffset,
 
 
-    // correct by body offsets (fixes Safari)
-    if (parent == document.body) {
-      delta[0] -= document.body.offsetLeft;
-      delta[1] -= document.body.offsetTop;
+  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;
+  };
 
 
-    // set position
-    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
-    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
-    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
-    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
-  },
+  return function(className, parentElement) {
+    return $(parentElement || document.body).getElementsByClassName(className);
+  };
+}(Element.Methods);
 
 
-  absolutize: function(element) {
-    element = $(element);
-    if (element.style.position == 'absolute') return;
-    Position.prepare();
+/*--------------------------------------------------------------------------*/
 
 
-    var offsets = Position.positionedOffset(element);
-    var top     = offsets[1];
-    var left    = offsets[0];
-    var width   = element.clientWidth;
-    var height  = element.clientHeight;
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
 
 
-    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;
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
 
 
-    element.style.position = 'absolute';
-    element.style.top    = top + 'px';
-    element.style.left   = left + 'px';
-    element.style.width  = width + 'px';
-    element.style.height = height + 'px';
+  set: function(className) {
+    this.element.className = className;
   },
 
   },
 
-  relativize: function(element) {
-    element = $(element);
-    if (element.style.position == 'relative') return;
-    Position.prepare();
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
 
 
-    element.style.position = 'relative';
-    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
-    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
 
 
-    element.style.top    = top + 'px';
-    element.style.left   = left + 'px';
-    element.style.height = element._originalHeight;
-    element.style.width  = element._originalWidth;
+  toString: function() {
+    return $A(this).join(' ');
   }
   }
-}
-
-// Safari returns margins on body which is incorrect if the child is absolutely
-// positioned.  For performance reasons, redefine Position.cumulativeOffset for
-// KHTML/WebKit only.
-if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
-  Position.cumulativeOffset = function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      if (element.offsetParent == document.body)
-        if (Element.getStyle(element, 'position') == 'absolute') break;
+};
 
 
-      element = element.offsetParent;
-    } while (element);
+Object.extend(Element.ClassNames.prototype, Enumerable);
 
 
-    return [valueL, valueT];
-  }
-}
+/*--------------------------------------------------------------------------*/
 
 Element.addMethods();
\ No newline at end of file
 
 Element.addMethods();
\ No newline at end of file
diff --git a/script/dbconsole b/script/dbconsole
new file mode 100755 (executable)
index 0000000..caa60ce
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/dbconsole'
diff --git a/script/performance/request b/script/performance/request
new file mode 100755 (executable)
index 0000000..ae3f38c
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/performance/request'
diff --git a/test/functional/sparklines_controller_test.rb b/test/functional/sparklines_controller_test.rb
deleted file mode 100644 (file)
index 7d58be4..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'sparklines_controller'
-
-# Re-raise errors caught by the controller.
-class SparklinesController; def rescue_action(e) raise e end; end
-
-class SparklinesControllerTest < Test::Unit::TestCase
-
-  #fixtures :data
-
-  def setup
-    @controller = SparklinesController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
-  def test_index
-    get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black'
-    assert_response :success
-    assert_equal 'image/png', @response.headers['Content-Type']
-  end
-
-  # TODO Replace this with your actual tests
-  def test_show
-    get :show
-    assert_response :success
-    assert_equal 'image/png', @response.headers['Content-Type']
-  end
-  
-end
diff --git a/vendor/plugins/acts_as_list/README b/vendor/plugins/acts_as_list/README
new file mode 100644 (file)
index 0000000..36ae318
--- /dev/null
@@ -0,0 +1,23 @@
+ActsAsList
+==========
+
+This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table.
+
+
+Example
+=======
+
+  class TodoList < ActiveRecord::Base
+    has_many :todo_items, :order => "position"
+  end
+
+  class TodoItem < ActiveRecord::Base
+    belongs_to :todo_list
+    acts_as_list :scope => :todo_list
+  end
+
+  todo_list.first.move_to_bottom
+  todo_list.last.move_higher
+
+
+Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_list/init.rb b/vendor/plugins/acts_as_list/init.rb
new file mode 100644 (file)
index 0000000..eb87e87
--- /dev/null
@@ -0,0 +1,3 @@
+$:.unshift "#{File.dirname(__FILE__)}/lib"
+require 'active_record/acts/list'
+ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
diff --git a/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb b/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb
new file mode 100644 (file)
index 0000000..00d8692
--- /dev/null
@@ -0,0 +1,256 @@
+module ActiveRecord
+  module Acts #:nodoc:
+    module List #:nodoc:
+      def self.included(base)
+        base.extend(ClassMethods)
+      end
+
+      # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
+      # The class that has this specified needs to have a +position+ column defined as an integer on
+      # the mapped database table.
+      #
+      # Todo list example:
+      #
+      #   class TodoList < ActiveRecord::Base
+      #     has_many :todo_items, :order => "position"
+      #   end
+      #
+      #   class TodoItem < ActiveRecord::Base
+      #     belongs_to :todo_list
+      #     acts_as_list :scope => :todo_list
+      #   end
+      #
+      #   todo_list.first.move_to_bottom
+      #   todo_list.last.move_higher
+      module ClassMethods
+        # Configuration options are:
+        #
+        # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
+        # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt> 
+        #   (if it hasn't already been added) and use that as the foreign key restriction. It's also possible 
+        #   to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
+        #   Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
+        def acts_as_list(options = {})
+          configuration = { :column => "position", :scope => "1 = 1" }
+          configuration.update(options) if options.is_a?(Hash)
+
+          configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
+
+          if configuration[:scope].is_a?(Symbol)
+            scope_condition_method = %(
+              def scope_condition
+                if #{configuration[:scope].to_s}.nil?
+                  "#{configuration[:scope].to_s} IS NULL"
+                else
+                  "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
+                end
+              end
+            )
+          else
+            scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
+          end
+
+          class_eval <<-EOV
+            include ActiveRecord::Acts::List::InstanceMethods
+
+            def acts_as_list_class
+              ::#{self.name}
+            end
+
+            def position_column
+              '#{configuration[:column]}'
+            end
+
+            #{scope_condition_method}
+
+            before_destroy :remove_from_list
+            before_create  :add_to_list_bottom
+          EOV
+        end
+      end
+
+      # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
+      # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
+      # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
+      # the first in the list of all chapters.
+      module InstanceMethods
+        # Insert the item at the given position (defaults to the top position of 1).
+        def insert_at(position = 1)
+          insert_at_position(position)
+        end
+
+        # Swap positions with the next lower item, if one exists.
+        def move_lower
+          return unless lower_item
+
+          acts_as_list_class.transaction do
+            lower_item.decrement_position
+            increment_position
+          end
+        end
+
+        # Swap positions with the next higher item, if one exists.
+        def move_higher
+          return unless higher_item
+
+          acts_as_list_class.transaction do
+            higher_item.increment_position
+            decrement_position
+          end
+        end
+
+        # Move to the bottom of the list. If the item is already in the list, the items below it have their
+        # position adjusted accordingly.
+        def move_to_bottom
+          return unless in_list?
+          acts_as_list_class.transaction do
+            decrement_positions_on_lower_items
+            assume_bottom_position
+          end
+        end
+
+        # Move to the top of the list. If the item is already in the list, the items above it have their
+        # position adjusted accordingly.
+        def move_to_top
+          return unless in_list?
+          acts_as_list_class.transaction do
+            increment_positions_on_higher_items
+            assume_top_position
+          end
+        end
+
+        # Removes the item from the list.
+        def remove_from_list
+          if in_list?
+            decrement_positions_on_lower_items
+            update_attribute position_column, nil
+          end
+        end
+
+        # Increase the position of this item without adjusting the rest of the list.
+        def increment_position
+          return unless in_list?
+          update_attribute position_column, self.send(position_column).to_i + 1
+        end
+
+        # Decrease the position of this item without adjusting the rest of the list.
+        def decrement_position
+          return unless in_list?
+          update_attribute position_column, self.send(position_column).to_i - 1
+        end
+
+        # Return +true+ if this object is the first in the list.
+        def first?
+          return false unless in_list?
+          self.send(position_column) == 1
+        end
+
+        # Return +true+ if this object is the last in the list.
+        def last?
+          return false unless in_list?
+          self.send(position_column) == bottom_position_in_list
+        end
+
+        # Return the next higher item in the list.
+        def higher_item
+          return nil unless in_list?
+          acts_as_list_class.find(:first, :conditions =>
+            "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
+          )
+        end
+
+        # Return the next lower item in the list.
+        def lower_item
+          return nil unless in_list?
+          acts_as_list_class.find(:first, :conditions =>
+            "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
+          )
+        end
+
+        # Test if this record is in a list
+        def in_list?
+          !send(position_column).nil?
+        end
+
+        private
+          def add_to_list_top
+            increment_positions_on_all_items
+          end
+
+          def add_to_list_bottom
+            self[position_column] = bottom_position_in_list.to_i + 1
+          end
+
+          # Overwrite this method to define the scope of the list changes
+          def scope_condition() "1" end
+
+          # Returns the bottom position number in the list.
+          #   bottom_position_in_list    # => 2
+          def bottom_position_in_list(except = nil)
+            item = bottom_item(except)
+            item ? item.send(position_column) : 0
+          end
+
+          # Returns the bottom item
+          def bottom_item(except = nil)
+            conditions = scope_condition
+            conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
+            acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
+          end
+
+          # Forces item to assume the bottom position in the list.
+          def assume_bottom_position
+            update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
+          end
+
+          # Forces item to assume the top position in the list.
+          def assume_top_position
+            update_attribute(position_column, 1)
+          end
+
+          # This has the effect of moving all the higher items up one.
+          def decrement_positions_on_higher_items(position)
+            acts_as_list_class.update_all(
+              "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
+            )
+          end
+
+          # This has the effect of moving all the lower items up one.
+          def decrement_positions_on_lower_items
+            return unless in_list?
+            acts_as_list_class.update_all(
+              "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
+            )
+          end
+
+          # This has the effect of moving all the higher items down one.
+          def increment_positions_on_higher_items
+            return unless in_list?
+            acts_as_list_class.update_all(
+              "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
+            )
+          end
+
+          # This has the effect of moving all the lower items down one.
+          def increment_positions_on_lower_items(position)
+            acts_as_list_class.update_all(
+              "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
+           )
+          end
+
+          # Increments position (<tt>position_column</tt>) of all items in the list.
+          def increment_positions_on_all_items
+            acts_as_list_class.update_all(
+              "#{position_column} = (#{position_column} + 1)",  "#{scope_condition}"
+            )
+          end
+
+          def insert_at_position(position)
+            remove_from_list
+            increment_positions_on_lower_items(position)
+            self.update_attribute(position_column, position)
+          end
+      end 
+    end
+  end
+end
diff --git a/vendor/plugins/acts_as_list/test/list_test.rb b/vendor/plugins/acts_as_list/test/list_test.rb
new file mode 100644 (file)
index 0000000..e89cb8e
--- /dev/null
@@ -0,0 +1,332 @@
+require 'test/unit'
+
+require 'rubygems'
+gem 'activerecord', '>= 1.15.4.7794'
+require 'active_record'
+
+require "#{File.dirname(__FILE__)}/../init"
+
+ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
+
+def setup_db
+  ActiveRecord::Schema.define(:version => 1) do
+    create_table :mixins do |t|
+      t.column :pos, :integer
+      t.column :parent_id, :integer
+      t.column :created_at, :datetime      
+      t.column :updated_at, :datetime
+    end
+  end
+end
+
+def teardown_db
+  ActiveRecord::Base.connection.tables.each do |table|
+    ActiveRecord::Base.connection.drop_table(table)
+  end
+end
+
+class Mixin < ActiveRecord::Base
+end
+
+class ListMixin < Mixin
+  acts_as_list :column => "pos", :scope => :parent
+
+  def self.table_name() "mixins" end
+end
+
+class ListMixinSub1 < ListMixin
+end
+
+class ListMixinSub2 < ListMixin
+end
+
+class ListWithStringScopeMixin < ActiveRecord::Base
+  acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
+
+  def self.table_name() "mixins" end
+end
+
+
+class ListTest < Test::Unit::TestCase
+
+  def setup
+    setup_db
+    (1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
+  end
+
+  def teardown
+    teardown_db
+  end
+
+  def test_reordering
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).move_lower
+    assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).move_higher
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    ListMixin.find(1).move_to_bottom
+    assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    ListMixin.find(1).move_to_top
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).move_to_bottom
+    assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    ListMixin.find(4).move_to_top
+    assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+  end
+
+  def test_move_to_bottom_with_next_to_last_item
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+    ListMixin.find(3).move_to_bottom
+    assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+  end
+
+  def test_next_prev
+    assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
+    assert_nil ListMixin.find(1).higher_item
+    assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
+    assert_nil ListMixin.find(4).lower_item
+  end
+
+  def test_injection
+    item = ListMixin.new(:parent_id => 1)
+    assert_equal "parent_id = 1", item.scope_condition
+    assert_equal "pos", item.position_column
+  end
+
+  def test_insert
+    new = ListMixin.create(:parent_id => 20)
+    assert_equal 1, new.pos
+    assert new.first?
+    assert new.last?
+
+    new = ListMixin.create(:parent_id => 20)
+    assert_equal 2, new.pos
+    assert !new.first?
+    assert new.last?
+
+    new = ListMixin.create(:parent_id => 20)
+    assert_equal 3, new.pos
+    assert !new.first?
+    assert new.last?
+
+    new = ListMixin.create(:parent_id => 0)
+    assert_equal 1, new.pos
+    assert new.first?
+    assert new.last?
+  end
+
+  def test_insert_at
+    new = ListMixin.create(:parent_id => 20)
+    assert_equal 1, new.pos
+
+    new = ListMixin.create(:parent_id => 20)
+    assert_equal 2, new.pos
+
+    new = ListMixin.create(:parent_id => 20)
+    assert_equal 3, new.pos
+
+    new4 = ListMixin.create(:parent_id => 20)
+    assert_equal 4, new4.pos
+
+    new4.insert_at(3)
+    assert_equal 3, new4.pos
+
+    new.reload
+    assert_equal 4, new.pos
+
+    new.insert_at(2)
+    assert_equal 2, new.pos
+
+    new4.reload
+    assert_equal 4, new4.pos
+
+    new5 = ListMixin.create(:parent_id => 20)
+    assert_equal 5, new5.pos
+
+    new5.insert_at(1)
+    assert_equal 1, new5.pos
+
+    new4.reload
+    assert_equal 5, new4.pos
+  end
+
+  def test_delete_middle
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).destroy
+
+    assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    assert_equal 1, ListMixin.find(1).pos
+    assert_equal 2, ListMixin.find(3).pos
+    assert_equal 3, ListMixin.find(4).pos
+
+    ListMixin.find(1).destroy
+
+    assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+    assert_equal 1, ListMixin.find(3).pos
+    assert_equal 2, ListMixin.find(4).pos
+  end
+
+  def test_with_string_based_scope
+    new = ListWithStringScopeMixin.create(:parent_id => 500)
+    assert_equal 1, new.pos
+    assert new.first?
+    assert new.last?
+  end
+
+  def test_nil_scope
+    new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
+    new2.move_higher
+    assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
+  end
+  
+  
+  def test_remove_from_list_should_then_fail_in_list? 
+    assert_equal true, ListMixin.find(1).in_list?
+    ListMixin.find(1).remove_from_list
+    assert_equal false, ListMixin.find(1).in_list?
+  end 
+  
+  def test_remove_from_list_should_set_position_to_nil 
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+  
+    ListMixin.find(2).remove_from_list 
+  
+    assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+  
+    assert_equal 1,   ListMixin.find(1).pos
+    assert_equal nil, ListMixin.find(2).pos
+    assert_equal 2,   ListMixin.find(3).pos
+    assert_equal 3,   ListMixin.find(4).pos
+  end 
+  
+  def test_remove_before_destroy_does_not_shift_lower_items_twice 
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+  
+    ListMixin.find(2).remove_from_list 
+    ListMixin.find(2).destroy 
+  
+    assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+  
+    assert_equal 1, ListMixin.find(1).pos
+    assert_equal 2, ListMixin.find(3).pos
+    assert_equal 3, ListMixin.find(4).pos
+  end 
+  
+end
+
+class ListSubTest < Test::Unit::TestCase
+
+  def setup
+    setup_db
+    (1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
+  end
+
+  def teardown
+    teardown_db
+  end
+
+  def test_reordering
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).move_lower
+    assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).move_higher
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    ListMixin.find(1).move_to_bottom
+    assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    ListMixin.find(1).move_to_top
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).move_to_bottom
+    assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    ListMixin.find(4).move_to_top
+    assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+  end
+
+  def test_move_to_bottom_with_next_to_last_item
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+    ListMixin.find(3).move_to_bottom
+    assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+  end
+
+  def test_next_prev
+    assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
+    assert_nil ListMixin.find(1).higher_item
+    assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
+    assert_nil ListMixin.find(4).lower_item
+  end
+
+  def test_injection
+    item = ListMixin.new("parent_id"=>1)
+    assert_equal "parent_id = 1", item.scope_condition
+    assert_equal "pos", item.position_column
+  end
+
+  def test_insert_at
+    new = ListMixin.create("parent_id" => 20)
+    assert_equal 1, new.pos
+
+    new = ListMixinSub1.create("parent_id" => 20)
+    assert_equal 2, new.pos
+
+    new = ListMixinSub2.create("parent_id" => 20)
+    assert_equal 3, new.pos
+
+    new4 = ListMixin.create("parent_id" => 20)
+    assert_equal 4, new4.pos
+
+    new4.insert_at(3)
+    assert_equal 3, new4.pos
+
+    new.reload
+    assert_equal 4, new.pos
+
+    new.insert_at(2)
+    assert_equal 2, new.pos
+
+    new4.reload
+    assert_equal 4, new4.pos
+
+    new5 = ListMixinSub1.create("parent_id" => 20)
+    assert_equal 5, new5.pos
+
+    new5.insert_at(1)
+    assert_equal 1, new5.pos
+
+    new4.reload
+    assert_equal 5, new4.pos
+  end
+
+  def test_delete_middle
+    assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    ListMixin.find(2).destroy
+
+    assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    assert_equal 1, ListMixin.find(1).pos
+    assert_equal 2, ListMixin.find(3).pos
+    assert_equal 3, ListMixin.find(4).pos
+
+    ListMixin.find(1).destroy
+
+    assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+    assert_equal 1, ListMixin.find(3).pos
+    assert_equal 2, ListMixin.find(4).pos
+  end
+
+end
diff --git a/vendor/plugins/sparklines/MIT-LICENSE b/vendor/plugins/sparklines/MIT-LICENSE
deleted file mode 100644 (file)
index 8d1b480..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2006 Geoffrey Grosenbach
-
-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.
diff --git a/vendor/plugins/sparklines/README b/vendor/plugins/sparklines/README
deleted file mode 100644 (file)
index 800b508..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-== Sparklines Plugin
-
-Make tiny graphs.
-
-Also has a "sparklines" generator for copying a controller and functional test to your app.
-
-  ./script/generate sparklines
-
-See examples at http://nubyonrails.com/pages/sparklines
-
-== Author
-
-Geoffrey Grosenbach boss@topfunky.com http://nubyonrails.com
-
-
diff --git a/vendor/plugins/sparklines/Rakefile b/vendor/plugins/sparklines/Rakefile
deleted file mode 100644 (file)
index d3456e4..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-require 'rake'
-require 'rake/testtask'
-require 'rake/rdoctask'
-
-desc 'Default: run unit tests.'
-task :default => :test
-
-desc 'Test the plugin.'
-Rake::TestTask.new(:test) do |t|
-  t.libs << 'lib'
-  t.pattern = 'test/**/test_*.rb'
-  t.verbose = true
-end
-
-desc 'Generate documentation for the plugin.'
-Rake::RDocTask.new(:rdoc) do |rdoc|
-  rdoc.rdoc_dir = 'rdoc'
-  rdoc.title    = 'CalendarHelper'
-  rdoc.options << '--line-numbers' << '--inline-source'
-  rdoc.rdoc_files.include('README')
-  rdoc.rdoc_files.include('lib/**/*.rb')
-end
-
diff --git a/vendor/plugins/sparklines/about.yml b/vendor/plugins/sparklines/about.yml
deleted file mode 100644 (file)
index f018110..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-author: topfunky
-summary: A library for making small graphs, a la Edward Tufte. A generator and helper are also included.
-homepage: http://nubyonrails.com/pages/sparklines
-plugin: http://topfunky.net/svn/plugins/sparklines
-license: MIT
-version: 0.4.0
-rails_version: 1.1+
diff --git a/vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb b/vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb
deleted file mode 100644 (file)
index 40e8dae..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-class SparklinesGenerator < Rails::Generator::Base
-
-  def manifest
-    record do |m|
-      m.directory File.join("app/controllers")
-      m.directory File.join("test/functional")
-
-      m.file "controller.rb", File.join("app/controllers/sparklines_controller.rb")
-      m.file "functional_test.rb", File.join("test/functional/sparklines_controller_test.rb")
-    end
-  end
-
-end
diff --git a/vendor/plugins/sparklines/generators/sparklines/templates/controller.rb b/vendor/plugins/sparklines/generators/sparklines/templates/controller.rb
deleted file mode 100644 (file)
index 1c69f29..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-class SparklinesController < ApplicationController
-
-  # Handles requests for sparkline graphs from views.
-  #
-  # Params are generated by the sparkline_tag helper method.
-  #
-       def index
-               # Make array from comma-delimited list of data values
-               ary = []
-               if params.has_key?('results') && !params['results'].nil?
-                 params['results'].split(',').each do |s|
-                         ary << s.to_i
-                 end
-               end
-               
-               send_data( Sparklines.plot( ary, params ), 
-                                       :disposition => 'inline',
-                                       :type => 'image/png',
-                                       :filename => "spark_#{params[:type]}.png" )
-       end
-
-
-  # Use this type of method for sparklines that can be cached. (Doesn't work with the helper.)
-  #
-  # To make caching easier, add a line like this to config/routes.rb:
-  # map.sparklines "sparklines/:action/:id/image.png", :controller => "sparklines"
-  #
-  # Then reference it with the named route:
-  #   image_tag sparklines_url(:action => 'show', :id => 42)
-  def show
-    send_data(Sparklines.plot(
-                [42, 37, 89, 74, 70, 50, 40, 30, 40, 50],
-                :type => 'bar', :above_color => 'orange'
-              ), 
-              :disposition => 'inline', 
-              :type => 'image/png', 
-              :filename => "sparkline.png")
-  end
-
-end
diff --git a/vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb b/vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb
deleted file mode 100644 (file)
index 7d58be4..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'sparklines_controller'
-
-# Re-raise errors caught by the controller.
-class SparklinesController; def rescue_action(e) raise e end; end
-
-class SparklinesControllerTest < Test::Unit::TestCase
-
-  #fixtures :data
-
-  def setup
-    @controller = SparklinesController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
-  def test_index
-    get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black'
-    assert_response :success
-    assert_equal 'image/png', @response.headers['Content-Type']
-  end
-
-  # TODO Replace this with your actual tests
-  def test_show
-    get :show
-    assert_response :success
-    assert_equal 'image/png', @response.headers['Content-Type']
-  end
-  
-end
diff --git a/vendor/plugins/sparklines/init.rb b/vendor/plugins/sparklines/init.rb
deleted file mode 100644 (file)
index c9c2349..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ActionView::Base.send :include, SparklinesHelper
diff --git a/vendor/plugins/sparklines/lib/sparklines.rb b/vendor/plugins/sparklines/lib/sparklines.rb
deleted file mode 100644 (file)
index 4c4e2de..0000000
+++ /dev/null
@@ -1,582 +0,0 @@
-
-require 'rubygems'
-require 'RMagick'
-
-=begin rdoc
-
-A library for generating small unmarked graphs (sparklines).
-
-Can be used to write an image to a file or make a web service with Rails or other Ruby CGI apps.
-
-Idea and much of the outline for the source lifted directly from {Joe Gregorio's Python Sparklines web service script}[http://bitworking.org/projects/sparklines].
-
-Requires the RMagick image library.
-
-==Authors
-
-{Dan Nugent}[mailto:nugend@gmail.com] Original port from Python Sparklines library.
-
-{Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com 
--- Conversion to module and further maintenance.
-
-==General Usage and Defaults
-
-To use in a script:
-
-       require 'rubygems'
-       require 'sparklines'
-       Sparklines.plot([1,25,33,46,89,90,85,77,42], 
-                       :type => 'discrete', 
-                       :height => 20)
-
-An image blob will be returned which you can print, write to STDOUT, etc.
-
-For use with Ruby on Rails, see the sparklines plugin:
-
-  http://nubyonrails.com/pages/sparklines
-
-In your view, call it like this:
-
-  <%= sparkline_tag [1,2,3,4,5,6] %>
-
-Or specify details:
-
-  <%= sparkline_tag [1,2,3,4,5,6], 
-                    :type => 'discrete', 
-                    :height => 10, 
-                    :upper => 80, 
-                    :above_color => 'green', 
-                    :below_color => 'blue' %>
-
-Graph types:
-
- area
- discrete
- pie
- smooth
- bar
- whisker
-
-General Defaults:
-
- :type              => 'smooth'
- :height            => 14px
- :upper             => 50
- :above_color       => 'red'
- :below_color       => 'grey'
- :background_color  => 'white'
- :line_color        => 'lightgrey'
-
-==License
-
-Licensed under the MIT license.
-
-=end
-class Sparklines
-
-  VERSION = '0.4.1'
-
-  @@label_margin = 5.0
-  @@pointsize = 10.0
-
-  class << self
-
-    # Does the actual plotting of the graph. 
-    # Calls the appropriate subclass based on the :type argument. 
-    # Defaults to 'smooth'
-    def plot(data=[], options={})
-      defaults = {
-        :type => 'smooth',
-        :height => 14,
-        :upper => 50,
-        :diameter => 20,
-        :step => 2,
-        :line_color => 'lightgrey',
-
-        :above_color => 'red',
-        :below_color => 'grey',
-        :background_color => 'white',
-        :share_color => 'red',
-        :remain_color => 'lightgrey',
-        :min_color => 'blue',
-        :max_color => 'green', 
-        :last_color => 'red',                
-
-        :has_min => false,
-        :has_max => false,
-        :has_last => false,
-
-        :label => nil
-      }
-
-      # HACK for HashWithIndifferentAccess
-      options_sym = Hash.new
-      options.keys.each do |key|
-        options_sym[key.to_sym] = options[key]
-      end
-
-      options_sym  = defaults.merge(options_sym)
-       
-      # Call the appropriate method for actual plotting.
-      sparkline = self.new(data, options_sym)
-      if %w(area bar pie smooth discrete whisker).include? options_sym[:type]
-        sparkline.send options_sym[:type]
-      else
-        sparkline.plot_error options_sym
-      end
-    end
-
-    # Writes a graph to disk with the specified filename, or "sparklines.png"
-    def plot_to_file(filename="sparklines.png", data=[], options={})
-      File.open( filename, 'wb' ) do |png|
-        png << self.plot( data, options)
-      end
-    end
-
-  end # class methods
-
-  def initialize(data=[], options={})
-    @data = Array(data)
-    @options = options
-    normalize_data
-  end
-
-  ##
-  # Creates a continuous area sparkline. Relevant options.
-  #
-  # :step - An integer that determines the distance between each point on the sparkline.  Defaults to 2.
-  #
-  # :height - An integer that determines what the height of the sparkline will be.  Defaults to 14
-  #
-  # :upper - An integer that determines the threshold for colorization purposes.  Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color.  Defaults to 50.
-  #
-  # :has_min - Determines whether a dot will be drawn at the lowest value or not.  Defaults to false.
-  #
-  # :has_max - Determines whether a dot will be drawn at the highest value or not.  Defaults to false.
-  #
-  # :has_last - Determines whether a dot will be drawn at the last value or not.  Defaults to false.
-  #
-  # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as.  Defaults to blue.
-  #
-  # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as.  Defaults to green.
-  #
-  # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as.  Defaults to red.
-  #
-  # :above_color - A string or color code representing the color to draw values above or equal the upper value.  Defaults to red.
-  #
-  # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
-
-  def area
-
-    step = @options[:step].to_i
-    height = @options[:height].to_i
-    background_color = @options[:background_color]
-    
-    create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
-
-    upper = @options[:upper].to_i
-
-    has_min = @options[:has_min]
-    has_max = @options[:has_max]
-    has_last = @options[:has_last]
-
-    min_color = @options[:min_color]
-    max_color = @options[:max_color]
-    last_color = @options[:last_color]
-    below_color = @options[:below_color]
-    above_color = @options[:above_color]
-
-
-    coords = [[0,(height - 3 - upper/(101.0/(height-4)))]]
-    i=0
-    @norm_data.each do |r|
-        coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))]
-        i += step
-    end
-    coords.push [(@norm_data.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))]
-
-    # TODO Refactor! Should take a block and do both.
-    #
-    # Block off the bottom half of the image and draw the sparkline
-    @draw.fill(above_color)
-    @draw.define_clip_path('top') do
-      @draw.rectangle(0,0,(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
-    end
-    @draw.clip_path('top')
-    @draw.polygon *coords.flatten
-
-    # Block off the top half of the image and draw the sparkline
-    @draw.fill(below_color)
-    @draw.define_clip_path('bottom') do
-      @draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,height)
-    end
-    @draw.clip_path('bottom')
-    @draw.polygon *coords.flatten
-
-    # The sparkline looks kinda nasty if either the above_color or below_color gets the center line
-    @draw.fill('black')
-    @draw.line(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
-
-    # After the parts have been masked, we need to let the whole canvas be drawable again
-    # so a max dot can be displayed
-    @draw.define_clip_path('all') do
-      @draw.rectangle(0,0,@canvas.columns,@canvas.rows)
-    end
-    @draw.clip_path('all')
-        
-    drawbox(coords[@norm_data.index(@norm_data.min)+1], 1, min_color) if has_min == true
-    drawbox(coords[@norm_data.index(@norm_data.max)+1], 1, max_color) if has_max == true
-    
-    drawbox(coords[-2], 1, last_color) if has_last == true
-
-    @draw.draw(@canvas)
-    @canvas.to_blob
-  end
-
-  ##
-  # A bar graph.
-
-  def bar
-    step = @options[:step].to_i
-    height = @options[:height].to_f
-    background_color = @options[:background_color]
-
-    create_canvas(@norm_data.length * step + 2, height, background_color)
-    
-    upper = @options[:upper].to_i
-    below_color = @options[:below_color]
-    above_color = @options[:above_color]
-
-    i = 1
-    @norm_data.each_with_index do |r, index|
-      color = (r >= upper) ? above_color : below_color
-      @draw.stroke('transparent')
-      @draw.fill(color)
-      @draw.rectangle( i, @canvas.rows, 
-           i + step - 2, @canvas.rows - ( (r / @maximum_value) * @canvas.rows) )
-      i += step
-    end
-
-    @draw.draw(@canvas)
-    @canvas.to_blob
-  end
-
-
-  ##
-  # Creates a discretized sparkline
-  #
-  # :height - An integer that determines what the height of the sparkline will be.  Defaults to 14
-  #
-  # :upper - An integer that determines the threshold for colorization purposes.  Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color.  Defaults to 50.
-  #
-  # :above_color - A string or color code representing the color to draw values above or equal the upper value.  Defaults to red.
-  #
-  # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
-
-  def discrete
-
-    height = @options[:height].to_i
-    upper = @options[:upper].to_i
-    background_color = @options[:background_color]
-    step = @options[:step].to_i
-        
-    create_canvas(@norm_data.size * step - 1, height, background_color)
-    
-    below_color = @options[:below_color]
-    above_color = @options[:above_color]
-
-    i = 0
-    @norm_data.each do |r|
-       color = (r >= upper) ? above_color : below_color
-       @draw.stroke(color)
-       @draw.line(i, (@canvas.rows - r/(101.0/(height-4))-4).to_i,
-                 i, (@canvas.rows - r/(101.0/(height-4))).to_i)
-       i += step
-    end
-
-    @draw.draw(@canvas)
-    @canvas.to_blob
-  end
-
-
-  ##
-  # Creates a pie-chart sparkline
-  #
-  # :diameter - An integer that determines what the size of the sparkline will be.  Defaults to 20
-  #
-  # :share_color - A string or color code representing the color to draw the share of the pie represented by percent.  Defaults to red.
-  #
-  # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey.
-
-  def pie
-    diameter = @options[:diameter].to_i
-    background_color = @options[:background_color]
-
-    create_canvas(diameter, diameter, background_color)
-    
-    share_color = @options[:share_color]
-    remain_color = @options[:remain_color]
-    percent = @norm_data[0]
-    
-    # Adjust the radius so there's some edge left in the pie
-    r = diameter/2.0 - 2
-    @draw.fill(remain_color)
-    @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
-    @draw.fill(share_color)
-
-    # Special exceptions
-    if percent == 0
-       # For 0% return blank
-       @draw.draw(@canvas)
-       return @canvas.to_blob
-    elsif percent == 100
-       # For 100% just draw a full circle
-       @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
-       @draw.draw(@canvas)
-       return @canvas.to_blob
-    end
-
-    # Okay, this part is as confusing as hell, so pay attention:
-    # This line determines the horizontal portion of the point on the circle where the X-Axis
-    # should end.  It's caculated by taking the center of the on-image circle and adding that
-    # to the radius multiplied by the formula for determinig the point on a unit circle that a
-    # angle corresponds to.  3.6 * percent gives us that angle, but it's in degrees, so we need to
-    # convert, hence the muliplication by Pi over 180
-    arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180)))
-
-    # The same goes for here, except it's the vertical point instead of the horizontal one
-    arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180)))
-
-    # Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
-    # if the angle of an arc is greater than 180 degrees.  I have no idea why this is, but it is.
-    percent > 50? large_arc_flag = 1: large_arc_flag = 0
-
-    # This is also confusing
-    # M tells us to move to an absolute point on the image.  We're moving to the center of the pie
-    # h tells us to move to a relative point.  We're moving to the right edge of the circle.
-    # A tells us to start an absolute elliptical arc.  The first two values are the radii of the ellipse
-    # the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun
-    # with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag,
-    # (again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously
-    # More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
-    path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
-    @draw.path(path)
-
-    @draw.draw(@canvas)
-    @canvas.to_blob
-  end
-
-
-  ##
-  # Creates a smooth sparkline.
-  #
-  # :step - An integer that determines the distance between each point on the sparkline.  Defaults to 2.
-  #
-  # :height - An integer that determines what the height of the sparkline will be.  Defaults to 14
-  #
-  # :has_min - Determines whether a dot will be drawn at the lowest value or not.  Defaults to false.
-  #
-  # :has_max - Determines whether a dot will be drawn at the highest value or not.  Defaults to false.
-  #
-  # :has_last - Determines whether a dot will be drawn at the last value or not.  Defaults to false.
-  #
-  # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as.  Defaults to blue.
-  #
-  # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as.  Defaults to green.
-  #
-  # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as.  Defaults to red.
-
-  def smooth
-
-    step = @options[:step].to_i
-    height = @options[:height].to_i
-    background_color = @options[:background_color]
-
-    create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
-    
-    min_color = @options[:min_color]
-    max_color = @options[:max_color]
-    last_color = @options[:last_color]
-    has_min = @options[:has_min]
-    has_max = @options[:has_max]
-    has_last = @options[:has_last]
-    line_color = @options[:line_color]
-
-    @draw.stroke(line_color)
-    coords = []
-    i=0
-    @norm_data.each do |r|
-      coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ]
-      i += step
-    end
-    
-    open_ended_polyline(coords)
-
-    drawbox(coords[@norm_data.index(@norm_data.min)], 2, min_color) if has_min == true
-
-    drawbox(coords[@norm_data.index(@norm_data.max)], 2, max_color) if has_max == true
-
-    drawbox(coords[-1], 2, last_color) if has_last == true
-
-    @draw.draw(@canvas)
-    @canvas.to_blob
-  end
-
-
-  ##
-  # Creates a whisker sparkline to track on/off type data. There are five states: 
-  # on, off, no value, exceptional on, exceptional off. On values create an up 
-  # whisker and off values create a down whisker. Exceptional values may be 
-  # colored differently than regular values to indicate, for example, a shut out.
-  # No value produces an empty row to indicate a tie.
-  # 
-  # * results - an array of integer values between -2 and 2. -2 is exceptional 
-  #   down, 1 is regular down, 0 is no value, 1 is up, and 2 is exceptional up.
-  # * options - a hash that takes parameters
-  #
-  # :height - height of the sparkline
-  #
-  # :whisker_color - the color of regular whiskers; defaults to black
-  #
-  # :exception_color - the color of exceptional whiskers; defaults to red
-  
-  def whisker
-
-    # step = @options[:step].to_i
-    height = @options[:height].to_i
-    background_color = @options[:background_color]
-
-    create_canvas((@data.size - 1) * 2, height, background_color)
-    
-    whisker_color = @options[:whisker_color] || 'black'
-    exception_color = @options[:exception_color] || 'red'
-
-    i = 0
-    @data.each do |r|
-      color = whisker_color
-
-      if ( (r == 2 || r == -2) && exception_color )
-        color = exception_color
-      end
-
-      y_mid_point = (r >= 1) ? (@canvas.rows/2.0 - 1).ceil : (@canvas.rows/2.0).floor
-
-      y_end_point = y_mid_point
-      if ( r > 0) 
-        y_end_point = 0
-      end
-
-      if ( r < 0 )
-        y_end_point = @canvas.rows
-      end
-
-      @draw.stroke( color )
-      @draw.line( i, y_mid_point, i, y_end_point )
-      i += 2
-    end
-
-    @draw.draw(@canvas)
-    @canvas.to_blob 
-  end
-
-  ##
-  # Draw the error Sparkline.
-
-  def plot_error(options={})
-    create_canvas(40, 15, 'white')
-
-    @draw.fill('red')
-    @draw.line(0,0,40,15)
-    @draw.line(0,15,40,0)
-
-    @draw.draw(@canvas)
-    @canvas.to_blob
-  end
-
-private
-
-  def normalize_data
-    @maximum_value = @data.max
-    if @options[:type].to_s == 'pie'
-      @norm_data = @data
-    else
-      @norm_data = @data.map { |value| value = (value.to_f / @maximum_value) * 100.0 }
-    end
-  end
-
-  ##
-  # * :arr - an array of points (represented as two element arrays)
-  
-  def open_ended_polyline(arr)
-    0.upto(arr.length - 2) { |i|
-      @draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1])
-    }
-  end
-
-  ##
-  # Create an image to draw on and a drawable to do the drawing with.
-  #
-  # TODO Refactor into smaller methods
-
-  def create_canvas(w, h, bkg_col)
-    @draw = Magick::Draw.new
-    @draw.pointsize = @@pointsize # TODO Use height
-    @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
-
-    # Make room for label and last value
-    unless @options[:label].nil?
-      @options[:has_last] = true
-      @label_width = calculate_width(@options[:label])
-      @data_last_width = calculate_width(@data.last)
-      # HACK The 7.0 is a severe hack. Must figure out correct spacing
-      @label_and_data_last_width = @label_width + @data_last_width + @@label_margin * 7.0
-      w += @label_and_data_last_width
-    end
-
-    @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
-    @canvas.format = "PNG"
-
-    # Draw label and last value
-    unless @options[:label].nil?
-      if ENV.has_key?('MAGICK_FONT_PATH')
-        vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
-        @font = File.exists?(vera_font_path) ? vera_font_path : nil
-      else
-        @font = nil
-      end
-
-      @draw.fill = 'black'
-      @draw.font = @font if @font
-      @draw.gravity = Magick::WestGravity
-      @draw.annotate( @canvas, 
-                      @label_width, 1.0,
-                      w - @label_and_data_last_width + @@label_margin, h - calculate_caps_height/2.0,
-                      @options[:label])
-
-      @draw.fill = 'red'
-      @draw.annotate( @canvas, 
-                      @data_last_width, 1.0,
-                      w - @data_last_width - @@label_margin * 2.0, h - calculate_caps_height/2.0,
-                      @data.last.to_s)
-    end
-  end
-
-  ##
-  # Utility to draw a coloured box
-  # Centred on pt, offset off in each direction, fill color is col
-
-  def drawbox(pt, offset, color)
-    @draw.stroke 'transparent'
-    @draw.fill(color)
-    @draw.rectangle(pt[0]-offset, pt[1]-offset, pt[0]+offset, pt[1]+offset)
-  end
-
-  def calculate_width(text)
-    @draw.get_type_metrics(@canvas, text.to_s).width
-  end
-
-  def calculate_caps_height
-    @draw.get_type_metrics(@canvas, 'X').height
-  end
-
-end

Benjamin Mako Hill || Want to submit a patch?