Upgrade to Rails 1.1.
author<mako@atdot.cc> <>
Wed, 16 Aug 2006 21:21:25 +0000 (17:21 -0400)
committer<mako@atdot.cc> <>
Wed, 16 Aug 2006 21:21:25 +0000 (17:21 -0400)
28 files changed:
README
Rakefile
config/boot.rb
config/environment.rb
config/environments/development.rb
config/environments/production.rb
config/environments/test.rb
config/routes.rb
public/500.html
public/images/rails.png [new file with mode: 0644]
public/javascripts/application.js [new file with mode: 0644]
public/javascripts/controls.js
public/javascripts/dragdrop.js
public/javascripts/effects.js
public/javascripts/prototype.js
script/about
script/breakpointer
script/console
script/destroy
script/generate
script/performance/benchmarker
script/performance/profiler
script/plugin
script/process/reaper
script/process/spawner
script/runner
script/server
vendor/rails

diff --git a/README b/README
index cd9d0ffe5802dc633270c76802b134d1a77b2ce9..7d8965e6bd4546317fc375ded48f421ff15554d9 100644 (file)
--- a/README
+++ b/README
@@ -27,12 +27,32 @@ link:files/vendor/rails/actionpack/README.html.
 
 == Getting started
 
-1. Run the WEBrick servlet: <tt>ruby script/server</tt> (run with --help for options)
-   ...or if you have lighttpd installed: <tt>ruby script/lighttpd</tt> (it's faster)
-2. Go to http://localhost:3000/ and get "Congratulations, you've put Ruby on Rails!"
-3. Follow the guidelines on the "Congratulations, you've put Ruby on Rails!" screen
+1. Start the web server: <tt>ruby script/server</tt> (run with --help for options)
+2. Go to http://localhost:3000/ and get "Welcome aboard: You’re riding the Rails!"
+3. Follow the guidelines to start developing your application
 
 
+== Web servers
+
+Rails uses the built-in web server in Ruby called WEBrick by default, so you don't
+have to install or configure anything to play around. 
+
+If you have lighttpd installed, though, it'll be used instead when running script/server.
+It's considerably faster than WEBrick and suited for production use, but requires additional
+installation and currently only works well on OS X/Unix (Windows users are encouraged
+to start with WEBrick). We recommend version 1.4.11 and higher. You can download it from
+http://www.lighttpd.net.
+
+If you want something that's halfway between WEBrick and lighttpd, we heartily recommend
+Mongrel. It's a Ruby-based web server with a C-component (so it requires compilation) that
+also works very well with Windows. See more at http://mongrel.rubyforge.org/.
+
+But of course its also possible to run Rails with the premiere open source web server Apache.
+To get decent performance, though, you'll need to install FastCGI. For Apache 1.3, you want
+to use mod_fastcgi. For Apache 2.0+, you want to use mod_fcgid.
+
+See http://wiki.rubyonrails.com/rails/pages/FastCGI for more information on FastCGI.
+
 == Example for Apache conf
 
   <VirtualHost *:80>
@@ -102,7 +122,10 @@ You can interact with the domain model by starting the console through script/co
 Here you'll have all parts of the application configured, just like it is when the
 application is running. You can inspect domain models, change values, and save to the
 database. Starting the script without arguments will launch it in the development environment.
-Passing an argument will specify a different environment, like <tt>console production</tt>.
+Passing an argument will specify a different environment, like <tt>script/console production</tt>.
+
+To reload your controllers and models after launching the console run <tt>reload!</tt>
+
 
 
 == Description of contents
@@ -128,12 +151,19 @@ app/views
 app/helpers
   Holds view helpers that should be named like weblog_helper.rb.
 
+app/apis
+  Holds API classes for web services.
+
 config
   Configuration files for the Rails environment, the routing map, the database, and other dependencies.
 
 components
   Self-contained mini-applications that can bundle together controllers, models, and views.
 
+db
+  Contains the database schema in schema.rb.  db/migrate contains all
+  the sequence of Migrations for your schema.
+
 lib
   Application specific libraries. Basically, any kind of custom code that doesn't
   belong under controllers, models, or helpers. This directory is in the load path.
index cffd19f0c1612d757eeba118b670a2d8093551f6..3bb0e8592a41ae3185ee32266c860714980dbed7 100644 (file)
--- a/Rakefile
+++ b/Rakefile
@@ -1,5 +1,5 @@
 # Add your own tasks in files placed in lib/tasks ending in .rake,
-# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake.
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
 
 require(File.join(File.dirname(__FILE__), 'config', 'boot'))
 
@@ -7,4 +7,4 @@ require 'rake'
 require 'rake/testtask'
 require 'rake/rdoctask'
 
-require 'tasks/rails'
\ No newline at end of file
+require 'tasks/rails'
index 96aa96bc4e08b4c8afdd376b235bfd0b26c13597..9a094cbc6dc9302dc151346f15aec13f76973ef3 100644 (file)
@@ -1,17 +1,44 @@
+# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
+
 unless defined?(RAILS_ROOT)
   root_path = File.join(File.dirname(__FILE__), '..')
+
   unless RUBY_PLATFORM =~ /mswin32/
     require 'pathname'
     root_path = Pathname.new(root_path).cleanpath(true).to_s
   end
+
   RAILS_ROOT = root_path
 end
 
-if File.directory?("#{RAILS_ROOT}/vendor/rails")
-  require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
-else
-  require 'rubygems'
-  require 'initializer'
-end
+unless defined?(Rails::Initializer)
+  if File.directory?("#{RAILS_ROOT}/vendor/rails")
+    require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+  else
+    require 'rubygems'
+
+    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
+
+    if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version
+      rails_gem = Gem.cache.search('rails', "=#{version}").first
+
+      if rails_gem
+        require_gem "rails", "=#{version}"
+        require rails_gem.full_gem_path + '/lib/initializer'
+      else
+        STDERR.puts %(Cannot find gem for Rails =#{version}:
+    Install the missing gem with 'gem install -v=#{version} rails', or
+    change environment.rb to define RAILS_GEM_VERSION with your desired version.
+  )
+        exit 1
+      end
+    else
+      require_gem "rails"
+      require 'initializer'
+    end
+  end
 
-Rails::Initializer.run(:set_load_path)
+  Rails::Initializer.run(:set_load_path)
+end
\ No newline at end of file
index a0ed78ec7203fa80140a323524c4afc5e33ea18d..34a2d87ec383b4d871844e3311313a2fc99297e4 100644 (file)
@@ -1,14 +1,19 @@
 # Be sure to restart your web server when you modify this file.
 
-# Uncomment below to force Rails into production mode
-# (Use only when you can't set environment variables through your web/app server)
+# Uncomment below to force Rails into production mode when 
+# you don't control web/app server and can't set it the proper way
 # ENV['RAILS_ENV'] ||= 'production'
 
+# Specifies gem version of Rails to use when vendor/rails is not present
+RAILS_GEM_VERSION = '1.1.6'
+
 # Bootstrap the Rails environment, frameworks, and default configuration
 require File.join(File.dirname(__FILE__), 'boot')
 
 Rails::Initializer.run do |config|
-  # Skip frameworks you're not going to use
+  # Settings in config/environments/* take precedence those specified here
+  
+  # Skip frameworks you're not going to use (only works if using vendor/rails)
   # config.frameworks -= [ :action_web_service, :action_mailer ]
 
   # Add additional load paths for your own custom dirs
@@ -19,12 +24,13 @@ Rails::Initializer.run do |config|
   # config.log_level = :debug
 
   # Use the database for sessions instead of the file system
-  # (create the session table with 'rake create_sessions_table')
+  # (create the session table with 'rake db:sessions:create')
   # config.action_controller.session_store = :active_record_store
 
-  # Enable page/fragment caching by setting a file-based store
-  # (remember to create the caching directory and make it readable to the application)
-  # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
+  # Use SQL instead of Active Record's schema dumper when creating the test database.
+  # This is necessary if your schema can't be completely dumped by the schema dumper, 
+  # like if you have constraints or database-specific column types
+  # config.active_record.schema_format = :sql
 
   # Activate observers that should always be running
   # config.active_record.observers = :cacher, :garbage_collector
@@ -32,10 +38,6 @@ Rails::Initializer.run do |config|
   # Make Active Record use UTC-base instead of local time
   # config.active_record.default_timezone = :utc
   
-  # Use Active Record's schema dumper instead of SQL when creating the test database
-  # (enables use of different database adapters for development and test environments)
-  # config.active_record.schema_format = :ruby
-
   # See Rails::Configuration for more options
 end
 
@@ -49,7 +51,7 @@ end
 # end
 
 # Include your application configuration below
-
-require 'uniq_token' 
-require 'randarray' 
+require 'uniq_token'
+require 'randarray'
 require 'rubyvote'
+
index a151c3ba113675149c79a961d45cc5a0c5be0b8a..0589aa979e30b065d7cd758e71246203c2024516 100644 (file)
@@ -1,10 +1,12 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
 # In the development environment your application's code is reloaded on
 # every request.  This slows down response time but is perfect for development
 # since you don't have to restart the webserver when you make code changes.
-config.cache_classes     = false
+config.cache_classes = false
 
 # Log error messages when you accidentally call methods on nil.
-config.whiny_nils        = true
+config.whiny_nils = true
 
 # Enable the breakpoint server that script/breakpointer connects to
 config.breakpoint_server = true
@@ -12,6 +14,8 @@ 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
+config.action_view.cache_template_extensions         = false
+config.action_view.debug_rjs                         = true
 
 # Don't care if the mailer can't send
 config.action_mailer.raise_delivery_errors = false
index f2b9ed6810ed68ac019268511ab4a7cfc7de672a..5a4e2b1c53cc6201773f0622e2c14b7c2ee24da9 100644 (file)
@@ -1,10 +1,11 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
 # The production environment is meant for finished, "live" apps.
 # Code is not reloaded between requests
 config.cache_classes = true
 
 # Use a different logger for distributed setups
-# config.logger        = SyslogLogger.new
-
+# config.logger = SyslogLogger.new
 
 # Full error reports are disabled and caching is turned on
 config.action_controller.consider_all_requests_local = false
index 3609f90f8dc05372e274eeb3de94bce2c2f983e4..f0689b924bf935c9ca8cb91c578f2cba521e250c 100644 (file)
@@ -1,3 +1,5 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
 # The test environment is used exclusively to run your application's
 # test suite.  You never need to work with it otherwise.  Remember that
 # your test database is "scratch space" for the test suite and is wiped
@@ -5,7 +7,7 @@
 config.cache_classes = true
 
 # Log error messages when you accidentally call methods on nil.
-config.whiny_nils    = true
+config.whiny_nils = true
 
 # Show full error reports and disable caching
 config.action_controller.consider_all_requests_local = true
index c9beb55e174dca775d5d5976e19e085a57c32c70..ed0625d8db889e12d5135761263b766cdadf73b7 100644 (file)
@@ -1,11 +1,14 @@
 ActionController::Routing::Routes.draw do |map|
-  # Add your own custom routes here.
   # The priority is based upon order of creation: first created -> highest priority.
   
-  # Here's a sample route:
+  # Sample of regular route:
   # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
   # Keep in mind you can assign values other than :controller and :action
 
+  # Sample of named route:
+  # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
+  # This route can be invoked with purchase_url(:id => product.id)
+
   # You can have the root of your site routed by hooking up '' 
   # -- just remember to delete public/index.html.
   map.connect '', :controller => "site"
index a1001a008d3be062bde1d4a30611e2071645fc17..ab95f74c4661b6ebc19908a47d1ce39f4b66087a 100644 (file)
@@ -2,7 +2,7 @@
    "http://www.w3.org/TR/html4/loose.dtd">
 <html>
 <body>
-  <h1>Application error (Apache)</h1>
+  <h1>Application error</h1>
   <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
 </body>
 </html>
\ No newline at end of file
diff --git a/public/images/rails.png b/public/images/rails.png
new file mode 100644 (file)
index 0000000..b8441f1
Binary files /dev/null and b/public/images/rails.png differ
diff --git a/public/javascripts/application.js b/public/javascripts/application.js
new file mode 100644 (file)
index 0000000..fe45776
--- /dev/null
@@ -0,0 +1,2 @@
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
index 6da588543bc6b9605bffd4632a4fb5faf45ac7d0..de0261ed54843afa8dd651ffcce7fdeb685f23c4 100644 (file)
@@ -80,7 +80,10 @@ Autocompleter.Base.prototype = {
 
   show: function() {
     if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
-    if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
+    if(!this.iefix && 
+      (navigator.appVersion.indexOf('MSIE')>0) &&
+      (navigator.userAgent.indexOf('Opera')<0) &&
+      (Element.getStyle(this.update, 'position')=='absolute')) {
       new Insertion.After(this.update, 
        '<iframe id="' + this.update.id + '_iefix" '+
        'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
@@ -138,8 +141,8 @@ Autocompleter.Base.prototype = {
          return;
       }
      else 
-      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
-        return;
+       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
+         (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
 
     this.changed = true;
     this.hasFocus = true;
@@ -149,6 +152,12 @@ Autocompleter.Base.prototype = {
         setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
   },
 
+  activate: function() {
+    this.changed = false;
+    this.hasFocus = true;
+    this.getUpdatedChoices();
+  },
+
   onHover: function(event) {
     var element = Event.findElement(event, 'LI');
     if(this.index != element.autocompleteIndex) 
@@ -218,8 +227,13 @@ Autocompleter.Base.prototype = {
       this.options.updateElement(selectedElement);
       return;
     }
-
-    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    var value = '';
+    if (this.options.select) {
+      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+    } else
+      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    
     var lastTokenPos = this.findLastToken();
     if (lastTokenPos != -1) {
       var newValue = this.element.value.substr(0, lastTokenPos + 1);
@@ -302,7 +316,7 @@ Autocompleter.Base.prototype = {
 Ajax.Autocompleter = Class.create();
 Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
   initialize: function(element, update, url, options) {
-         this.baseInitialize(element, update, options);
+    this.baseInitialize(element, update, options);
     this.options.asynchronous  = true;
     this.options.onComplete    = this.onComplete.bind(this);
     this.options.defaultParams = this.options.parameters || null;
@@ -445,7 +459,9 @@ Ajax.InPlaceEditor.prototype = {
     this.element = $(element);
 
     this.options = Object.extend({
+      okButton: true,
       okText: "ok",
+      cancelLink: true,
       cancelText: "cancel",
       savingText: "Saving...",
       clickToEditText: "Click to edit",
@@ -467,8 +483,10 @@ Ajax.InPlaceEditor.prototype = {
       formClassName: 'inplaceeditor-form',
       highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
       highlightendcolor: "#FFFFFF",
-      externalControl: null,
-      ajaxOptions: {}
+      externalControl: null,
+      submitOnBlur: false,
+      ajaxOptions: {},
+      evalScripts: false
     }, options || {});
 
     if(!this.options.formId && this.element.id) {
@@ -533,16 +551,22 @@ Ajax.InPlaceEditor.prototype = {
       this.form.appendChild(br);
     }
 
-    okButton = document.createElement("input");
-    okButton.type = "submit";
-    okButton.value = this.options.okText;
-    this.form.appendChild(okButton);
+    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);
+    }
 
-    cancelLink = document.createElement("a");
-    cancelLink.href = "#";
-    cancelLink.appendChild(document.createTextNode(this.options.cancelText));
-    cancelLink.onclick = this.onclickCancel.bind(this);
-    this.form.appendChild(cancelLink);
+    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);
+    }
   },
   hasHTMLLineBreaks: function(string) {
     if (!this.options.handleLineBreaks) return false;
@@ -558,24 +582,34 @@ Ajax.InPlaceEditor.prototype = {
     } else {
       text = this.getText();
     }
+
+    var obj = this;
     
     if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
       this.options.textarea = false;
       var textField = document.createElement("input");
+      textField.obj = this;
       textField.type = "text";
       textField.name = "value";
       textField.value = text;
       textField.style.backgroundColor = this.options.highlightcolor;
+      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;
     } else {
       this.options.textarea = true;
       var textArea = document.createElement("textarea");
+      textArea.obj = this;
       textArea.name = "value";
       textArea.value = this.convertHTMLLineBreaks(text);
       textArea.rows = this.options.rows;
       textArea.cols = this.options.cols || 40;
+      textArea.className = 'editor_field';      
+      if (this.options.submitOnBlur)
+        textArea.onblur = this.onSubmit.bind(this);
       this.editField = textArea;
     }
     
@@ -626,19 +660,26 @@ Ajax.InPlaceEditor.prototype = {
     // to be displayed indefinitely
     this.onLoading();
     
-    new Ajax.Updater(
-      { 
-        success: this.element,
-         // don't update on failure (this could be an option)
-        failure: null
-      },
-      this.url,
-      Object.extend({
-        parameters: this.options.callback(form, value),
-        onComplete: this.onComplete.bind(this),
-        onFailure: this.onFailure.bind(this)
-      }, this.options.ajaxOptions)
-    );
+    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]);
@@ -718,4 +759,57 @@ Ajax.InPlaceEditor.prototype = {
       Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
     }
   }
-};
\ No newline at end of file
+};
+
+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(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;
+    }
+
+    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);
+    }
+  }
+});
+
+// Delayed observer, like Form.Element.Observer, 
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+  initialize: function(element, delay, callback) {
+    this.delay     = delay || 0.5;
+    this.element   = $(element);
+    this.callback  = callback;
+    this.timer     = null;
+    this.lastValue = $F(this.element); 
+    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+  },
+  delayedListener: function(event) {
+    if(this.lastValue == $F(this.element)) return;
+    if(this.timer) clearTimeout(this.timer);
+    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+    this.lastValue = $F(this.element);
+  },
+  onTimerEvent: function() {
+    this.timer = null;
+    this.callback(this.element, $F(this.element));
+  }
+};
index 63a68243b6c2d38d0b5a6e289ccd22a8cdfbeb71..a01b7be2077981531a10aad90173c0d774b7a819 100644 (file)
@@ -1,6 +1,5 @@
 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-// 
-// Element.Class part Copyright (c) 2005 by Rick Olson
+//           (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
 // 
 // See scriptaculous.js for full license.
 
@@ -10,14 +9,15 @@ var Droppables = {
   drops: [],
 
   remove: function(element) {
-    this.drops = this.drops.reject(function(d) { return d.element==element });
+    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
   },
 
   add: function(element) {
     element = $(element);
     var options = Object.extend({
       greedy:     true,
-      hoverclass: null  
+      hoverclass: null,
+      tree:       false
     }, arguments[1] || {});
 
     // cache containers
@@ -31,67 +31,83 @@ var Droppables = {
         options._containers.push($(containment));
       }
     }
+    
+    if(options.accept) options.accept = [options.accept].flatten();
 
     Element.makePositioned(element); // fix IE
     options.element = element;
 
     this.drops.push(options);
   },
+  
+  findDeepestChild: function(drops) {
+    deepest = drops[0];
+      
+    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 parentNode = element.parentNode;
-    return drop._containers.detect(function(c) { return parentNode == c });
+    var containmentNode;
+    if(drop.tree) {
+      containmentNode = element.treeNode; 
+    } else {
+      containmentNode = element.parentNode;
+    }
+    return drop._containers.detect(function(c) { return containmentNode == c });
   },
-
-  isAffected: function(pX, pY, element, drop) {
+  
+  isAffected: function(point, element, drop) {
     return (
       (drop.element!=element) &&
       ((!drop._containers) ||
         this.isContained(element, drop)) &&
       ((!drop.accept) ||
-        (Element.Class.has_any(element, drop.accept))) &&
-      Position.within(drop.element, pX, pY) );
+        (Element.classNames(element).detect( 
+          function(v) { return drop.accept.include(v) } ) )) &&
+      Position.within(drop.element, point[0], point[1]) );
   },
 
   deactivate: function(drop) {
     if(drop.hoverclass)
-      Element.Class.remove(drop.element, drop.hoverclass);
+      Element.removeClassName(drop.element, drop.hoverclass);
     this.last_active = null;
   },
 
   activate: function(drop) {
-    if(this.last_active) this.deactivate(this.last_active);
     if(drop.hoverclass)
-      Element.Class.add(drop.element, drop.hoverclass);
+      Element.addClassName(drop.element, drop.hoverclass);
     this.last_active = drop;
   },
 
-  show: function(event, element) {
+  show: function(point, element) {
     if(!this.drops.length) return;
-    var pX = Event.pointerX(event);
-    var pY = Event.pointerY(event);
-    Position.prepare();
-
-    var i = this.drops.length-1; do {
-      var drop = this.drops[i];
-      if(this.isAffected(pX, pY, element, drop)) {
-        if(drop.onHover)
-           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
-        if(drop.greedy) { 
-          this.activate(drop);
-          return;
-        }
-      }
-    } while (i--);
+    var affected = [];
     
     if(this.last_active) this.deactivate(this.last_active);
+    this.drops.each( function(drop) {
+      if(Droppables.isAffected(point, element, drop))
+        affected.push(drop);
+    });
+        
+    if(affected.length>0) {
+      drop = Droppables.findDeepestChild(affected);
+      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);
+    }
   },
 
   fire: function(event, element) {
     if(!this.last_active) return;
     Position.prepare();
 
-    if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
+    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
       if (this.last_active.onDrop) 
         this.last_active.onDrop(element, this.last_active.element, event);
   },
@@ -103,15 +119,85 @@ var Droppables = {
 }
 
 var Draggables = {
+  drags: [],
   observers: [],
+  
+  register: function(draggable) {
+    if(this.drags.length == 0) {
+      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+      
+      Event.observe(document, "mouseup", this.eventMouseUp);
+      Event.observe(document, "mousemove", this.eventMouseMove);
+      Event.observe(document, "keypress", this.eventKeypress);
+    }
+    this.drags.push(draggable);
+  },
+  
+  unregister: function(draggable) {
+    this.drags = this.drags.reject(function(d) { return d==draggable });
+    if(this.drags.length == 0) {
+      Event.stopObserving(document, "mouseup", this.eventMouseUp);
+      Event.stopObserving(document, "mousemove", this.eventMouseMove);
+      Event.stopObserving(document, "keypress", this.eventKeypress);
+    }
+  },
+  
+  activate: function(draggable) {
+    window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+    this.activeDraggable = draggable;
+  },
+  
+  deactivate: function() {
+    this.activeDraggable = null;
+  },
+  
+  updateDrag: function(event) {
+    if(!this.activeDraggable) return;
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    // Mozilla-based browsers fire successive mousemove events with
+    // the same coordinates, prevent needless redrawing (moz bug?)
+    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+    this._lastPointer = pointer;
+    this.activeDraggable.updateDrag(event, pointer);
+  },
+  
+  endDrag: function(event) {
+    if(!this.activeDraggable) return;
+    this._lastPointer = null;
+    this.activeDraggable.endDrag(event);
+    this.activeDraggable = null;
+  },
+  
+  keyPress: function(event) {
+    if(this.activeDraggable)
+      this.activeDraggable.keyPress(event);
+  },
+  
   addObserver: function(observer) {
-    this.observers.push(observer);    
+    this.observers.push(observer);
+    this._cacheObserverCallbacks();
   },
-  removeObserver: function(element) {  // element instead of obsever fixes mem leaks
+  
+  removeObserver: function(element) {  // element instead of observer fixes mem leaks
     this.observers = this.observers.reject( function(o) { return o.element==element });
+    this._cacheObserverCallbacks();
+  },
+  
+  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
+    if(this[eventName+'Count'] > 0)
+      this.observers.each( function(o) {
+        if(o[eventName]) o[eventName](eventName, draggable, event);
+      });
   },
-  notify: function(eventName, draggable) {  // 'onStart', 'onEnd'
-    this.observers.invoke(eventName, draggable);
+  
+  _cacheObserverCallbacks: function() {
+    ['onStart','onEnd','onDrag'].each( function(eventName) {
+      Draggables[eventName+'Count'] = Draggables.observers.select(
+        function(o) { return o[eventName]; }
+      ).length;
+    });
   }
 }
 
@@ -127,89 +213,144 @@ Draggable.prototype = {
       },
       reverteffect: function(element, top_offset, left_offset) {
         var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
-        new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
+        element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
       },
       endeffect: function(element) { 
-         new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
+        new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
       },
       zindex: 1000,
-      revert: false
+      revert: false,
+      scroll: false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      snap: false   // false, or xy or [x,y] or function(x,y){ return [x,y] }
     }, arguments[1] || {});
 
-    this.element      = $(element);
-    if(options.handle && (typeof options.handle == 'string'))
-      this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
-      
+    this.element = $(element);
+    
+    if(options.handle && (typeof options.handle == 'string')) {
+      var h = Element.childrenWithClassName(this.element, options.handle, true);
+      if(h.length>0) this.handle = h[0];
+    }
     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);
 
     Element.makePositioned(this.element); // fix IE    
 
-    this.offsetX      = 0;
-    this.offsetY      = 0;
-    this.originalLeft = this.currentLeft();
-    this.originalTop  = this.currentTop();
-    this.originalX    = this.element.offsetLeft;
-    this.originalY    = this.element.offsetTop;
-
-    this.options      = options;
-
-    this.active       = false;
-    this.dragging     = false;   
+    this.delta    = this.currentDelta();
+    this.options  = options;
+    this.dragging = false;   
 
-    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
-    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
-    this.eventMouseMove = this.update.bindAsEventListener(this);
-    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+    Event.observe(this.handle, "mousedown", this.eventMouseDown);
     
-    this.registerEvents();
+    Draggables.register(this);
   },
+  
   destroy: function() {
     Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
-    this.unregisterEvents();
-  },
-  registerEvents: function() {
-    Event.observe(document, "mouseup", this.eventMouseUp);
-    Event.observe(document, "mousemove", this.eventMouseMove);
-    Event.observe(document, "keypress", this.eventKeypress);
-    Event.observe(this.handle, "mousedown", this.eventMouseDown);
+    Draggables.unregister(this);
   },
-  unregisterEvents: function() {
-    //if(!this.active) return;
-    //Event.stopObserving(document, "mouseup", this.eventMouseUp);
-    //Event.stopObserving(document, "mousemove", this.eventMouseMove);
-    //Event.stopObserving(document, "keypress", this.eventKeypress);
+  
+  currentDelta: function() {
+    return([
+      parseInt(Element.getStyle(this.element,'left') || '0'),
+      parseInt(Element.getStyle(this.element,'top') || '0')]);
   },
-  currentLeft: function() {
-    return parseInt(this.element.style.left || '0');
-  },
-  currentTop: function() {
-    return parseInt(this.element.style.top || '0')
-  },
-  startDrag: function(event) {
-    if(Event.isLeftClick(event)) {
-      
+  
+  initDrag: function(event) {
+    if(Event.isLeftClick(event)) {    
       // abort on form elements, fixes a Firefox issue
       var src = Event.element(event);
       if(src.tagName && (
         src.tagName=='INPUT' ||
         src.tagName=='SELECT' ||
+        src.tagName=='OPTION' ||
         src.tagName=='BUTTON' ||
         src.tagName=='TEXTAREA')) return;
+        
+      if(this.element._revert) {
+        this.element._revert.cancel();
+        this.element._revert = null;
+      }
       
-      // this.registerEvents();
-      this.active = true;
       var pointer = [Event.pointerX(event), Event.pointerY(event)];
-      var offsets = Position.cumulativeOffset(this.element);
-      this.offsetX =  (pointer[0] - offsets[0]);
-      this.offsetY =  (pointer[1] - offsets[1]);
+      var pos     = Position.cumulativeOffset(this.element);
+      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+      
+      Draggables.activate(this);
       Event.stop(event);
     }
   },
+  
+  startDrag: function(event) {
+    this.dragging = true;
+    
+    if(this.options.zindex) {
+      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+      this.element.style.zIndex = this.options.zindex;
+    }
+    
+    if(this.options.ghosting) {
+      this._clone = this.element.cloneNode(true);
+      Position.absolutize(this.element);
+      this.element.parentNode.insertBefore(this._clone, this.element);
+    }
+    
+    if(this.options.scroll) {
+      if (this.options.scroll == window) {
+        var where = this._getWindowScroll(this.options.scroll);
+        this.originalScrollLeft = where.left;
+        this.originalScrollTop = where.top;
+      } else {
+        this.originalScrollLeft = this.options.scroll.scrollLeft;
+        this.originalScrollTop = this.options.scroll.scrollTop;
+      }
+    }
+    
+    Draggables.notify('onStart', this, event);
+    if(this.options.starteffect) this.options.starteffect(this.element);
+  },
+  
+  updateDrag: function(event, pointer) {
+    if(!this.dragging) this.startDrag(event);
+    Position.prepare();
+    Droppables.show(pointer, this.element);
+    Draggables.notify('onDrag', this, event);
+    this.draw(pointer);
+    if(this.options.change) this.options.change(this);
+    
+    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 ]; }
+      } else {
+        p = Position.page(this.options.scroll);
+        p[0] += this.options.scroll.scrollLeft;
+        p[1] += this.options.scroll.scrollTop;
+        p.push(p[0]+this.options.scroll.offsetWidth);
+        p.push(p[1]+this.options.scroll.offsetHeight);
+      }
+      var speed = [0,0];
+      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+      this.startScrolling(speed);
+    }
+    
+    // fix AppleWebKit rendering
+    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+    
+    Event.stop(event);
+  },
+  
   finishDrag: function(event, success) {
-    // this.unregisterEvents();
-
-    this.active = false;
     this.dragging = false;
 
     if(this.options.ghosting) {
@@ -219,18 +360,17 @@ Draggable.prototype = {
     }
 
     if(success) Droppables.fire(event, this.element);
-    Draggables.notify('onEnd', this);
+    Draggables.notify('onEnd', this, event);
 
     var revert = this.options.revert;
     if(revert && typeof revert == 'function') revert = revert(this.element);
-
+    
+    var d = this.currentDelta();
     if(revert && this.options.reverteffect) {
       this.options.reverteffect(this.element, 
-      this.currentTop()-this.originalTop,
-      this.currentLeft()-this.originalLeft);
+        d[1]-this.delta[1], d[0]-this.delta[0]);
     } else {
-      this.originalLeft = this.currentLeft();
-      this.originalTop  = this.currentTop();
+      this.delta = d;
     }
 
     if(this.options.zindex)
@@ -239,70 +379,125 @@ Draggable.prototype = {
     if(this.options.endeffect) 
       this.options.endeffect(this.element);
 
-
+    Draggables.deactivate(this);
     Droppables.reset();
   },
+  
   keyPress: function(event) {
-    if(this.active) {
-      if(event.keyCode==Event.KEY_ESC) {
-        this.finishDrag(event, false);
-        Event.stop(event);
-      }
-    }
+    if(event.keyCode!=Event.KEY_ESC) return;
+    this.finishDrag(event, false);
+    Event.stop(event);
   },
+  
   endDrag: function(event) {
-    if(this.active && this.dragging) {
-      this.finishDrag(event, true);
-      Event.stop(event);
-    }
-    this.active = false;
-    this.dragging = false;
+    if(!this.dragging) return;
+    this.stopScrolling();
+    this.finishDrag(event, true);
+    Event.stop(event);
   },
-  draw: function(event) {
-    var pointer = [Event.pointerX(event), Event.pointerY(event)];
-    var offsets = Position.cumulativeOffset(this.element);
-    offsets[0] -= this.currentLeft();
-    offsets[1] -= this.currentTop();
+  
+  draw: function(point) {
+    var pos = Position.cumulativeOffset(this.element);
+    var d = this.currentDelta();
+    pos[0] -= d[0]; pos[1] -= d[1];
+    
+    if(this.options.scroll && (this.options.scroll != window)) {
+      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]) 
+    }.bind(this));
+    
+    if(this.options.snap) {
+      if(typeof this.options.snap == 'function') {
+        p = this.options.snap(p[0],p[1]);
+      } else {
+      if(this.options.snap instanceof Array) {
+        p = p.map( function(v, i) {
+          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+      } else {
+        p = p.map( function(v) {
+          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+      }
+    }}
+    
     var style = this.element.style;
     if((!this.options.constraint) || (this.options.constraint=='horizontal'))
-      style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
+      style.left = p[0] + "px";
     if((!this.options.constraint) || (this.options.constraint=='vertical'))
-      style.top  = (pointer[1] - offsets[1] - this.offsetY) + "px";
+      style.top  = p[1] + "px";
     if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
   },
-  update: function(event) {
-   if(this.active) {
-      if(!this.dragging) {
-        var style = this.element.style;
-        this.dragging = true;
-        
-        if(Element.getStyle(this.element,'position')=='') 
-          style.position = "relative";
-        
-        if(this.options.zindex) {
-          this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
-          style.zIndex = this.options.zindex;
-        }
-
-        if(this.options.ghosting) {
-          this._clone = this.element.cloneNode(true);
-          Position.absolutize(this.element);
-          this.element.parentNode.insertBefore(this._clone, this.element);
+  
+  stopScrolling: function() {
+    if(this.scrollInterval) {
+      clearInterval(this.scrollInterval);
+      this.scrollInterval = null;
+      Draggables._lastScrollPointer = null;
+    }
+  },
+  
+  startScrolling: function(speed) {
+    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;
+    this.lastScrolled = current;
+    if(this.options.scroll == window) {
+      with (this._getWindowScroll(this.options.scroll)) {
+        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+          var d = delta / 1000;
+          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
         }
-
-        Draggables.notify('onStart', this);
-        if(this.options.starteffect) this.options.starteffect(this.element);
       }
-
-      Droppables.show(event, this.element);
-      this.draw(event);
-      if(this.options.change) this.options.change(this);
-
-      // fix AppleWebKit rendering
-      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 
-
-      Event.stop(event);
-   }
+    } else {
+      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);
+    Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+    Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+    Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+    if (Draggables._lastScrollPointer[0] < 0)
+      Draggables._lastScrollPointer[0] = 0;
+    if (Draggables._lastScrollPointer[1] < 0)
+      Draggables._lastScrollPointer[1] = 0;
+    this.draw(Draggables._lastScrollPointer);
+    
+    if(this.options.change) this.options.change(this);
+  },
+  
+  _getWindowScroll: function(w) {
+    var T, L, W, H;
+    with (w.document) {
+      if (w.document.documentElement && documentElement.scrollTop) {
+        T = documentElement.scrollTop;
+        L = documentElement.scrollLeft;
+      } else if (w.document.body) {
+        T = body.scrollTop;
+        L = body.scrollLeft;
+      }
+      if (w.innerWidth) {
+        W = w.innerWidth;
+        H = w.innerHeight;
+      } else if (w.document.documentElement && documentElement.clientWidth) {
+        W = documentElement.clientWidth;
+        H = documentElement.clientHeight;
+      } else {
+        W = body.offsetWidth;
+        H = body.offsetHeight
+      }
+    }
+    return { top: T, left: L, width: W, height: H };
   }
 }
 
@@ -315,9 +510,11 @@ SortableObserver.prototype = {
     this.observer  = observer;
     this.lastValue = Sortable.serialize(this.element);
   },
+  
   onStart: function() {
     this.lastValue = Sortable.serialize(this.element);
   },
+  
   onEnd: function() {
     Sortable.unmark();
     if(this.lastValue != Sortable.serialize(this.element))
@@ -326,27 +523,41 @@ SortableObserver.prototype = {
 }
 
 var Sortable = {
-  sortables: new Array(),
-  options: function(element){
-    element = $(element);
-    return this.sortables.detect(function(s) { return s.element == element });
+  sortables: {},
+  
+  _findRootElement: function(element) {
+    while (element.tagName != "BODY") {  
+      if(element.id && Sortable.sortables[element.id]) return element;
+      element = element.parentNode;
+    }
   },
+
+  options: function(element) {
+    element = Sortable._findRootElement($(element));
+    if(!element) return;
+    return Sortable.sortables[element.id];
+  },
+  
   destroy: function(element){
-    element = $(element);
-    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
+    var s = Sortable.options(element);
+    
+    if(s) {
       Draggables.removeObserver(s.element);
       s.droppables.each(function(d){ Droppables.remove(d) });
       s.draggables.invoke('destroy');
-    });
-    this.sortables = this.sortables.reject(function(s) { return s.element == element });
+      
+      delete Sortable.sortables[s.element.id];
+    }
   },
+
   create: function(element) {
     element = $(element);
     var options = Object.extend({ 
       element:     element,
       tag:         'li',       // assumes li children, override with tag: 'tagname'
       dropOnEmpty: false,
-      tree:        false,      // fixme: unimplemented
+      tree:        false,
+      treeTag:     'ul',
       overlap:     'vertical', // one of 'vertical', 'horizontal'
       constraint:  'vertical', // one of 'vertical', 'horizontal', false
       containment: element,    // also takes array of elements (or id's); or false
@@ -354,7 +565,10 @@ var Sortable = {
       only:        false,
       hoverclass:  null,
       ghosting:    false,
-      format:      null,
+      scroll:      false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      format:      /^[^_]*_(.*)$/,
       onChange:    Prototype.emptyFunction,
       onUpdate:    Prototype.emptyFunction
     }, arguments[1] || {});
@@ -365,6 +579,9 @@ var Sortable = {
     // build options for the draggables
     var options_for_draggable = {
       revert:      true,
+      scroll:      options.scroll,
+      scrollSpeed: options.scrollSpeed,
+      scrollSensitivity: options.scrollSensitivity,
       ghosting:    options.ghosting,
       constraint:  options.constraint,
       handle:      options.handle };
@@ -390,9 +607,17 @@ var Sortable = {
     var options_for_droppable = {
       overlap:     options.overlap,
       containment: options.containment,
+      tree:        options.tree,
       hoverclass:  options.hoverclass,
-      onHover:     Sortable.onHover,
-      greedy:      !options.dropOnEmpty
+      onHover:     Sortable.onHover
+      //greedy:      !options.dropOnEmpty
+    }
+    
+    var options_for_tree = {
+      onHover:      Sortable.onEmptyHover,
+      overlap:      options.overlap,
+      containment:  options.containment,
+      hoverclass:   options.hoverclass
     }
 
     // fix for gecko engine
@@ -401,27 +626,33 @@ var Sortable = {
     options.draggables = [];
     options.droppables = [];
 
-    // make it so
-
     // drop on empty handling
-    if(options.dropOnEmpty) {
-      Droppables.add(element,
-        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
+    if(options.dropOnEmpty || options.tree) {
+      Droppables.add(element, options_for_tree);
       options.droppables.push(element);
     }
 
     (this.findElements(element, options) || []).each( function(e) {
       // handles are per-draggable
       var handle = options.handle ? 
-        Element.Class.childrenWith(e, options.handle)[0] : e;    
+        Element.childrenWithClassName(e, options.handle)[0] : e;    
       options.draggables.push(
         new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
       Droppables.add(e, options_for_droppable);
+      if(options.tree) e.treeNode = element;
       options.droppables.push(e);      
     });
+    
+    if(options.tree) {
+      (Sortable.findTreeElements(element, options) || []).each( function(e) {
+        Droppables.add(e, options_for_tree);
+        e.treeNode = element;
+        options.droppables.push(e);
+      });
+    }
 
     // keep reference
-    this.sortables.push(options);
+    this.sortables[element.id] = options;
 
     // for onupdate
     Draggables.addObserver(new SortableObserver(element, options.onUpdate));
@@ -430,23 +661,21 @@ var Sortable = {
 
   // return all suitable-for-sortable elements in a guaranteed order
   findElements: function(element, options) {
-    if(!element.hasChildNodes()) return null;
-    var elements = [];
-    $A(element.childNodes).each( function(e) {
-      if(e.tagName && e.tagName==options.tag.toUpperCase() &&
-        (!options.only || (Element.Class.has(e, options.only))))
-          elements.push(e);
-      if(options.tree) {
-        var grandchildren = this.findElements(e, options);
-        if(grandchildren) elements.push(grandchildren);
-      }
-    });
-
-    return (elements.length>0 ? elements.flatten() : null);
+    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);
   },
 
   onHover: function(element, dropon, overlap) {
-    if(overlap>0.5) {
+    if(Element.isParent(dropon, element)) return;
+
+    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+      return;
+    } else if(overlap>0.5) {
       Sortable.mark(dropon, 'before');
       if(dropon.previousSibling != element) {
         var oldParentNode = element.parentNode;
@@ -469,13 +698,37 @@ var Sortable = {
       }
     }
   },
-
-  onEmptyHover: function(element, dropon) {
-    if(element.parentNode!=dropon) {
-      var oldParentNode = element.parentNode;
-      dropon.appendChild(element);
+  
+  onEmptyHover: function(element, dropon, overlap) {
+    var oldParentNode = element.parentNode;
+    var droponOptions = Sortable.options(dropon);
+        
+    if(!Element.isParent(dropon, element)) {
+      var index;
+      
+      var children = Sortable.findElements(dropon, {tag: droponOptions.tag});
+      var child = null;
+            
+      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);
+          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+            child = index + 1 < children.length ? children[index + 1] : null;
+            break;
+          } else {
+            child = children[index];
+            break;
+          }
+        }
+      }
+      
+      dropon.insertBefore(element, child);
+      
       Sortable.options(oldParentNode).onChange(element);
-      Sortable.options(dropon).onChange(element);
+      droponOptions.onChange(element);
     }
   },
 
@@ -491,29 +744,170 @@ var Sortable = {
     if(!Sortable._marker) {
       Sortable._marker = $('dropmarker') || document.createElement('DIV');
       Element.hide(Sortable._marker);
-      Element.Class.add(Sortable._marker, 'dropmarker');
+      Element.addClassName(Sortable._marker, 'dropmarker');
       Sortable._marker.style.position = 'absolute';
       document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
     }    
     var offsets = Position.cumulativeOffset(dropon);
-    Sortable._marker.style.top  = offsets[1] + 'px';
-    if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
     Sortable._marker.style.left = offsets[0] + 'px';
+    Sortable._marker.style.top = offsets[1] + 'px';
+    
+    if(position=='after')
+      if(sortable.overlap == 'horizontal') 
+        Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
+      else
+        Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+    
     Element.show(Sortable._marker);
   },
+  
+  _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;
+      
+      var child = {
+        id: encodeURIComponent(match ? match[1] : null),
+        element: element,
+        parent: parent,
+        children: new Array,
+        position: parent.children.length,
+        container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
+      }
+      
+      /* Get the element containing the children and recurse over it */
+      if (child.container)
+        this._tree(child.container, options, child)
+      
+      parent.children.push (child);
+    }
+
+    return parent; 
+  },
 
-  serialize: function(element) {
+  /* Finds the first element of the given tag type within a parent element.
+    Used for finding the first LI[ST] within a L[IST]I[TEM].*/
+  _findChildrenElement: function (element, containerTag) {
+    if (element && element.hasChildNodes)
+      for (var i = 0; i < element.childNodes.length; ++i)
+        if (element.childNodes[i].tagName == containerTag)
+          return element.childNodes[i];
+  
+    return null;
+  },
+
+  tree: function(element) {
     element = $(element);
     var sortableOptions = this.options(element);
     var options = Object.extend({
-      tag:  sortableOptions.tag,
+      tag: sortableOptions.tag,
+      treeTag: sortableOptions.treeTag,
       only: sortableOptions.only,
       name: element.id,
-      format: sortableOptions.format || /^[^_]*_(.*)$/
+      format: sortableOptions.format
     }, arguments[1] || {});
-    return $(this.findElements(element, options) || []).collect( function(item) {
-      return (encodeURIComponent(options.name) + "[]=" + 
-              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
-    }).join("&");
+    
+    var root = {
+      id: null,
+      parent: null,
+      children: new Array,
+      container: element,
+      position: 0
+    }
+    
+    return Sortable._tree (element, options, root);
+  },
+
+  /* Construct a [i] index for a particular node */
+  _constructIndex: function(node) {
+    var index = '';
+    do {
+      if (node.id) index = '[' + node.position + ']' + index;
+    } while ((node = node.parent) != null);
+    return index;
+  },
+
+  sequence: function(element) {
+    element = $(element);
+    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] : '';
+    });
+  },
+
+  setSequence: function(element, new_sequence) {
+    element = $(element);
+    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);
+    });
+   
+    new_sequence.each(function(ident) {
+      var n = nodeMap[ident];
+      if (n) {
+        n[1].appendChild(n[0]);
+        delete nodeMap[ident];
+      }
+    });
+  },
+  
+  serialize: function(element) {
+    element = $(element);
+    var options = Object.extend(Sortable.options(element), arguments[1] || {});
+    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) {
+        return [name + Sortable._constructIndex(item) + "=" + 
+                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+      }).flatten().join('&');
+    } else {
+      return Sortable.sequence(element, arguments[1]).map( function(item) {
+        return name + "[]=" + encodeURIComponent(item);
+      }).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);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {    
+  if(!element.hasChildNodes()) return null;
+  tagName = tagName.toUpperCase();
+  if(only) only = [only].flatten();
+  var elements = [];
+  $A(element.childNodes).each( function(e) {
+    if(e.tagName && e.tagName.toUpperCase()==tagName &&
+      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+        elements.push(e);
+    if(recursive) {
+      var grandchildren = Element.findChildren(e, only, recursive, tagName);
+      if(grandchildren) elements.push(grandchildren);
+    }
+  });
+
+  return (elements.length>0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+  if (type == 'vertical' || type == 'height')
+    return element.offsetHeight;
+  else
+    return element.offsetWidth;
 }
\ No newline at end of file
index 3f92992065c038f58676ec614a37e8197130ffe2..927400502dd64a21198003e419b83d38c4ee6f80 100644 (file)
@@ -6,13 +6,11 @@
 // 
 // See scriptaculous.js for full license.  
 
-/* ------------- element ext -------------- */  
 // converts rgb() and #xxx to #xxxxxx format,  
 // returns self (or first argument) if not convertable  
 String.prototype.parseColor = function() {  
-  color = "#";  
-  if(this.slice(0,4) == "rgb(") {  
+  var 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 {  
@@ -22,176 +20,100 @@ String.prototype.parseColor = function() {
     }  
   }  
   return(color.length==7 ? color : (arguments[0] || this));  
-}  
+}
 
-Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {  
-  var children = $(element).childNodes;  
-  var text     = "";  
-  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");  
-  for (var i = 0; i < children.length; i++) {  
-    if(children[i].nodeType==3) {  
-      text+=children[i].nodeValue;  
-    } else {  
-      if((!children[i].className.match(classtest)) && children[i].hasChildNodes())  
-        text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);  
-    }  
-  }  
-  return text;
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+  }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 
+        Element.collectTextNodesIgnoreClass(node, className) : ''));
+  }).flatten().join('');
 }
 
-Element.setContentZoom = function(element, percent) {  
+Element.setContentZoom = function(element, percent) {
   element = $(element);  
-  element.style.fontSize = (percent/100) + "em";   
-  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);  
+  Element.setStyle(element, {fontSize: (percent/100) + 'em'});   
+  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
 }
 
 Element.getOpacity = function(element){  
-  var opacity;  
-  if (opacity = Element.getStyle(element, "opacity"))  
+  var opacity;
+  if (opacity = Element.getStyle(element, 'opacity'))  
     return parseFloat(opacity);  
-  if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/))  
+  if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))  
     if(opacity[1]) return parseFloat(opacity[1]) / 100;  
   return 1.0;  
 }
 
 Element.setOpacity = function(element, value){  
   element= $(element);  
-  var els = element.style;  
-  if (value == 1){  
-    els.opacity = '0.999999';  
+  if (value == 1){
+    Element.setStyle(element, { opacity: 
+      (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
+      0.999999 : null });
     if(/MSIE/.test(navigator.userAgent))  
-      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'');  
+      Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
   } else {  
     if(value < 0.00001) value = 0;  
-    els.opacity = value;  
+    Element.setStyle(element, {opacity: value});
     if(/MSIE/.test(navigator.userAgent))  
-      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +  
-        "alpha(opacity="+value*100+")";  
-  }   
+     Element.setStyle(element, 
+       { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
+                 'alpha(opacity='+value*100+')' });  
+  }
 }  
  
 Element.getInlineOpacity = function(element){  
-  element= $(element);  
-  var op;  
-  op = element.style.opacity;  
-  if (typeof op != "undefined" && op != "") return op;  
-  return "";  
-}  
-Element.setInlineOpacity = function(element, value){  
-  element= $(element);  
-  var els = element.style;  
-  els.opacity = value;  
-}  
-/*--------------------------------------------------------------------------*/  
-Element.Class = {  
-    // Element.toggleClass(element, className) toggles the class being on/off  
-    // Element.toggleClass(element, className1, className2) toggles between both classes,  
-    //   defaulting to className1 if neither exist  
-    toggle: function(element, className) {  
-      if(Element.Class.has(element, className)) {  
-        Element.Class.remove(element, className);  
-        if(arguments.length == 3) Element.Class.add(element, arguments[2]);  
-      } else {  
-        Element.Class.add(element, className);  
-        if(arguments.length == 3) Element.Class.remove(element, arguments[2]);  
-      }  
-    },  
-    // gets space-delimited classnames of an element as an array  
-    get: function(element) {  
-      return $(element).className.split(' ');  
-    },  
-    // functions adapted from original functions by Gavin Kistner  
-    remove: function(element) {  
-      element = $(element);  
-      var removeClasses = arguments;  
-      $R(1,arguments.length-1).each( function(index) {  
-        element.className =  
-          element.className.split(' ').reject(  
-            function(klass) { return (klass == removeClasses[index]) } ).join(' ');  
-      });  
-    },  
-    add: function(element) {  
-      element = $(element);  
-      for(var i = 1; i < arguments.length; i++) {  
-        Element.Class.remove(element, arguments[i]);  
-        element.className += (element.className.length > 0 ? ' ' : '') + arguments[i];  
-      }  
-    },  
-    // returns true if all given classes exist in said element  
-    has: function(element) {  
-      element = $(element);  
-      if(!element || !element.className) return false;  
-      var regEx;  
-      for(var i = 1; i < arguments.length; i++) {  
-        if((typeof arguments[i] == 'object') &&  
-          (arguments[i].constructor == Array)) {  
-          for(var j = 0; j < arguments[i].length; j++) {  
-            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");  
-            if(!regEx.test(element.className)) return false;  
-          }  
-        } else {  
-          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");  
-          if(!regEx.test(element.className)) return false;  
-        }  
-      }  
-      return true;  
-    },  
-    // expects arrays of strings and/or strings as optional paramters  
-    // Element.Class.has_any(element, ['classA','classB','classC'], 'classD')  
-    has_any: function(element) {  
-      element = $(element);  
-      if(!element || !element.className) return false;  
-      var regEx;  
-      for(var i = 1; i < arguments.length; i++) {  
-        if((typeof arguments[i] == 'object') &&  
-          (arguments[i].constructor == Array)) {  
-          for(var j = 0; j < arguments[i].length; j++) {  
-            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");  
-            if(regEx.test(element.className)) return true;  
-          }  
-        } else {  
-          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");  
-          if(regEx.test(element.className)) return true;  
-        }  
-      }  
-      return false;  
-    },  
-    childrenWith: function(element, className) {  
-      var children = $(element).getElementsByTagName('*');  
-      var elements = new Array();  
-      for (var i = 0; i < children.length; i++)  
-        if (Element.Class.has(children[i], className))  
-          elements.push(children[i]);  
-      return elements;  
-    }  
+  return $(element).style.opacity || '';
 }  
+
+Element.childrenWithClassName = function(element, className, findFirst) {
+  var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
+  var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { 
+    return (c.className && c.className.match(classNameRegExp));
+  });
+  if(!results) results = [];
+  return results;
+}
+
+Element.forceRerendering = function(element) {
+  try {
+    element = $(element);
+    var n = document.createTextNode(' ');
+    element.appendChild(n);
+    element.removeChild(n);
+  } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Array.prototype.call = function() {
+  var args = arguments;
+  this.each(function(f){ f.apply(this, args) });
+}
+
 /*--------------------------------------------------------------------------*/
 
 var Effect = {
   tagifyText: function(element) {
-    var tagifyStyle = "position:relative";
-    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1";
+    var tagifyStyle = 'position:relative';
+    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
     element = $(element);
     $A(element.childNodes).each( function(child) {
       if(child.nodeType==3) {
         child.nodeValue.toArray().each( function(character) {
           element.insertBefore(
             Builder.node('span',{style: tagifyStyle},
-              character == " " ? String.fromCharCode(160) : character), 
+              character == ' ' ? String.fromCharCode(160) : character), 
               child);
         });
         Element.remove(child);
@@ -211,12 +133,25 @@ var Effect = {
       speed: 0.1,
       delay: 0.0
     }, arguments[2] || {});
-    var speed = options.speed;
-    var delay = options.delay;
+    var masterDelay = options.delay;
 
     $A(elements).each( function(element, index) {
-      new effect(element, Object.extend(options, { delay: delay + index * speed }));
+      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
     });
+  },
+  PAIRS: {
+    'slide':  ['SlideDown','SlideUp'],
+    'blind':  ['BlindDown','BlindUp'],
+    'appear': ['Appear','Fade']
+  },
+  toggle: function(element, effect) {
+    element = $(element);
+    effect = (effect || 'appear').toLowerCase();
+    var options = Object.extend({
+      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+    }, arguments[2] || {});
+    Effect[element.visible() ? 
+      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
   }
 };
 
@@ -254,16 +189,22 @@ Effect.Transitions.full = function(pos) {
 
 /* ------------- core effects ------------- */
 
-Effect.Queue = {
-  effects:  [],
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+  initialize: function() {
+    this.effects  = [];
+    this.interval = null;
+  },
   _each: function(iterator) {
     this.effects._each(iterator);
   },
-  interval: null,
   add: function(effect) {
     var timestamp = new Date().getTime();
     
-    switch(effect.options.queue) {
+    var position = (typeof effect.options.queue == 'string') ? 
+      effect.options.queue : effect.options.queue.position;
+    
+    switch(position) {
       case 'front':
         // move unstarted effects after this effect  
         this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
@@ -279,7 +220,10 @@ Effect.Queue = {
     
     effect.startOn  += timestamp;
     effect.finishOn += timestamp;
-    this.effects.push(effect);
+
+    if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+      this.effects.push(effect);
+    
     if(!this.interval) 
       this.interval = setInterval(this.loop.bind(this), 40);
   },
@@ -294,32 +238,45 @@ Effect.Queue = {
     var timePos = new Date().getTime();
     this.effects.invoke('loop', timePos);
   }
+});
+
+Effect.Queues = {
+  instances: $H(),
+  get: function(queueName) {
+    if(typeof queueName != 'string') return queueName;
+    
+    if(!this.instances[queueName])
+      this.instances[queueName] = new Effect.ScopedQueue();
+      
+    return this.instances[queueName];
+  }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+  transition: Effect.Transitions.sinoidal,
+  duration:   1.0,   // seconds
+  fps:        25.0,  // max. 25fps due to Effect.Queue implementation
+  sync:       false, // true for combining
+  from:       0.0,
+  to:         1.0,
+  delay:      0.0,
+  queue:      'parallel'
 }
-Object.extend(Effect.Queue, Enumerable);
 
 Effect.Base = function() {};
 Effect.Base.prototype = {
   position: null,
-  setOptions: function(options) {
-    this.options = Object.extend({
-      transition: Effect.Transitions.sinoidal,
-      duration:   1.0,   // seconds
-      fps:        25.0,  // max. 25fps due to Effect.Queue implementation
-      sync:       false, // true for combining
-      from:       0.0,
-      to:         1.0,
-      delay:      0.0,
-      queue:      'parallel'
-    }, options || {});
-  },
   start: function(options) {
-    this.setOptions(options || {});
+    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
     this.currentFrame = 0;
     this.state        = 'idle';
     this.startOn      = this.options.delay*1000;
     this.finishOn     = this.startOn + (this.options.duration*1000);
     this.event('beforeStart');
-    if(!this.options.sync) Effect.Queue.add(this);
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).add(this);
   },
   loop: function(timePos) {
     if(timePos >= this.startOn) {
@@ -346,21 +303,28 @@ Effect.Base.prototype = {
       if(this.setup) this.setup();
       this.event('afterSetup');
     }
-    if(this.options.transition) pos = this.options.transition(pos);
-    pos *= (this.options.to-this.options.from);
-    pos += this.options.from;
-    this.position = pos;
-    this.event('beforeUpdate');
-    if(this.update) this.update(pos);
-    this.event('afterUpdate');
+    if(this.state == 'running') {
+      if(this.options.transition) pos = this.options.transition(pos);
+      pos *= (this.options.to-this.options.from);
+      pos += this.options.from;
+      this.position = pos;
+      this.event('beforeUpdate');
+      if(this.update) this.update(pos);
+      this.event('afterUpdate');
+    }
   },
   cancel: function() {
-    if(!this.options.sync) Effect.Queue.remove(this);
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).remove(this);
     this.state = 'finished';
   },
   event: function(eventName) {
     if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
     if(this.options[eventName]) this.options[eventName](this);
+  },
+  inspect: function() {
+    return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
   }
 }
 
@@ -390,47 +354,57 @@ Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
     this.element = $(element);
     // make this work on IE on elements without 'layout'
     if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
-      this.element.style.zoom = 1;
+      this.element.setStyle({zoom: 1});
     var options = Object.extend({
-      from: Element.getOpacity(this.element) || 0.0,
+      from: this.element.getOpacity() || 0.0,
       to:   1.0
     }, arguments[1] || {});
     this.start(options);
   },
   update: function(position) {
-    Element.setOpacity(this.element, position);
+    this.element.setOpacity(position);
   }
 });
 
-Effect.MoveBy = Class.create();
-Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
-  initialize: function(element, toTop, toLeft) {
-    this.element      = $(element);
-    this.toTop        = toTop;
-    this.toLeft       = toLeft;
-    this.start(arguments[3]);
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    var options = Object.extend({
+      x:    0,
+      y:    0,
+      mode: 'relative'
+    }, arguments[1] || {});
+    this.start(options);
   },
   setup: function() {
     // Bug in Opera: Opera returns the "real" position of a static element or
     // relative element that does not have top/left explicitly set.
     // ==> Always set top and left for position relative elements in your stylesheets 
-    // (to 0 if you do not need them)
-    
-    Element.makePositioned(this.element);
-    this.originalTop  = parseFloat(Element.getStyle(this.element,'top')  || '0');
-    this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
+    // (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');
+    if(this.options.mode == 'absolute') {
+      // absolute movement, so we need to calc deltaX and deltaY
+      this.options.x = this.options.x - this.originalLeft;
+      this.options.y = this.options.y - this.originalTop;
+    }
   },
   update: function(position) {
-    var topd  = this.toTop  * position + this.originalTop;
-    var leftd = this.toLeft * position + this.originalLeft;
-    this.setPosition(topd, leftd);
-  },
-  setPosition: function(topd, leftd) {
-    this.element.style.top  = topd  + "px";
-    this.element.style.left = leftd + "px";
+    this.element.setStyle({
+      left: this.options.x  * position + this.originalLeft + 'px',
+      top:  this.options.y  * position + this.originalTop  + 'px'
+    });
   }
 });
 
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+  return new Effect.Move(element, 
+    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
 Effect.Scale = Class.create();
 Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
   initialize: function(element, percent) {
@@ -447,33 +421,31 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
     this.start(options);
   },
   setup: function() {
-    var effect = this;
-    
     this.restoreAfterFinish = this.options.restoreAfterFinish || false;
-    this.elementPositioning = Element.getStyle(this.element,'position');
+    this.elementPositioning = this.element.getStyle('position');
     
-    effect.originalStyle = {};
+    this.originalStyle = {};
     ['top','left','width','height','fontSize'].each( function(k) {
-      effect.originalStyle[k] = effect.element.style[k];
-    });
+      this.originalStyle[k] = this.element.style[k];
+    }.bind(this));
       
     this.originalTop  = this.element.offsetTop;
     this.originalLeft = this.element.offsetLeft;
     
-    var fontSize = Element.getStyle(this.element,'font-size') || "100%";
+    var fontSize = this.element.getStyle('font-size') || '100%';
     ['em','px','%'].each( function(fontSizeType) {
       if(fontSize.indexOf(fontSizeType)>0) {
-        effect.fontSize     = parseFloat(fontSize);
-        effect.fontSizeType = fontSizeType;
+        this.fontSize     = parseFloat(fontSize);
+        this.fontSizeType = fontSizeType;
       }
-    });
+    }.bind(this));
     
     this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
     
     this.dims = null;
     if(this.options.scaleMode=='box')
-      this.dims = [this.element.clientHeight, this.element.clientWidth];
-    if(this.options.scaleMode=='content')
+      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+    if(/^content/.test(this.options.scaleMode))
       this.dims = [this.element.scrollHeight, this.element.scrollWidth];
     if(!this.dims)
       this.dims = [this.options.scaleMode.originalHeight,
@@ -482,32 +454,28 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
   update: function(position) {
     var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
     if(this.options.scaleContent && this.fontSize)
-      this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType;
+      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
     this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
   },
   finish: function(position) {
-    if (this.restoreAfterFinish) {
-      var effect = this;
-      ['top','left','width','height','fontSize'].each( function(k) {
-        effect.element.style[k] = effect.originalStyle[k];
-      });
-    }
+    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
   },
   setDimensions: function(height, width) {
-    var els = this.element.style;
-    if(this.options.scaleX) els.width = width + 'px';
-    if(this.options.scaleY) els.height = height + 'px';
+    var d = {};
+    if(this.options.scaleX) d.width = width + 'px';
+    if(this.options.scaleY) d.height = height + 'px';
     if(this.options.scaleFromCenter) {
       var topd  = (height - this.dims[0])/2;
       var leftd = (width  - this.dims[1])/2;
       if(this.elementPositioning == 'absolute') {
-        if(this.options.scaleY) els.top = this.originalTop-topd + "px";
-        if(this.options.scaleX) els.left = this.originalLeft-leftd + "px";
+        if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+        if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
       } else {
-        if(this.options.scaleY) els.top = -topd + "px";
-        if(this.options.scaleX) els.left = -leftd + "px";
+        if(this.options.scaleY) d.top = -topd + 'px';
+        if(this.options.scaleX) d.left = -leftd + 'px';
       }
     }
+    this.element.setStyle(d);
   }
 });
 
@@ -515,41 +483,32 @@ Effect.Highlight = Class.create();
 Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
   initialize: function(element) {
     this.element = $(element);
-    var options = Object.extend({
-      startcolor:   "#ffff99"
-    }, arguments[1] || {});
+    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
     this.start(options);
   },
   setup: function() {
     // Prevent executing on elements not in the layout flow
-    if(this.element.style.display=='none') { this.cancel(); return; }
+    if(this.element.getStyle('display')=='none') { this.cancel(); return; }
     // Disable background image during the effect
-    this.oldBgImage = this.element.style.backgroundImage;
-    this.element.style.backgroundImage = "none";
+    this.oldStyle = {
+      backgroundImage: this.element.getStyle('background-image') };
+    this.element.setStyle({backgroundImage: 'none'});
     if(!this.options.endcolor)
-      this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
-    if (typeof this.options.restorecolor == "undefined")
-      this.options.restorecolor = this.element.style.backgroundColor;
+      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+    if(!this.options.restorecolor)
+      this.options.restorecolor = this.element.getStyle('background-color');
     // init color calculations
-    this.colors_base = [
-      parseInt(this.options.startcolor.slice(1,3),16),
-      parseInt(this.options.startcolor.slice(3,5),16),
-      parseInt(this.options.startcolor.slice(5),16) ];
-    this.colors_delta = [
-      parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
-      parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
-      parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
+    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
   },
   update: function(position) {
-    var effect = this; var colors = $R(0,2).map( function(i){ 
-      return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position))
-    });
-    this.element.style.backgroundColor = "#" +
-      colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
+    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)) });
   },
   finish: function() {
-    this.element.style.backgroundColor = this.options.restorecolor;
-    this.element.style.backgroundImage = this.oldBgImage;
+    this.element.setStyle(Object.extend(this.oldStyle, {
+      backgroundColor: this.options.restorecolor
+    }));
   }
 });
 
@@ -562,6 +521,7 @@ Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
   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 - 
@@ -580,70 +540,70 @@ Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
 /* ------------- combination effects ------------- */
 
 Effect.Fade = function(element) {
-  var oldOpacity = Element.getInlineOpacity(element);
+  element = $(element);
+  var oldOpacity = element.getInlineOpacity();
   var options = Object.extend({
-  from: Element.getOpacity(element) || 1.0,
+  from: element.getOpacity() || 1.0,
   to:   0.0,
-  afterFinishInternal: function(effect) 
-    { if (effect.options.to == 0) {
-        Element.hide(effect.element);
-        Element.setInlineOpacity(effect.element, oldOpacity);
-      }  
-    }
-  }, arguments[1] || {});
+  afterFinishInternal: function(effect) { 
+    if(effect.options.to!=0) return;
+    effect.element.hide();
+    effect.element.setStyle({opacity: oldOpacity}); 
+  }}, arguments[1] || {});
   return new Effect.Opacity(element,options);
 }
 
 Effect.Appear = function(element) {
+  element = $(element);
   var options = Object.extend({
-  from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0),
+  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
   to:   1.0,
-  beforeSetup: function(effect)  
-    { Element.setOpacity(effect.element, effect.options.from);
-      Element.show(effect.element); }
-  }, arguments[1] || {});
+  // force Safari to render floated elements properly
+  afterFinishInternal: function(effect) {
+    effect.element.forceRerendering();
+  },
+  beforeSetup: function(effect) {
+    effect.element.setOpacity(effect.options.from);
+    effect.element.show(); 
+  }}, arguments[1] || {});
   return new Effect.Opacity(element,options);
 }
 
 Effect.Puff = function(element) {
   element = $(element);
-  var oldOpacity = Element.getInlineOpacity(element);
-  var oldPosition = element.style.position;
+  var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') };
   return new Effect.Parallel(
    [ new Effect.Scale(element, 200, 
       { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
      new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
      Object.extend({ duration: 1.0, 
-      beforeSetupInternal: function(effect) 
-       { effect.effects[0].element.style.position = 'absolute'; },
-      afterFinishInternal: function(effect)
-       { Element.hide(effect.effects[0].element);
-         effect.effects[0].element.style.position = oldPosition;
-         Element.setInlineOpacity(effect.effects[0].element, oldOpacity); }
+      beforeSetupInternal: function(effect) {
+        effect.effects[0].element.setStyle({position: 'absolute'}); },
+      afterFinishInternal: function(effect) {
+         effect.effects[0].element.hide();
+         effect.effects[0].element.setStyle(oldStyle); }
      }, arguments[1] || {})
    );
 }
 
 Effect.BlindUp = function(element) {
   element = $(element);
-  Element.makeClipping(element);
+  element.makeClipping();
   return new Effect.Scale(element, 0, 
     Object.extend({ scaleContent: false, 
       scaleX: false, 
       restoreAfterFinish: true,
-      afterFinishInternal: function(effect)
-        { 
-          Element.hide(effect.element);
-          Element.undoClipping(effect.element);
-        } 
+      afterFinishInternal: function(effect) {
+        effect.element.hide();
+        effect.element.undoClipping();
+      } 
     }, arguments[1] || {})
   );
 }
 
 Effect.BlindDown = function(element) {
   element = $(element);
-  var oldHeight = element.style.height;
-  var elementDimensions = Element.getDimensions(element);
+  var elementDimensions = element.getDimensions();
   return new Effect.Scale(element, 100, 
     Object.extend({ scaleContent: false, 
       scaleX: false,
@@ -651,13 +611,12 @@ Effect.BlindDown = function(element) {
       scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
       restoreAfterFinish: true,
       afterSetup: function(effect) {
-        Element.makeClipping(effect.element);
-        effect.element.style.height = "0px";
-        Element.show(effect.element); 
+        effect.element.makeClipping();
+        effect.element.setStyle({height: '0px'});
+        effect.element.show(); 
       },  
       afterFinishInternal: function(effect) {
-        Element.undoClipping(effect.element);
-        effect.element.style.height = oldHeight;
+        effect.element.undoClipping();
       }
     }, arguments[1] || {})
   );
@@ -665,7 +624,7 @@ Effect.BlindDown = function(element) {
 
 Effect.SwitchOff = function(element) {
   element = $(element);
-  var oldOpacity = Element.getInlineOpacity(element);
+  var oldOpacity = element.getInlineOpacity();
   return new Effect.Appear(element, { 
     duration: 0.4,
     from: 0,
@@ -675,14 +634,14 @@ Effect.SwitchOff = function(element) {
         duration: 0.3, scaleFromCenter: true,
         scaleX: false, scaleContent: false, restoreAfterFinish: true,
         beforeSetup: function(effect) { 
-          Element.makePositioned(effect.element); 
-          Element.makeClipping(effect.element);
+          effect.element.makePositioned();
+          effect.element.makeClipping();
         },
-        afterFinishInternal: function(effect) { 
-          Element.hide(effect.element); 
-          Element.undoClipping(effect.element);
-          Element.undoPositioned(effect.element);
-          Element.setInlineOpacity(effect.element, oldOpacity);
+        afterFinishInternal: function(effect) {
+          effect.element.hide();
+          effect.element.undoClipping();
+          effect.element.undoPositioned();
+          effect.element.setStyle({opacity: oldOpacity});
         }
       })
     }
@@ -691,195 +650,199 @@ Effect.SwitchOff = function(element) {
 
 Effect.DropOut = function(element) {
   element = $(element);
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  var oldOpacity = Element.getInlineOpacity(element);
+  var oldStyle = {
+    top: element.getStyle('top'),
+    left: element.getStyle('left'),
+    opacity: element.getInlineOpacity() };
   return new Effect.Parallel(
-    [ new Effect.MoveBy(element, 100, 0, { 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) { 
-          Element.makePositioned(effect.effects[0].element); },
-        afterFinishInternal: function(effect) { 
-          Element.hide(effect.effects[0].element); 
-          Element.undoPositioned(effect.effects[0].element);
-          effect.effects[0].element.style.left = oldLeft;
-          effect.effects[0].element.style.top = oldTop;
-          Element.setInlineOpacity(effect.effects[0].element, oldOpacity); 
+        beforeSetup: function(effect) {
+          effect.effects[0].element.makePositioned(); 
+        },
+        afterFinishInternal: function(effect) {
+          effect.effects[0].element.hide();
+          effect.effects[0].element.undoPositioned();
+          effect.effects[0].element.setStyle(oldStyle);
+        } 
       }, arguments[1] || {}));
 }
 
 Effect.Shake = function(element) {
   element = $(element);
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  return new Effect.MoveBy(element, 0, 20, 
-    { duration: 0.05, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, -40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, 40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, -40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, 40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, -20, 
-    { duration: 0.05, afterFinishInternal: function(effect) {
-        Element.undoPositioned(effect.element);
-        effect.element.style.left = oldLeft;
-        effect.element.style.top = oldTop;
+  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) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+        effect.element.undoPositioned();
+        effect.element.setStyle(oldStyle);
   }}) }}) }}) }}) }}) }});
 }
 
 Effect.SlideDown = function(element) {
   element = $(element);
-  Element.cleanWhitespace(element);
+  element.cleanWhitespace();
   // SlideDown need to have the content of the element wrapped in a container element with fixed height!
-  var oldInnerBottom = element.firstChild.style.bottom;
-  var elementDimensions = Element.getDimensions(element);
-  return new Effect.Scale(element, 100, 
-   Object.extend({ scaleContent: false, 
+  var oldInnerBottom = $(element.firstChild).getStyle('bottom');
+  var elementDimensions = element.getDimensions();
+  return new Effect.Scale(element, 100, Object.extend({ 
+    scaleContent: false, 
     scaleX: false, 
     scaleFrom: 0,
-    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},    
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
     restoreAfterFinish: true,
     afterSetup: function(effect) {
-      Element.makePositioned(effect.element.firstChild);
-      if (window.opera) effect.element.firstChild.style.top = "";
-      Element.makeClipping(effect.element);
-      element.style.height = '0';
-      Element.show(element); 
-    },  
-    afterUpdateInternal: function(effect) { 
-      effect.element.firstChild.style.bottom = 
-        (effect.dims[0] - effect.element.clientHeight) + 'px'; },
-    afterFinishInternal: function(effect) { 
-      Element.undoClipping(effect.element); 
-      Element.undoPositioned(effect.element.firstChild);
-      effect.element.firstChild.style.bottom = oldInnerBottom; }
+      effect.element.makePositioned();
+      effect.element.firstChild.makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping();
+      effect.element.setStyle({height: '0px'});
+      effect.element.show(); },
+    afterUpdateInternal: function(effect) {
+      effect.element.firstChild.setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
+    },
+    afterFinishInternal: function(effect) {
+      effect.element.undoClipping(); 
+      // IE will crash if child is undoPositioned first
+      if(/MSIE/.test(navigator.userAgent)){
+        effect.element.undoPositioned();
+        effect.element.firstChild.undoPositioned();
+      }else{
+        effect.element.firstChild.undoPositioned();
+        effect.element.undoPositioned();
+      }
+      effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
     }, arguments[1] || {})
   );
 }
   
 Effect.SlideUp = function(element) {
   element = $(element);
-  Element.cleanWhitespace(element);
-  var oldInnerBottom = element.firstChild.style.bottom;
+  element.cleanWhitespace();
+  var oldInnerBottom = $(element.firstChild).getStyle('bottom');
   return new Effect.Scale(element, 0, 
    Object.extend({ scaleContent: false, 
     scaleX: false, 
     scaleMode: 'box',
     scaleFrom: 100,
     restoreAfterFinish: true,
-    beforeStartInternal: function(effect) { 
-      Element.makePositioned(effect.element.firstChild);
-      if (window.opera) effect.element.firstChild.style.top = "";
-      Element.makeClipping(effect.element);
-      Element.show(element); 
-    },  
-    afterUpdateInternal: function(effect) { 
-     effect.element.firstChild.style.bottom = 
-       (effect.dims[0] - effect.element.clientHeight) + 'px'; },
-    afterFinishInternal: function(effect) { 
-        Element.hide(effect.element);
-        Element.undoClipping(effect.element); 
-        Element.undoPositioned(effect.element.firstChild);
-        effect.element.firstChild.style.bottom = oldInnerBottom; }
+    beforeStartInternal: function(effect) {
+      effect.element.makePositioned();
+      effect.element.firstChild.makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping();
+      effect.element.show(); },  
+    afterUpdateInternal: function(effect) {
+      effect.element.firstChild.setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); },
+    afterFinishInternal: function(effect) {
+      effect.element.hide();
+      effect.element.undoClipping();
+      effect.element.firstChild.undoPositioned();
+      effect.element.undoPositioned();
+      effect.element.setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || {})
   );
 }
 
+// Bug in opera makes the TD containing this element expand for a instance after finish 
 Effect.Squish = function(element) {
-  // Bug in opera makes the TD containing this element expand for a instance after finish 
   return new Effect.Scale(element, window.opera ? 1 : 0, 
     { restoreAfterFinish: true,
-      beforeSetup: function(effect) { 
-        Element.makeClipping(effect.element); },  
-      afterFinishInternal: function(effect) { 
-        Element.hide(effect.element); 
-        Element.undoClipping(effect.element); } 
+      beforeSetup: function(effect) {
+        effect.element.makeClipping(effect.element); },  
+      afterFinishInternal: function(effect) {
+        effect.element.hide(effect.element); 
+        effect.element.undoClipping(effect.element); }
   });
 }
 
 Effect.Grow = function(element) {
   element = $(element);
-  var options = arguments[1] || {};
-  
-  var elementDimensions = Element.getDimensions(element);
-  var originalWidth = elementDimensions.width;
-  var originalHeight = elementDimensions.height;
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  var oldHeight = element.style.height;
-  var oldWidth = element.style.width;
-  var oldOpacity = Element.getInlineOpacity(element);
-  
-  var direction = options.direction || 'center';
-  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
-  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
-  var opacityTransition = options.opacityTransition || Effect.Transitions.full;
-  
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.full
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();    
   var initialMoveX, initialMoveY;
   var moveX, moveY;
   
-  switch (direction) {
+  switch (options.direction) {
     case 'top-left':
       initialMoveX = initialMoveY = moveX = moveY = 0; 
       break;
     case 'top-right':
-      initialMoveX = originalWidth;
+      initialMoveX = dims.width;
       initialMoveY = moveY = 0;
-      moveX = -originalWidth;
+      moveX = -dims.width;
       break;
     case 'bottom-left':
       initialMoveX = moveX = 0;
-      initialMoveY = originalHeight;
-      moveY = -originalHeight;
+      initialMoveY = dims.height;
+      moveY = -dims.height;
       break;
     case 'bottom-right':
-      initialMoveX = originalWidth;
-      initialMoveY = originalHeight;
-      moveX = -originalWidth;
-      moveY = -originalHeight;
+      initialMoveX = dims.width;
+      initialMoveY = dims.height;
+      moveX = -dims.width;
+      moveY = -dims.height;
       break;
     case 'center':
-      initialMoveX = originalWidth / 2;
-      initialMoveY = originalHeight / 2;
-      moveX = -originalWidth / 2;
-      moveY = -originalHeight / 2;
+      initialMoveX = dims.width / 2;
+      initialMoveY = dims.height / 2;
+      moveX = -dims.width / 2;
+      moveY = -dims.height / 2;
       break;
   }
   
-  return new Effect.MoveBy(element, initialMoveY, initialMoveX, { 
+  return new Effect.Move(element, {
+    x: initialMoveX,
+    y: initialMoveY,
     duration: 0.01, 
-    beforeSetup: function(effect) { 
-      Element.hide(effect.element);
-      Element.makeClipping(effect.element);
-      Element.makePositioned(effect.element);
+    beforeSetup: function(effect) {
+      effect.element.hide();
+      effect.element.makeClipping();
+      effect.element.makePositioned();
     },
     afterFinishInternal: function(effect) {
       new Effect.Parallel(
-        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
-          new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }),
+        [ new Effect.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: originalHeight, originalWidth: originalWidth }, 
-            sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true})
+            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
+            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
         ], Object.extend({
              beforeSetup: function(effect) {
-              effect.effects[0].element.style.height = 0;
-              Element.show(effect.effects[0].element);
-             },              
+               effect.effects[0].element.setStyle({height: '0px'});
+               effect.effects[0].element.show(); 
+             },
              afterFinishInternal: function(effect) {
-               var el = effect.effects[0].element;
-               var els = el.style;
-               Element.undoClipping(el); 
-               Element.undoPositioned(el);
-               els.top = oldTop;
-               els.left = oldLeft;
-               els.height = oldHeight;
-               els.width = originalWidth + 'px';
-               Element.setInlineOpacity(el, oldOpacity);
+               effect.effects[0].element.undoClipping();
+               effect.effects[0].element.undoPositioned();
+               effect.effects[0].element.setStyle(oldStyle); 
              }
            }, options)
       )
@@ -889,66 +852,57 @@ Effect.Grow = function(element) {
 
 Effect.Shrink = function(element) {
   element = $(element);
-  var options = arguments[1] || {};
-  
-  var originalWidth = element.clientWidth;
-  var originalHeight = element.clientHeight;
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  var oldHeight = element.style.height;
-  var oldWidth = element.style.width;
-  var oldOpacity = Element.getInlineOpacity(element);
-
-  var direction = options.direction || 'center';
-  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
-  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
-  var opacityTransition = options.opacityTransition || Effect.Transitions.none;
-  
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.none
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();
   var moveX, moveY;
   
-  switch (direction) {
+  switch (options.direction) {
     case 'top-left':
       moveX = moveY = 0;
       break;
     case 'top-right':
-      moveX = originalWidth;
+      moveX = dims.width;
       moveY = 0;
       break;
     case 'bottom-left':
       moveX = 0;
-      moveY = originalHeight;
+      moveY = dims.height;
       break;
     case 'bottom-right':
-      moveX = originalWidth;
-      moveY = originalHeight;
+      moveX = dims.width;
+      moveY = dims.height;
       break;
     case 'center':  
-      moveX = originalWidth / 2;
-      moveY = originalHeight / 2;
+      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: opacityTransition }),
-      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}),
-      new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition })
+    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
     ], Object.extend({            
-         beforeStartInternal: function(effect) { 
-           Element.makePositioned(effect.effects[0].element);
-           Element.makeClipping(effect.effects[0].element);
-         },
+         beforeStartInternal: function(effect) {
+           effect.effects[0].element.makePositioned();
+           effect.effects[0].element.makeClipping(); },
          afterFinishInternal: function(effect) {
-           var el = effect.effects[0].element;
-           var els = el.style;
-           Element.hide(el);
-           Element.undoClipping(el); 
-           Element.undoPositioned(el);
-           els.top = oldTop;
-           els.left = oldLeft;
-           els.height = oldHeight;
-           els.width = oldWidth;
-           Element.setInlineOpacity(el, oldOpacity);
-         }
+           effect.effects[0].element.hide();
+           effect.effects[0].element.undoClipping();
+           effect.effects[0].element.undoPositioned();
+           effect.effects[0].element.setStyle(oldStyle); }
        }, options)
   );
 }
@@ -956,22 +910,23 @@ Effect.Shrink = function(element) {
 Effect.Pulsate = function(element) {
   element = $(element);
   var options    = arguments[1] || {};
-  var oldOpacity = Element.getInlineOpacity(element);
+  var oldOpacity = element.getInlineOpacity();
   var transition = options.transition || Effect.Transitions.sinoidal;
   var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
   reverser.bind(transition);
   return new Effect.Opacity(element, 
     Object.extend(Object.extend({  duration: 3.0, from: 0,
-      afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); }
+      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
     }, options), {transition: reverser}));
 }
 
 Effect.Fold = function(element) {
   element = $(element);
-  var originalTop = element.style.top;
-  var originalLeft = element.style.left;
-  var originalWidth = element.style.width;
-  var originalHeight = element.style.height;
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height };
   Element.makeClipping(element);
   return new Effect.Scale(element, 5, Object.extend({   
     scaleContent: false,
@@ -980,13 +935,24 @@ Effect.Fold = function(element) {
     new Effect.Scale(element, 1, { 
       scaleContent: false, 
       scaleY: false,
-      afterFinishInternal: function(effect) { 
-        Element.hide(effect.element);  
-        Element.undoClipping(effect.element); 
-        effect.element.style.top = originalTop;
-        effect.element.style.left = originalLeft;
-        effect.element.style.width = originalWidth;
-        effect.element.style.height = originalHeight;
+      afterFinishInternal: function(effect) {
+        effect.element.hide();
+        effect.element.undoClipping(); 
+        effect.element.setStyle(oldStyle);
       } });
   }}, arguments[1] || {}));
-}
+};
+
+['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( 
+  function(f) { Element.Methods[f] = Element[f]; }
+);
+
+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);
+};
+
+Element.addMethods();
\ No newline at end of file
index 0ba70a77da3d5281a8e5cbc48e818d791d9950cf..0caf9cd7f0b7d902b0dada3752de348ed27c02cd 100644 (file)
@@ -1,17 +1,14 @@
-/*  Prototype JavaScript framework, version 1.4.0_rc2
+/*  Prototype JavaScript framework, version 1.5.0_rc0
  *  (c) 2005 Sam Stephenson <sam@conio.net>
  *
- *  THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
- *  against the source tree, available from the Prototype darcs repository.
- *
  *  Prototype is freely distributable under the terms of an MIT-style license.
- *
  *  For details, see the Prototype web site: http://prototype.conio.net/
  *
 /*--------------------------------------------------------------------------*/
 
 var Prototype = {
-  Version: '1.4.0_rc2',
+  Version: '1.5.0_rc0',
+  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
 
   emptyFunction: function() {},
   K: function(x) {return x}
@@ -28,7 +25,7 @@ var Class = {
 var Abstract = new Object();
 
 Object.extend = function(destination, source) {
-  for (property in source) {
+  for (var property in source) {
     destination[property] = source[property];
   }
   return destination;
@@ -45,10 +42,10 @@ Object.inspect = function(object) {
   }
 }
 
-Function.prototype.bind = function(object) {
-  var __method = this;
+Function.prototype.bind = function() {
+  var __method = this, args = $A(arguments), object = args.shift();
   return function() {
-    return __method.apply(object, arguments);
+    return __method.apply(object, args.concat($A(arguments)));
   }
 }
 
@@ -119,30 +116,69 @@ PeriodicalExecuter.prototype = {
     }
   }
 }
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += (replacement(match) || '').toString();
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
 
-/*--------------------------------------------------------------------------*/
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
 
-function $() {
-  var elements = new Array();
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
 
-  for (var i = 0; i < arguments.length; i++) {
-    var element = arguments[i];
-    if (typeof element == 'string')
-      element = document.getElementById(element);
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return this;
+  },
 
-    if (arguments.length == 1)
-      return element;
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : this;
+  },
 
-    elements.push(element);
-  }
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
 
-  return elements;
-}
-Object.extend(String.prototype, {
   stripTags: function() {
     return this.replace(/<\/?[^>]+>/gi, '');
   },
 
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
   escapeHTML: function() {
     var div = document.createElement('div');
     var text = document.createTextNode(this);
@@ -186,12 +222,35 @@ Object.extend(String.prototype, {
   },
 
   inspect: function() {
-    return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
+    return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'";
   }
 });
 
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (typeof replacement == 'function') return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+}
+
 String.prototype.parseQuery = String.prototype.toQueryParams;
 
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern  = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    return this.template.gsub(this.pattern, function(match) {
+      var before = match[1];
+      if (before == '\\') return match[2];
+      return before + (object[match[3]] || '').toString();
+    });
+  }
+}
+
 var $break    = new Object();
 var $continue = new Object();
 
@@ -214,8 +273,8 @@ var Enumerable = {
   all: function(iterator) {
     var result = true;
     this.each(function(value, index) {
-      if (!(result &= (iterator || Prototype.K)(value, index)))
-        throw $break;
+      result = result && !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
     });
     return result;
   },
@@ -223,7 +282,7 @@ var Enumerable = {
   any: function(iterator) {
     var result = true;
     this.each(function(value, index) {
-      if (result &= (iterator || Prototype.K)(value, index))
+      if (result = !!(iterator || Prototype.K)(value, index))
         throw $break;
     });
     return result;
@@ -296,7 +355,7 @@ var Enumerable = {
     var result;
     this.each(function(value, index) {
       value = (iterator || Prototype.K)(value, index);
-      if (value >= (result || value))
+      if (result == undefined || value >= result)
         result = value;
     });
     return result;
@@ -306,7 +365,7 @@ var Enumerable = {
     var result;
     this.each(function(value, index) {
       value = (iterator || Prototype.K)(value, index);
-      if (value <= (result || value))
+      if (result == undefined || value < result)
         result = value;
     });
     return result;
@@ -358,8 +417,7 @@ var Enumerable = {
 
     var collections = [this].concat(args).map($A);
     return this.map(function(value, index) {
-      iterator(value = collections.pluck(index));
-      return value;
+      return iterator(collections.pluck(index));
     });
   },
 
@@ -376,6 +434,7 @@ Object.extend(Enumerable, {
   entries: Enumerable.toArray
 });
 var $A = Array.from = function(iterable) {
+  if (!iterable) return [];
   if (iterable.toArray) {
     return iterable.toArray();
   } else {
@@ -388,12 +447,20 @@ var $A = Array.from = function(iterable) {
 
 Object.extend(Array.prototype, Enumerable);
 
+if (!Array.prototype._reverse)
+  Array.prototype._reverse = Array.prototype.reverse;
+
 Object.extend(Array.prototype, {
   _each: function(iterator) {
     for (var i = 0; i < this.length; i++)
       iterator(this[i]);
   },
 
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
   first: function() {
     return this[0];
   },
@@ -410,7 +477,7 @@ Object.extend(Array.prototype, {
 
   flatten: function() {
     return this.inject([], function(array, value) {
-      return array.concat(value.constructor == Array ?
+      return array.concat(value && value.constructor == Array ?
         value.flatten() : [value]);
     });
   },
@@ -425,14 +492,11 @@ Object.extend(Array.prototype, {
   indexOf: function(object) {
     for (var i = 0; i < this.length; i++)
       if (this[i] == object) return i;
-    return false;
+    return -1;
   },
 
-  reverse: function() {
-    var result = [];
-    for (var i = this.length; i > 0; i--)
-      result.push(this[i-1]);
-    return result;
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
   },
 
   inspect: function() {
@@ -441,7 +505,7 @@ Object.extend(Array.prototype, {
 });
 var Hash = {
   _each: function(iterator) {
-    for (key in this) {
+    for (var key in this) {
       var value = this[key];
       if (typeof value == 'function') continue;
 
@@ -486,9 +550,9 @@ function $H(object) {
   Object.extend(hash, Hash);
   return hash;
 }
-var Range = Class.create();
-Object.extend(Range.prototype, Enumerable);
-Object.extend(Range.prototype, {
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
   initialize: function(start, end, exclusive) {
     this.start = start;
     this.end = end;
@@ -513,15 +577,15 @@ Object.extend(Range.prototype, {
 });
 
 var $R = function(start, end, exclusive) {
-  return new Range(start, end, exclusive);
+  return new ObjectRange(start, end, exclusive);
 }
 
 var Ajax = {
   getTransport: function() {
     return Try.these(
+      function() {return new XMLHttpRequest()},
       function() {return new ActiveXObject('Msxml2.XMLHTTP')},
-      function() {return new ActiveXObject('Microsoft.XMLHTTP')},
-      function() {return new XMLHttpRequest()}
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
     ) || false;
   },
 
@@ -549,8 +613,7 @@ Ajax.Responders = {
       if (responder[callback] && typeof responder[callback] == 'function') {
         try {
           responder[callback].apply(responder, [request, transport, json]);
-        } catch (e) {
-        }
+        } catch (e) {}
       }
     });
   }
@@ -574,6 +637,7 @@ Ajax.Base.prototype = {
     this.options = {
       method:       'post',
       asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
       parameters:   ''
     }
     Object.extend(this.options, options || {});
@@ -626,19 +690,18 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
       this.transport.send(this.options.method == 'post' ? body : null);
 
     } catch (e) {
-      (this.options.onException || Prototype.emptyFunction)(this, e);
-      Ajax.Responders.dispatch('onException', this, e);
+      this.dispatchException(e);
     }
   },
 
   setRequestHeaders: function() {
     var requestHeaders =
       ['X-Requested-With', 'XMLHttpRequest',
-       'X-Prototype-Version', Prototype.Version];
+       'X-Prototype-Version', Prototype.Version,
+       'Accept', 'text/javascript, text/html, application/xml, text/xml, */*'];
 
     if (this.options.method == 'post') {
-      requestHeaders.push('Content-type',
-        'application/x-www-form-urlencoded');
+      requestHeaders.push('Content-type', this.options.contentType);
 
       /* Force "Connection: close" for Mozilla browsers to work around
        * a bug where XMLHttpReqeuest sends an incorrect Content-length
@@ -661,12 +724,23 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
       this.respondToReadyState(this.transport.readyState);
   },
 
+  header: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) {}
+  },
+
   evalJSON: function() {
     try {
-      var json = this.transport.getResponseHeader('X-JSON'), object;
-      object = eval(json);
-      return object;
+      return eval('(' + this.header('X-JSON') + ')');
+    } catch (e) {}
+  },
+
+  evalResponse: function() {
+    try {
+      return eval(this.transport.responseText);
     } catch (e) {
+      this.dispatchException(e);
     }
   },
 
@@ -674,22 +748,38 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
     var event = Ajax.Request.Events[readyState];
     var transport = this.transport, json = this.evalJSON();
 
-    if (event == 'Complete')
-      (this.options['on' + this.transport.status]
-       || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
-       || Prototype.emptyFunction)(transport, json);
+    if (event == 'Complete') {
+      try {
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
 
-    (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
-    Ajax.Responders.dispatch('on' + event, this, transport, json);
+      if ((this.header('Content-type') || '').match(/^text\/javascript/i))
+        this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + event, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
 
     /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
     if (event == 'Complete')
       this.transport.onreadystatechange = Prototype.emptyFunction;
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
   }
 });
 
 Ajax.Updater = Class.create();
-Ajax.Updater.ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:<\/script>)';
 
 Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
   initialize: function(container, url, options) {
@@ -714,16 +804,16 @@ Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
   updateContent: function() {
     var receiver = this.responseIsSuccess() ?
       this.containers.success : this.containers.failure;
+    var response = this.transport.responseText;
 
-    var match    = new RegExp(Ajax.Updater.ScriptFragment, 'img');
-    var response = this.transport.responseText.replace(match, '');
-    var scripts  = this.transport.responseText.match(match);
+    if (!this.options.evalScripts)
+      response = response.stripScripts();
 
     if (receiver) {
       if (this.options.insertion) {
         new this.options.insertion(receiver, response);
       } else {
-        receiver.innerHTML = response;
+        Element.update(receiver, response);
       }
     }
 
@@ -731,14 +821,6 @@ Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
       if (this.onComplete)
         setTimeout(this.onComplete.bind(this), 10);
     }
-
-    if (this.options.evalScripts && scripts) {
-      match = new RegExp(Ajax.Updater.ScriptFragment, 'im');
-      setTimeout((function() {
-        for (var i = 0; i < scripts.length; i++)
-          eval(scripts[i].match(match)[1]);
-      }).bind(this), 10);
-    }
   }
 });
 
@@ -784,22 +866,57 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
     this.updater = new Ajax.Updater(this.container, this.url, this.options);
   }
 });
+function $() {
+  var results = [], element;
+  for (var i = 0; i < arguments.length; i++) {
+    element = arguments[i];
+    if (typeof element == 'string')
+      element = document.getElementById(element);
+    results.push(Element.extend(element));
+  }
+  return results.length < 2 ? results[0] : results;
+}
+
 document.getElementsByClassName = function(className, parentElement) {
   var children = ($(parentElement) || document.body).getElementsByTagName('*');
   return $A(children).inject([], function(elements, child) {
     if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
-      elements.push(child);
+      elements.push(Element.extend(child));
     return elements;
   });
 }
 
 /*--------------------------------------------------------------------------*/
 
-if (!window.Element) {
+if (!window.Element)
   var Element = new Object();
+
+Element.extend = function(element) {
+  if (!element) return;
+  if (_nativeExtensions) return element;
+
+  if (!element._extended && element.tagName && element != window) {
+    var methods = Element.Methods, cache = Element.extend.cache;
+    for (property in methods) {
+      var value = methods[property];
+      if (typeof value == 'function')
+        element[property] = cache.findOrStore(value);
+    }
+  }
+
+  element._extended = true;
+  return element;
 }
 
-Object.extend(Element, {
+Element.extend.cache = {
+  findOrStore: function(value) {
+    return this[value] = this[value] || function() {
+      return value.apply(null, [this].concat($A(arguments)));
+    }
+  }
+}
+
+Element.Methods = {
   visible: function(element) {
     return $(element).style.display != 'none';
   },
@@ -830,6 +947,24 @@ Object.extend(Element, {
     element.parentNode.removeChild(element);
   },
 
+  update: function(element, html) {
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+  },
+
+  replace: function(element, html) {
+    element = $(element);
+    if (element.outerHTML) {
+      element.outerHTML = html.stripScripts();
+    } else {
+      var range = element.ownerDocument.createRange();
+      range.selectNodeContents(element);
+      element.parentNode.replaceChild(
+        range.createContextualFragment(html.stripScripts()), element);
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+  },
+
   getHeight: function(element) {
     element = $(element);
     return element.offsetHeight;
@@ -868,6 +1003,13 @@ Object.extend(Element, {
     return $(element).innerHTML.match(/^\s*$/);
   },
 
+  childOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
   scrollTo: function(element) {
     element = $(element);
     var x = element.x ? element.x : element.offsetLeft,
@@ -893,6 +1035,12 @@ Object.extend(Element, {
     return value == 'auto' ? null : value;
   },
 
+  setStyle: function(element, style) {
+    element = $(element);
+    for (var name in style)
+      element.style[name.camelize()] = style[name];
+  },
+
   getDimensions: function(element) {
     element = $(element);
     if (Element.getStyle(element, 'display') != 'none')
@@ -955,7 +1103,32 @@ Object.extend(Element, {
     element.style.overflow = element._overflow;
     element._overflow = undefined;
   }
-});
+}
+
+Object.extend(Element, Element.Methods);
+
+var _nativeExtensions = false;
+
+if(!HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  var HTMLElement = {}
+  HTMLElement.prototype = document.createElement('div').__proto__;
+}
+
+Element.addMethods = function(methods) {
+  Object.extend(Element.Methods, methods || {});
+
+  if(typeof HTMLElement != 'undefined') {
+    var methods = Element.Methods, cache = Element.extend.cache;
+    for (property in methods) {
+      var value = methods[property];
+      if (typeof value == 'function')
+        HTMLElement.prototype[property] = cache.findOrStore(value);
+    }
+    _nativeExtensions = true;
+  }
+}
+
+Element.addMethods();
 
 var Toggle = new Object();
 Toggle.display = Element.toggle;
@@ -969,13 +1142,14 @@ Abstract.Insertion = function(adjacency) {
 Abstract.Insertion.prototype = {
   initialize: function(element, content) {
     this.element = $(element);
-    this.content = content;
+    this.content = content.stripScripts();
 
     if (this.adjacency && this.element.insertAdjacentHTML) {
       try {
         this.element.insertAdjacentHTML(this.adjacency, this.content);
       } catch (e) {
-        if (this.element.tagName.toLowerCase() == 'tbody') {
+        var tagName = this.element.tagName.toLowerCase();
+        if (tagName == 'tbody' || tagName == 'tr') {
           this.insertContent(this.contentFromAnonymousTable());
         } else {
           throw e;
@@ -986,6 +1160,8 @@ Abstract.Insertion.prototype = {
       if (this.initializeRange) this.initializeRange();
       this.insertContent([this.range.createContextualFragment(this.content)]);
     }
+
+    setTimeout(function() {content.evalScripts()}, 10);
   },
 
   contentFromAnonymousTable: function() {
@@ -1018,7 +1194,7 @@ Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
   },
 
   insertContent: function(fragments) {
-    fragments.reverse().each((function(fragment) {
+    fragments.reverse(false).each((function(fragment) {
       this.element.insertBefore(fragment, this.element.firstChild);
     }).bind(this));
   }
@@ -1079,7 +1255,7 @@ Element.ClassNames.prototype = {
     if (!this.include(classNameToRemove)) return;
     this.set(this.select(function(className) {
       return className != classNameToRemove;
-    }));
+    }).join(' '));
   },
 
   toString: function() {
@@ -1088,6 +1264,116 @@ Element.ClassNames.prototype = {
 }
 
 Object.extend(Element.ClassNames.prototype, Enumerable);
+var Selector = Class.create();
+Selector.prototype = {
+  initialize: function(expression) {
+    this.params = {classNames: []};
+    this.expression = expression.toString().strip();
+    this.parseExpression();
+    this.compileMatcher();
+  },
+
+  parseExpression: function() {
+    function abort(message) { throw 'Parse error in selector: ' + message; }
+
+    if (this.expression == '')  abort('empty expression');
+
+    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];
+    }
+
+    if (expr == '*') return this.params.wildcard = true;
+
+    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());
+      }
+      expr = rest;
+    }
+
+    if (expr.length > 0) abort(expr.inspect());
+  },
+
+  buildMatchExpression: function() {
+    var params = this.params, conditions = [], clause;
+
+    if (params.wildcard)
+      conditions.push('true');
+    if (clause = params.id)
+      conditions.push('element.id == ' + clause.inspect());
+    if (clause = params.tagName)
+      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
+    if ((clause = params.classNames).length > 0)
+      for (var i = 0; i < clause.length; i++)
+        conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
+    if (clause = params.attributes) {
+      clause.each(function(attribute) {
+        var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
+        var splitValueBy = function(delimiter) {
+          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
+        }
+
+        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(value + ' != null'); break;
+          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
+        }
+      });
+    }
+
+    return conditions.join(' && ');
+  },
+
+  compileMatcher: function() {
+    this.match = new Function('element', 'if (!element.tagName) return false; \
+      return ' + this.buildMatchExpression());
+  },
+
+  findElements: function(scope) {
+    var element;
+
+    if (element = $(this.params.id))
+      if (this.match(element))
+        if (!scope || Element.childOf(element, scope))
+          return [element];
+
+    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+
+    var results = [];
+    for (var i = 0; i < scope.length; i++)
+      if (this.match(element = scope[i]))
+        results.push(Element.extend(element));
+
+    return results;
+  },
+
+  toString: function() {
+    return this.expression;
+  }
+}
+
+function $$() {
+  return $A(arguments).map(function(expression) {
+    return expression.strip().split(/\s+/).inject([null], function(results, expr) {
+      var selector = new Selector(expr);
+      return results.map(selector.findElements.bind(selector)).flatten();
+    });
+  }).flatten();
+}
 var Field = {
   clear: function() {
     for (var i = 0; i < arguments.length; i++)
@@ -1109,8 +1395,10 @@ var Field = {
   },
 
   activate: function(element) {
-    $(element).focus();
-    $(element).select();
+    element = $(element);
+    element.focus();
+    if (element.select)
+      element.select();
   }
 }
 
@@ -1134,7 +1422,7 @@ var Form = {
     form = $(form);
     var elements = new Array();
 
-    for (tagName in Form.Element.Serializers) {
+    for (var tagName in Form.Element.Serializers) {
       var tagElements = form.getElementsByTagName(tagName);
       for (var j = 0; j < tagElements.length; j++)
         elements.push(tagElements[j]);
@@ -1178,16 +1466,15 @@ var Form = {
     }
   },
 
+  findFirstElement: function(form) {
+    return Form.getElements(form).find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
   focusFirstElement: function(form) {
-    form = $(form);
-    var elements = Form.getElements(form);
-    for (var i = 0; i < elements.length; i++) {
-      var element = elements[i];
-      if (element.type != 'hidden' && !element.disabled) {
-        Field.activate(element);
-        break;
-      }
-    }
+    Field.activate(Form.findFirstElement(form));
   },
 
   reset: function(form) {
@@ -1201,9 +1488,17 @@ Form.Element = {
     var method = element.tagName.toLowerCase();
     var parameter = Form.Element.Serializers[method](element);
 
-    if (parameter)
-      return encodeURIComponent(parameter[0]) + '=' +
-        encodeURIComponent(parameter[1]);
+    if (parameter) {
+      var key = encodeURIComponent(parameter[0]);
+      if (key.length == 0) return;
+
+      if (parameter[1].constructor != Array)
+        parameter[1] = [parameter[1]];
+
+      return parameter[1].map(function(value) {
+        return key + '=' + encodeURIComponent(value);
+      }).join('&');
+    }
   },
 
   getValue: function(element) {
@@ -1249,23 +1544,17 @@ Form.Element.Serializers = {
     var value = '', opt, index = element.selectedIndex;
     if (index >= 0) {
       opt = element.options[index];
-      value = opt.value;
-      if (!value && !('value' in opt))
-        value = opt.text;
+      value = opt.value || opt.text;
     }
     return [element.name, value];
   },
 
   selectMany: function(element) {
-    var value = new Array();
+    var value = [];
     for (var i = 0; i < element.length; i++) {
       var opt = element.options[i];
-      if (opt.selected) {
-        var optValue = opt.value;
-        if (!optValue && !('value' in opt))
-          optValue = opt.text;
-        value.push(optValue);
-      }
+      if (opt.selected)
+        value.push(opt.value || opt.text);
     }
     return [element.name, value];
   }
@@ -1349,24 +1638,14 @@ Abstract.EventObserver.prototype = {
       switch (element.type.toLowerCase()) {
         case 'checkbox':
         case 'radio':
-          element.target = this;
-          element.prev_onclick = element.onclick || Prototype.emptyFunction;
-          element.onclick = function() {
-            this.prev_onclick();
-            this.target.onElementEvent();
-          }
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
           break;
         case 'password':
         case 'text':
         case 'textarea':
         case 'select-one':
         case 'select-multiple':
-          element.target = this;
-          element.prev_onchange = element.onchange || Prototype.emptyFunction;
-          element.onchange = function() {
-            this.prev_onchange();
-            this.target.onElementEvent();
-          }
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
           break;
       }
     }
@@ -1492,7 +1771,8 @@ Object.extend(Event, {
 });
 
 /* prevent memory leaks in IE */
-Event.observe(window, 'unload', Event.unloadCache, false);
+if (navigator.appVersion.match(/\bMSIE\b/))
+  Event.observe(window, 'unload', Event.unloadCache, false);
 var Position = {
   // set to true if needed, warning: firefox performance problems
   // NOT neeeded for page scrolling, only if draggable contained in
index f2314fd57320619487693ad5b6fc3e4cb457d4e0..7b07d46a30131ad938be2400261b722ed82ef3cc 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/about'
\ No newline at end of file
index 9ed1faf9a1f77d001fa94470c3d00577a7c7d140..64af76edd40190569b170f3e02d80ba0cdbcf835 100755 (executable)
@@ -1,5 +1,3 @@
-#!/usr/bin/ruby1.8
-
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/breakpointer'
\ No newline at end of file
index 0227d7ba8d2d6b5100efd77174ebe4eabf0fce02..42f28f7d6d3d6510cb88436de2742ca323c51b07 100755 (executable)
@@ -1,5 +1,3 @@
-#!/usr/bin/ruby1.8
-
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/console'
\ No newline at end of file
index 893b70b16642513b7369046b2e1f317635fe77c6..fa0e6fcd0b2b2b4514f9042103e30d9613e64ae5 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/destroy'
\ No newline at end of file
index e3e110a2d49dd0023ffe06ddc754c3545bedba14..ef976e09f35cee42521f7c4715550cddd5342bc3 100755 (executable)
@@ -1,5 +1,3 @@
-#!/usr/bin/ruby1.8
-
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/generate'
\ No newline at end of file
index 654d5422b64152f93d9b7c621549420b22007890..c842d35d339c45800583a252dc2dbe280d967ad9 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../../config/boot'
 require 'commands/performance/benchmarker'
index f6df3e45b374690c2f68a16ee13c4ed63b9a8cfd..d855ac8b1383a4a320fef98b75f33774e69207f8 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../../config/boot'
 require 'commands/performance/profiler'
index fe1011bddc921463537c0d9ccad8335d125fd278..26ca64c06b26fc7825788af9da56a50abeea0f60 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/plugin'
\ No newline at end of file
index 44beefc57cbafad513957b1cbb5bb059fb1c2a00..c77f04535f97fe2b973707033dbcb48732965842 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../../config/boot'
 require 'commands/process/reaper'
index 36e57e5d72dc8b804d8281e9c4562acc5ee7e4e0..7118f3983c75075a6b276d77e060aef68551fb57 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../../config/boot'
 require 'commands/process/spawner'
index f0c193fec85a0021f58764ec9ff581043ee556de..ccc30f9d2436b5370ab9d28f15df94694f389a2c 100755 (executable)
@@ -1,3 +1,3 @@
-#!/usr/bin/ruby1.8
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/runner'
\ No newline at end of file
index a8cf6e2a3c88b307df24da6cf9cd51f67cd154f5..dfabcb88128f41f0fdab1711b696274e166f8bc0 100755 (executable)
@@ -1,5 +1,3 @@
-#!/usr/bin/ruby1.8
-
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
 require File.dirname(__FILE__) + '/../config/boot'
 require 'commands/server'
\ No newline at end of file
index 86620633864fd5a55fd719c1cc6d34ad2cc5d478..945c9b46d684f08ec84cb316e1dc0061e361f794 120000 (symlink)
@@ -1 +1 @@
-/home/mako/votingbooth/vendor
\ No newline at end of file
+.
\ No newline at end of file

Benjamin Mako Hill || Want to submit a patch?