From 7812004f0440560fe590f44f1fc0e6fa48ccda6b Mon Sep 17 00:00:00 2001 From: Coleman Date: Wed, 1 Oct 2008 01:19:35 -0500 Subject: [PATCH] adding first revision for deployment --- README | 14 + Rakefile | 75 + app/controllers/application.rb | 9 + app/controllers/exceptions.rb | 13 + app/controllers/favorites.rb | 9 + app/controllers/home.rb | 9 + app/controllers/photos.rb | 9 + app/controllers/sessions.rb | 27 + app/controllers/stats.rb | 9 + app/controllers/users.rb | 71 + app/controllers/votes.rb | 9 + app/helpers/favorites_helper.rb | 5 + app/helpers/global_helpers.rb | 15 + app/helpers/home_helper.rb | 5 + app/helpers/photos_helper.rb | 5 + app/helpers/session_helper.rb | 5 + app/helpers/stats_helper.rb | 5 + app/helpers/users_helper.rb | 5 + app/helpers/votes_helper.rb | 5 + app/models/merb/session.rb | 9 + app/models/photo.rb | 8 + app/models/user.rb | 44 + app/models/vote.rb | 34 + .../exceptions/internal_server_error.html.erb | 216 + app/views/exceptions/not_acceptable.html.erb | 63 + app/views/exceptions/not_found.html.erb | 47 + app/views/favorites/index.html.haml | 1 + app/views/home/acceptable_use.html.haml | 12 + app/views/home/index.html.haml | 61 + app/views/layout/application.html.haml | 59 + app/views/photos/index.html.haml | 1 + app/views/sessions/new.html.haml | 12 + app/views/stats/index.html.haml | 1 + app/views/users/edit.html.haml | 12 + app/views/users/index.html.haml | 9 + app/views/users/new.html.haml | 18 + app/views/votes/index.html.haml | 1 + autotest/discover.rb | 1 + autotest/merb.rb | 149 + autotest/merb_rspec.rb | 165 + config/environments/development.rb | 8 + config/environments/production.rb | 7 + config/environments/rake.rb | 7 + config/environments/test.rb | 6 + config/init.rb | 28 + config/rack.rb | 12 + config/recaptcha.yml.template | 3 + config/router.rb | 13 + lib/LICENSE_for_recaptcha | 19 + lib/recaptcha.rb | 78 + merb.thor | 822 ++++ public/images/ajax-loader.gif | Bin 0 -> 4176 bytes public/images/binaryattraction.png | Bin 0 -> 9075 bytes public/images/binaryattraction.xcf | Bin 0 -> 11518 bytes public/images/camera-photo.png | Bin 0 -> 1256 bytes public/images/emblem-favorite.png | Bin 0 -> 1124 bytes public/images/footer.png | Bin 0 -> 626 bytes public/images/merb.jpg | Bin 0 -> 5815 bytes public/images/system-lock-screen.png | Bin 0 -> 1005 bytes public/images/system-log-out.png | Bin 0 -> 1084 bytes public/images/system-users.png | Bin 0 -> 1277 bytes public/images/user-trash.png | Bin 0 -> 1149 bytes public/images/utilities-system-monitor.png | Bin 0 -> 990 bytes public/images/vote.png | Bin 0 -> 1002 bytes public/javascripts/application.js | 28 + public/javascripts/dragdrop.js | 974 ++++ public/javascripts/effects.js | 1122 +++++ public/javascripts/prototype.js | 4221 +++++++++++++++++ public/stylesheets/ba.css | 121 + public/stylesheets/master.css | 119 + .../migrations/001_create_users_migration.rb | 15 + .../migrations/002_create_photos_migration.rb | 16 + .../migrations/003_create_votes_migration.rb | 14 + .../004_create_sessions_migration.rb | 14 + schema/schema.rb | 50 + spec/controllers/favorites_spec.rb | 7 + spec/controllers/home_spec.rb | 7 + spec/controllers/photos_spec.rb | 7 + spec/controllers/sessions_spec.rb | 7 + spec/controllers/stats_spec.rb | 7 + spec/controllers/users_spec.rb | 7 + spec/controllers/votes_spec.rb | 7 + spec/helpers/favorites_helper_spec.rb | 5 + spec/helpers/home_helper_spec.rb | 5 + spec/helpers/photos_helper_spec.rb | 5 + spec/helpers/sessions_helper_spec.rb | 5 + spec/helpers/stats_helper_spec.rb | 5 + spec/helpers/users_helper_spec.rb | 5 + spec/helpers/votes_helper_spec.rb | 5 + spec/models/photo_spec.rb | 7 + spec/models/user_spec.rb | 7 + spec/models/vote_spec.rb | 7 + spec/spec.opts | 0 spec/spec_helper.rb | 13 + test/test_helper.rb | 14 + 95 files changed, 9056 insertions(+) create mode 100644 README create mode 100644 Rakefile create mode 100644 app/controllers/application.rb create mode 100644 app/controllers/exceptions.rb create mode 100644 app/controllers/favorites.rb create mode 100644 app/controllers/home.rb create mode 100644 app/controllers/photos.rb create mode 100644 app/controllers/sessions.rb create mode 100644 app/controllers/stats.rb create mode 100644 app/controllers/users.rb create mode 100644 app/controllers/votes.rb create mode 100644 app/helpers/favorites_helper.rb create mode 100644 app/helpers/global_helpers.rb create mode 100644 app/helpers/home_helper.rb create mode 100644 app/helpers/photos_helper.rb create mode 100644 app/helpers/session_helper.rb create mode 100644 app/helpers/stats_helper.rb create mode 100644 app/helpers/users_helper.rb create mode 100644 app/helpers/votes_helper.rb create mode 100644 app/models/merb/session.rb create mode 100644 app/models/photo.rb create mode 100644 app/models/user.rb create mode 100644 app/models/vote.rb create mode 100644 app/views/exceptions/internal_server_error.html.erb create mode 100644 app/views/exceptions/not_acceptable.html.erb create mode 100644 app/views/exceptions/not_found.html.erb create mode 100644 app/views/favorites/index.html.haml create mode 100644 app/views/home/acceptable_use.html.haml create mode 100644 app/views/home/index.html.haml create mode 100644 app/views/layout/application.html.haml create mode 100644 app/views/photos/index.html.haml create mode 100644 app/views/sessions/new.html.haml create mode 100644 app/views/stats/index.html.haml create mode 100644 app/views/users/edit.html.haml create mode 100644 app/views/users/index.html.haml create mode 100644 app/views/users/new.html.haml create mode 100644 app/views/votes/index.html.haml create mode 100644 autotest/discover.rb create mode 100644 autotest/merb.rb create mode 100644 autotest/merb_rspec.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/rake.rb create mode 100644 config/environments/test.rb create mode 100644 config/init.rb create mode 100644 config/rack.rb create mode 100644 config/recaptcha.yml.template create mode 100644 config/router.rb create mode 100644 lib/LICENSE_for_recaptcha create mode 100644 lib/recaptcha.rb create mode 100644 merb.thor create mode 100644 public/images/ajax-loader.gif create mode 100644 public/images/binaryattraction.png create mode 100644 public/images/binaryattraction.xcf create mode 100644 public/images/camera-photo.png create mode 100644 public/images/emblem-favorite.png create mode 100644 public/images/footer.png create mode 100644 public/images/merb.jpg create mode 100644 public/images/system-lock-screen.png create mode 100644 public/images/system-log-out.png create mode 100644 public/images/system-users.png create mode 100644 public/images/user-trash.png create mode 100644 public/images/utilities-system-monitor.png create mode 100644 public/images/vote.png create mode 100644 public/javascripts/application.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/stylesheets/ba.css create mode 100644 public/stylesheets/master.css create mode 100644 schema/migrations/001_create_users_migration.rb create mode 100644 schema/migrations/002_create_photos_migration.rb create mode 100644 schema/migrations/003_create_votes_migration.rb create mode 100644 schema/migrations/004_create_sessions_migration.rb create mode 100644 schema/schema.rb create mode 100644 spec/controllers/favorites_spec.rb create mode 100644 spec/controllers/home_spec.rb create mode 100644 spec/controllers/photos_spec.rb create mode 100644 spec/controllers/sessions_spec.rb create mode 100644 spec/controllers/stats_spec.rb create mode 100644 spec/controllers/users_spec.rb create mode 100644 spec/controllers/votes_spec.rb create mode 100644 spec/helpers/favorites_helper_spec.rb create mode 100644 spec/helpers/home_helper_spec.rb create mode 100644 spec/helpers/photos_helper_spec.rb create mode 100644 spec/helpers/sessions_helper_spec.rb create mode 100644 spec/helpers/stats_helper_spec.rb create mode 100644 spec/helpers/users_helper_spec.rb create mode 100644 spec/helpers/votes_helper_spec.rb create mode 100644 spec/models/photo_spec.rb create mode 100644 spec/models/user_spec.rb create mode 100644 spec/models/vote_spec.rb create mode 100644 spec/spec.opts create mode 100644 spec/spec_helper.rb create mode 100644 test/test_helper.rb diff --git a/README b/README new file mode 100644 index 0000000..941334e --- /dev/null +++ b/README @@ -0,0 +1,14 @@ +BinaryAttraction + +A little social program i am writing for my senior design class. This site will +let you vote in the new digital age on photos. + +Dependencies: + * Merb + * Haml / Sass + * Merb Helpers + * Merb Has Flash + * Recaptcha account + +Released under GNU GPLv2 + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..cba35ee --- /dev/null +++ b/Rakefile @@ -0,0 +1,75 @@ +require 'rubygems' +Gem.clear_paths +Gem.path.unshift(File.join(File.dirname(__FILE__), "gems")) + +require 'rake' +require 'rake/rdoctask' +require 'rake/testtask' +require 'spec/rake/spectask' +require 'fileutils' + +## +# requires frozen merb-core (from /framework) +# adds the other components to the load path +def require_frozen_framework + framework = File.join(File.dirname(__FILE__), "framework") + if File.directory?(framework) + puts "Running from frozen framework" + core = File.join(framework,"merb-core") + if File.directory?(core) + puts "using merb-core from #{core}" + $:.unshift File.join(core,"lib") + require 'merb-core' + end + more = File.join(framework,"merb-more") + if File.directory?(more) + Dir.new(more).select {|d| d =~ /merb-/}.each do |d| + $:.unshift File.join(more,d,'lib') + end + end + plugins = File.join(framework,"merb-plugins") + if File.directory?(plugins) + Dir.new(plugins).select {|d| d =~ /merb_/}.each do |d| + $:.unshift File.join(plugins,d,'lib') + end + end + require "merb-core/core_ext/kernel" + require "merb-core/core_ext/rubygems" + else + p "merb doesn't seem to be frozen in /framework" + require 'merb-core' + end +end + +if ENV['FROZEN'] + require_frozen_framework +else + require 'merb-core' +end + +require 'merb-core/tasks/merb' +include FileUtils + +# Load the basic runtime dependencies; this will include +# any plugins and therefore plugin rake tasks. +init_env = ENV['MERB_ENV'] || 'rake' +Merb.load_dependencies(:environment => init_env) + +# Get Merb plugins and dependencies +Merb::Plugins.rakefiles.each { |r| require r } + +# Load any app level custom rakefile extensions from lib/tasks +tasks_path = File.join(File.dirname(__FILE__), "lib", "tasks") +rake_files = Dir["#{tasks_path}/*.rake"] +rake_files.each{|rake_file| load rake_file } + + +desc "start runner environment" +task :merb_env do + Merb.start_environment(:environment => init_env, :adapter => 'runner') +end + +############################################################################## +# ADD YOUR CUSTOM TASKS IN /lib/tasks +# NAME YOUR RAKE FILES file_name.rake +############################################################################## diff --git a/app/controllers/application.rb b/app/controllers/application.rb new file mode 100644 index 0000000..2b8bdfd --- /dev/null +++ b/app/controllers/application.rb @@ -0,0 +1,9 @@ +class Application < Merb::Controller + def current_user + (@user ||= User.find(session[:user_id])) + end + + def reset_session + session[:user_id] = nil + end +end diff --git a/app/controllers/exceptions.rb b/app/controllers/exceptions.rb new file mode 100644 index 0000000..8f462b1 --- /dev/null +++ b/app/controllers/exceptions.rb @@ -0,0 +1,13 @@ +class Exceptions < Application + + # handle NotFound exceptions (404) + def not_found + render :format => :html + end + + # handle NotAcceptable exceptions (406) + def not_acceptable + render :format => :html + end + +end \ No newline at end of file diff --git a/app/controllers/favorites.rb b/app/controllers/favorites.rb new file mode 100644 index 0000000..5fa6366 --- /dev/null +++ b/app/controllers/favorites.rb @@ -0,0 +1,9 @@ +class Favorites < Application + + # ...and remember, everything returned from an action + # goes to the client... + def index + render + end + +end diff --git a/app/controllers/home.rb b/app/controllers/home.rb new file mode 100644 index 0000000..25585ef --- /dev/null +++ b/app/controllers/home.rb @@ -0,0 +1,9 @@ +class Home < Application + def index + render + end + + def acceptable_use + render + end +end diff --git a/app/controllers/photos.rb b/app/controllers/photos.rb new file mode 100644 index 0000000..f6997f4 --- /dev/null +++ b/app/controllers/photos.rb @@ -0,0 +1,9 @@ +class Photos < Application + + # ...and remember, everything returned from an action + # goes to the client... + def index + render + end + +end diff --git a/app/controllers/sessions.rb b/app/controllers/sessions.rb new file mode 100644 index 0000000..ff8c78c --- /dev/null +++ b/app/controllers/sessions.rb @@ -0,0 +1,27 @@ +class Sessions < Application + def index + redirect '/' + end + + def new + render + end + + def create + user = User.find_by_user_name params[:user_name] + if user.authenticated_against?(params[:password]) + session[:user_id] = user.id + flash[:notice] = 'Great success!' + redirect '/' + else + flash[:error] = 'Login failed' + render :new + end + end + + def delete + reset_session + flash[:notice] = 'Goodbye!' + redirect '/' + end +end diff --git a/app/controllers/stats.rb b/app/controllers/stats.rb new file mode 100644 index 0000000..b94a7ae --- /dev/null +++ b/app/controllers/stats.rb @@ -0,0 +1,9 @@ +class Stats < Application + + # ...and remember, everything returned from an action + # goes to the client... + def index + render + end + +end diff --git a/app/controllers/users.rb b/app/controllers/users.rb new file mode 100644 index 0000000..b51a90b --- /dev/null +++ b/app/controllers/users.rb @@ -0,0 +1,71 @@ +class Users < Application + before :prepare_user, :only => [ :show, :edit, :update, :delete ] + + include Ambethia::ReCaptcha::Controller + + def index + if current_user.administrator? + @users = User.find :all, :order => 'user_name ASC' + render + else + redirect url(:user, :id => current_user.user_name) + end + end + + def show + render + end + + def new + @user = User.new + render + end + + def create + @user = User.new params[:user] + @user.user_name = params[:user][:user_name] rescue nil + if verify_recaptcha(@user) and @user.save + flash[:notice] = 'Great success' + redirect '/' + else + flash[:error] = 'The user could not be created...' + render :new + end + end + + def edit + render + end + + def update + if @user.save + flash[:notice] = 'Great success' + redirect url(:users) + else + render :edit + end + end + + def delete + if @user.destroy + flash[:notice] = "Epic failure, goodbye #{@user.user_name}" + reset_session if @user.id == session[:user_id] + else + flash[:error] = 'That does not work...' + end + redirect url(:users) + end + + protected + + def prepare_user + @user = if current_user.administrator? + User.find_by_user_name params[:id] + else + current_user + end + raise NotFound if @user.nil? + @user.attributes = params[:user] if params[:user] and request.post? + true + end +end diff --git a/app/controllers/votes.rb b/app/controllers/votes.rb new file mode 100644 index 0000000..a8dce36 --- /dev/null +++ b/app/controllers/votes.rb @@ -0,0 +1,9 @@ +class Votes < Application + + # ...and remember, everything returned from an action + # goes to the client... + def index + render + end + +end diff --git a/app/helpers/favorites_helper.rb b/app/helpers/favorites_helper.rb new file mode 100644 index 0000000..0597bd9 --- /dev/null +++ b/app/helpers/favorites_helper.rb @@ -0,0 +1,5 @@ +module Merb + module FavoritesHelper + + end +end # Merb \ No newline at end of file diff --git a/app/helpers/global_helpers.rb b/app/helpers/global_helpers.rb new file mode 100644 index 0000000..f7b1077 --- /dev/null +++ b/app/helpers/global_helpers.rb @@ -0,0 +1,15 @@ +module Merb + module GlobalHelpers + def logged_in? + !session[:user_id].nil? + end + + def error_messages(obj) + if obj.errors.empty? + nil + else + "
" + end + end + end +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000..aab1536 --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,5 @@ +module Merb + module HomeHelper + + end +end # Merb \ No newline at end of file diff --git a/app/helpers/photos_helper.rb b/app/helpers/photos_helper.rb new file mode 100644 index 0000000..32ce477 --- /dev/null +++ b/app/helpers/photos_helper.rb @@ -0,0 +1,5 @@ +module Merb + module PhotosHelper + + end +end # Merb \ No newline at end of file diff --git a/app/helpers/session_helper.rb b/app/helpers/session_helper.rb new file mode 100644 index 0000000..d75293c --- /dev/null +++ b/app/helpers/session_helper.rb @@ -0,0 +1,5 @@ +module Merb + module SessionHelper + + end +end # Merb \ No newline at end of file diff --git a/app/helpers/stats_helper.rb b/app/helpers/stats_helper.rb new file mode 100644 index 0000000..62531c0 --- /dev/null +++ b/app/helpers/stats_helper.rb @@ -0,0 +1,5 @@ +module Merb + module StatsHelper + + end +end # Merb \ No newline at end of file diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..ea6e3c0 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,5 @@ +module Merb + module UsersHelper + include Ambethia::ReCaptcha::Helper + end +end # Merb diff --git a/app/helpers/votes_helper.rb b/app/helpers/votes_helper.rb new file mode 100644 index 0000000..9a60c95 --- /dev/null +++ b/app/helpers/votes_helper.rb @@ -0,0 +1,5 @@ +module Merb + module VotesHelper + + end +end # Merb \ No newline at end of file diff --git a/app/models/merb/session.rb b/app/models/merb/session.rb new file mode 100644 index 0000000..5d05316 --- /dev/null +++ b/app/models/merb/session.rb @@ -0,0 +1,9 @@ +module Merb + module Session + + # The Merb::Session module gets mixed into Merb::SessionContainer to allow + # app-level functionality; it will be included and methods will be available + # through request.session as instance methods. + + end +end \ No newline at end of file diff --git a/app/models/photo.rb b/app/models/photo.rb new file mode 100644 index 0000000..5d97abd --- /dev/null +++ b/app/models/photo.rb @@ -0,0 +1,8 @@ +class Photo < ActiveRecord::Base + #property :id, Integer, :serial => true + #property :filename, String + #property :email_hash, String + #property :created_at, DateTime + validates_presence_of :filename + has_many :votes, :dependent => :destroy +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..3a22c93 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,44 @@ +class User < ActiveRecord::Base + attr_accessor :password, :password_confirmation + attr_protected :user_name + attr_protected :auth_token + attr_protected :authorized + + validates_presence_of :user_name + validates_length_of :user_name, :within => (6..32) + validates_uniqueness_of :user_name + validates_format_of :user_name, :with => /[\w_-]+/ + + has_many :votes, :dependent => :destroy + + before_validation :saltify_password + + def authenticated_against?(str) + ss = User.salted_string(str) + if self.auth_token.to_s == ss + true + else + false + end + end + + def self.salted_string(str) + Digest::SHA1.hexdigest("#{Merb::Config[:session_secret_key]}--#{str}--") + end + + protected + + def saltify_password + if !self.password.to_s.empty? + if self.password.to_s.size < 6 + self.errors.add(:password, 'is too short') + elsif self.password != self.password_confirmation + self.errors.add(:passwords, 'do not match') + else + self.auth_token = User.salted_string(self.password) + end + elsif self.auth_token.to_s.empty? + self.errors.add(:password, 'is missing') + end + end +end diff --git a/app/models/vote.rb b/app/models/vote.rb new file mode 100644 index 0000000..d2dab15 --- /dev/null +++ b/app/models/vote.rb @@ -0,0 +1,34 @@ +class Vote < ActiveRecord::Base + belongs_to :photo + belongs_to :user + + validates_presence_of :vote + + ## + # Checks if this vote is anonymous, or not an authenticated User vote. + # + def anonymous? + self.user_id.nil? + end + + ## + # Convert this Vote to a number. + # + def to_i + self.vote? ? 1 : 0 + end + + ## + # Is this a 'no' vote. + # + def zero? + self.to_i == 0 + end + + ## + # Is this a 'yes' vote. + # + def one? + self.to_i == 1 + end +end diff --git a/app/views/exceptions/internal_server_error.html.erb b/app/views/exceptions/internal_server_error.html.erb new file mode 100644 index 0000000..a3d175e --- /dev/null +++ b/app/views/exceptions/internal_server_error.html.erb @@ -0,0 +1,216 @@ + + + + <%= @exception_name %> + + + +
+ +
+

<%= @exception_name %> <%= @exception.class::STATUS %>

+ <% if show_details = ::Merb::Config[:exception_details] -%> +

<%=h @exception.message %>

+ <% else -%> +

Sorry about that...

+ <% end -%> +

Parameters

+
    + <% params[:original_params].each do |param, value| %> +
  • <%= param %>: <%= value.inspect %>
  • + <% end %> + <%= "
  • None
  • " if params[:original_params].empty? %> +
+ +

Session

+
    + <% params[:original_session].each do |param, value| %> +
  • <%= param %>: <%= value.inspect %>
  • + <% end %> + <%= "
  • None
  • " if params[:original_session].empty? %> +
+ +

Cookies

+
    + <% params[:original_cookies].each do |param, value| %> +
  • <%= param %>: <%= value.inspect %>
  • + <% end %> + <%= "
  • None
  • " if params[:original_cookies].empty? %> +
+
+ + <% if show_details %> + + <% @exception.backtrace.each_with_index do |line, index| %> + + + + + + + + + + + + <% end %> +
+ + <%= (line.match(/^([^:]+)/)[1] rescue 'unknown').sub(/\/((opt|usr)\/local\/lib\/(ruby\/)?(gems\/)?(1.8\/)?(gems\/)?|.+\/app\/)/, '') %> + <% unless line.match(/\.erb:/) %> + in "<%= line.match(/:in `(.+)'$/)[1] rescue '?' %>" + <% else %> + (ERB Template) + <% end %> + + <%=lineno%>  +
+ <% (__caller_lines__(file, lineno, 5) rescue []).each do |llineno, lcode, lcurrent| %> +<%= llineno %><%='' if llineno==lineno.to_i %><%= lcode.size > 90 ? CGI.escapeHTML(lcode[0..90])+'......' : CGI.escapeHTML(lcode) %><%='' if llineno==lineno.to_i %> +<% end %> + +
+ + <% end %> + +
+ + \ No newline at end of file diff --git a/app/views/exceptions/not_acceptable.html.erb b/app/views/exceptions/not_acceptable.html.erb new file mode 100644 index 0000000..f632712 --- /dev/null +++ b/app/views/exceptions/not_acceptable.html.erb @@ -0,0 +1,63 @@ +
+
+ + +

pocket rocket web framework

+
+
+ +
+

Exception:

+

<%= params[:exception] %>

+
+ +
+

Why am I seeing this page?

+

Merb couldn't find an appropriate content_type to return, + based on what you said was available via provides() and + what the client requested.

+ +

How to add a mime-type

+

+      Merb.add_mime_type :pdf, :to_pdf, %w[application/pdf], "Content-Encoding" => "gzip"
+    
+

What this means is:

+
    +
  • Add a mime-type for :pdf
  • +
  • Register the method for converting objects to PDF as #to_pdf.
  • +
  • Register the incoming mime-type "Accept" header as application/pdf.
  • +
  • Specify a new header for PDF types so it will set Content-Encoding to gzip.
  • +
+ +

You can then do:

+

+      class Foo < Application
+        provides :pdf
+      end
+    
+ +

Where can I find help?

+

If you have any questions or if you can't figure something out, please take a + look at our project page, + feel free to come chat at irc.freenode.net, channel #merb, + or post to merb mailing list + on Google Groups.

+ +

What if I've found a bug?

+

If you want to file a bug or make your own contribution to Merb, + feel free to register and create a ticket at our + project development page + on Lighthouse.

+ +

How do I edit this page?

+

You can change what people see when this happens by editing app/views/exceptions/not_acceptable.html.erb.

+ +
+ + +
diff --git a/app/views/exceptions/not_found.html.erb b/app/views/exceptions/not_found.html.erb new file mode 100644 index 0000000..388c72c --- /dev/null +++ b/app/views/exceptions/not_found.html.erb @@ -0,0 +1,47 @@ +
+
+ + +

pocket rocket web framework

+
+
+ +
+

Exception:

+

<%= params[:exception] %>

+
+ +
+

Welcome to Merb!

+

Merb is a light-weight MVC framework written in Ruby. We hope you enjoy it.

+ +

Where can I find help?

+

If you have any questions or if you can't figure something out, please take a + look at our project page, + feel free to come chat at irc.freenode.net, channel #merb, + or post to merb mailing list + on Google Groups.

+ +

What if I've found a bug?

+

If you want to file a bug or make your own contribution to Merb, + feel free to register and create a ticket at our + project development page + on Lighthouse.

+ +

How do I edit this page?

+

You're seeing this page because you need to edit the following files: +

    +
  • config/router.rb (recommended)
  • +
  • app/views/exceptions/not_found.html.erb (recommended)
  • +
  • app/views/layout/application.html.erb (change this layout)
  • +
+

+
+ + +
diff --git a/app/views/favorites/index.html.haml b/app/views/favorites/index.html.haml new file mode 100644 index 0000000..4596a79 --- /dev/null +++ b/app/views/favorites/index.html.haml @@ -0,0 +1 @@ +You're in index of the Favorites controller. \ No newline at end of file diff --git a/app/views/home/acceptable_use.html.haml b/app/views/home/acceptable_use.html.haml new file mode 100644 index 0000000..e2a77e0 --- /dev/null +++ b/app/views/home/acceptable_use.html.haml @@ -0,0 +1,12 @@ +%h1 Acceptable use policy + +%p There are a few rules you must abide by to use this site. Violation will at least mean a permenant ban and at worst prosecution under federal law. + +%ol + %li You must be 18 years old. + %li The pictures must be of a person. One consenting adult person. + %li Group pictures are allowed on the grounds that you are voting for the entire group. + %li Nudity is not allowed. Partial nudity is allowed. We will use any discretionary method to determine what is allowed. Be warned. + %li Always offer to send a statistics link to the person you are photographing. It's just polite :) + +%p We will never share any personal information gathered on this site. The only information we gather is so that you, the user, will have access to the data later. diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml new file mode 100644 index 0000000..d7d3daf --- /dev/null +++ b/app/views/home/index.html.haml @@ -0,0 +1,61 @@ +%style{ :type => 'text/css' } + :sass + #front_page + blockquote + :font-size 16px + :padding 5px 0px 5px 15px + :background-color #eeeeee + :border-top 1px dashed #9f9f9f + :border-right 1px dashed #9f9f9f + :border-bottom 1px solid #d4d4d4 + :border-left 1px solid #d4d4d4 + em + :margin-left 20px + h1 + :text-align center + +%span#front_page + %h1 What this is all about + + %blockquote + All you young guys are on a binary system. It's either 0 or 1. + %br + %em Larry Bell + + %blockquote + All you old guys are on the analog system. Join the digital revolution. + %br + %em Ross Bagwell + + %h1 Getting started + + %ul.no_list_style + - if logged_in? + %li + %a{ :href => url(:edit_user, :id => current_user.user_name) } + %img{ :src => '/images/system-lock-screen.png' } + Change your password + %li + %a{ :href => url(:favorite, :id => current_user.user_name) } + %img{ :src => '/images/emblem-favorite.png' } + Check your favorites + %li + %a{ :href => url(:new_photo) } + %img{ :src => '/images/camera-photo.png' } + Upload photos + %li + %a{ :href => url(:votes) } + %img{ :src => '/images/vote.png' } + Vote on new photos + %li + %a{ :href => url(:stat, :id => current_user.user_name) } + %img{ :src => '/images/utilities-system-monitor.png' } + Check stats on photos of yourself + - else + %li Sign up for an account + %li Log in if you have one + %li Check your favorites + %li Upload photos + %li Vote on new photos + %li Check stats on photos of yourself + diff --git a/app/views/layout/application.html.haml b/app/views/layout/application.html.haml new file mode 100644 index 0000000..ca68e1d --- /dev/null +++ b/app/views/layout/application.html.haml @@ -0,0 +1,59 @@ +!!! Strict +%html{html_attrs{'en-us'}} + %head + %title Binary Attraction + %meta{ 'http-equiv' => "content-type", :content => "text/html; charset=utf8" } + %link{ :href => "/stylesheets/ba.css", :rel => "stylesheet", :type => "text/css", :media => "screen", :charset => "utf-8" } + %script{ :src => "/javascripts/prototype.js", :type => "text/javascript" } + %script{ :src => "/javascripts/effects.js", :type => "text/javascript" } + %script{ :src => "/javascripts/dragdrop.js", :type => "text/javascript" } + %script{ :src => "/javascripts/application.js", :type => "text/javascript" } + - unless flash.keys.empty? + :javascript + hide_flashes(); + %body + #container + - unless flash.keys.empty? + #flash_container + - flash.keys.each do |key| + %div{ :class => key }= flash[key] + #header + %span#header_image + %a{ :href => '/', :title => 'B.A. Home' } + %img{ :src => '/images/binaryattraction.png', :alt => 'Binary Attraction' } + #tool_bar + - if logged_in? + %a{ :href => url(:new_vote), :title => 'Vote' } + %img{ :src => '/images/vote.png' } + Vote + | + %a{ :href => url(:new_photo), :title => 'Upload a photo' } + %img{ :src => '/images/camera-photo.png' } + Upload a photo + | + %a{ :href => url(:favorite, :id => session[:user_id]), :title => 'Favorites' } + %img{ :src => '/images/emblem-favorite.png' } + Favorites + | + %a{ :href => url(:stats), :title => 'Stats' } + %img{ :src => '/images/utilities-system-monitor.png' } + Check Stats + | + %a{ :href => url(:delete_session, :id => session[:user_id]), :title => 'Log out' } + %img{ :src => '/images/system-log-out.png' } + Log out + - else + %a{ :href => url(:new_user), :title => 'Sign Up' } + %img{ :src => '/images/system-users.png' } + Sign Up + | + %a{ :href => url(:new_session), :title => 'Log In' } + %img{ :src => '/images/system-lock-screen.png' } + Log In + #content + = catch_content :for_layout + #footer + © 2008 + %a{ :href => 'http://penguincoder.org' } penguincoder + | Usage of this site requires + %a{ :href => '/acceptable_use' } acceptable use policy diff --git a/app/views/photos/index.html.haml b/app/views/photos/index.html.haml new file mode 100644 index 0000000..6c2a32a --- /dev/null +++ b/app/views/photos/index.html.haml @@ -0,0 +1 @@ +You're in index of the Photos controller. \ No newline at end of file diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml new file mode 100644 index 0000000..a1386d0 --- /dev/null +++ b/app/views/sessions/new.html.haml @@ -0,0 +1,12 @@ += form :action => url(:sessions) do + %fieldset + %p + %label{ :for => 'user_name' } + User Name + = text_field :name => 'user_name', :id => 'user_name' + %p + %label{ :for => 'password' } + Password + = password_field :name => 'password', :id => 'password' + = submit 'Login' + diff --git a/app/views/stats/index.html.haml b/app/views/stats/index.html.haml new file mode 100644 index 0000000..fc81d5a --- /dev/null +++ b/app/views/stats/index.html.haml @@ -0,0 +1 @@ +You're in index of the Stats controller. \ No newline at end of file diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml new file mode 100644 index 0000000..19b15ff --- /dev/null +++ b/app/views/users/edit.html.haml @@ -0,0 +1,12 @@ += error_messages @user + += form :action => url(:edit_user, :id => @user.user_name) do + %fieldset + %legend== Changing settings for user #{@user.user_name} + %p + %label{ :for => 'password' } Password + = password_field :name => 'user[password]', :id => 'password' + %p + %label{ :for => 'password_confirmation' } Confirm + = password_field :name => 'user[password]', :id => 'password' + = submit 'Save' diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml new file mode 100644 index 0000000..c26f77c --- /dev/null +++ b/app/views/users/index.html.haml @@ -0,0 +1,9 @@ +%h1 Peoples. + +%ul + - @users.each do |user| + %li + %a{ :href => url(:edit_user, :id => user.user_name) }= user.user_name + %a{ :href => url(:delete_user, :id => user.user_name), :onclick => "return confirm('Are you sure?');" } + %img{ :src => '/images/user-trash.png' } + diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml new file mode 100644 index 0000000..589ba5b --- /dev/null +++ b/app/views/users/new.html.haml @@ -0,0 +1,18 @@ += error_messages @user + += form_for @user do + %fieldset + %legend Create a new user + %p + %label{ :for => 'user_name' } User Name + = text_field :name => 'user[user_name]', :id => 'user_name' + %p + %label{ :for => 'password' } Password + = password_field :name => 'user[password]', :id => 'password' + %p + %label{ :for => 'password_confirmation' } Confirm + = password_field :name => 'user[password_confirmation]', :id => 'password_confirmation' + %p + %label{ :for => 'recaptcha_response_field' } Captcha + #recaptcha_container= recaptcha_tags + = submit 'Create' diff --git a/app/views/votes/index.html.haml b/app/views/votes/index.html.haml new file mode 100644 index 0000000..26ec367 --- /dev/null +++ b/app/views/votes/index.html.haml @@ -0,0 +1 @@ +You're in index of the Votes controller. \ No newline at end of file diff --git a/autotest/discover.rb b/autotest/discover.rb new file mode 100644 index 0000000..fddd126 --- /dev/null +++ b/autotest/discover.rb @@ -0,0 +1 @@ +Autotest.add_discovery { "merb" } \ No newline at end of file diff --git a/autotest/merb.rb b/autotest/merb.rb new file mode 100644 index 0000000..b5d8ccb --- /dev/null +++ b/autotest/merb.rb @@ -0,0 +1,149 @@ +# Adapted from Autotest::Rails +require 'autotest' + +class Autotest::Merb < Autotest + + # +model_tests_dir+:: the directory to find model-centric tests + # +controller_tests_dir+:: the directory to find controller-centric tests + # +view_tests_dir+:: the directory to find view-centric tests + # +fixtures_dir+:: the directory to find fixtures in + attr_accessor :model_tests_dir, :controller_tests_dir, :view_tests_dir, :fixtures_dir + + def initialize + super + + initialize_test_layout + + # Ignore any happenings in these directories + add_exception %r%^\./(?:doc|log|public|tmp)% + + # Ignore any mappings that Autotest may have already set up + clear_mappings + + # Any changes to a file in the root of the 'lib' directory will run any + # model test with a corresponding name. + add_mapping %r%^lib\/.*\.rb% do |filename, _| + files_matching Regexp.new(["^#{model_test_for(filename)}$"]) + end + + # Any changes to a fixture will run corresponding view, controller and + # model tests + add_mapping %r%^#{fixtures_dir}/(.*)s.yml% do |_, m| + [ + model_test_for(m[1]), + controller_test_for(m[1]), + view_test_for(m[1]) + ] + end + + # Any change to a test or test will cause it to be run + add_mapping %r%^test/(unit|models|integration|controllers|views|functional)/.*rb$% do |filename, _| + filename + end + + # Any change to a model will cause it's corresponding test to be run + add_mapping %r%^app/models/(.*)\.rb$% do |_, m| + model_test_for(m[1]) + end + + # Any change to the global helper will result in all view and controller + # tests being run + add_mapping %r%^app/helpers/global_helpers.rb% do + files_matching %r%^test/(views|functional|controllers)/.*_test\.rb$% + end + + # Any change to a helper will run it's corresponding view and controller + # tests, unless the helper is the global helper. Changes to the global + # helper run all view and controller tests. + add_mapping %r%^app/helpers/(.*)_helper(s)?.rb% do |_, m| + if m[1] == "global" then + files_matching %r%^test/(views|functional|controllers)/.*_test\.rb$% + else + [ + view_test_for(m[1]), + controller_test_for(m[1]) + ] + end + end + + # Changes to views result in their corresponding view and controller test + # being run + add_mapping %r%^app/views/(.*)/% do |_, m| + [ + view_test_for(m[1]), + controller_test_for(m[1]) + ] + end + + # Changes to a controller result in its corresponding test being run. If + # the controller is the exception or application controller, all + # controller tests are run. + add_mapping %r%^app/controllers/(.*)\.rb$% do |_, m| + if ["application", "exception"].include?(m[1]) + files_matching %r%^test/(controllers|views|functional)/.*_test\.rb$% + else + controller_test_for(m[1]) + end + end + + # If a change is made to the router, run all controller and view tests + add_mapping %r%^config/router.rb$% do # FIX + files_matching %r%^test/(controllers|views|functional)/.*_test\.rb$% + end + + # If any of the major files governing the environment are altered, run + # everything + add_mapping %r%^test/test_helper.rb|config/(init|rack|environments/test.rb|database.yml)% do # FIX + files_matching %r%^test/(unit|models|controllers|views|functional)/.*_test\.rb$% + end + end + +private + + # Determines the paths we can expect tests or specs to reside, as well as + # corresponding fixtures. + def initialize_test_layout + self.model_tests_dir = "test/unit" + self.controller_tests_dir = "test/functional" + self.view_tests_dir = "test/views" + self.fixtures_dir = "test/fixtures" + end + + # Given a filename and the test type, this method will return the + # corresponding test's or spec's name. + # + # ==== Arguments + # +filename+:: the file name of the model, view, or controller + # +kind_of_test+:: the type of test we that we should run + # + # ==== Returns + # String:: the name of the corresponding test or spec + # + # ==== Example + # + # > test_for("user", :model) + # => "user_test.rb" + # > test_for("login", :controller) + # => "login_controller_test.rb" + # > test_for("form", :view) + # => "form_view_spec.rb" # If you're running a RSpec-like suite + def test_for(filename, kind_of_test) + name = [filename] + name << kind_of_test.to_s if kind_of_test == :view + name << "test" + return name.join("_") + ".rb" + end + + def model_test_for(filename) + [model_tests_dir, test_for(filename, :model)].join("/") + end + + def controller_test_for(filename) + [controller_tests_dir, test_for(filename, :controller)].join("/") + end + + def view_test_for(filename) + [view_tests_dir, test_for(filename, :view)].join("/") + end + +end \ No newline at end of file diff --git a/autotest/merb_rspec.rb b/autotest/merb_rspec.rb new file mode 100644 index 0000000..a991a65 --- /dev/null +++ b/autotest/merb_rspec.rb @@ -0,0 +1,165 @@ +# Adapted from Autotest::Rails, RSpec's autotest class, as well as merb-core's. +require 'autotest' + +class RspecCommandError < StandardError; end + +# This class maps your application's structure so Autotest can understand what +# specs to run when files change. +# +# Fixtures are _not_ covered by this class. If you change a fixture file, you +# will have to run your spec suite manually, or, better yet, provide your own +# Autotest map explaining how your fixtures are set up. +class Autotest::MerbRspec < Autotest + def initialize + super + + # Ignore any happenings in these directories + add_exception %r%^\./(?:doc|log|public|tmp|\.git|\.hg|\.svn|framework|gems|schema|\.DS_Store|autotest|bin|.*\.sqlite3)% + # Ignore SCM directories and custom Autotest mappings + %w[.svn .hg .git .autotest].each { |exception| add_exception(exception) } + + # Ignore any mappings that Autotest may have already set up + clear_mappings + + # Anything in /lib could have a spec anywhere, if at all. So, look for + # files with roughly the same name as the file in /lib + add_mapping %r%^lib\/(.*)\.rb% do |_, m| + files_matching %r%^spec\/#{m[1]}% + end + + add_mapping %r%^spec/(spec_helper|shared/.*)\.rb$% do + all_specs + end + + # Changing a spec will cause it to run itself + add_mapping %r%^spec/.*\.rb$% do |filename, _| + filename + end + + # Any change to a model will cause it's corresponding test to be run + add_mapping %r%^app/models/(.*)\.rb$% do |_, m| + spec_for(m[1], 'model') + end + + # Any change to global_helpers will result in all view and controller + # tests being run + add_mapping %r%^app/helpers/global_helpers\.rb% do + files_matching %r%^spec/(views|controllers|helpers)/.*_spec\.rb$% + end + + # Any change to a helper will cause its spec to be run + add_mapping %r%^app/helpers/((.*)_helper(s)?)\.rb% do |_, m| + spec_for(m[1], 'helper') + end + + # Changes to a view cause its spec to be run + add_mapping %r%^app/views/(.*)/% do |_, m| + spec_for(m[1], 'view') + end + + # Changes to a controller result in its corresponding spec being run. If + # the controller is the exception or application controller, all + # controller specs are run. + add_mapping %r%^app/controllers/(.*)\.rb$% do |_, m| + if ["application", "exception"].include?(m[1]) + files_matching %r%^spec/controllers/.*_spec\.rb$% + else + spec_for(m[1], 'controller') + end + end + + # If a change is made to the router, run controller, view and helper specs + add_mapping %r%^config/router.rb$% do + files_matching %r%^spec/(controllers|views|helpers)/.*_spec\.rb$% + end + + # If any of the major files governing the environment are altered, run + # everything + add_mapping %r%^config/(init|rack|environments/test).*\.rb|database\.yml% do + all_specs + end + end + + def failed_results(results) + results.scan(/^\d+\)\n(?:\e\[\d*m)?(?:.*?Error in )?'([^\n]*)'(?: FAILED)?(?:\e\[\d*m)?\n(.*?)\n\n/m) + end + + def handle_results(results) + @failures = failed_results(results) + @files_to_test = consolidate_failures(@failures) + @files_to_test.empty? && !$TESTING ? hook(:green) : hook(:red) + @tainted = !@files_to_test.empty? + end + + def consolidate_failures(failed) + filters = Hash.new { |h,k| h[k] = [] } + failed.each do |spec, failed_trace| + if f = test_files_for(failed).find { |f| f =~ /spec\// } + filters[f] << spec + break + end + end + filters + end + + def make_test_cmd(specs_to_runs) + [ + ruby, + "-S", + spec_command, + add_options_if_present, + files_to_test.keys.flatten.join(' ') + ].join(' ') + end + + def add_options_if_present + File.exist?("spec/spec.opts") ? "-O spec/spec.opts " : "" + end + + # Finds the proper spec command to use. Precendence is set in the + # lazily-evaluated method spec_commands. Alias + Override that in + # ~/.autotest to provide a different spec command then the default + # paths provided. + def spec_command(separator=File::ALT_SEPARATOR) + unless defined?(@spec_command) + @spec_command = spec_commands.find { |cmd| File.exists?(cmd) } + + raise RspecCommandError, "No spec command could be found" unless @spec_command + + @spec_command.gsub!(File::SEPARATOR, separator) if separator + end + @spec_command + end + + # Autotest will look for spec commands in the following + # locations, in this order: + # + # * default spec bin/loader installed in Rubygems + # * any spec command found in PATH + def spec_commands + [File.join(Config::CONFIG['bindir'], 'spec'), 'spec'] + end + +private + + # Runs +files_matching+ for all specs + def all_specs + files_matching %r%^spec/.*_spec\.rb$% + end + + # Generates a path to some spec given its kind and the match from a mapping + # + # ==== Arguments + # match:: the match from a mapping + # kind:: the kind of spec that the match represents + # + # ==== Returns + # String + # + # ==== Example + # > spec_for('post', :view') + # => "spec/views/post_spec.rb" + def spec_for(match, kind) + File.join("spec", kind + 's', "#{match}_spec.rb") + end +end diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..228f4a1 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,8 @@ +Merb.logger.info("Loaded DEVELOPMENT Environment...") +Merb::Config.use { |c| + c[:exception_details] = true + c[:reload_classes] = true + c[:reload_time] = 0.5 + c[:log_auto_flush ] = true + c[:log_level] = :debug +} diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..e389e6f --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,7 @@ +Merb.logger.info("Loaded PRODUCTION Environment...") +Merb::Config.use { |c| + c[:exception_details] = false + c[:reload_classes] = false + c[:log_level] = :error + c[:log_file] = Merb.log_path + "/production.log" +} \ No newline at end of file diff --git a/config/environments/rake.rb b/config/environments/rake.rb new file mode 100644 index 0000000..a7970ef --- /dev/null +++ b/config/environments/rake.rb @@ -0,0 +1,7 @@ +Merb.logger.info("Loaded RAKE Environment...") +Merb::Config.use { |c| + c[:exception_details] = true + c[:reload_classes] = false + c[:log_auto_flush ] = true + c[:log_file] = Merb.log_path / 'merb_rake.log' +} \ No newline at end of file diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..6865fb4 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,6 @@ +Merb.logger.info("Loaded TEST Environment...") +Merb::Config.use { |c| + c[:testing] = true + c[:exception_details] = true + c[:log_auto_flush ] = true +} \ No newline at end of file diff --git a/config/init.rb b/config/init.rb new file mode 100644 index 0000000..09a04e8 --- /dev/null +++ b/config/init.rb @@ -0,0 +1,28 @@ +Gem.clear_paths +Gem.path.unshift(Merb.root / "gems") +$LOAD_PATH.unshift(Merb.root / "lib") +# Merb.push_path(:lib, Merb.root / "lib") # uses **/*.rb as path glob + +dependencies 'haml', 'sass', 'merb_helpers', 'merb_has_flash', 'digest/sha1', 'recaptcha' + +Merb::BootLoader.after_app_loads do + recaptcha_path = File.join(Merb.root, 'config', 'recaptcha.yml') + if File.file?(recaptcha_path) and File.readable?(recaptcha_path) + rc = YAML::load_file(recaptcha_path) + ENV['RECAPTCHA_PUBLIC_KEY'] = rc[:public] + ENV['RECAPTCHA_PRIVATE_KEY'] = rc[:private] + else + raise "ReCaptcha configuration file not found!" + end +end + +use_orm :activerecord +use_test :rspec +use_template_engine :haml +Merb::Config.use do |c| + c[:session_secret_key] = 'ccf75249b0efbdb3edff96d0a1b16b19cf91f31e' + c[:session_store] = :activerecord + c[:sass] ||= {} + c[:sass][:style] = :compact +end + diff --git a/config/rack.rb b/config/rack.rb new file mode 100644 index 0000000..a8ec99d --- /dev/null +++ b/config/rack.rb @@ -0,0 +1,12 @@ + +# use PathPrefix Middleware if :path_prefix is set in Merb::Config +if prefix = ::Merb::Config[:path_prefix] + use Merb::Rack::PathPrefix, prefix +end + +# comment this out if you are running merb behind a load balancer +# that serves static files +use Merb::Rack::Static, Merb.dir_for(:public) + +# this is our main merb application +run Merb::Rack::Application.new \ No newline at end of file diff --git a/config/recaptcha.yml.template b/config/recaptcha.yml.template new file mode 100644 index 0000000..f38814a --- /dev/null +++ b/config/recaptcha.yml.template @@ -0,0 +1,3 @@ +--- +:public: PUBKEY +:private: PRIVKEY diff --git a/config/router.rb b/config/router.rb new file mode 100644 index 0000000..022dfca --- /dev/null +++ b/config/router.rb @@ -0,0 +1,13 @@ +Merb.logger.info("Compiling routes...") +Merb::Router.prepare do |r| + r.resources :sessions + r.resources :users + r.resources :people + r.resources :votes + r.resources :photos + r.resources :favorites + r.resources :stats + # r.default_routes + r.match('/').to( :controller => 'home', :action => 'index' ) + r.match('/acceptable_use').to( :controller => 'home', :action => 'acceptable_use' ) +end diff --git a/lib/LICENSE_for_recaptcha b/lib/LICENSE_for_recaptcha new file mode 100644 index 0000000..dc9c67e --- /dev/null +++ b/lib/LICENSE_for_recaptcha @@ -0,0 +1,19 @@ +Copyright (c) 2007 Jason L Perry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/lib/recaptcha.rb b/lib/recaptcha.rb new file mode 100644 index 0000000..1026763 --- /dev/null +++ b/lib/recaptcha.rb @@ -0,0 +1,78 @@ +# ReCAPTCHA +module Ambethia + module ReCaptcha + RECAPTCHA_API_SERVER = 'http://api.recaptcha.net'; + RECAPTCHA_API_SECURE_SERVER = 'https://api-secure.recaptcha.net'; + RECAPTCHA_VERIFY_SERVER = 'api-verify.recaptcha.net'; + + SKIP_VERIFY_ENV = ['test'] + + module Helper + # Your public API can be specified in the +options+ hash or preferably the environment + # variable +RECAPTCHA_PUBLIC_KEY+. + def recaptcha_tags(options = {}) + # Default options + key = options[:public_key] ||= ENV['RECAPTCHA_PUBLIC_KEY'] + error = options[:error] ||= session[:recaptcha_error] + uri = options[:ssl] ? RECAPTCHA_API_SECURE_SERVER : RECAPTCHA_API_SERVER + xhtml = Builder::XmlMarkup.new :target => out=(''), :indent => 2 # Because I can. + if options[:display] + xhtml.script(:type => "text/javascript"){ xhtml.text! "var RecaptchaOptions = #{options[:display].to_json};\n"} + end + if options[:ajax] + xhtml.div(:id => 'dynamic_recaptcha') {} + xhtml.script(:type => "text/javascript", :src => "#{uri}/js/recaptcha_ajax.js") {} + xhtml.script(:type => "text/javascript") do + xhtml.text! "Recaptcha.create('#{key}', document.getElementById('dynamic_recaptcha') );" + end + else + xhtml.script(:type => "text/javascript", :src => "#{uri}/challenge?k=#{key}&error=#{error}") {} + unless options[:noscript] == false + xhtml.noscript do + xhtml.iframe(:src => "#{uri}/noscript?k=#{key}", + :height => options[:iframe_height] ||= 300, + :width => options[:iframe_width] ||= 500, + :frameborder => 0) {}; xhtml.br + xhtml.textarea(:name => "recaptcha_challenge_field", :rows => 3, :cols => 40) {} + xhtml.input :name => "recaptcha_response_field", + :type => "hidden", :value => "manual_challenge" + end + end + end + raise ReCaptchaError, "No public key specified." unless key + return out + end # recaptcha_tags + end # Helpers + + module Controller + # Your private API key must be specified in the environment variable +RECAPTCHA_PRIVATE_KEY+ + def verify_recaptcha(model = nil) + return true if SKIP_VERIFY_ENV.include? ENV['RAILS_ENV'] + raise ReCaptchaError, "No private key specified." unless ENV['RECAPTCHA_PRIVATE_KEY'] + begin + recaptcha = Net::HTTP.post_form URI.parse("http://#{RECAPTCHA_VERIFY_SERVER}/verify"), { + :privatekey => ENV['RECAPTCHA_PRIVATE_KEY'], + :remoteip => request.remote_ip, + :challenge => params[:recaptcha_challenge_field], + :response => params[:recaptcha_response_field] + } + answer, error = recaptcha.body.split.map { |s| s.chomp } + unless answer == 'true' + session[:recaptcha_error] = error + model.valid? if model + model.errors.add_to_base "Captcha response is incorrect, please try again." if model + return false + else + session[:recaptcha_error] = nil + return true + end + rescue Exception => e + raise ReCaptchaError, e + end + end # verify_recaptcha + end # ControllerHelpers + + class ReCaptchaError < StandardError; end + + end # ReCaptcha +end # Ambethia \ No newline at end of file diff --git a/merb.thor b/merb.thor new file mode 100644 index 0000000..441566e --- /dev/null +++ b/merb.thor @@ -0,0 +1,822 @@ +#!/usr/bin/env ruby +require 'rubygems' +require 'rubygems/dependency_installer' +require 'rubygems/uninstaller' +require 'thor' +require 'fileutils' +require 'yaml' + +module MerbThorHelper + + private + + # The current working directory, or Merb app root (--merb-root option). + def working_dir + @_working_dir ||= File.expand_path(options['merb-root'] || Dir.pwd) + end + + # We should have a ./src dir for local and system-wide management. + def source_dir + @_source_dir ||= File.join(working_dir, 'src') + create_if_missing(@_source_dir) + @_source_dir + end + + # If a local ./gems dir is found, it means we are in a Merb app. + def application? + gem_dir + end + + # If a local ./gems dir is found, return it. + def gem_dir + if File.directory?(dir = File.join(working_dir, 'gems')) + dir + end + end + + # If we're in a Merb app, we can have a ./bin directory; + # create it if it's not there. + def bin_dir + @_bin_dir ||= begin + if gem_dir + dir = File.join(working_dir, 'bin') + create_if_missing(dir) + dir + end + end + end + + # Helper to create dir unless it exists. + def create_if_missing(path) + FileUtils.mkdir(path) unless File.exists?(path) + end + + # Create a modified executable wrapper in the app's ./bin directory. + def ensure_local_bin_for(*gems) + if bin_dir && File.directory?(bin_dir) + gems.each do |gem| + if gemspec_path = Dir[File.join(gem_dir, 'specifications', "#{gem}-*.gemspec")].last + spec = Gem::Specification.load(gemspec_path) + spec.executables.each do |exec| + if File.exists?(executable = File.join(gem_dir, 'bin', exec)) + local_executable = File.join(bin_dir, exec) + puts "Adding local executable #{local_executable}" + File.open(local_executable, 'w', 0755) do |f| + f.write(executable_wrapper(spec, exec)) + end + end + end + end + end + end + end + + def executable_wrapper(spec, bin_file_name) + <<-TEXT +#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]} +# +# This file was generated by merb.thor. +# +# The application '#{spec.name}' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +if File.directory?(gems_dir = File.join(File.dirname(__FILE__), '..', 'gems')) + $BUNDLE = true; Gem.clear_paths; Gem.path.unshift(gems_dir) +end + +version = "#{Gem::Requirement.default}" + +if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then + version = $1 + ARGV.shift +end + +gem '#{spec.name}', version +load '#{bin_file_name}' +TEXT + end + +end + +# TODO +# - a task to figure out an app's dependencies +# - pulling a specific UUID/Tag (gitspec hash) with clone/update +# - a 'deploy' task (in addition to 'redeploy' ?) +# - eventually take a --orm option for the 'merb-stack' type of tasks + +class Merb < Thor + + class SourcePathMissing < Exception + end + + class GemPathMissing < Exception + end + + class GemInstallError < Exception + end + + class GemUninstallError < Exception + end + + # Install a Merb stack from stable RubyForge gems. Optionally install a + # suitable Rack adapter/server when setting --adapter to one of the + # following: mongrel, emongrel, thin or ebb. + + desc 'stable', 'Install extlib, merb-core and merb-more from rubygems' + method_options "--merb-root" => :optional, + "--adapter" => :optional + def stable + adapters = %w[mongrel emongrel thin ebb] + stable = Stable.new + stable.options = options + if stable.core && stable.more + puts "Installed extlib, merb-core and merb-more" + if options[:adapter] && adapters.include?(options[:adapter]) && + stable.refresh_from_gems(options[:adapter]) + puts "Installed #{options[:adapter]}" + elsif options[:adapter] + puts "Please specify one of the following adapters: #{adapters.join(' ')}" + end + end + end + + class Stable < Thor + + # The Stable tasks deal with known -stable- gems; available + # as shortcuts to Merb and DataMapper gems. + # + # These are pulled from rubyforge and installed into into the + # desired gems dir (either system-wide or into the application's + # gems directory). + + include MerbThorHelper + + # Gets latest gem versions from RubyForge and installs them. + # + # Examples: + # + # thor merb:edge:core + # thor merb:edge:core --merb-root ./path/to/your/app + # thor merb:edge:core --sources ./path/to/sources.yml + + desc 'core', 'Install extlib and merb-core from git HEAD' + method_options "--merb-root" => :optional + def core + refresh_from_gems 'extlib', 'merb-core' + ensure_local_bin_for('merb-core', 'rake', 'rspec', 'thor') + end + + desc 'more', 'Install merb-more from rubygems' + method_options "--merb-root" => :optional + def more + refresh_from_gems 'merb-more' + ensure_local_bin_for('merb-gen') + end + + desc 'plugins', 'Install merb-plugins from rubygems' + method_options "--merb-root" => :optional + def plugins + refresh_from_gems 'merb-plugins' + end + + desc 'dm_core', 'Install dm-core from rubygems' + method_options "--merb-root" => :optional + def dm_core + refresh_from_gems 'extlib', 'dm-core' + end + + desc 'dm_more', 'Install dm-more from rubygems' + method_options "--merb-root" => :optional + def dm_more + refresh_from_gems 'extlib', 'dm-core', 'dm-more' + end + + # Pull from RubyForge and install. + def refresh_from_gems(*components) + gems = Gems.new + gems.options = options + components.all? { |name| gems.install(name) } + end + + end + + # Retrieve latest Merb versions from git and optionally install them. + # + # Note: the --sources option takes a path to a YAML file + # with a regular Hash mapping gem names to git urls. + # + # Examples: + # + # thor merb:edge + # thor merb:edge --install + # thor merb:edge --merb-root ./path/to/your/app + # thor merb:edge --sources ./path/to/sources.yml + + desc 'edge', 'Install extlib, merb-core and merb-more from git HEAD' + method_options "--merb-root" => :optional, + "--sources" => :optional, + "--install" => :boolean + def edge + edge = Edge.new + edge.options = options + edge.core + edge.more + end + + class Edge < Thor + + # The Edge tasks deal with known gems from the bleeding edge; available + # as shortcuts to Merb and DataMapper gems. + # + # These are pulled from git and optionally installed into into the + # desired gems dir (either system-wide or into the application's + # gems directory). + + include MerbThorHelper + + # Gets latest gem versions from git - optionally installs them. + # + # Note: the --sources option takes a path to a YAML file + # with a regular Hash mapping gem names to git urls, + # allowing pulling forks of the official repositories. + # + # Examples: + # + # thor merb:edge:core + # thor merb:edge:core --install + # thor merb:edge:core --merb-root ./path/to/your/app + # thor merb:edge:core --sources ./path/to/sources.yml + + desc 'core', 'Update extlib and merb-core from git HEAD' + method_options "--merb-root" => :optional, + "--sources" => :optional, + "--install" => :boolean + def core + refresh_from_source 'thor', 'extlib', 'merb-core' + ensure_local_bin_for('merb-core', 'rake', 'rspec', 'thor') + end + + desc 'more', 'Update merb-more from git HEAD' + method_options "--merb-root" => :optional, + "--sources" => :optional, + "--install" => :boolean + def more + refresh_from_source 'merb-more' + ensure_local_bin_for('merb-gen') + end + + desc 'plugins', 'Update merb-plugins from git HEAD' + method_options "--merb-root" => :optional, + "--sources" => :optional, + "--install" => :boolean + def plugins + refresh_from_source 'merb-plugins' + end + + desc 'dm_core', 'Update dm-core from git HEAD' + method_options "--merb-root" => :optional, + "--sources" => :optional, + "--install" => :boolean + def dm_core + refresh_from_source 'extlib', 'dm-core' + end + + desc 'dm_more', 'Update dm-more from git HEAD' + method_options "--merb-root" => :optional, + "--sources" => :optional, + "--install" => :boolean + def dm_more + refresh_from_source 'extlib', 'dm-core', 'dm-more' + end + + private + + # Pull from git and optionally install the resulting gems. + def refresh_from_source(*components) + source = Source.new + source.options = options + components.each do |name| + source.clone(name) + source.install(name) if options[:install] + end + end + + end + + class Source < Thor + + # The Source tasks deal with gem source packages - mainly from github. + # Any directory inside ./src is regarded as a gem that can be packaged + # and installed from there into the desired gems dir (either system-wide + # or into the application's gems directory). + + include MerbThorHelper + + # Install a particular gem from source. + # + # If a local ./gems dir is found, or --merb-root is given + # the gems will be installed locally into that directory. + # + # Note that this task doesn't retrieve any (new) source from git; + # To update and install you'd execute the following two tasks: + # + # thor merb:source:update merb-core + # thor merb:source:install merb-core + # + # Alternatively, look at merb:edge and merb:edge:* with --install. + # + # Examples: + # + # thor merb:source:install merb-core + # thor merb:source:install merb-more + # thor merb:source:install merb-more/merb-slices + # thor merb:source:install merb-plugins/merb_helpers + # thor merb:source:install merb-core --merb-root ./path/to/your/app + + desc 'install GEM_NAME', 'Install a rubygem from (git) source' + method_options "--merb-root" => :optional + def install(name) + puts "Installing #{name}..." + gem_src_dir = File.join(source_dir, name) + opts = {} + opts[:install_dir] = gem_dir if gem_dir + Merb.install_gem_from_src(gem_src_dir, opts) + rescue Merb::SourcePathMissing + puts "Missing rubygem source path: #{gem_src_dir}" + rescue Merb::GemPathMissing + puts "Missing rubygems path: #{gem_dir}" + rescue => e + puts "Failed to install #{name} (#{e.message})" + end + + # Clone a git repository into ./src. The repository can be + # a direct git url or a known -named- repository. + # + # Examples: + # + # thor merb:source:clone dm-core + # thor merb:source:clone dm-core --sources ./path/to/sources.yml + # thor merb:source:clone git://github.com/sam/dm-core.git + + desc 'clone REPOSITORY', 'Clone a git repository into ./src' + method_options "--sources" => :optional + def clone(repository) + if repository =~ /^git:\/\// + repository_url = repository + elsif url = Merb.repos(options[:sources])[repository] + repository_url = url + end + + if repository_url + repository_name = repository_url[/([\w+|-]+)\.git/u, 1] + fork_name = repository_url[/.com\/+?(.+)\/.+\.git/u, 1] + local_repo_path = "#{source_dir}/#{repository_name}" + + if File.directory?(local_repo_path) + puts "\n#{repository_name} repository exists, updating or branching instead of cloning..." + FileUtils.cd(local_repo_path) do + + # to avoid conflicts we need to set a remote branch for non official repos + existing_repos = `git remote -v`.split("\n").map{|branch| branch.split(/\s+/)} + origin_repo_url = existing_repos.detect{ |r| r.first == "origin" }.last + + # pull from the original repository - no branching needed + if repository_url == origin_repo_url + puts "Pulling from #{repository_url}" + system %{ + git fetch + git checkout master + git rebase origin/master + } + # update and switch to a branch for a particular github fork + elsif existing_repos.map{ |r| r.last }.include?(repository_url) + puts "Switching to remote branch: #{fork_name}" + `git checkout -b #{fork_name} #{fork_name}/master` + `git rebase #{fork_name}/master` + # create a new remote branch for a particular github fork + else + puts "Add a new remote branch: #{fork_name}" + `git remote add -f #{fork_name} #{repository_url}` + `git checkout -b#{fork_name} #{fork_name}/master` + end + end + else + FileUtils.cd(source_dir) do + puts "\nCloning #{repository_name} repository from #{repository_url}..." + system("git clone --depth=1 #{repository_url} ") + end + end + else + puts "No valid repository url given" + end + end + + # Update a specific gem source directory from git. See #clone. + + desc 'update REPOSITORY_URL', 'Update a git repository in ./src' + alias :update :clone + + # Update all gem sources from git - based on the current branch. + + desc 'refresh', 'Pull fresh copies of all source gems' + def refresh + repos = Dir["#{source_dir}/*"] + repos.each do |repo| + next unless File.directory?(repo) && File.exists?(File.join(repo, '.git')) + FileUtils.cd(repo) do + puts "Refreshing #{File.basename(repo)}" + branch = `git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1) /'`[/\* (.+)/, 1] + system %{git rebase #{branch}} + end + end + end + + end + + class Gems < Thor + + # The Gems tasks deal directly with rubygems, either through remotely + # available sources (rubyforge for example) or by searching the + # system-wide gem cache for matching gems. The gems are installed from + # there into the desired gems dir (either system-wide or into the + # application's gems directory). + + include MerbThorHelper + + # Install a gem and its dependencies. + # + # If a local ./gems dir is found, or --merb-root is given + # the gems will be installed locally into that directory. + # + # The option --cache will look in the system's gem cache + # for the latest version and install it in the apps' gems. + # This is particularly handy for gems that aren't available + # through rubyforge.org - like in-house merb slices etc. + # + # Examples: + # + # thor merb:gems:install merb-core + # thor merb:gems:install merb-core --cache + # thor merb:gems:install merb-core --version 0.9.7 + # thor merb:gems:install merb-core --merb-root ./path/to/your/app + + desc 'install GEM_NAME', 'Install a gem from rubygems' + method_options "--version" => :optional, + "--merb-root" => :optional, + "--cache" => :boolean + def install(name) + puts "Installing #{name}..." + opts = {} + opts[:version] = options[:version] + opts[:cache] = options[:cache] if gem_dir + opts[:install_dir] = gem_dir if gem_dir + Merb.install_gem(name, opts) + rescue => e + puts "Failed to install #{name} (#{e.message})" + end + + # Update a gem and its dependencies. + # + # If a local ./gems dir is found, or --merb-root is given + # the gems will be installed locally into that directory. + # + # The option --cache will look in the system's gem cache + # for the latest version and install it in the apps' gems. + # This is particularly handy for gems that aren't available + # through rubyforge.org - like in-house merb slices etc. + # + # Examples: + # + # thor merb:gems:update merb-core + # thor merb:gems:update merb-core --cache + # thor merb:gems:update merb-core --merb-root ./path/to/your/app + + desc 'update GEM_NAME', 'Update a gem from rubygems' + method_options "--merb-root" => :optional, + "--cache" => :boolean + def update(name) + puts "Updating #{name}..." + opts = {} + if gem_dir + if gemspec_path = Dir[File.join(gem_dir, 'specifications', "#{name}-*.gemspec")].last + gemspec = Gem::Specification.load(gemspec_path) + opts[:version] = Gem::Requirement.new [">#{gemspec.version}"] + end + opts[:install_dir] = gem_dir + opts[:cache] = options[:cache] + end + Merb.install_gem(name, opts) + rescue => e + puts "Failed to update #{name} (#{e.message})" + end + + # Uninstall a gem - ignores dependencies. + # + # If a local ./gems dir is found, or --merb-root is given + # the gems will be uninstalled locally from that directory. + # + # Examples: + # + # thor merb:gems:uninstall merb-core + # thor merb:gems:uninstall merb-core --all + # thor merb:gems:uninstall merb-core --version 0.9.7 + # thor merb:gems:uninstall merb-core --merb-root ./path/to/your/app + + desc 'install GEM_NAME', 'Install a gem from rubygems' + desc 'uninstall GEM_NAME', 'Uninstall a gem' + method_options "--version" => :optional, + "--merb-root" => :optional, + "--all" => :boolean + def uninstall(name) + puts "Uninstalling #{name}..." + opts = {} + opts[:ignore] = true + opts[:all] = options[:all] + opts[:executables] = true + opts[:version] = options[:version] + opts[:install_dir] = gem_dir if gem_dir + Merb.uninstall_gem(name, opts) + rescue => e + puts "Failed to uninstall #{name} (#{e.message})" + end + + # Completely remove a gem and all its versions - ignores dependencies. + # + # If a local ./gems dir is found, or --merb-root is given + # the gems will be uninstalled locally from that directory. + # + # Examples: + # + # thor merb:gems:wipe merb-core + # thor merb:gems:wipe merb-core --merb-root ./path/to/your/app + + desc 'wipe GEM_NAME', 'Remove a gem completely' + method_options "--merb-root" => :optional + def wipe(name) + puts "Wiping #{name}..." + opts = {} + opts[:ignore] = true + opts[:all] = true + opts[:executables] = true + opts[:install_dir] = gem_dir if gem_dir + Merb.uninstall_gem(name, opts) + rescue => e + puts "Failed to wipe #{name} (#{e.message})" + end + + # This task should be executed as part of a deployment setup, where + # the deployment system runs this after the app has been installed. + # Usually triggered by Capistrano, God... + # + # It will regenerate gems from the bundled gems cache for any gem + # that has C extensions - which need to be recompiled for the target + # deployment platform. + + desc 'redeploy', 'Recreate any binary gems on the target deployment platform' + def redeploy + require 'tempfile' # for + if File.directory?(specs_dir = File.join(gem_dir, 'specifications')) && + File.directory?(cache_dir = File.join(gem_dir, 'cache')) + Dir[File.join(specs_dir, '*.gemspec')].each do |gemspec_path| + unless (gemspec = Gem::Specification.load(gemspec_path)).extensions.empty? + if File.exists?(gem_file = File.join(cache_dir, "#{gemspec.full_name}.gem")) + gem_file_copy = File.join(Dir::tmpdir, File.basename(gem_file)) + # Copy the gem to a temporary file, because otherwise RubyGems/FileUtils + # will complain about copying identical files (same source/destination). + FileUtils.cp(gem_file, gem_file_copy) + Merb.install_gem(gem_file_copy, :install_dir => gem_dir) + File.delete(gem_file_copy) + end + end + end + else + puts "No application local gems directory found" + end + end + + end + + class << self + + # Default Git repositories - pass source_config option + # to load a yaml configuration file. + def repos(source_config = nil) + @_repos ||= begin + repositories = { + 'merb-core' => "git://github.com/wycats/merb-core.git", + 'merb-more' => "git://github.com/wycats/merb-more.git", + 'merb-plugins' => "git://github.com/wycats/merb-plugins.git", + 'extlib' => "git://github.com/sam/extlib.git", + 'dm-core' => "git://github.com/sam/dm-core.git", + 'dm-more' => "git://github.com/sam/dm-more.git", + 'thor' => "git://github.com/wycats/thor.git" + } + end + if source_config && File.exists?(source_config) + @_repos.merge(YAML.load(File.read(source_config))) + else + @_repos + end + end + + # Install a gem - looks remotely and local gem cache; + # won't process rdoc or ri options. + def install_gem(gem, options = {}) + from_cache = (options.key?(:cache) && options.delete(:cache)) + if from_cache + install_gem_from_cache(gem, options) + else + version = options.delete(:version) + Gem.configuration.update_sources = false + installer = Gem::DependencyInstaller.new(options.merge(:user_install => false)) + exception = nil + begin + installer.install gem, version + rescue Gem::InstallError => e + exception = e + rescue Gem::GemNotFoundException => e + if from_cache && gem_file = find_gem_in_cache(gem, version) + puts "Located #{gem} in gem cache..." + installer.install gem_file + else + exception = e + end + rescue => e + exception = e + end + if installer.installed_gems.empty? && exception + puts "Failed to install gem '#{gem}' (#{exception.message})" + end + installer.installed_gems.each do |spec| + puts "Successfully installed #{spec.full_name}" + end + end + end + + # Install a gem - looks in the system's gem cache instead of remotely; + # won't process rdoc or ri options. + def install_gem_from_cache(gem, options = {}) + version = options.delete(:version) + Gem.configuration.update_sources = false + installer = Gem::DependencyInstaller.new(options.merge(:user_install => false)) + exception = nil + begin + if gem_file = find_gem_in_cache(gem, version) + puts "Located #{gem} in gem cache..." + installer.install gem_file + else + raise Gem::InstallError, "Unknown gem #{gem}" + end + rescue Gem::InstallError => e + exception = e + end + if installer.installed_gems.empty? && exception + puts "Failed to install gem '#{gem}' (#{e.message})" + end + installer.installed_gems.each do |spec| + puts "Successfully installed #{spec.full_name}" + end + end + + # Install a gem from source - builds and packages it first then installs it. + def install_gem_from_src(gem_src_dir, options = {}) + raise SourcePathMissing unless File.directory?(gem_src_dir) + raise GemPathMissing if options[:install_dir] && !File.directory?(options[:install_dir]) + + gem_name = File.basename(gem_src_dir) + gem_pkg_dir = File.expand_path(File.join(gem_src_dir, 'pkg')) + + # We need to use local bin executables if available. + thor = which('thor') + rake = which('rake') + + # Handle pure Thor installation instead of Rake + if File.exists?(File.join(gem_src_dir, 'Thorfile')) + # Remove any existing packages. + FileUtils.rm_rf(gem_pkg_dir) if File.directory?(gem_pkg_dir) + # Create the package. + FileUtils.cd(gem_src_dir) { system("#{thor} :package") } + # Install the package using rubygems. + if package = Dir[File.join(gem_pkg_dir, "#{gem_name}-*.gem")].last + FileUtils.cd(File.dirname(package)) do + install_gem(File.basename(package), options.dup) + return + end + else + raise Merb::GemInstallError, "No package found for #{gem_name}" + end + # Handle standard installation through Rake + else + # Clean and regenerate any subgems for meta gems. + Dir[File.join(gem_src_dir, '*', 'Rakefile')].each do |rakefile| + FileUtils.cd(File.dirname(rakefile)) { system("#{rake} clobber_package; #{rake} package") } + end + + # Handle the main gem install. + if File.exists?(File.join(gem_src_dir, 'Rakefile')) + # Remove any existing packages. + FileUtils.cd(gem_src_dir) { system("#{rake} clobber_package") } + # Create the main gem pkg dir if it doesn't exist. + FileUtils.mkdir_p(gem_pkg_dir) unless File.directory?(gem_pkg_dir) + # Copy any subgems to the main gem pkg dir. + Dir[File.join(gem_src_dir, '**', 'pkg', '*.gem')].each do |subgem_pkg| + FileUtils.cp(subgem_pkg, gem_pkg_dir) + end + + # Finally generate the main package and install it; subgems + # (dependencies) are local to the main package. + FileUtils.cd(gem_src_dir) do + system("#{rake} package") + if package = Dir[File.join(gem_pkg_dir, "#{gem_name}-*.gem")].last + FileUtils.cd(File.dirname(package)) do + install_gem(File.basename(package), options.dup) + return + end + else + raise Merb::GemInstallError, "No package found for #{gem_name}" + end + end + end + end + raise Merb::GemInstallError, "No Rakefile found for #{gem_name}" + end + + # Uninstall a gem. + def uninstall_gem(gem, options = {}) + if options[:version] && !options[:version].is_a?(Gem::Requirement) + options[:version] = Gem::Requirement.new ["= #{version}"] + end + begin + Gem::Uninstaller.new(gem, options).uninstall + rescue => e + raise GemUninstallError, "Failed to uninstall #{gem}" + end + end + + # Will prepend sudo on a suitable platform. + def sudo + @_sudo ||= begin + windows = PLATFORM =~ /win32|cygwin/ rescue nil + windows ? "" : "sudo " + end + end + + # Use the local bin/* executables if available. + def which(executable) + if File.executable?(exec = File.join(Dir.pwd, 'bin', executable)) + exec + else + executable + end + end + + private + + def find_gem_in_cache(gem, version) + spec = if version + version = Gem::Requirement.new ["= #{version}"] unless version.is_a?(Gem::Requirement) + Gem.source_index.find_name(gem, version).first + else + Gem.source_index.find_name(gem).sort_by { |g| g.version }.last + end + if spec && File.exists?(gem_file = "#{spec.installation_path}/cache/#{spec.full_name}.gem") + gem_file + end + end + + end + + class Tasks < Thor + + include MerbThorHelper + + # Install Thor, Rake and RSpec into the local gems dir, by copying it from + # the system-wide rubygems cache - which is OK since we needed it to run + # this task already. + # + # After this we don't need the system-wide rubygems anymore, as all required + # executables are available in the local ./bin directory. + # + # RSpec is needed here because source installs might fail when running + # rake tasks where spec/rake/spectask has been required. + + desc 'setup', 'Install Thor, Rake and RSpec in the local gems dir' + method_options "--merb-root" => :optional + def setup + if $0 =~ /^(\.\/)?bin\/thor$/ + puts "You cannot run the setup from #{$0} - try #{File.basename($0)} merb:tasks:setup instead" + return + end + create_if_missing(File.join(working_dir, 'gems')) + Merb.install_gem('thor', :cache => true, :install_dir => gem_dir) + Merb.install_gem('rake', :cache => true, :install_dir => gem_dir) + Merb.install_gem('rspec', :cache => true, :install_dir => gem_dir) + ensure_local_bin_for('thor', 'rake', 'rspec') + end + + end + +end diff --git a/public/images/ajax-loader.gif b/public/images/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..9d5d9ed0d6f6d7e40d77d0cd0c55e821e66ebdd1 GIT binary patch literal 4176 zcmb`KX;f2(w#QFSPI69?Lxw|62tz^w1Ok!}R45`~PDmn?m|##UcpD}!DpgceuG%&u zBw;W>q(EC65O8V*6&0`57Z53;)#$ZO9TpC~MQdBy($)^EKKs`Ddi%b-{dupw_S(Po z-+TX;W@}Rxm5Kln@Dl($fBw91)r#MK`|Yc*zM8&2{oujhmJmc`0(eSf4+0~PJ^R<)ysud>;L}b$>Z7C+4JYmT^Jwx>8Brujt#D@ zs;Jw!!++gBGc)t=e}6G>pnv!4yEbipb<5V8@r&d8`+EOoEZOktx`~O4Eqj{2|Ngt9 zgYU1et}6Lk@rRQWe_vO5@7~?=%F^OBFK^sb{oeaWZr%F$^*44K%UEbH zD%O@CKXL5p)sJeoZ@qEj`stBVCd(U-zIZe?HhS~po1c7g@6*pdb+_%Q-nik!@bQC( z4tRUsJ$&TQ>XM>={p-f$rO5*a2S(1E?tZK5t+%_ce01f@FTeD3v|qk5H8pjquYdpN zpMQ4by~BIlE!%h0-oAb7>C>mb{`$+@+}t4-+ue9cOUTo7vkrP ze2rm6CZW^Jmn+Dm7nQ|-gk)~1EUm4q5bao3TO}%8Us_kaskB1AZsXcbfcO`PMt_kN z|7Q<>LImLFCct8~+3On|PFG`7b2D6z#DXpLvCu+FtQ8Df0PVF2cx>p9yxsJF)i<~^;+G7@Jf=fd;~RW5NAM5hub?;>PR2t$iPM(-$8 zF0ZMmt}r2-hRb2nJH|4yvz^5#n68O1lbcFe>2;-IIANEwGx?uZlWGEDFG;>ADKk$L zY>P7T$@8P+4I%R6ip-lN4O%YNN;6GF=n70yrRlL8v{oz573GBK@<;C%t|F9ND2=&+nRl(r<+pWE9QRDBniQwP{G|bWb+vvI8<4bhFIv z$l)Ay4D=G0^w|fp-hxU zs13oe5G@<*9suy%K(9A28$V#DG9er!w0ZA&wZt(C%nXpCMa$IMvzX-Ee*xsEBUq|5 zKl_GPK4`)5rafR<=-I&FG^xUyT%6f|D4X1csKirIxMnqqE`X3v^}Q*RQNF2^mGib3 zRqhCiWfQQG61&%FX0h>Zrz66uP6OcXIghU_lhJd4U7MK0 zF_B^l-(LUu6(tn^Dsy0A)S-X4_>Fjr+y{ValUjm7}gm{c%L-83UahNqC{o zoj=Br<_iQvH}mz9k4>!Fz9i0Dvy!Yj^UhD4Hz(w1?q%9jUmBqMUV!AT8B9f@8< z#4YWM=i~=RU&R<bW?iLh@^zVlxeiaWJLhz%(DjjZ6mM-q>fB#Ykdw!GhnIWt26hS>{s1o%PjVn$8Y0AxIMjZuvoD(~8xr6Fj zV`Udj-+0w(A9^>grYh$k$c)s+-yC23SV?CwQ#T8j-3z3WSLd=)mB1w|yiM9KnwBK> zF!c+40CL7}4NLOqI-yc>Y@n`eUQQ37wMDdBs?GWb4?lys^-QcrL+4(12~(VH|5yN^ zk-br3<;|Gv97GB|UOYKE61nK}BhdyE;fzd~IsLAPTH+P%BJ(W7cAF$?wdgNM!XRva zu_rB5XvH$Fvw5$b6;vFm%!NE1dto&y!LYJm-0MP&hy{_A4PX*I6T~%ag4IEm1qTrz zk{>aAiUP%<481J28IKDMNzWF*!g9sQP+mgO(g6u)#bBd`F`PsLkY#iR$`hs~4PznM zhZ_@;Iy>(fHGVT)AWdWYdmA!}h5SODvDfC5Lj0tcbyFT^@E=@3UC-ELTl9Hefd58h zJ;ax$^cC0o3450dN|`LRnL#m8FkNyI$#lzC)x=+_sr1Dt1XT27Enu+-<#_<{U=z?H z)2`-==w6T(^lNenATrV&Z=xsXV-olBaFj9|Z{yEc<4FyyOF)W}e9;z3x3~i^vpqwQ z4fRi72jJF2&B8}j>*!Rg*vjrx%sLM?2pBu79a}2nU)I8BQ@T{Zg$+75XWn6*sA;3t zq1;w6{RKM>zn?J=6^w})O+`$>gUR`5L)cChRhIC zD6ri-W?fD-ggO0R{QNb+D$RP%?d$oN+z|GanrpHLGlSF4?srsvSp z_-$=4MXSIRdJSyte+yw9fHOj|L1h{UAOa21?Sz>@JhdB6;7BkDx(1}u=%_vtcM6Kq z=j+-1_!4CqgNGxT#i>1vmF|Yjq``FFmv|U&nV-L8{vbAQfxjMBU%*21-wv#$o6KQL zzd2JkAJEeo(ujpKx*;|wWaKDfNvbUQ>$t>iW2o4T6h$y*lG4arK7XV&r^Bp8&<1S%R6_vD~!~FIjOW#s#d)6lBCJ zRVUpfne|)q(3TI_W?3NjV7jL)du~ zxb`&g6I3g>pt}=ZqfJ*XB6qr3x@b&NtcI=dVK@AymBpJspoI>@+ypiLzJt1E6)^;A z){0Ob4!}?jgR_7*ZwG0`qHLiRVXk1G17fp=u}~G?!4C+;7#TP1T>1;y&s%-D0g-Xi z>8G*6DxHfGR0$3i&3v!qfJi~|O`i(5)k8)UMR{{3A1-8u)K85+8MQV?a>U4%FUA>U z8`F5uB$5Cmc5lt&Ry6tqUA1;0CwRTDefxk;$+j60dT_Y1Im*<#@0>7(%F(e&uJbyg zx2O`dz^B1AeJy!n$id`CT*|1Ga<6?%X7PH}clCV(9}yt+8UK1Og9PHbfi!m11&dI$ zQL)=_T1RJuKFvm2D~>9K(C;9y!g$-1K(-YGfG!|${QOxT6;8mBU7W*Bkl+fAWSOCHJLAtx&`#M zykF?q%625L=&MfRgsOr5xQ}bYxi)ji`Bfplfo z|0UfQOBlojv2atng%V8X;hrXh7aAgEx4q?jCqlwlxF6tixvVCB>5vu5*6O+~IIn=7 zAM2v7TAjwXB3ugoVsdLxg^aOI#iW9SKwk~SY0stbS>mxBN0aYo`Y}Fp4zqw#79m3ams6lg;ug zIY&1;!f>YPy8?&E5aezV>M{ngX-zm^1mUxAz302Y)XXl-vKmP>VbccWS=4kz1KR95qzt`}sN zzd(x10%TqnD99im1!K9x7BCZyMrnwP$}5(QJCOVmEHvndleKKI`ic{#&^b}iQ3oHt z=R!K5r*;@bKzQctPmh@yE7x#Am0+g=%7sMI?RCD>FT4p#A|WEI(hkui>PP;MC*QG$ zWhncH{11CCA!>dE-J%(9m4&g7MuE-e?2uu``x3_zq;K((tw&ld}POdf3uxunTPQli;GLaJ^1hX^%;* zb`9yo8a8NHNt?a@!vj#*Pb-|%%|)~aL|RJRmIs-EC{k~@IO1D6OCTu<|EPNGNpvL7 z=Qc)6HNpZzwc=Q*^H%1mUbRtaA(OhJ&lD+4UXxwIW7oX14rlv_K<1qRb#>5>fSm-0 ybR(?Cs;4^!5RnkZab6_T*3gx!{JdjWm%pZ@cI;=chfm!et# literal 0 HcmV?d00001 diff --git a/public/images/binaryattraction.png b/public/images/binaryattraction.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a3032ca04d3c83af4441f076614345526c65b4 GIT binary patch literal 9075 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 literal 0 HcmV?d00001 diff --git a/public/images/binaryattraction.xcf b/public/images/binaryattraction.xcf new file mode 100644 index 0000000000000000000000000000000000000000..fa2b047af9e296d889be159deddd4e84de2a5f7d GIT binary patch literal 11518 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 literal 0 HcmV?d00001 diff --git a/public/images/merb.jpg b/public/images/merb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a19dcf404840b13bc42f60042ef61e69b97bffb0 GIT binary patch literal 5815 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 Z78m6-S}H0e;H~?5YOg{bUw(2m`F|2_d0zkk literal 0 HcmV?d00001 diff --git a/public/images/system-lock-screen.png b/public/images/system-lock-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..dc6b825b189dfd8faa28b71e6843be35f01523a2 GIT binary patch literal 1005 zcmV*BVSU&=jl}6cf{eV#W00gHQy6B@fmoK~RJECLvJqMGGS0 zo5d8Nhzf$VvLby6m9*8g1+gakV4$(9l(eLqTeG|WzdN_%!){YI*(iz~7-r`1pKs2Y z|6Je>4F&N22S;87#{MVfYojBFkHu<-vBAMXQmJ%BYwb3ED~JFh2!ePJd`$)-f^}9> zsYG)olNkfnzyQ4Y#;3_#?#jP5vA_Qj4m`J=&d$y?Ssgf+yTZ`W0NxAU3nCux{o2`k zkBIz5<^hFytWf7_Et81z$%GR!C+B=fOVv3cO6;@Y^JlfmN z*|X>9>DkuUVt{hltOJ7go}TTk?707K9(?El63GOSh$0q^Q7VO8e)uxk>@=xVa&xfO zt_~_9+}oX|f7cG4*|(oxvX?kBagOWPui>1dqhkxZpL&u*FTKc#6DM)jZU*+Xw`J{u zuB~bIJUzhdZ?l~GZk!8~*C-S`plEFi$mdV7cmHm7?&@Rmhl>D2)`N}lXzj7qv%DOT z%iW-*B}QvY8{^}r`T3_jK@cYzO%aQw2!lA&Q_IZ!I>VOEyC|2-n^`P`p`la?(b^J? z#>waND5aR2yGjryNhBhGLaD0G;^GpE3rnPufAhu*3q?dgsVIp=45bwLd>%oFL?R^O ziJGIg!6MbQ`<`tZQd$NF2N^o}9^Kvdp=ugRDW2W=J&*NVKv9iZ>p)vD>!+!%+Jc9V z>n!f=>m@s#t@EJo`Sa|1B}X*kK#GVj;9Q7z0nP?k8(?h_Qz>F}fnsrb& z0^|Mx$tgHRJ(UHT$z;KP#1hkEezIA@jz}{y5psT7Bf|S7D z1hTU z#^J5SJBN1`YaG@rHBJz~0k&K&Pk#R87e}q9=IVdwAPq(uXa`!q{YcFm7`~)pY8l+` zczd-P2`0dp08=TVjlmd)v7WG0;_J^&d-V^OCQg2sKI&}vvau3r7{9k-_;%m%u{g)> bR4?UEMCrl+R4 za^)kWYznT|Ykc+1*PjAQfN3f31p|1Vht`@f42hzMIF3n@1OTlyLqkI-rBF%%kWOb9 z86E~m1KWL50pR;ST5F;xB1sb3xi`k(c^;We1|cLuG!+X0T6;Z=zF@62zVG)yg%C(7 z(OToRa@%dKw1|FS*&XCK4xZ=X`#!$!<9S}kwbqEf2>2nJedBu}ZuM0xrR=y;3deDn zn3y1rV~WKhuIu)N{yaT>@$l5t_2AJX^_>tOw;o3w#qDEv`{w2*zV9*& zbL(5O`TXK*SFd_XsYohSmX1>vQ0z(kUa+;6LZQH^Q>QS-bmGDw;LiL!Si%<%i zBn-^XBBW%WVj)E511C<6inv+@`gBbj9EM@1#H5rI z3I(K;jE#+vdu5#2d-wS1>>28toApu0@qb%eGxp!U_*)(X?yhIYaVV8a>;yYhDrMq0 zMhzaKv3QMgt+w)bW20svxn!-|@)!4k+i;~6T5F2MBJ20p(Viy%Ql4BcM-)XEW2l-2 z=eM^%sg=uL3V7Vj=sQ5P)&xPo`uaUy9v|cK<@Y&uY^*u`qL45OiQ|}HcNd^>F^WD1 zKI;p%)}oZcTFZk6_c=5;z=sQ$$>nmmu1g$;)anreuojXeCI|w61_y1BIF6B0^8Byo z2rO^CJx40#`ai+JPQV|3{?5d!&Eq00jYge@X*7Wys^~D1#8Ro0 zp}9T!gGY{zEZ_d|_F{MEiGyZjSL(FZ%j{W6ZTko3rFxN?8gVq)SYK~ZDk zGY|Npi5R0XNQg#FBry^d5g`ywi%{AWO1ZYQ-R^~MclYesv*$aX4_zv>80x%CW+wS( z=0B7F2&UQg&4K3y>Mey>3LsED3bE&l!{$N$hl$&R9h(E6)Rw3%?M?nbonPtzH%uXq z4_UdZrugdf!?{;x3#?Cd+V0;{=V#aUCBD*Bpjt}?>WJ%QI0|h|dEPscmGhhK^9-K3 z2_fDFIQD+If7ZYX7T5Zu zZhJ%ceLf;pl>`HRO1vI&nh#s^>FXk@>2!xL9lDmvp57{C=T2j+>qW8cZotOvfl>;m z;9%wRBoa5V@_FongOrlFOt5gtqHtvOCbU2$wiy??EJUIwf8N)%wYRlxKd;@Ds?X`@pvP*p+?V z^vs~bRcg=gLwR?hmLQuLBN~eli^p*%Vl-e;v*9_i74 z6X=-1@uN?!d}uhdYX7h=WPEd3Ql&DqYEkCt8A^m8HSQpFAFgAA5D1~&+BniqBbD$f zSa}D>o1R(M2K@2ZhH`F<1jN9=1lO-gl&ydq%A@gWDJbP4a|wiIfbK!&61e#kNE;~~ zOF(e?@;W!H3EN|_es4M(rDerx@>ZJui#^yjw6wI4y?h2I6UEKNue54_=9}->k!dPqbz&s zX`JuBdlz`mNT)KNp6H}wX>+xzs&|Deg2lVmNO{woyPCq41rUO9ymXB^?ArS>bNn85 ze|eTg^#bKeT*u+!NS5Xz#m1>$I(M(y&}Q@y+A*2?S6`~_5K6fy>EHq%A36^}d+Ru* z0R!a}ICSa?u9Rr{9qQmSfb^e7x?Ts~0ZNLOVgTV6RxCWNWC4Ica)fMY6#zf|bq&+9 zxo|xVN)k*ZrfNm=8-N330JA9kT2Vtx088pLH&jp{$DFD%%)F#`c#@iMI6T>&EMSzC zVHo-hbyI+x_E{!wn#s?~fBfMk0AI0CaSt7yuu>lZIdPBg;sXLjuT2-OTl#gIw~CWm nVirG9oAw+33;5RLE};Ga6c7WIAH0-{00000NkvXXu0mjf<;zyj literal 0 HcmV?d00001 diff --git a/public/images/user-trash.png b/public/images/user-trash.png new file mode 100644 index 0000000000000000000000000000000000000000..05ff0365f5ef4c1934836f934a89ac128b5c1cb3 GIT binary patch literal 1149 zcmV-@1cLjCP)j9)w*!{!&m3ee%jpcY3(0-N2BMwFpS@L_Lge+@fOo^vTlC;TkYlgxI#uT6eh6neu+?r>oZb{Zx5nJ{bk)oS)5WAJJrLo#x za{yvX#0q)2%5v*6BO~|I>$b?V1@@0Wfl?uHWhbUIEZv;zuK@=*lBDNb+0t%SXtdjH z59hFA5MwZ6$-7HfQIsY}c_BeqLrR0Ee12WLK$a&s=g>N&EHlbdLg#300*j@QBv~Xj z!#ahr1~Gy$md45)rVvVFnVtI%-ydLMX$DgY)^4 zoim@GJ1!za+xZ8r)mN_06d(78E?3Sulu`;90O~;cuTeK71`43#oGX3@F$QB=SBZKw P00000NkvXXu0mjfHdq!i literal 0 HcmV?d00001 diff --git a/public/images/utilities-system-monitor.png b/public/images/utilities-system-monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..f2d266f794a3ae7008cb8981f24c412f15c053ce GIT binary patch literal 990 zcmV<410np0P)XXn)&;tcg z5uqpu)`|t8Mzogt;8Sc0_91A~mZV^tLQ@!BQ5ZQ>ttN25mGZ(nwtbV*!g43WX(DE?<&mdq~Se z*dA?Zc%q||bxi_cdq~?uIB}n4dswc6v^~Zq|6H*(E8rM#xi=+fs51zAJ;|mBWxH68 zOKZ{~Z5J%XBjJ>@o8l45>IcQLA1R>cH$WAb!`+E2icUo%mWX7Nmbu={?6B- zbp&CFDAM$wfry|oeDIx%2qD?@@P2YFk zF*>6E%qkhTn74TFk#Onz^i&O_Gx5E=#=>DJh`3YQym;0=)ssT&{{*JF*V^avF zkd|T~+lX`=?z<}wz|X%npd6dGdpA=G{yn@PpyU^UaP`C6)7Ol0U6kvgJcYC@&R)D7 zo&Sb&v-K$DP$EQWO`t8LC6Sh_e2-n%z0x2hQV1X(G&s4Ap8j(T{gwn3Pll8zDdQ13 z{@tsps#a!f+YYHzV_bw53M^8%CWH5eJz%(E5lW>RQA(|Zt8cDmU*=Kf=I6-g=L!6P zAPoLfWpf+{DJ4=~*%Fb-yw*{BTcUrh3jOPu?qW7OzC{4u3okzZc)InT-nD>xdk<|r z`s2Wna*_3bdY}eK0Pa<>ZUw_%_I?St4EzBU#j;@UI)GOJF0R)04}ZW#BUP0;!~g&Q M07*qoM6N<$f-X7Cr2qf` literal 0 HcmV?d00001 diff --git a/public/images/vote.png b/public/images/vote.png new file mode 100644 index 0000000000000000000000000000000000000000..afca0732a75933c67a30ff16d2c9b4c3abf1cf1e GIT binary patch literal 1002 zcmVF#3cAXC>M*>I!?t!N+*jbfk$)$>PW!K6lLY0xqWQKxp>LKNI;Epw48ym!mJI5u3@ z&HFre&vTyV^hfu~lTGm+_`*4yb3UB&JBJ7%kZud9>=}uXuK`S1Tr`Ijempft(gi~* z^HN3zU#@O`Kq3-4E_|hpOkNqaxOhIS@ZV{=r=t>SDGjWh|KP#`nBMDnrTQIT`BNJT zBu3r1g_;dKups>-F78p`lP42>_j|U%GM+f6QFol79c_of?w&|8BwuKcg?imcJEQ3m9 zf9<+P|B&8;Nh5+4^S7d_zumZ*hZs%H-JV6d~NgcLj7Xf(Fw#A33D}+>5?3XjPS3ObZxg0-(X=?&cFW7;$j=h>? z*^Oa^PdhiB}RX#t7 zDiYPY!Kep$o)Uc1^_gy&cHLa%VFd8GW#*!SLAPQUh<}ITWhKAa`jN;j>T{zwM#jI}3w1yS_$6O#a4Xgg-+4d_Zdk-cG<+bq91&AchBksC}n)03U z3>m|a{v&d{k0GI(IQQ$RX^V@NJ3Sx~LLj6vkHl!}{aH(Dc5ZB$CtR}_h<^`_$1p2y zg4?sGy6-J~e5_f!Iv#m3tnd?e1ye1gvdwO{`@l=jHToY}QcAmH2Qg_x@MLB!Caz7{ z?VYVZoN6*E?u7jhOrrS`qbEuNtMj*%y(*6x{m7=vvFC7urYE#OSmA%&4TKQlR#J5> zs-|+K>e^rXX-@pxp*`ZYqd~pmUA7m148RM(1Hg@c|1otsL0) + 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.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this.element._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.element._originallyAbsolute) + Position.relativize(this.element); + delete this.element._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){ + var s = Sortable.options(element); + + 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')]; +} diff --git a/public/javascripts/effects.js b/public/javascripts/effects.js new file mode 100644 index 0000000..b8c0259 --- /dev/null +++ b/public/javascripts/effects.js @@ -0,0 +1,1122 @@ +// script.aculo.us effects.js v1.8.1, Thu Jan 03 22:07:12 -0500 2008 + +// Copyright (c) 2005-2007 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) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + ((pos % (1/pulses)) * pulses).round() == 0 ? + ((pos * pulses * 2) - (pos * pulses * 2).floor()) : + 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor()) + ); + }, + 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(), + max = (window.height || document.body.scrollHeight) - document.viewport.getHeight(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1] > max ? max : 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] || { }; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + 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); diff --git a/public/javascripts/prototype.js b/public/javascripts/prototype.js new file mode 100644 index 0000000..6385503 --- /dev/null +++ b/public/javascripts/prototype.js @@ -0,0 +1,4221 @@ +/* Prototype JavaScript framework, version 1.6.0.2 + * (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.2', + + Browser: { + IE: !!(window.attachEvent && !window.opera), + Opera: !!window.opera, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div').__proto__ && + document.createElement('div').__proto__ !== + document.createElement('form').__proto__ + }, + + ScriptFragment: ']*>([\\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 = Object.extend((function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method), { + valueOf: function() { return method }, + toString: function() { return method.toString() } + }); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (Object.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].split(",").invoke("strip"); + 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); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Function.prototype.defer = Function.prototype.delay.curry(0.01); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = 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.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('') +}); + +with (String.prototype.escapeHTML) div.appendChild(text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + iterator = iterator.bind(context); + try { + this._each(function(value) { + iterator(value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + iterator = iterator.bind(context); + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator(value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = 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) { + iterator = iterator.bind(context); + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == null || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == null || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + iterator = iterator.bind(context); + return this.map(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#'; + } +}; + +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 []; + if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && + 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) { + $R(0, this, true).each(iterator); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + + 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) { + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return values.map(toQueryPair.curry(key)).join('&'); + } + return toQueryPair(key, values); + }).join('&'); + }, + + inspect: function() { + return '#'; + }, + + 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 || { }); +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + $(element).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, 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(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); + var originalAncestor = ancestor; + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (element.sourceIndex && !Prototype.Browser.Opera) { + var e = element.sourceIndex, a = ancestor.sourceIndex, + nextAncestor = ancestor.nextSibling; + if (!nextAncestor) { + do { ancestor = ancestor.parentNode; } + while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); + } + if (nextAncestor && nextAncestor.sourceIndex) + return (e > a && e < nextAncestor.sourceIndex); + } + + while (element = element.parentNode) + if (element == originalAncestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (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 (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || element.tagName == 'BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + +if (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); + 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); + 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.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').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine 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, 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 = { }; + var B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] : + (B.Opera) ? document.body['client' + 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(); + 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(this.expression)) + return false; + + return true; + }, + + compileMatcher: function() { + if (this.shouldUseXPath()) + return this.compileXPathMatcher(); + + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + if (this.xpath) return document._getElementsByXPath(this.xpath, root); + return this.matcher(root); + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#"; + } +}); + +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 or translate(text(), ' \t\r\n', '') = '')]", + 'checked': "[@checked]", + 'disabled': "[@disabled]", + 'enabled': "[not(@disabled)]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, 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]+)\]/, + 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 && !node.innerHTML.match(/^\s*$/))) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._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) results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv.startsWith(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } + }, + + 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 != '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.blur(); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (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, index) { + if (Object.isUndefined(index)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, value, single = !Object.isArray(index); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + value = this.optionValue(opt); + if (single) { + if (value == index) { + opt.selected = true; + return; + } + } + else opt.selected = index.include(value); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + var node = Event.extend(event).target; + return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + return { + x: event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._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; + } + + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + 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("