From 09cd438e77dd418b03b67e3e6a4b48ec95b72d1d Mon Sep 17 00:00:00 2001 From: Neil Smith Date: Mon, 2 Feb 2009 20:08:35 +0000 Subject: [PATCH] Redid up to end of Chapter 6 under Rails2.2.2 and Gems 1.3.1, with Product.date_available --- README | 256 + Rakefile | 10 + app/controllers/application.rb | 15 + app/controllers/products_controller.rb | 85 + app/helpers/application_helper.rb | 3 + app/helpers/products_helper.rb | 2 + app/models/product.rb | 14 + app/views/layouts/products.html.erb | 17 + app/views/products/edit.html.erb | 32 + app/views/products/index.html.erb | 34 + app/views/products/new.html.erb | 31 + app/views/products/show.html.erb | 28 + config/boot.rb | 109 + config/database.yml | 45 + config/environment.rb | 75 + config/environments/development.rb | 17 + config/environments/production.rb | 24 + config/environments/test.rb | 22 + config/initializers/inflections.rb | 10 + config/initializers/mime_types.rb | 5 + config/initializers/new_rails_defaults.rb | 17 + config/locales/en.yml | 5 + config/routes.rb | 45 + db/migrate/20090202160210_create_products.rb | 15 + .../20090202160745_add_price_to_product.rb | 10 + ...202160925_add_date_available_to_product.rb | 9 + db/migrate/20090202163608_add_test_data.rb | 71 + db/schema.rb | 24 + doc/README_FOR_APP | 5 + log/development.log | 798 +++ log/production.log | 0 log/server.log | 0 log/test.log | 0 nbproject/project.properties | 2 + nbproject/project.xml | 9 + public/404.html | 30 + public/422.html | 30 + public/500.html | 33 + public/dispatch.cgi | 10 + public/dispatch.fcgi | 24 + public/dispatch.rb | 10 + public/favicon.ico | 0 public/images/can-of-air.jpg | Bin 0 -> 10384 bytes public/images/cardboard-box.jpg | Bin 0 -> 3590 bytes public/images/daisy.gif | Bin 0 -> 7939 bytes public/images/rails.png | Bin 0 -> 6646 bytes public/images/snowball.jpg | Bin 0 -> 25523 bytes public/index.html | 274 ++ public/javascripts/application.js | 2 + public/javascripts/controls.js | 963 ++++ public/javascripts/dragdrop.js | 973 ++++ public/javascripts/effects.js | 1128 +++++ public/javascripts/prototype.js | 4320 +++++++++++++++++ public/robots.txt | 5 + public/stylesheets/depot.css | 145 + public/stylesheets/scaffold.css | 54 + script/about | 4 + script/console | 3 + script/dbconsole | 3 + script/destroy | 3 + script/generate | 3 + script/performance/benchmarker | 3 + script/performance/profiler | 3 + script/performance/request | 3 + script/plugin | 3 + script/process/inspector | 3 + script/process/reaper | 3 + script/process/spawner | 3 + script/runner | 3 + script/server | 3 + test/fixtures/products.yml | 11 + test/functional/products_controller_test.rb | 45 + test/performance/browsing_test.rb | 9 + test/test_helper.rb | 38 + test/unit/product_test.rb | 8 + 75 files changed, 9999 insertions(+) create mode 100644 README create mode 100644 Rakefile create mode 100644 app/controllers/application.rb create mode 100644 app/controllers/products_controller.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/helpers/products_helper.rb create mode 100644 app/models/product.rb create mode 100644 app/views/layouts/products.html.erb create mode 100644 app/views/products/edit.html.erb create mode 100644 app/views/products/index.html.erb create mode 100644 app/views/products/new.html.erb create mode 100644 app/views/products/show.html.erb create mode 100644 config/boot.rb create mode 100644 config/database.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/initializers/mime_types.rb create mode 100644 config/initializers/new_rails_defaults.rb create mode 100644 config/locales/en.yml create mode 100644 config/routes.rb create mode 100644 db/migrate/20090202160210_create_products.rb create mode 100644 db/migrate/20090202160745_add_price_to_product.rb create mode 100644 db/migrate/20090202160925_add_date_available_to_product.rb create mode 100644 db/migrate/20090202163608_add_test_data.rb create mode 100644 db/schema.rb create mode 100644 doc/README_FOR_APP create mode 100644 log/development.log create mode 100644 log/production.log create mode 100644 log/server.log create mode 100644 log/test.log create mode 100644 nbproject/project.properties create mode 100644 nbproject/project.xml create mode 100644 public/404.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100755 public/dispatch.cgi create mode 100755 public/dispatch.fcgi create mode 100755 public/dispatch.rb create mode 100644 public/favicon.ico create mode 100644 public/images/can-of-air.jpg create mode 100644 public/images/cardboard-box.jpg create mode 100644 public/images/daisy.gif create mode 100644 public/images/rails.png create mode 100644 public/images/snowball.jpg create mode 100644 public/index.html create mode 100644 public/javascripts/application.js create mode 100644 public/javascripts/controls.js create mode 100644 public/javascripts/dragdrop.js create mode 100644 public/javascripts/effects.js create mode 100644 public/javascripts/prototype.js create mode 100644 public/robots.txt create mode 100644 public/stylesheets/depot.css create mode 100644 public/stylesheets/scaffold.css create mode 100755 script/about create mode 100755 script/console create mode 100755 script/dbconsole create mode 100755 script/destroy create mode 100755 script/generate create mode 100755 script/performance/benchmarker create mode 100755 script/performance/profiler create mode 100755 script/performance/request create mode 100755 script/plugin create mode 100755 script/process/inspector create mode 100755 script/process/reaper create mode 100755 script/process/spawner create mode 100755 script/runner create mode 100755 script/server create mode 100644 test/fixtures/products.yml create mode 100644 test/functional/products_controller_test.rb create mode 100644 test/performance/browsing_test.rb create mode 100644 test/test_helper.rb create mode 100644 test/unit/product_test.rb diff --git a/README b/README new file mode 100644 index 0000000..2af0fb1 --- /dev/null +++ b/README @@ -0,0 +1,256 @@ +== Welcome to Rails + +Rails is a web-application framework that includes everything needed to create +database-backed web applications according to the Model-View-Control pattern. + +This pattern splits the view (also called the presentation) into "dumb" templates +that are primarily responsible for inserting pre-built data in between HTML tags. +The model contains the "smart" domain objects (such as Account, Product, Person, +Post) that holds all the business logic and knows how to persist themselves to +a database. The controller handles the incoming requests (such as Save New Account, +Update Product, Show Post) by manipulating the model and directing data to the view. + +In Rails, the model is handled by what's called an object-relational mapping +layer entitled Active Record. This layer allows you to present the data from +database rows as objects and embellish these data objects with business logic +methods. You can read more about Active Record in +link:files/vendor/rails/activerecord/README.html. + +The controller and view are handled by the Action Pack, which handles both +layers by its two parts: Action View and Action Controller. These two layers +are bundled in a single package due to their heavy interdependence. This is +unlike the relationship between the Active Record and Action Pack that is much +more separate. Each of these packages can be used independently outside of +Rails. You can read more about Action Pack in +link:files/vendor/rails/actionpack/README.html. + + +== Getting Started + +1. At the command prompt, start a new Rails application using the rails command + and your application name. Ex: rails myapp +2. Change directory into myapp and start the web server: script/server (run with --help for options) +3. Go to http://localhost:3000/ and get "Welcome aboard: You're riding the Rails!" +4. Follow the guidelines to start developing your application + + +== Web Servers + +By default, Rails will try to use Mongrel and lighttpd if they are installed, otherwise +Rails will use WEBrick, the webserver that ships with Ruby. When you run script/server, +Rails will check if Mongrel exists, then lighttpd and finally fall back to WEBrick. This ensures +that you can always get up and running quickly. + +Mongrel is a Ruby-based webserver with a C component (which requires compilation) that is +suitable for development and deployment of Rails applications. If you have Ruby Gems installed, +getting up and running with mongrel is as easy as: gem install mongrel. +More info at: http://mongrel.rubyforge.org + +If Mongrel is not installed, Rails will look for lighttpd. It's considerably faster than +Mongrel and WEBrick and also suited for production use, but requires additional +installation and currently only works well on OS X/Unix (Windows users are encouraged +to start with Mongrel). We recommend version 1.4.11 and higher. You can download it from +http://www.lighttpd.net. + +And finally, if neither Mongrel or lighttpd are installed, Rails will use the built-in Ruby +web server, WEBrick. WEBrick is a small Ruby web server suitable for development, but not +for production. + +But of course its also possible to run Rails on any platform that supports FCGI. +Apache, LiteSpeed, IIS are just a few. For more information on FCGI, +please visit: http://wiki.rubyonrails.com/rails/pages/FastCGI + + +== Apache .htaccess example + +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "

Application error

Rails application failed to start properly" + + +== Debugging Rails + +Sometimes your application goes wrong. Fortunately there are a lot of tools that +will help you debug it and get it back on the rails. + +First area to check is the application log files. Have "tail -f" commands running +on the server.log and development.log. Rails will automatically display debugging +and runtime information to these files. Debugging info will also be shown in the +browser on requests from 127.0.0.1. + +You can also log your own messages directly into the log file from your code using +the Ruby logger class from inside your controllers. Example: + + class WeblogController < ActionController::Base + def destroy + @weblog = Weblog.find(params[:id]) + @weblog.destroy + logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") + end + end + +The result will be a message in your log file along the lines of: + + Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1 + +More information on how to use the logger is at http://www.ruby-doc.org/core/ + +Also, Ruby documentation can be found at http://www.ruby-lang.org/ including: + +* The Learning Ruby (Pickaxe) Book: http://www.ruby-doc.org/docs/ProgrammingRuby/ +* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) + +These two online (and free) books will bring you up to speed on the Ruby language +and also on programming in general. + + +== Debugger + +Debugger support is available through the debugger command when you start your Mongrel or +Webrick server with --debugger. This means that you can break out of execution at any point +in the code, investigate and change the model, AND then resume execution! +You need to install ruby-debug to run the server in debugging mode. With gems, use 'gem install ruby-debug' +Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.find(:all) + debugger + end + end + +So the controller will accept the action, run the first line, then present you +with a IRB prompt in the server window. Here you can do things like: + + >> @posts.inspect + => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>, + #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" + >> @posts.first.title = "hello from a debugger" + => "hello from a debugger" + +...and even better is that you can examine how your runtime objects actually work: + + >> f = @posts.first + => #nil, "body"=>nil, "id"=>"1"}> + >> f. + Display all 152 possibilities? (y or n) + +Finally, when you're ready to resume execution, you enter "cont" + + +== Console + +You can interact with the domain model by starting the console through script/console. +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 script/console production. + +To reload your controllers and models after launching the console run reload! + +== dbconsole + +You can go to the command line of your database directly through script/dbconsole. +You would be connected to the database with the credentials defined in database.yml. +Starting the script without arguments will connect you to the development database. Passing an +argument will connect you to a different database, like script/dbconsole production. +Currently works for mysql, postgresql and sqlite. + +== Description of Contents + +app + Holds all the code that's specific to this particular application. + +app/controllers + Holds controllers that should be named like weblogs_controller.rb for + automated URL mapping. All controllers should descend from ApplicationController + which itself descends from ActionController::Base. + +app/models + Holds models that should be named like post.rb. + Most models will descend from ActiveRecord::Base. + +app/views + Holds the template files for the view that should be named like + weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby + syntax. + +app/views/layouts + Holds the template files for layouts to be used with views. This models the common + header/footer method of wrapping views. In your views, define a layout using the + layout :default and create a file named default.html.erb. Inside default.html.erb, + call <% yield %> to render the view using this layout. + +app/helpers + Holds view helpers that should be named like weblogs_helper.rb. These are generated + for you automatically when using script/generate for controllers. Helpers can be used to + wrap functionality for your views into methods. + +config + Configuration files for the Rails environment, the routing map, the database, and other dependencies. + +db + Contains the database schema in schema.rb. db/migrate contains all + the sequence of Migrations for your schema. + +doc + This directory is where your application documentation will be stored when generated + using rake doc:app + +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. + +public + The directory available for the web server. Contains subdirectories for images, stylesheets, + and javascripts. Also contains the dispatchers and the default HTML files. This should be + set as the DOCUMENT_ROOT of your web server. + +script + Helper scripts for automation and generation. + +test + Unit and functional tests along with fixtures. When using the script/generate scripts, template + test files will be generated for you and placed in this directory. + +vendor + External libraries that the application depends on. Also includes the plugins subdirectory. + If the app has frozen rails, those gems also go here, under vendor/rails/. + This directory is in the load path. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..3bb0e85 --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff --git a/app/controllers/application.rb b/app/controllers/application.rb new file mode 100644 index 0000000..8dfb326 --- /dev/null +++ b/app/controllers/application.rb @@ -0,0 +1,15 @@ +# Filters added to this controller apply to all controllers in the application. +# Likewise, all the methods added will be available for all controllers. + +class ApplicationController < ActionController::Base + helper :all # include all helpers, all the time + + # See ActionController::RequestForgeryProtection for details + # Uncomment the :secret if you're not using the cookie session store + protect_from_forgery # :secret => 'd7e9713fb540572dab37a045152d442a' + + # See ActionController::Base for details + # Uncomment this to filter the contents of submitted sensitive data parameters + # from your application log (in this case, all fields with names like "password"). + # filter_parameter_logging :password +end diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb new file mode 100644 index 0000000..1685ca5 --- /dev/null +++ b/app/controllers/products_controller.rb @@ -0,0 +1,85 @@ +class ProductsController < ApplicationController + # GET /products + # GET /products.xml + def index + @products = Product.find(:all) + + respond_to do |format| + format.html # index.html.erb + format.xml { render :xml => @products } + end + end + + # GET /products/1 + # GET /products/1.xml + def show + @product = Product.find(params[:id]) + + respond_to do |format| + format.html # show.html.erb + format.xml { render :xml => @product } + end + end + + # GET /products/new + # GET /products/new.xml + def new + @product = Product.new + + respond_to do |format| + format.html # new.html.erb + format.xml { render :xml => @product } + end + end + + # GET /products/1/edit + def edit + @product = Product.find(params[:id]) + end + + # POST /products + # POST /products.xml + def create + @product = Product.new(params[:product]) + + respond_to do |format| + if @product.save + flash[:notice] = 'Product was successfully created.' + format.html { redirect_to(@product) } + format.xml { render :xml => @product, :status => :created, :location => @product } + else + format.html { render :action => "new" } + format.xml { render :xml => @product.errors, :status => :unprocessable_entity } + end + end + end + + # PUT /products/1 + # PUT /products/1.xml + def update + @product = Product.find(params[:id]) + + respond_to do |format| + if @product.update_attributes(params[:product]) + flash[:notice] = 'Product was successfully updated.' + format.html { redirect_to(@product) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @product.errors, :status => :unprocessable_entity } + end + end + end + + # DELETE /products/1 + # DELETE /products/1.xml + def destroy + @product = Product.find(params[:id]) + @product.destroy + + respond_to do |format| + format.html { redirect_to(products_url) } + format.xml { head :ok } + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..22a7940 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,3 @@ +# Methods added to this helper will be available to all templates in the application. +module ApplicationHelper +end diff --git a/app/helpers/products_helper.rb b/app/helpers/products_helper.rb new file mode 100644 index 0000000..ab5c42b --- /dev/null +++ b/app/helpers/products_helper.rb @@ -0,0 +1,2 @@ +module ProductsHelper +end diff --git a/app/models/product.rb b/app/models/product.rb new file mode 100644 index 0000000..4cd770b --- /dev/null +++ b/app/models/product.rb @@ -0,0 +1,14 @@ +class Product < ActiveRecord::Base + validates_presence_of :title, :description, :image_url + validates_numericality_of :price + validate :price_must_be_at_least_a_penny + validates_uniqueness_of :title + validates_format_of :image_url, + :with => %r{\.(gif|jpg|png)$}i, + :message => "must be a URL for GIF, JPG, or PNG image." + +protected + def price_must_be_at_least_a_penny + errors.add(:price, 'should be at least 0.01') if price.nil? || price < 0.01 + end +end diff --git a/app/views/layouts/products.html.erb b/app/views/layouts/products.html.erb new file mode 100644 index 0000000..07fed7f --- /dev/null +++ b/app/views/layouts/products.html.erb @@ -0,0 +1,17 @@ + + + + + + Products: <%= controller.action_name %> + <%= stylesheet_link_tag 'scaffold', 'depot' %> + + + +

<%= flash[:notice] %>

+ +<%= yield %> + + + diff --git a/app/views/products/edit.html.erb b/app/views/products/edit.html.erb new file mode 100644 index 0000000..18efd55 --- /dev/null +++ b/app/views/products/edit.html.erb @@ -0,0 +1,32 @@ +

Editing product

+ +<% form_for(@product) do |f| %> + <%= f.error_messages %> + +

+ <%= f.label :title %>
+ <%= f.text_field :title %> +

+

+ <%= f.label :description %>
+ <%= f.text_area :description %> +

+

+ <%= f.label :image_url %>
+ <%= f.text_field :image_url %> +

+

+ <%= f.label :price %>
+ <%= f.text_field :price %> +

+

+ <%= f.label :date_available %>
+ <%= f.date_select :date_available, :order => [:day, :month, :year] %> +

+

+ <%= f.submit "Update" %> +

+<% end %> + +<%= link_to 'Show', @product %> | +<%= link_to 'Back', products_path %> diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb new file mode 100644 index 0000000..ef89bd9 --- /dev/null +++ b/app/views/products/index.html.erb @@ -0,0 +1,34 @@ +
+

Listing products

+ + + <% for product in @products %> + + + + + + + <% end %> +
+ <%= image_tag product.image_url, :class => 'list-image' %> + +
+
<%=h product.title %>
+
<%=h truncate(product.description.gsub(/<.*?>/, '|'), + :length => 80) %>
+
<%=number_to_currency product.price, :unit => "£" %> + <% if product.date_available.past? %> +
Available since <%= product.date_available %>
+ <% else %> +
Available from <%= product.date_available %>
+ <% end %> +
+
+ <%= link_to 'Show', product %>
+ <%= link_to 'Edit', edit_product_path(product) %>
+ <%= link_to 'Destroy', product, :confirm => 'Are you sure?', :method => :delete %>
+
+
+
+<%= link_to 'New product', new_product_path %> diff --git a/app/views/products/new.html.erb b/app/views/products/new.html.erb new file mode 100644 index 0000000..9eac9ae --- /dev/null +++ b/app/views/products/new.html.erb @@ -0,0 +1,31 @@ +

New product

+ +<% form_for(@product) do |f| %> + <%= f.error_messages %> + +

+ <%= f.label :title %>
+ <%= f.text_field :title %> +

+

+ <%= f.label :description %>
+ <%= f.text_area :description %> +

+

+ <%= f.label :image_url %>
+ <%= f.text_field :image_url %> +

+

+ <%= f.label :price %>
+ <%= f.text_field :price %> +

+

+ <%= f.label :date_available %>
+ <%= f.date_select :date_available, :order => [:day, :month, :year] %> +

+

+ <%= f.submit "Create" %> +

+<% end %> + +<%= link_to 'Back', products_path %> diff --git a/app/views/products/show.html.erb b/app/views/products/show.html.erb new file mode 100644 index 0000000..fb3631e --- /dev/null +++ b/app/views/products/show.html.erb @@ -0,0 +1,28 @@ +

+ Title: + <%=h @product.title %> +

+ +

+ Description: + <%=h @product.description %> +

+ +

+ Image url: + <%=h @product.image_url %> +

+ +

+ Price: + <%=h @product.price %> +

+ +

+ Date available: + <%=h @product.date_available %> +

+ + +<%= link_to 'Edit', edit_product_path(@product) %> | +<%= link_to 'Back', products_path %> diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..0a51688 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,109 @@ +# Don't change this file! +# Configure your app in config/environment.rb and config/environments/*.rb + +RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) + +module Rails + class << self + def boot! + unless booted? + preinitialize + pick_boot.run + end + end + + def booted? + defined? Rails::Initializer + end + + def pick_boot + (vendor_rails? ? VendorBoot : GemBoot).new + end + + def vendor_rails? + File.exist?("#{RAILS_ROOT}/vendor/rails") + end + + def preinitialize + load(preinitializer_path) if File.exist?(preinitializer_path) + end + + def preinitializer_path + "#{RAILS_ROOT}/config/preinitializer.rb" + end + end + + class Boot + def run + load_initializer + Rails::Initializer.run(:set_load_path) + end + end + + class VendorBoot < Boot + def load_initializer + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:install_gem_spec_stubs) + end + end + + class GemBoot < Boot + def load_initializer + self.class.load_rubygems + load_rails_gem + require 'initializer' + end + + def load_rails_gem + if version = self.class.gem_version + gem 'rails', version + else + gem 'rails' + end + rescue Gem::LoadError => load_error + $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) + exit 1 + end + + class << self + def rubygems_version + Gem::RubyGemsVersion rescue nil + end + + def gem_version + if defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION + elsif ENV.include?('RAILS_GEM_VERSION') + ENV['RAILS_GEM_VERSION'] + else + parse_gem_version(read_environment_rb) + end + end + + def load_rubygems + require 'rubygems' + min_version = '1.3.1' + unless rubygems_version >= min_version + $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) + exit 1 + end + + rescue LoadError + $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) + exit 1 + end + + def parse_gem_version(text) + $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ + end + + private + def read_environment_rb + File.read("#{RAILS_ROOT}/config/environment.rb") + end + end + end +end + +# All that for this: +Rails.boot! diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..f955bb4 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,45 @@ +# MySQL. Versions 4.1 and 5.0 are recommended. +# +# Install the MySQL driver: +# gem install mysql +# On Mac OS X: +# sudo gem install mysql -- --with-mysql-dir=/usr/local/mysql +# On Mac OS X Leopard: +# sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config +# This sets the ARCHFLAGS environment variable to your native architecture +# On Windows: +# gem install mysql +# Choose the win32 build. +# Install MySQL and put its /bin directory on your path. +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +development: + adapter: mysql + encoding: utf8 + database: depot_development + pool: 5 + username: depot + password: + socket: /var/run/mysqld/mysqld.sock + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: mysql + encoding: utf8 + database: depot_test + pool: 5 + username: depot + password: + socket: /var/run/mysqld/mysqld.sock + +production: + adapter: mysql + encoding: utf8 + database: depot_production + pool: 5 + username: root + password: + socket: /var/run/mysqld/mysqld.sock diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..3884979 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,75 @@ +# Be sure to restart your server when you modify this file + +# 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 = '2.2.2' unless defined? RAILS_GEM_VERSION + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + # See Rails::Configuration for more options. + + # Skip frameworks you're not going to use. To use Rails without a database + # you must remove the Active Record framework. + # config.frameworks -= [ :active_record, :active_resource, :action_mailer ] + + # Specify gems that this application depends on. + # They can then be installed with "rake gems:install" on new installations. + # You have to specify the :lib option for libraries, where the Gem name (sqlite3-ruby) differs from the file itself (sqlite3) + # config.gem "bj" + # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net" + # config.gem "sqlite3-ruby", :lib => "sqlite3" + # config.gem "aws-s3", :lib => "aws/s3" + + # Only load the plugins named here, in the order given. By default, all plugins + # in vendor/plugins are loaded in alphabetical order. + # :all can be used as a placeholder for all plugins not explicitly named + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/extras ) + + # Force all environments to use the same logger level + # (by default production uses :info, the others :debug) + # config.log_level = :debug + + # Make Time.zone default to the specified zone, and make Active Record store time values + # in the database in UTC, and return them converted to the specified local zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Comment line to use default local time. + config.time_zone = 'UTC' + + # The internationalization framework can be changed to have another default locale (standard is :en) or more load paths. + # All files from config/locales/*.rb,yml are added automatically. + # config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'my', 'locales', '*.{rb,yml}')] + # config.i18n.default_locale = :de + + # Your secret key for verifying cookie session data integrity. + # If you change this key, all old sessions will become invalid! + # Make sure the secret is at least 30 characters and all random, + # no regular words or you'll be exposed to dictionary attacks. + config.action_controller.session = { + :session_key => '_depot_session', + :secret => '85857e9647cd79624deed16baa3dfca210a27d35a6351c8712d97550affaec58ae5c9fc8551f9902dd0e35aaf6cfdd7450181eb3f2aafbeb832f501eb4506007' + } + + # Use the database for sessions instead of the cookie-based default, + # which shouldn't be used to store highly confidential information + # (create the session table with "rake db:sessions:create") + # config.action_controller.session_store = :active_record_store + + # 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 + # Please note that observers generated using script/generate observer need to have an _observer suffix + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer +end diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..85c9a60 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,17 @@ +# 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 + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_view.debug_rjs = true +config.action_controller.perform_caching = false + +# Don't care if the mailer can't send +config.action_mailer.raise_delivery_errors = false \ No newline at end of file diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..ec5b7bc --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,24 @@ +# 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 + +# Enable threaded mode +# config.threadsafe! + +# Use a different logger for distributed setups +# config.logger = SyslogLogger.new + +# Full error reports are disabled and caching is turned on +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true + +# Use a different cache store in production +# config.cache_store = :mem_cache_store + +# Enable serving of images, stylesheets, and javascripts from an asset server +# config.action_controller.asset_host = "http://assets.example.com" + +# Disable delivery errors, bad email addresses will be ignored +# config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..1e709e1 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,22 @@ +# 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 +# and recreated between test runs. Don't rely on the data there! +config.cache_classes = true + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Disable request forgery protection in test environment +config.action_controller.allow_forgery_protection = false + +# Tell Action Mailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +config.action_mailer.delivery_method = :test diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..d531b8b --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,10 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format +# (all these examples are active by default): +# ActiveSupport::Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 0000000..72aca7e --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +# Mime::Type.register_alias "text/html", :iphone diff --git a/config/initializers/new_rails_defaults.rb b/config/initializers/new_rails_defaults.rb new file mode 100644 index 0000000..78e0117 --- /dev/null +++ b/config/initializers/new_rails_defaults.rb @@ -0,0 +1,17 @@ +# These settings change the behavior of Rails 2 apps and will be defaults +# for Rails 3. You can remove this initializer when Rails 3 is released. + +if defined?(ActiveRecord) + # Include Active Record class name as root for JSON serialized output. + ActiveRecord::Base.include_root_in_json = true + + # Store the full class name (including module namespace) in STI type column. + ActiveRecord::Base.store_full_sti_class = true +end + +# Use ISO 8601 format for JSON serialized times and dates. +ActiveSupport.use_standard_json_time_format = true + +# Don't escape HTML entities in JSON, leave that for the #json_escape helper. +# if you're including raw json in an HTML page. +ActiveSupport.escape_html_entities_in_json = false \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..f265c06 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,5 @@ +# Sample localization file for English. Add more files in this directory for other locales. +# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. + +en: + hello: "Hello world" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..61ff96b --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,45 @@ +ActionController::Routing::Routes.draw do |map| + map.resources :products + + # The priority is based upon order of creation: first created -> highest priority. + + # 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) + + # Sample resource route (maps HTTP verbs to controller actions automatically): + # map.resources :products + + # Sample resource route with options: + # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get } + + # Sample resource route with sub-resources: + # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller + + # Sample resource route with more complex sub-resources + # map.resources :products do |products| + # products.resources :comments + # products.resources :sales, :collection => { :recent => :get } + # end + + # Sample resource route within a namespace: + # map.namespace :admin do |admin| + # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb) + # admin.resources :products + # end + + # You can have the root of your site routed with map.root -- just remember to delete public/index.html. + # map.root :controller => "welcome" + + # See how all your routes lay out with "rake routes" + + # Install the default routes as the lowest priority. + # Note: These default routes make all actions in every controller accessible via GET requests. You should + # consider removing the them or commenting them out if you're using named routes and resources. + map.connect ':controller/:action/:id' + map.connect ':controller/:action/:id.:format' +end diff --git a/db/migrate/20090202160210_create_products.rb b/db/migrate/20090202160210_create_products.rb new file mode 100644 index 0000000..6b51c3f --- /dev/null +++ b/db/migrate/20090202160210_create_products.rb @@ -0,0 +1,15 @@ +class CreateProducts < ActiveRecord::Migration + def self.up + create_table :products do |t| + t.string :title + t.text :description + t.string :image_url + + t.timestamps + end + end + + def self.down + drop_table :products + end +end diff --git a/db/migrate/20090202160745_add_price_to_product.rb b/db/migrate/20090202160745_add_price_to_product.rb new file mode 100644 index 0000000..da517b8 --- /dev/null +++ b/db/migrate/20090202160745_add_price_to_product.rb @@ -0,0 +1,10 @@ +class AddPriceToProduct < ActiveRecord::Migration + def self.up + add_column :products, :price, :decimal, + :precision => 8, :scale => 2, :default => 0 + end + + def self.down + remove_column :products, :price + end +end diff --git a/db/migrate/20090202160925_add_date_available_to_product.rb b/db/migrate/20090202160925_add_date_available_to_product.rb new file mode 100644 index 0000000..e624b2b --- /dev/null +++ b/db/migrate/20090202160925_add_date_available_to_product.rb @@ -0,0 +1,9 @@ +class AddDateAvailableToProduct < ActiveRecord::Migration + def self.up + add_column :products, :date_available, :date + end + + def self.down + remove_column :products, :date_available + end +end diff --git a/db/migrate/20090202163608_add_test_data.rb b/db/migrate/20090202163608_add_test_data.rb new file mode 100644 index 0000000..5b79b4f --- /dev/null +++ b/db/migrate/20090202163608_add_test_data.rb @@ -0,0 +1,71 @@ +class AddTestData < ActiveRecord::Migration + def self.up + Product.delete_all + + Product.create(:title => 'Snowball', + :description => + %{

+ The perfect gift for a hot summer's day. What could be better + than to surprise a loved one a gift of winter coolness? This + snowball has been loving crafted from the purest snow, hand-collected + from the pristine lawn of our back garden. +

+

+ Note that this is a perisiable item and some melting may occur + while the snowball is in transit. +

+ }, + :image_url => '/images/snowball.jpg', + :price => 12.00, + :date_available => Time.utc(2008, 12, 31) + ) + + Product.create(:title => 'Daisy', + :description => + %{

+ All the world is better for having flowers in it. And your home + will be the better for having this fine example of Bellis + perennis gracing its walls. The range of uses of this item are + truly remarkable. You can place it in an eggcup of water, you can + insert it in a jacket for a subtle buttonhole posy, or you even + purchase a multi-pack to create your own daisy chain! +

+ }, + :image_url => '/images/daisy.gif', + :price => 5.00, + :date_available => Time.utc(2011, 7, 1) + ) + + Product.create(:title => 'Cardboard Box', + :description => + %{

The gift that keeps on giving, you'll wonder how you ever did + without a cardboard box in your home. Us it for a range of uses, + including putting things in and keeping things in. We can provide + you with a cardboard box that is versatile and suitable for any + occasion. Some of our directors even use them for sleeping in! +

+ }, + :image_url => '/images/cardboard-box.jpg', + :price => 10.00, + :date_available => Time.utc(2007, 1, 1) + ) + + Product.create(:title => 'Air', + :description => + %{

An essential for all activities, Air is vital for your + wellbeing and vitality. Our air is carefully packaged to contain + only 100% pure air, with no artificial colours or preservatives. +

+

May contain nuts. +

+ }, + :image_url => '/images/can-of-air.jpg', + :price => 5000.00, + :date_available => Time.utc(2009, 1, 1) + ) + end + + def self.down + Product.delete_all + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..dc43da5 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,24 @@ +# This file is auto-generated from the current state of the database. Instead of editing this file, +# please use the migrations feature of Active Record to incrementally modify your database, and +# then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your database schema. If you need +# to create the application database on another system, you should be using db:schema:load, not running +# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended to check this file into your version control system. + +ActiveRecord::Schema.define(:version => 20090202163608) do + + create_table "products", :force => true do |t| + t.string "title" + t.text "description" + t.string "image_url" + t.datetime "created_at" + t.datetime "updated_at" + t.decimal "price", :precision => 8, :scale => 2, :default => 0.0 + t.date "date_available" + end + +end diff --git a/doc/README_FOR_APP b/doc/README_FOR_APP new file mode 100644 index 0000000..e33b858 --- /dev/null +++ b/doc/README_FOR_APP @@ -0,0 +1,5 @@ +To build the guides: + +* Install source-highlighter (http://www.gnu.org/software/src-highlite/source-highlight.html) +* Install the mizuho gem (http://github.com/FooBarWidget/mizuho/tree/master) +* Run `rake guides` from the railties directory \ No newline at end of file diff --git a/log/development.log b/log/development.log new file mode 100644 index 0000000..617525e --- /dev/null +++ b/log/development.log @@ -0,0 +1,798 @@ + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (4.4ms) SHOW TABLES + SQL (7.1ms) CREATE TABLE `schema_migrations` (`version` varchar(255) NOT NULL) ENGINE=InnoDB + SQL (41.8ms) CREATE UNIQUE INDEX `unique_schema_migrations` ON `schema_migrations` (`version`) + SQL (0.3ms) SHOW TABLES + SQL (0.4ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.4ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.3ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations + SQL (0.3ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.4ms) SHOW TABLES + SQL (0.2ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) + SQL (22.1ms) CREATE TABLE `products` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, `title` varchar(255), `description` text, `image_url` varchar(255), `created_at` datetime, `updated_at` datetime) ENGINE=InnoDB + SQL (0.7ms) INSERT INTO schema_migrations (version) VALUES ('20090202160210') + SQL (0.4ms) SHOW TABLES + SQL (0.3ms) SELECT version FROM schema_migrations + SQL (0.3ms) SHOW TABLES + SQL (1.1ms) SHOW FIELDS FROM `products` + SQL (1.2ms) describe `products` + SQL (0.4ms) SHOW KEYS FROM `products` + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:05:24) [GET] + + +CGI::Session::CookieStore::TamperedWithCookie (CGI::Session::CookieStore::TamperedWithCookie): + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/session/cookie_store.rb:145:in `unmarshal' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/session/cookie_store.rb:102:in `restore' + /usr/lib/ruby/1.8/cgi/session.rb:304:in `[]' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/cgi_process.rb:98:in `session' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/cgi_process.rb:130:in `stale_session_check!' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/cgi_process.rb:78:in `session' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:1205:in `assign_shortcuts_without_flash' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/flash.rb:166:in `assign_shortcuts' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:519:in `process_without_filters' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/filters.rb:606:in `process_without_session_management_support' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/session_management.rb:134:in `process' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:392:in `process' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:183:in `handle_request' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:110:in `dispatch_unlocked' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:123:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `synchronize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:132:in `dispatch_cgi' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:39:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:103:in `handle_dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:74:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run' + /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:95:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `each' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:23:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:82:in `start' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:60:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/servers/webrick.rb:66 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/server.rb:49 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + script/server:3 + +Rendered /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/_trace (45.0ms) +Rendered /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/_request_and_response (1.3ms) +Rendering /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/layout.erb (internal_server_error) + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:06:12) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.4ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index +Completed in 22ms (View: 8, DB: 1) | 200 OK [http://localhost/products] + + +Processing ApplicationController#index (for 127.0.0.1 at 2009-02-02 16:06:21) [GET] + + +ActionController::RoutingError (No route matches "/store/index" with {:method=>:get}): + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/recognition_optimisation.rb:66:in `recognize_path' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/route_set.rb:386:in `recognize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:182:in `handle_request' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:110:in `dispatch_unlocked' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:123:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `synchronize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:132:in `dispatch_cgi' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:39:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:103:in `handle_dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:74:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run' + /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:95:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `each' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:23:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:82:in `start' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:60:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/servers/webrick.rb:66 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/server.rb:49 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + script/server:3 + +Rendering /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/layout.erb (not_found) + + +Processing ApplicationController#index (for 127.0.0.1 at 2009-02-02 16:06:28) [GET] + + +ActionController::RoutingError (No route matches "/store/index" with {:method=>:get}): + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/recognition_optimisation.rb:66:in `recognize_path' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/route_set.rb:386:in `recognize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:182:in `handle_request' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:110:in `dispatch_unlocked' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:123:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `synchronize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:132:in `dispatch_cgi' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:39:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:103:in `handle_dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:74:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run' + /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:95:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `each' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:23:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:82:in `start' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:60:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/servers/webrick.rb:66 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/server.rb:49 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + script/server:3 + +Rendering /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/layout.erb (not_found) + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.6ms) SHOW TABLES + SQL (0.2ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) +Migrating to AddPriceToProduct (20090202160745) + SQL (47.0ms) ALTER TABLE `products` ADD `price` decimal(8,2) DEFAULT 2 + SQL (0.8ms) INSERT INTO schema_migrations (version) VALUES ('20090202160745') +Migrating to AddDateAvailableToProduct (20090202160925) + SQL (8.3ms) ALTER TABLE `products` ADD `date_available` date + SQL (0.8ms) INSERT INTO schema_migrations (version) VALUES ('20090202160925') + SQL (0.5ms) SHOW TABLES + SQL (0.4ms) SELECT version FROM schema_migrations + SQL (0.6ms) SHOW TABLES + SQL (1.9ms) SHOW FIELDS FROM `products` + SQL (1.8ms) describe `products` + SQL (0.6ms) SHOW KEYS FROM `products` + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:15:31) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.6ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index +Completed in 22ms (View: 8, DB: 1) | 200 OK [http://localhost/products] + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:15:56) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.2ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index +Completed in 22ms (View: 11, DB: 0) | 200 OK [http://localhost/products] + + +Processing ProductsController#new (for 127.0.0.1 at 2009-02-02 16:16:00) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (2.5ms) SHOW FIELDS FROM `products` +Rendering template within layouts/products +Rendering products/new +Completed in 57ms (View: 34, DB: 3) | 200 OK [http://localhost/products/new] + + +Processing ProductsController#create (for 127.0.0.1 at 2009-02-02 16:16:18) [POST] + Parameters: {"commit"=>"Create", "authenticity_token"=>"8cca4a5b8c4003866a07c7a42d5a3b724d9e5d00", "product"=>{"image_url"=>"", "price"=>"2.0", "title"=>"frist produce", "date_available(1i)"=>"2009", "date_available(2i)"=>"2", "description"=>"a bunch of descirpiton", "date_available(3i)"=>"2"}} + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.5ms) SHOW FIELDS FROM `products` + SQL (0.1ms) BEGIN + Product Create (0.3ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('', '2009-02-02 16:16:18', 'frist produce', 2.0, '2009-02-02', 'a bunch of descirpiton', '2009-02-02 16:16:18') + SQL (4.4ms) COMMIT +Redirected to # +Completed in 27ms (DB: 7) | 302 Found [http://localhost/products] + + +Processing ProductsController#show (for 127.0.0.1 at 2009-02-02 16:16:18) [GET] + Parameters: {"id"=>"1"} + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.1ms) SHOW FIELDS FROM `products` + Product Load (0.4ms) SELECT * FROM `products` WHERE (`products`.`id` = 1)  +Rendering template within layouts/products +Rendering products/show +Completed in 30ms (View: 12, DB: 2) | 200 OK [http://localhost/products/1] + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:16:20) [GET] + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.5ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index + Product Columns (1.1ms) SHOW FIELDS FROM `products` +Completed in 32ms (View: 18, DB: 2) | 200 OK [http://localhost/products] + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.4ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations + SQL (0.3ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations +Migrating to AddDateAvailableToProduct (20090202160925) + SQL (29.2ms) ALTER TABLE `products` DROP `date_available` + SQL (7.7ms) DELETE FROM schema_migrations WHERE version = '20090202160925' +Migrating to AddPriceToProduct (20090202160745) + SQL (24.1ms) ALTER TABLE `products` DROP `price` + SQL (0.7ms) DELETE FROM schema_migrations WHERE version = '20090202160745' +Migrating to CreateProducts (20090202160210) + SQL (1.4ms) DROP TABLE `products` + SQL (0.7ms) DELETE FROM schema_migrations WHERE version = '20090202160210' + SQL (0.4ms) SHOW TABLES + SQL (0.3ms) SELECT version FROM schema_migrations + SQL (0.5ms) SHOW TABLES + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.8ms) SHOW TABLES + SQL (0.2ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) + SQL (175.4ms) CREATE TABLE `products` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, `title` varchar(255), `description` text, `image_url` varchar(255), `created_at` datetime, `updated_at` datetime) ENGINE=InnoDB + SQL (7.5ms) INSERT INTO schema_migrations (version) VALUES ('20090202160210') +Migrating to AddPriceToProduct (20090202160745) + SQL (7.8ms) ALTER TABLE `products` ADD `price` decimal(8,2) DEFAULT 0 + SQL (0.6ms) INSERT INTO schema_migrations (version) VALUES ('20090202160745') +Migrating to AddDateAvailableToProduct (20090202160925) + SQL (12.4ms) ALTER TABLE `products` ADD `date_available` date + SQL (4.0ms) INSERT INTO schema_migrations (version) VALUES ('20090202160925') + SQL (0.4ms) SHOW TABLES + SQL (0.4ms) SELECT version FROM schema_migrations + SQL (0.3ms) SHOW TABLES + SQL (1.7ms) SHOW FIELDS FROM `products` + SQL (1.1ms) describe `products` + SQL (0.4ms) SHOW KEYS FROM `products` + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:18:14) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.3ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index +Completed in 19ms (View: 8, DB: 1) | 200 OK [http://localhost/products] + + +Processing ProductsController#new (for 127.0.0.1 at 2009-02-02 16:18:16) [GET] + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.1ms) SHOW FIELDS FROM `products` +Rendering template within layouts/products +Rendering products/new +Completed in 41ms (View: 28, DB: 1) | 200 OK [http://localhost/products/new] + + +Processing ProductsController#create (for 127.0.0.1 at 2009-02-02 16:18:39) [POST] + Parameters: {"commit"=>"Create", "authenticity_token"=>"8cca4a5b8c4003866a07c7a42d5a3b724d9e5d00", "product"=>{"image_url"=>"ss.png", "price"=>"10", "title"=>"fist prod", "date_available(1i)"=>"2009", "date_available(2i)"=>"2", "description"=>"desription and stuff", "date_available(3i)"=>"2"}} + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.5ms) SHOW FIELDS FROM `products` + SQL (0.1ms) BEGIN + Product Create (0.5ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('ss.png', '2009-02-02 16:18:39', 'fist prod', 10.0, '2009-02-02', 'desription and stuff', '2009-02-02 16:18:39') + SQL (5.2ms) COMMIT +Redirected to # +Completed in 37ms (DB: 8) | 302 Found [http://localhost/products] + + +Processing ProductsController#show (for 127.0.0.1 at 2009-02-02 16:18:39) [GET] + Parameters: {"id"=>"1"} + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (2.3ms) SHOW FIELDS FROM `products` + Product Load (0.6ms) SELECT * FROM `products` WHERE (`products`.`id` = 1)  +Rendering template within layouts/products +Rendering products/show +Completed in 49ms (View: 29, DB: 3) | 200 OK [http://localhost/products/1] + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:18:41) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.4ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index + Product Columns (1.2ms) SHOW FIELDS FROM `products` +Completed in 33ms (View: 20, DB: 2) | 200 OK [http://localhost/products] + + +Processing ApplicationController#index (for 127.0.0.1 at 2009-02-02 16:18:47) [GET] + + +ActionController::RoutingError (No route matches "/store/index" with {:method=>:get}): + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/recognition_optimisation.rb:66:in `recognize_path' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/route_set.rb:386:in `recognize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:182:in `handle_request' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:110:in `dispatch_unlocked' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:123:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `synchronize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:132:in `dispatch_cgi' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:39:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:103:in `handle_dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:74:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run' + /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:95:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `each' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:23:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:82:in `start' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:60:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/servers/webrick.rb:66 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/server.rb:49 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + script/server:3 + +Rendering /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/layout.erb (not_found) + + +Processing ApplicationController#index (for 127.0.0.1 at 2009-02-02 16:18:51) [GET] + + +ActionController::RoutingError (No route matches "/store" with {:method=>:get}): + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/recognition_optimisation.rb:66:in `recognize_path' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/route_set.rb:386:in `recognize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:182:in `handle_request' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:110:in `dispatch_unlocked' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:123:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `synchronize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:132:in `dispatch_cgi' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:39:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:103:in `handle_dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:74:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run' + /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:95:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `each' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:23:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:82:in `start' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:60:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/servers/webrick.rb:66 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/server.rb:49 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + script/server:3 + +Rendering /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/layout.erb (not_found) + + +Processing ProductsController#new (for 127.0.0.1 at 2009-02-02 16:24:25) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + + +SyntaxError (/home/neil/programming/rails/depot/app/models/product.rb:7: syntax error, unexpected $undefined + :with %r{\.(gif|jpg|png)$}i, + ^ +/home/neil/programming/rails/depot/app/models/product.rb:7: syntax error, unexpected ')', expecting '}' + :with %r{\.(gif|jpg|png)$}i, + ^ +/home/neil/programming/rails/depot/app/models/product.rb:8: syntax error, unexpected tSTRING_BEG, expecting tCOLON2 or '[' or '.' + :message "must be a URL for GIF, JPG, or PNG image." + ^ +/home/neil/programming/rails/depot/app/models/product.rb:14: syntax error, unexpected kEND, expecting '}'): + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:382:in `load_without_new_constant_marking' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:382:in `load_file' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:381:in `load_file' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:256:in `require_or_load' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:427:in `load_missing_constant' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:77:in `const_missing' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:89:in `const_missing' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:439:in `load_missing_constant' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:93:in `const_missing' + /app/controllers/products_controller.rb:27:in `new' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:1253:in `send' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:1253:in `perform_action_without_filters' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/filters.rb:617:in `call_filters' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/filters.rb:610:in `perform_action_without_benchmark' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/benchmarking.rb:68:in `perform_action_without_rescue' + /usr/lib/ruby/1.8/benchmark.rb:293:in `measure' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/benchmarking.rb:68:in `perform_action_without_rescue' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/rescue.rb:136:in `perform_action_without_caching' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/caching/sql_cache.rb:13:in `perform_action' + /usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/connection_adapters/abstract/query_cache.rb:34:in `cache' + /usr/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/query_cache.rb:8:in `cache' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/caching/sql_cache.rb:12:in `perform_action' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:524:in `send' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:524:in `process_without_filters' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/filters.rb:606:in `process_without_session_management_support' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/session_management.rb:134:in `process' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb:392:in `process' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:183:in `handle_request' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:110:in `dispatch_unlocked' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:123:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `synchronize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:132:in `dispatch_cgi' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:39:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:103:in `handle_dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:74:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run' + /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:95:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `each' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:23:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:82:in `start' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:60:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/servers/webrick.rb:66 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/server.rb:49 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + script/server:3 + +Rendered /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/_trace (91.3ms) +Rendered /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/_request_and_response (2.1ms) +Rendering /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/layout.erb (internal_server_error) + + +Processing ProductsController#new (for 127.0.0.1 at 2009-02-02 16:25:14) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (2.9ms) SHOW FIELDS FROM `products` +Rendering template within layouts/products +Rendering products/new +Completed in 53ms (View: 33, DB: 3) | 200 OK [http://localhost/products/new] + + +Processing ProductsController#create (for 127.0.0.1 at 2009-02-02 16:25:16) [POST] + Parameters: {"commit"=>"Create", "authenticity_token"=>"8cca4a5b8c4003866a07c7a42d5a3b724d9e5d00", "product"=>{"image_url"=>"", "price"=>"0.0", "title"=>"", "date_available(1i)"=>"2009", "date_available(2i)"=>"2", "description"=>"", "date_available(3i)"=>"2"}} + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.5ms) SHOW FIELDS FROM `products` + SQL (0.1ms) BEGIN + Product Exists (0.5ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY '') LIMIT 1 + SQL (0.2ms) ROLLBACK +Rendering template within layouts/products +Rendering products/new +Completed in 67ms (View: 24, DB: 3) | 200 OK [http://localhost/products] + + +Processing ProductsController#new (for 127.0.0.1 at 2009-02-02 16:26:36) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (2.3ms) SHOW FIELDS FROM `products` +Rendering template within layouts/products +Rendering products/new +Completed in 39ms (View: 21, DB: 3) | 200 OK [http://localhost/products/new] + + +Processing ProductsController#create (for 127.0.0.1 at 2009-02-02 16:26:45) [POST] + Parameters: {"commit"=>"Create", "authenticity_token"=>"8cca4a5b8c4003866a07c7a42d5a3b724d9e5d00", "product"=>{"image_url"=>"", "price"=>"0.0", "title"=>"Second product", "date_available(1i)"=>"2009", "date_available(2i)"=>"2", "description"=>"kjhkjh", "date_available(3i)"=>"2"}} + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.1ms) SHOW FIELDS FROM `products` + SQL (0.1ms) BEGIN + Product Exists (0.6ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Second product') LIMIT 1 + SQL (0.2ms) ROLLBACK +Rendering template within layouts/products +Rendering products/new +Completed in 47ms (View: 17, DB: 2) | 200 OK [http://localhost/products] + + +Processing ProductsController#create (for 127.0.0.1 at 2009-02-02 16:26:56) [POST] + Parameters: {"commit"=>"Create", "authenticity_token"=>"8cca4a5b8c4003866a07c7a42d5a3b724d9e5d00", "product"=>{"image_url"=>"host.png", "price"=>"aardvark", "title"=>"Second product", "date_available(1i)"=>"2009", "date_available(2i)"=>"2", "description"=>"kjhkjh", "date_available(3i)"=>"2"}} + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.3ms) SHOW FIELDS FROM `products` + SQL (0.1ms) BEGIN + Product Exists (0.2ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Second product') LIMIT 1 + SQL (0.2ms) ROLLBACK +Rendering template within layouts/products +Rendering products/new +Completed in 62ms (View: 24, DB: 2) | 200 OK [http://localhost/products] + + +Processing ProductsController#create (for 127.0.0.1 at 2009-02-02 16:27:03) [POST] + Parameters: {"commit"=>"Create", "authenticity_token"=>"8cca4a5b8c4003866a07c7a42d5a3b724d9e5d00", "product"=>{"image_url"=>"host.png", "price"=>"22", "title"=>"Second product", "date_available(1i)"=>"2009", "date_available(2i)"=>"2", "description"=>"kjhkjh", "date_available(3i)"=>"2"}} + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.6ms) SHOW FIELDS FROM `products` + SQL (0.1ms) BEGIN + Product Exists (0.1ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Second product') LIMIT 1 + Product Create (0.3ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('host.png', '2009-02-02 16:27:03', 'Second product', 22.0, '2009-02-02', 'kjhkjh', '2009-02-02 16:27:03') + SQL (45.2ms) COMMIT +Redirected to # +Completed in 80ms (DB: 48) | 302 Found [http://localhost/products] + + +Processing ProductsController#show (for 127.0.0.1 at 2009-02-02 16:27:03) [GET] + Parameters: {"id"=>"2"} + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (2.1ms) SHOW FIELDS FROM `products` + Product Load (0.6ms) SELECT * FROM `products` WHERE (`products`.`id` = 2)  +Rendering template within layouts/products +Rendering products/show +Completed in 43ms (View: 19, DB: 3) | 200 OK [http://localhost/products/2] + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:27:04) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.4ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index + Product Columns (1.1ms) SHOW FIELDS FROM `products` +Completed in 33ms (View: 17, DB: 2) | 200 OK [http://localhost/products] + + +Processing ProductsController#edit (for 127.0.0.1 at 2009-02-02 16:27:10) [GET] + Parameters: {"id"=>"1"} + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.2ms) SHOW FIELDS FROM `products` + Product Load (0.5ms) SELECT * FROM `products` WHERE (`products`.`id` = 1)  +Rendering template within layouts/products +Rendering products/edit +Completed in 39ms (View: 25, DB: 2) | 200 OK [http://localhost/products/1/edit] + + +Processing ProductsController#update (for 127.0.0.1 at 2009-02-02 16:27:18) [PUT] + Parameters: {"commit"=>"Update", "authenticity_token"=>"8cca4a5b8c4003866a07c7a42d5a3b724d9e5d00", "product"=>{"image_url"=>"ss.png", "price"=>"10.00", "title"=>"fist prod", "date_available(1i)"=>"2009", "date_available(2i)"=>"2", "description"=>"desription and stuff", "date_available(3i)"=>"31"}, "id"=>"1"} + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.1ms) SHOW FIELDS FROM `products` + Product Load (0.1ms) SELECT * FROM `products` WHERE (`products`.`id` = 1)  + SQL (0.2ms) BEGIN + Product Exists (0.5ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'fist prod' AND `products`.id <> 1) LIMIT 1 + Product Update (0.4ms) UPDATE `products` SET `date_available` = '2009-03-03', `updated_at` = '2009-02-02 16:27:18' WHERE `id` = 1 + SQL (2.2ms) COMMIT +Redirected to # +Completed in 37ms (DB: 5) | 302 Found [http://localhost/products/1] + + +Processing ProductsController#show (for 127.0.0.1 at 2009-02-02 16:27:18) [GET] + Parameters: {"id"=>"1"} + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Columns (1.3ms) SHOW FIELDS FROM `products` + Product Load (0.5ms) SELECT * FROM `products` WHERE (`products`.`id` = 1)  +Rendering template within layouts/products +Rendering products/show +Completed in 37ms (View: 20, DB: 2) | 200 OK [http://localhost/products/1] + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 16:27:22) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.4ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index + Product Columns (1.1ms) SHOW FIELDS FROM `products` +Completed in 35ms (View: 22, DB: 2) | 200 OK [http://localhost/products] + SQL (0.5ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (16.7ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) +Migrating to AddPriceToProduct (20090202160745) +Migrating to AddDateAvailableToProduct (20090202160925) +Migrating to AddTestData (20090202163608) + Product Delete all (44.9ms) DELETE FROM `products`  + Product Columns (1.1ms) SHOW FIELDS FROM `products` + SQL (0.1ms) BEGIN + Product Exists (0.6ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Snowball') LIMIT 1 + Product Create (0.4ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/nowball.jpg', '2009-02-02 17:21:21', 'Snowball', 12.0, '2008-12-31 00:00:00', '

\n The perfect gift for a hot summer\'s day. What could be better \n than to surprise a loved one a gift of winter coolness? This\n snowball has been loving crafted from the purest snow, hand-collected\n from the pristine lawn of our back garden. \n

\n

\n Note that this is a perisiable item and some melting may occur \n while the snowball is in transit. \n

\n ', '2009-02-02 17:21:21') + SQL (5.9ms) COMMIT + SQL (0.1ms) BEGIN + Product Exists (0.4ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Daisy') LIMIT 1 + Product Create (0.4ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/daisy.gif', '2009-02-02 17:21:21', 'Daisy', 5.0, '2011-07-01 00:00:00', '

\n All the world is better for having flowers in it. And your home \n will be the better for having this fine example of Bellis \n perennis gracing its walls. The range of uses of this item are\n truly remarkable. You can place it in an eggcup of water, you can \n insert it in a jacket for a subtle buttonhole posy, or you even \n purchase a multi-pack to create your own daisy chain!\n

\n ', '2009-02-02 17:21:21') + SQL (0.6ms) COMMIT + SQL (0.2ms) BEGIN + Product Exists (0.4ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Cardboard Box') LIMIT 1 + Product Create (0.3ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/cardboard-box.jpg', '2009-02-02 17:21:21', 'Cardboard Box', 10.0, '2007-01-01 00:00:00', '

The gift that keeps on giving, you\'ll wonder how you ever did \n without a cardboard box in your home. Us it for a range of uses, \n including putting things in and keeping things in. We can provide\n you with a cardboard box that is versatile and suitable for any\n occasion. Some of our directors even use them for sleeping in!\n

\n ', '2009-02-02 17:21:21') + SQL (0.5ms) COMMIT + SQL (0.2ms) BEGIN + Product Exists (0.4ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Air') LIMIT 1 + Product Create (0.6ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/can-of-air.jpg', '2009-02-02 17:21:21', 'Air', 5000.0, '2009-01-01 00:00:00', '

An essential for all activities, Air is vital for your\n wellbeing and vitality. Our air is carefully packaged to contain\n only 100% pure air, with no artificial colours or preservatives.\n

\n

May contain nuts.\n

\n ', '2009-02-02 17:21:21') + SQL (11.8ms) COMMIT + SQL (0.6ms) INSERT INTO schema_migrations (version) VALUES ('20090202163608') + SQL (0.4ms) SHOW TABLES + SQL (0.3ms) SELECT version FROM schema_migrations + SQL (0.3ms) SHOW TABLES + SQL (1.5ms) SHOW FIELDS FROM `products` + SQL (1.5ms) describe `products` + SQL (0.7ms) SHOW KEYS FROM `products` + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 17:21:25) [GET] + SQL (0.1ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.5ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index + Product Columns (1.1ms) SHOW FIELDS FROM `products` +Completed in 166ms (View: 150, DB: 2) | 200 OK [http://localhost/products] + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 17:36:09) [GET] + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.2ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index + Product Columns (1.1ms) SHOW FIELDS FROM `products` +Completed in 55ms (View: 38, DB: 2) | 200 OK [http://localhost/products] + + +Processing ApplicationController#index (for 127.0.0.1 at 2009-02-02 17:36:09) [GET] + + +ActionController::RoutingError (No route matches "/images/nowball.jpg" with {:method=>:get}): + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/recognition_optimisation.rb:66:in `recognize_path' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing/route_set.rb:386:in `recognize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:182:in `handle_request' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:110:in `dispatch_unlocked' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:123:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `synchronize' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:122:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:132:in `dispatch_cgi' + /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/dispatcher.rb:39:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:103:in `handle_dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:74:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service' + /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run' + /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread' + /usr/lib/ruby/1.8/webrick/server.rb:95:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `each' + /usr/lib/ruby/1.8/webrick/server.rb:92:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:23:in `start' + /usr/lib/ruby/1.8/webrick/server.rb:82:in `start' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/webrick_server.rb:60:in `dispatch' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/servers/webrick.rb:66 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:521:in `new_constants_in' + /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/dependencies.rb:153:in `require' + /usr/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/commands/server.rb:49 + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require' + /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' + script/server:3 + +Rendering /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/templates/rescues/layout.erb (not_found) + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.4ms) SHOW TABLES + SQL (0.2ms) SELECT version FROM schema_migrations + SQL (0.3ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations +Migrating to AddTestData (20090202163608) + Product Delete all (28.7ms) DELETE FROM `products`  + SQL (5.7ms) DELETE FROM schema_migrations WHERE version = '20090202163608' +Migrating to AddDateAvailableToProduct (20090202160925) + SQL (60.2ms) ALTER TABLE `products` DROP `date_available` + SQL (3.3ms) DELETE FROM schema_migrations WHERE version = '20090202160925' +Migrating to AddPriceToProduct (20090202160745) + SQL (33.2ms) ALTER TABLE `products` DROP `price` + SQL (0.7ms) DELETE FROM schema_migrations WHERE version = '20090202160745' +Migrating to CreateProducts (20090202160210) + SQL (1.4ms) DROP TABLE `products` + SQL (0.8ms) DELETE FROM schema_migrations WHERE version = '20090202160210' + SQL (0.4ms) SHOW TABLES + SQL (0.3ms) SELECT version FROM schema_migrations + SQL (0.3ms) SHOW TABLES + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.1ms) SET SQL_AUTO_IS_NULL=0 + SQL (0.7ms) SHOW TABLES + SQL (0.1ms) SELECT version FROM schema_migrations +Migrating to CreateProducts (20090202160210) + SQL (9.2ms) CREATE TABLE `products` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, `title` varchar(255), `description` text, `image_url` varchar(255), `created_at` datetime, `updated_at` datetime) ENGINE=InnoDB + SQL (0.7ms) INSERT INTO schema_migrations (version) VALUES ('20090202160210') +Migrating to AddPriceToProduct (20090202160745) + SQL (29.7ms) ALTER TABLE `products` ADD `price` decimal(8,2) DEFAULT 0 + SQL (7.3ms) INSERT INTO schema_migrations (version) VALUES ('20090202160745') +Migrating to AddDateAvailableToProduct (20090202160925) + SQL (6.3ms) ALTER TABLE `products` ADD `date_available` date + SQL (12.2ms) INSERT INTO schema_migrations (version) VALUES ('20090202160925') +Migrating to AddTestData (20090202163608) + Product Delete all (0.3ms) DELETE FROM `products`  + Product Columns (1.1ms) SHOW FIELDS FROM `products` + SQL (0.2ms) BEGIN + Product Exists (0.5ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Snowball') LIMIT 1 + Product Create (0.6ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/snowball.jpg', '2009-02-02 17:36:52', 'Snowball', 12.0, '2008-12-31 00:00:00', '

\n The perfect gift for a hot summer\'s day. What could be better \n than to surprise a loved one a gift of winter coolness? This\n snowball has been loving crafted from the purest snow, hand-collected\n from the pristine lawn of our back garden. \n

\n

\n Note that this is a perisiable item and some melting may occur \n while the snowball is in transit. \n

\n ', '2009-02-02 17:36:52') + SQL (1.1ms) COMMIT + SQL (0.1ms) BEGIN + Product Exists (0.3ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Daisy') LIMIT 1 + Product Create (0.4ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/daisy.gif', '2009-02-02 17:36:52', 'Daisy', 5.0, '2011-07-01 00:00:00', '

\n All the world is better for having flowers in it. And your home \n will be the better for having this fine example of Bellis \n perennis gracing its walls. The range of uses of this item are\n truly remarkable. You can place it in an eggcup of water, you can \n insert it in a jacket for a subtle buttonhole posy, or you even \n purchase a multi-pack to create your own daisy chain!\n

\n ', '2009-02-02 17:36:52') + SQL (0.6ms) COMMIT + SQL (0.1ms) BEGIN + Product Exists (0.3ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Cardboard Box') LIMIT 1 + Product Create (0.3ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/cardboard-box.jpg', '2009-02-02 17:36:52', 'Cardboard Box', 10.0, '2007-01-01 00:00:00', '

The gift that keeps on giving, you\'ll wonder how you ever did \n without a cardboard box in your home. Us it for a range of uses, \n including putting things in and keeping things in. We can provide\n you with a cardboard box that is versatile and suitable for any\n occasion. Some of our directors even use them for sleeping in!\n

\n ', '2009-02-02 17:36:52') + SQL (5.4ms) COMMIT + SQL (0.3ms) BEGIN + Product Exists (0.3ms) SELECT `products`.id FROM `products` WHERE (`products`.`title` = BINARY 'Air') LIMIT 1 + Product Create (0.3ms) INSERT INTO `products` (`image_url`, `updated_at`, `title`, `price`, `date_available`, `description`, `created_at`) VALUES('/images/can-of-air.jpg', '2009-02-02 17:36:52', 'Air', 5000.0, '2009-01-01 00:00:00', '

An essential for all activities, Air is vital for your\n wellbeing and vitality. Our air is carefully packaged to contain\n only 100% pure air, with no artificial colours or preservatives.\n

\n

May contain nuts.\n

\n ', '2009-02-02 17:36:52') + SQL (0.7ms) COMMIT + SQL (0.7ms) INSERT INTO schema_migrations (version) VALUES ('20090202163608') + SQL (0.4ms) SHOW TABLES + SQL (3.5ms) SELECT version FROM schema_migrations + SQL (0.5ms) SHOW TABLES + SQL (2.4ms) SHOW FIELDS FROM `products` + SQL (2.3ms) describe `products` + SQL (1.5ms) SHOW KEYS FROM `products` + + +Processing ProductsController#index (for 127.0.0.1 at 2009-02-02 17:36:57) [GET] + SQL (0.2ms) SET NAMES 'utf8' + SQL (0.2ms) SET SQL_AUTO_IS_NULL=0 + Product Load (0.7ms) SELECT * FROM `products`  +Rendering template within layouts/products +Rendering products/index + Product Columns (1.1ms) SHOW FIELDS FROM `products` +Completed in 48ms (View: 35, DB: 2) | 200 OK [http://localhost/products] diff --git a/log/production.log b/log/production.log new file mode 100644 index 0000000..e69de29 diff --git a/log/server.log b/log/server.log new file mode 100644 index 0000000..e69de29 diff --git a/log/test.log b/log/test.log new file mode 100644 index 0000000..e69de29 diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 0000000..0b610ac --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,2 @@ +rails.port=3000 +source.encoding=UTF-8 diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 0000000..f6f65c9 --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.ruby.railsprojects + + + depot + + + diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..eff660b --- /dev/null +++ b/public/404.html @@ -0,0 +1,30 @@ + + + + + + + The page you were looking for doesn't exist (404) + + + + + +
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+ + \ No newline at end of file diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..b54e4a3 --- /dev/null +++ b/public/422.html @@ -0,0 +1,30 @@ + + + + + + + The change you wanted was rejected (422) + + + + + +
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+ + \ No newline at end of file diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..0cd07c1 --- /dev/null +++ b/public/500.html @@ -0,0 +1,33 @@ + + + + + + + We're sorry, but something went wrong (500) + + + + + +
+

We're sorry, but something went wrong.

+

We've been notified about this issue and we'll take a look at it shortly.

+

(If you're the administrator of this website, then please read + the log file "<%=h RAILS_ENV %>.log" + to find out what went wrong.)

+
+ + diff --git a/public/dispatch.cgi b/public/dispatch.cgi new file mode 100755 index 0000000..430812d --- /dev/null +++ b/public/dispatch.cgi @@ -0,0 +1,10 @@ +#!/usr/bin/ruby1.8 + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch diff --git a/public/dispatch.fcgi b/public/dispatch.fcgi new file mode 100755 index 0000000..3169ba2 --- /dev/null +++ b/public/dispatch.fcgi @@ -0,0 +1,24 @@ +#!/usr/bin/ruby1.8 +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# +require File.dirname(__FILE__) + "/../config/environment" +require 'fcgi_handler' + +RailsFCGIHandler.process! diff --git a/public/dispatch.rb b/public/dispatch.rb new file mode 100755 index 0000000..430812d --- /dev/null +++ b/public/dispatch.rb @@ -0,0 +1,10 @@ +#!/usr/bin/ruby1.8 + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/images/can-of-air.jpg b/public/images/can-of-air.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e633c2a5c373ddf1cba1a03a41d9d5b2220f39c9 GIT binary patch literal 10384 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 literal 0 HcmV?d00001 diff --git a/public/images/cardboard-box.jpg b/public/images/cardboard-box.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4386bd47aff811c17342721c611ab17b6dda9267 GIT binary patch literal 3590 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 literal 0 HcmV?d00001 diff --git a/public/images/daisy.gif b/public/images/daisy.gif new file mode 100644 index 0000000000000000000000000000000000000000..388699fe5bf607796bf03b7477126f1b52cf6f11 GIT binary patch literal 7939 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 literal 0 HcmV?d00001 diff --git a/public/images/rails.png b/public/images/rails.png new file mode 100644 index 0000000000000000000000000000000000000000..d5edc04e65f555e3ba4dcdaad39dc352e75b575e GIT binary patch literal 6646 zcmVpVcQya!6@Dsmj@#jv7C*qh zIhOJ6_K0n?*d`*T7TDuW-}m`9Kz3~>+7`DUkbAraU%yi+R{N~~XA2B%zt-4=tLimUer9!2M~N{G5bftFij_O&)a zsHnOppFIzebQ`RA0$!yUM-lg#*o@_O2wf422iLnM6cU(ktYU8#;*G!QGhIy9+ZfzKjLuZo%@a z-i@9A`X%J{^;2q&ZHY3C(B%gqCPW!8{9C0PMcNZccefK){s|V5-xxtHQc@uf>XqhD z7#N^siWqetgq29aX>G^olMf=bbRF6@Y(}zYxw6o!9WBdG1unP}<(V;zKlcR2p86fq zYjaqB^;Ycq>Wy@5T1xOzG3tucG3e%nPvajaN{CrFbnzv^9&K3$NrDm*eQe4`BGQ2bI;dFEwyt>hK%X!L6)82aOZp zsrGcJ#7PoX7)s|~t6is?FfX*7vWdREi58tiY4S)t6u*|kv?J)d_$r+CH#eZ?Ef+I_ z(eVlX8dh~4QP?o*E`_MgaNFIKj*rtN(0Raj3ECjSXcWfd#27NYs&~?t`QZFT}!Zaf=ldZIhi}LhQlqLo+o5(Pvui&{7PD__^53f9j>HW`Q z_V8X5j~$|GP9qXu0C#!@RX2}lXD35@3N5{BkUi%jtaPQ*H6OX2zIz4QPuqmTv3`vG{zc>l3t0B9E75h< z8&twGh%dp7WPNI+tRl%#gf2}Epg8st+~O4GjtwJsXfN;EjAmyr6z5dnaFU(;IV~QK zW62fogF~zA``(Q>_SmD!izc6Y4zq*97|NAPHp1j5X7Op2%;GLYm>^HEMyObo6s7l) zE3n|aOHi5~B84!}b^b*-aL2E)>OEJX_tJ~t<#VJ?bT?lDwyDB&5SZ$_1aUhmAY}#* zs@V1I+c5md9%R-o#_DUfqVtRk>59{+Opd5Yu%dAU#VQW}^m}x-30ftBx#527{^pI4 z6l2C6C7QBG$~NLYb3rVdLD#Z{+SleOp`(Lg5J}`kxdTHe(nV5BdpLrD=l|)e$gEqA zwI6vuX-PFCtcDIH>bGY2dwq&^tf+&R?)nY-@7_j%4CMRAF}C9w%p86W<2!aSY$p+k zrkFtG=cGo38RnrG28;?PNk%7a@faaXq&MS*&?1Z`7Ojw7(#>}ZG4nMAs3VXxfdW>i zY4VX02c5;f7jDPY_7@Oa)CHH}cH<3y#}_!nng^W+h1e-RL*YFYOteC@h?BtJZ+?sE zy)P5^8Mregx{nQaw1NY-|3>{Z)|0`?zc?G2-acYiSU`tj#sSGfm7k86ZQ0SQgPevcklHxM9<~4yW zR796sisf1|!#{Z=e^)0;_8iUhL8g(;j$l=02FTPZ(dZV@s#aQ`DHkLM6=YsbE4iQ!b#*374l0Jw5;jD%J;vQayq=nD8-kHI~f9Ux|32SJUM`> zGp2UGK*4t?cRKi!2he`zI#j0f${I#f-jeT?u_C7S4WsA0)ryi-1L0(@%pa^&g5x=e z=KW9+Nn(=)1T&S8g_ug%dgk*~l2O-$r9#zEGBdQsweO%t*6F4c8JC36JtTizCyy+E4h%G(+ z5>y$%0txMuQ$e~wjFgN(xrAndHQo`Za+K*?gUVDTBV&Ap^}|{w#CIq{DRe}+l@(Ec zCCV6f_?dY_{+f{}6XGn!pL_up?}@>KijT^$w#Lb6iHW&^8RP~g6y=vZBXx~B9nI^i zGexaPjcd(%)zGw!DG_dDwh-7x6+ST#R^${iz_M$uM!da8SxgB_;Z0G%Y*HpvLjKw; zX=ir7i1O$-T|*TBoH$dlW+TLf5j5sep^DlDtkox;Kg{Q%EXWedJq@J@%VAcK)j3y1 zShM!CS#qax;D@RND%2t3W6kv+#Ky0F9<3YKDbV^XJ=^$s(Vtza8V72YY)577nnldI zHMA0PUo!F3j(ubV*CM@PiK<^|RM2(DuCbG7`W}Rg(xdYC>C~ z;1KJGLN&$cRxSZunjXcntykmpFJ7;dk>shY(DdK&3K_JDJ6R%D`e~6Qv67@Rwu+q9 z*|NG{r}4F8f{Dfzt0+cZMd$fvlX3Q`dzM46@r?ISxr;9gBTG2rmfiGOD*#c*3f)cc zF+PFZobY$-^}J8 z%n=h4;x2}cP!@SiVd!v;^Wwo0(N??-ygDr7gG^NKxDjSo{5T{?$|Qo5;8V!~D6O;F*I zuY!gd@+2j_8Rn=UWDa#*4E2auWoGYDddMW7t0=yuC(xLWky?vLimM~!$3fgu!dR>p z?L?!8z>6v$|MsLb&dU?ob)Zd!B)!a*Z2eTE7 zKCzP&e}XO>CT%=o(v+WUY`Az*`9inbTG& z_9_*oQKw;sc8{ipoBC`S4Tb7a%tUE)1fE+~ib$;|(`|4QbXc2>VzFi%1nX%ti;^s3~NIL0R}!!a{0A zyCRp0F7Y&vcP&3`&Dzv5!&#h}F2R-h&QhIfq*ts&qO13{_CP}1*sLz!hI9VoTSzTu zok5pV0+~jrGymE~{TgbS#nN5+*rF7ij)cnSLQw0Ltc70zmk|O!O(kM<3zw-sUvkx~ z2`y+{xAwKSa-0}n7{$I@Zop7CWy%_xIeN1e-7&OjQ6vZZPbZ^3_ z(~=;ZSP98S2oB#35b1~_x`2gWiPdIVddEf`AD9<@c_s)TM;3J$T_l?pr{<7PTgdiy zBc5IGx)g~n=s+Z$RzYCmv8PlJu%gkh^;%mTGMc)UwRINVD~K;`Rl!5@hhGg;y>5qj zq|u-Yf0q_~Y+Mbivkkfa0nAOzB1acnytogsj_m7FB(-FjihMek#GAU4M!iXCgdK8a zjoKm?*|iz7;dHm4$^hh(`Ufl>yb>$hjIA-;>{>C}G0Di%bGvUsJkfLAV|xq32c>RqJqTBJ3Dx zYC;*Dt|S$b6)aCJFnK(Eey$M1DpVV~_MIhwK> zygo(jWC|_IRw|456`roEyXtkNLWNAt-4N1qyN$I@DvBzt;e|?g<*HK1%~cq|^u*}C zmMrwh>{QAq?Ar~4l^DqT%SQ)w)FA(#7#u+N;>E975rYML>)LgE`2<7nN=C1pC{IkV zVw}_&v6j&S?QVh*)wF3#XmE@0($^BVl1969csLKUBNer{suVd!a~B!0MxWY?=(GD6 zy$G&ERFR#i6G4=2F?R4}Mz3B?3tnpoX3)qFF2sh9-Jn*e%9F>i{WG7$_~XyOO2!+@ z6k+38KyD@-0=uee54D0!Z1@B^ilj~StchdOn(*qvg~s5QJpWGc!6U^Aj!xt-HZn_V zS%|fyQ5YS@EP2lBIodXCLjG_+a)%En+7jzngk@J>6D~^xbxKkvf-R0-c%mX+o{?&j zZZ%RxFeav8Y0gkwtdtrwUb-i0Egd2C=ADu%w5VV-hNJvl)GZ?M;y$!?b=S+wKRK7Q zcOjPT!p<*#8m;TsBih=@Xc&c)?Vy`Ys>IvK@|1%N+M6J-^RCRaZcPP2eQh9DEGZr+ z?8B~wF14mk4Xkuen{wY^CWwS1PI<8gikY*)3?RSo5l8es4*J z43k_BIwc}of=6Pfs%xIxlMDGOJN zvl!a>G)52XMqA%fbgkZi%)%bN*ZzZw2!rn4@+J)2eK#kWuEW{)W~-`y1vhA5-7p%R z&f5N!a9f8cK1Xa=O}=9{wg%}Ur^+8Y(!UCeqw>%wj@|bYHD-bZO~mk3L$9_^MmF3G zvCiK^e@q6G?tHkM8%GqsBMZaB20W$UEt_5r~jc#WlR>Bv{6W>A=!#InoY zLOd04@Rz?*7PpW8u|+}bt`?+Z(GsX{Br4A2$ZZ(26Degmr9`O=t2KgHTL*==R3xcP z&Y(J7hC@6_x8zVz!CX3l4Xtss6i7r#E6kXMNN1~>9KTRzewfp))ij%)SBBl0fZdYP zd!zzQD5u8yk-u|41|Rqz7_tCFUMThZJVj)yQf6^Cwtn|Ew6cm5J|u1Bq>MWX-AfB&NE;C z62@=-0le`E6-CurMKjoIy)BuUmhMGJb}pPx!@GLWMT+wH2R?wA=MEy)o57~feFp8P zY@YXAyt4<1FD<|iw{FGQu~GEI<4C64)V*QiVk+VzOV^9GWf4ir#oYgHJz!wq>iZV#_6@_{)&lum)4x z_Of*CLVQ7wdT#XT-(h0qH%mcIF7yzMIvvTN3bPceK>PpJi(=3Nny zbSn}p$dGKQUlX&-t~RR)#F7I<8NCD^yke(vdf#4^aAh}M-{tS9-&^tC4`KU_pToXy z+|K8sx}a)Kh{h{;*V1#hs1xB%(?j>)g~`Wv(9F)f=Qn)(daVB7hZtcp^#LrEr1T1J zZSJ*lVyVVjhy)mkex9Whn=EinKDHe@KlfQI-Fl7M?-c~HnW0;C;+MbUY8?FToy;A+ zs&Nc7VZ=Of+e!G6s#+S5WBU)kgQq_I1@!uH74GJ-+O|%0HXm9Mqlvp|j%0`T>fr9^ zK;qo>XdwZW<>%tTA+<(1^6(>=-2N;hRgBnjvEjN;VbKMbFg--WrGy|XESoH1p|M4` z86(gC^vB4qScASZ&cdpT{~QDN-jC|GJ(RYoW1VW4!SSn- zhQds9&RBKn6M&GVK_Aayt(Hekbnw=tr>f z^o@v9_*iQO1*zeOrts9Q-$pc@!StS&kz$cF`s@pM`rmJXTP&h5G)A74!0e%ZJbl}( zssI|_!%~_hZFypv*S^JE5N&Kvmx7KiG<|fGMO=WrH+@Yhuj+KwiS#l4>@%2nl zS)mDikfmokO4q2A)hRVZBq2-5q&XC>%HOLkOYxZ66(s86?=0s4z5xbiOV)}L-&6b)h6(~CIaR#JNw~46+WBiU7IhB zq!NuR4!TsYnyBg>@G=Ib*cMq^k<}AMpCeYEf&dzfiGI-wOQ7hb+nA zkN7_){y&c3xC0 AQ~&?~ literal 0 HcmV?d00001 diff --git a/public/images/snowball.jpg b/public/images/snowball.jpg new file mode 100644 index 0000000000000000000000000000000000000000..36473aab242f206757d11d25361e1ca5cf937aa3 GIT binary patch literal 25523 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 + + + + Ruby on Rails: Welcome aboard + + + + + + +
+ + +
+ + + + +
+

Getting started

+

Here’s how to get rolling:

+ +
    +
  1. +

    Use script/generate to create your models and controllers

    +

    To see all available options, run it without parameters.

    +
  2. + +
  3. +

    Set up a default route and remove or rename this file

    +

    Routes are set up in config/routes.rb.

    +
  4. + +
  5. +

    Create your database

    +

    Run rake db:migrate to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    +
  6. +
+
+
+ + +
+ + \ No newline at end of file diff --git a/public/javascripts/application.js b/public/javascripts/application.js new file mode 100644 index 0000000..fe45776 --- /dev/null +++ b/public/javascripts/application.js @@ -0,0 +1,2 @@ +// Place your application-specific JavaScript functions and classes here +// This file is automatically included by javascript_include_tag :defaults diff --git a/public/javascripts/controls.js b/public/javascripts/controls.js new file mode 100644 index 0000000..ca29aef --- /dev/null +++ b/public/javascripts/controls.js @@ -0,0 +1,963 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { }; +Autocompleter.Base = Class.create({ + baseInitialize: function(element, update, options) { + element = $(element); + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + this.oldElementValue = this.element.value; + + if(this.setOptions) + this.setOptions(options); + else + this.options = options || { }; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (Prototype.Browser.IE) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + 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) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index--; + else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++; + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); + } else { + this.element.value = value; + } + this.oldElementValue = this.element.value; + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + this.index = 0; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + this.tokenBounds = null; + if(this.getToken().length>=this.options.minChars) { + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + this.oldElementValue = this.element.value; + }, + + getToken: function() { + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; + } + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); + } +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(Autocompleter.Base, { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); + return "
      " + ret.join('') + "
    "; + } + }, options || { }); + } +}); + +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve (April 2007). + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +}; + +Ajax.InPlaceEditor = Class.create({ + initialize: function(element, url, options) { + this.url = url; + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; + } + if (this.options.externalControl) + this.options.externalControl = $(this.options.externalControl); + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; + this.element.title = this.options.clickToEditText; + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; + } + }, + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; + } + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); + }, + getText: function() { + return this.element.innerHTML.unescapeHTML(); + }, + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; + } + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); + } + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); + }, + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, + showSaving: function() { + this._oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + }, + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); + } + }, + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); + }, + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw('Server returned an invalid collection representation.'); + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadCollectionURL, options); + }, + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; + } + tempOption.update((text || '').stripScripts().stripTags()); + }, + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); + }, + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); + } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' + } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create({ + 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)); + } +}); \ No newline at end of file diff --git a/public/javascripts/dragdrop.js b/public/javascripts/dragdrop.js new file mode 100644 index 0000000..07229f9 --- /dev/null +++ b/public/javascripts/dragdrop.js @@ -0,0 +1,973 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || { }); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if(Object.isArray(containment)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + 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 containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var drop, affected = []; + + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) 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.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +}; + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + 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._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +}; + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create({ + initialize: function(element) { + var defaults = { + handle: false, + 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.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); + + this.element = $(element); + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 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); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + if(!this.delta) + this.delta = this.currentDelta(); + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this._originallyAbsolute) + 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); + + if(!this.options.quiet){ + 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 + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + 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(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + + if(this.options.ghosting) { + if (!this._originallyAbsolute) + Position.relativize(this.element); + delete this._originallyAbsolute; + Element.remove(this._clone); + this._clone = null; + } + + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && Object.isFunction(revert)) revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); + } else { + if(Object.isArray(this.options.snap)) { + p = p.map( function(v, i) { + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); + } else { + p = p.map( function(v) { + return (v/this.options.snap).round()*this.options.snap }.bind(this)); + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + 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] ); + } + } + } 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); + if (this._isScrollChild) { + 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 }; + } +}); + +Draggable._dragging = { }; + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create({ + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +}); + +var Sortable = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "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); + var s = Sortable.sortables[element.id]; + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + 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 + handle: false, // or a CSS class + only: false, + delay: 0, + hoverclass: null, + ghosting: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || { }); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + }; + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + }; + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.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[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + 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(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; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, 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, only: droponOptions.only}); + 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); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _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: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + }; + + /* 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; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + 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) + "[id]=" + + 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) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +}; \ No newline at end of file diff --git a/public/javascripts/effects.js b/public/javascripts/effects.js new file mode 100644 index 0000000..5a639d2 --- /dev/null +++ b/public/javascripts/effects.js @@ -0,0 +1,1128 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if (this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if (this.slice(0,1) == '#') { + if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if (this.length==7) color = this.toLowerCase(); + } + } + return (color.length==7 ? color : (arguments[0] || this)); +}; + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + .5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; + }, + pulse: function(pos, pulses) { + return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if (child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if (((typeof element == 'object') || + Object.isFunction(element)) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || { }); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || { }); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + 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), 15); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if (this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + for(var i=0, len=this.effects.length;i= this.startOn) { + if (timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if (this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#'; + } +}); + +Effect.Parallel = Class.create(Effect.Base, { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if (effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || { }); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { + initialize: function(element, percent) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or { } with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || { }); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = { }; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if (fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if (this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if (/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if (!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if (this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if (this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._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) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()); } + ); +}; + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) + ); +}; + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) + ); +}; + +Effect.BlindDown = function(element) { + element = $(element); + 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}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }); + } + }, arguments[1] || { })); +}; + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; + +Effect.Shake = function(element) { + element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}); }}); }}); }}); }}); }}); +}; + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) + ); +}; + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); + } + }, arguments[1] || { }) + ); +}; + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +}; + +Effect.Grow = function(element) { + element = $(element); + 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 (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ); + } + }); +}; + +Effect.Shrink = function(element) { + element = $(element); + 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 (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +}; + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || { }, + oldOpacity = element.getInlineOpacity(), + transition = options.transition || Effect.Transitions.linear, + reverser = function(pos){ + return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); + }; + + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +}; + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + }; + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ); + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ); + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '
    '; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { + results[property] = css[property]; + return results; + }); + if (!styles.opacity) styles.opacity = element.getOpacity(); + return styles; + }; +} + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element); + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + }; + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods); \ No newline at end of file diff --git a/public/javascripts/prototype.js b/public/javascripts/prototype.js new file mode 100644 index 0000000..dfe8ab4 --- /dev/null +++ b/public/javascripts/prototype.js @@ -0,0 +1,4320 @@ +/* Prototype JavaScript framework, version 1.6.0.3 + * (c) 2005-2008 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.0.3', + + Browser: { + IE: !!(window.attachEvent && + navigator.userAgent.indexOf('Opera') === -1), + Opera: navigator.userAgent.indexOf('Opera') > -1, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && + navigator.userAgent.indexOf('KHTML') === -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div')['__proto__'] && + document.createElement('div')['__proto__'] !== + document.createElement('form')['__proto__'] + }, + + ScriptFragment: ']*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +/* Based on Alex Arnell's inheritance implementation. */ +var Class = { + create: function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value; + value = (function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method); + + value.valueOf = method.valueOf.bind(method); + value.toString = method.toString.bind(method); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (Object.isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : String(object); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (!Object.isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return !!(object && object.nodeType == 1); + }, + + isArray: function(object) { + return object != null && typeof object == "object" && + 'splice' in object && 'join' in object; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; + } +}); + +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1] + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; + }, + + bind: function() { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, + + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + defer: function() { + var args = [0.01].concat($A(arguments)); + return this.delay.apply(this, args); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = new Element('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(//g,'>'); + }, + unescapeHTML: function() { + return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + try { + this._each(function(value) { + iterator.call(context, value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + var index = -number, slices = [], array = this.toArray(); + if (number < 1) return array; + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator || Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator.call(context, value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator || Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator.call(context, value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator.call(context, value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + var result; + this.each(function(value, index) { + if (iterator.call(context, value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + var results = []; + this.each(function(value, index) { + if (iterator.call(context, value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator.call(context, value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { + this.each(function(value, index) { + memo = iterator.call(context, memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator || Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator.call(context, value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + var results = []; + this.each(function(value, index) { + if (!iterator.call(context, value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + return this.map(function(value, index) { + return { + value: value, + criteria: iterator.call(context, value, index) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#'; + } +}; + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + filter: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any +}); +function $A(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + $A = function(iterable) { + if (!iterable) return []; + // In Safari, only use the `toArray` method if it's not a NodeList. + // A NodeList is a function, has an function `item` property, and a numeric + // `length` property. Adapted from Google Doctype. + if (!(typeof iterable === 'function' && typeof iterable.length === + 'number' && typeof iterable.item === 'function') && iterable.toArray) + return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; + }; +} + +Array.from = $A; + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(Object.isArray(value) ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator, context) { + $R(0, this, true).each(iterator, context); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: function(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + // simulating poorly supported hasOwnProperty + if (this._object[key] !== Object.prototype[key]) + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.inject([], function(results, pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return results.concat(values.map(toQueryPair.curry(key))); + } else results.push(toQueryPair(key, values)); + return results; + }).join('&'); + }, + + inspect: function() { + return '#'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +}; + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); + +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); + } +}); + +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && this.isSameOrigin() && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name) || null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); + if (element) this.Element.prototype = element.prototype; +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + element = $(element); + element.style.display = 'none'; + return element; + }, + + show: function(element) { + element = $(element); + element.style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, insert, tagName, childNodes; + + for (var position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + insert = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + insert(element, content); + continue; + } + + content = Object.toHTML(content); + + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $(element).select("*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + return Object.isNumber(expression) ? element.descendants()[expression] : + Element.select(element, expression)[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (ancestor.contains) + return ancestor.contains(element) && ancestor !== element; + + while (element = element.parentNode) + if (element == ancestor) return true; + + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value || value == 'auto') { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = element.getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (Prototype.Browser.Opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName.toUpperCase() == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + // returns '0px' for hidden elements; we want it to return null + if (!Element.visible(element)) return null; + + // returns the border-box dimensions rather than the content-box + // dimensions, so we subtract padding and borders from the value + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); + + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} + +else if (Prototype.Browser.IE) { + // IE doesn't report offsets correctly for static elements, so we change them + // to "relative" to get the values, then change them back. + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + // IE throws an error if element is not in document + try { element.offsetParent } + catch(e) { return $(document.body) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + // Trigger hasLayout on the offset parent so that IE6 reports + // accurate offsetTop and offsetLeft values for position: fixed. + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( + function(proceed, element) { + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + return proceed(element); + } + ); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName.toUpperCase() == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Element#cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if ('outerHTML' in document.createElement('div')) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + tags: { + TABLE: ['', '
    ', 1], + TBODY: ['', '
    ', 2], + TR: ['', '
    ', 3], + TD: ['
    ', '
    ', 4], + SELECT: ['', 1] + } +}; + +(function() { + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return !!(node && node.specified); + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div')['__proto__']) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div')['__proto__']; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName.toUpperCase(), property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName)['__proto__']; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }, B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + if (B.WebKit && !document.evaluate) { + // Safari <3.0 needs self.innerWidth/Height + dimensions[d] = self['inner' + D]; + } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) { + // Opera <9.5 needs document.body.clientWidth/Height + dimensions[d] = document.body['client' + D] + } else { + dimensions[d] = document.documentElement['client' + D]; + } + }); + return dimensions; + }, + + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; +/* Portions of the Selector class are derived from Jack Slocum's DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else if (this.shouldUseXPath()) { + this.mode = 'xpath'; + this.compileXPathMatcher(); + } else { + this.mode = "normal"; + this.compileMatcher(); + } + + }, + + shouldUseXPath: function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + // Safari 3 chokes on :*-of-type and :empty + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + // XPath can't do namespaced attributes, nor can it read + // the "checked" property from DOM nodes + if ((/(\[[\w-]*?:|:checked)/).test(e)) + return false; + + return true; + }, + + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + + if (!Selector._div) Selector._div = new Element('div'); + + // Make sure the browser treats the selector as valid. Test on an + // isolated element to minimize cost of this check. + try { + Selector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; + }, + + compileMatcher: function() { + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + var e = this.expression, results; + + switch (this.mode) { + case 'selectorsAPI': + // querySelectorAll queries document-wide, then filters to descendants + // of the context element. That's not what we want. + // Add an explicit context to the selector if necessary. + if (root !== document) { + var oldId = root.id, id = $(root).identify(); + e = "#" + id + " " + e; + } + + results = $A(root.querySelectorAll(e)).map(Element.extend); + root.id = oldId; + + return results; + case 'xpath': + return document._getElementsByXPath(this.xpath, root); + default: + return this.matcher(root); + } + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#"; + } +}); + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0)]", + 'checked': "[@checked]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: +/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, + attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + var _true = Prototype.emptyFunction; + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = _true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._countedByPrototype = Prototype.emptyFunction; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._countedByPrototype) { + n._countedByPrototype = Prototype.emptyFunction; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + var uTagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() === uTagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._countedByPrototype) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || node.firstChild) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._countedByPrototype) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, + '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, + '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + + '-').include('-' + (v || "").toUpperCase() + '-'); } + }, + + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + + matchElements: function(elements, expression) { + var matches = $$(expression), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._countedByPrototype) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + expressions = Selector.split(expressions.join(',')); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +if (Prototype.Browser.IE) { + Object.extend(Selector.handlers, { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }, + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML. + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } + }); +} + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + }, + + select: function(element, value) { + if (Object.isUndefined(value)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; + } + } + else opt.selected = value.include(currentValue); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + event = Event.extend(event); + + var node = event.target, + type = event.type, + currentTarget = event.currentTarget; + + if (currentTarget && currentTarget.tagName) { + // Firefox screws up the "click" event when moving between radio buttons + // via arrow keys. It also screws up the "load" and "error" events on images, + // reporting the document as the target instead of the original image. + if (type === 'load' || type === 'error' || + (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' + && currentTarget.type === 'radio')) + node = currentTarget; + } + if (node.nodeType == Node.TEXT_NODE) node = node.parentNode; + return Element.extend(node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + var docElement = document.documentElement, + body = document.body || { scrollLeft: 0, scrollTop: 0 }; + return { + x: event.pageX || (event.clientX + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)), + y: event.pageY || (event.clientY + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__']; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._prototypeEventID) return element._prototypeEventID[0]; + arguments.callee.id = arguments.callee.id || 1; + return element._prototypeEventID = [++arguments.callee.id]; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event); + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + + // Internet Explorer needs to remove event handlers on page unload + // in order to avoid memory leaks. + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + // Safari has a dummy event handler on page unload so that it won't + // use its bfcache. Safari <= 3.1 has an issue with restoring the "document" + // object when page is returned to via the back button using its bfcache. + if (Prototype.Browser.WebKit) { + window.addEventListener('unload', Prototype.emptyFunction, false); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + var event; + if (document.createEvent) { + event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return Event.extend(event); + } + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize(), + loaded: false +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer; + + function fireContentLoadedEvent() { + if (document.loaded) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + document.loaded = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); + } + + } else { + document.write("