├── config ├── deploy.rb ├── database.yml ├── environments │ ├── production.rb │ ├── development.rb │ └── test.rb ├── routes.rb ├── boot.rb └── environment.rb ├── public ├── favicon.ico ├── images │ ├── logo.gif │ └── rails.png ├── robots.txt ├── javascripts │ ├── application.js │ ├── controls.js │ ├── dragdrop.js │ └── effects.js ├── stylesheets │ └── all.css ├── dispatch.cgi ├── dispatch.rb ├── dispatch.fcgi ├── 500.html ├── 404.html └── .htaccess ├── app ├── views │ ├── kite │ │ ├── _divider.rhtml │ │ ├── view.rhtml │ │ ├── _task.rhtml │ │ ├── by_tag.rhtml │ │ ├── index.rhtml │ │ └── _kite.rhtml │ ├── search │ │ ├── _result.rhtml │ │ ├── _form.rhtml │ │ └── index.rhtml │ ├── layouts │ │ ├── _footer.rhtml │ │ ├── _header.rhtml │ │ └── standard.rhtml │ └── home │ │ ├── index.rhtml │ │ └── _examples.rhtml ├── helpers │ ├── home_helper.rb │ ├── search_helper.rb │ ├── kite_helper.rb │ └── application_helper.rb ├── controllers │ ├── home_controller.rb │ ├── application.rb │ ├── kite_controller.rb │ └── search_controller.rb └── models │ ├── kite.rb │ ├── task.rb │ └── kite_parser.rb ├── .gitignore ├── vendor └── plugins │ └── acts_as_taggable_on_steroids │ ├── lib │ ├── tag_counts_extension.rb │ ├── tagging.rb │ ├── tag.rb │ └── acts_as_taggable.rb │ ├── test │ ├── fixtures │ │ ├── user.rb │ │ ├── users.yml │ │ ├── post.rb │ │ ├── photo.rb │ │ ├── tags.yml │ │ ├── photos.yml │ │ ├── posts.yml │ │ └── taggings.yml │ ├── database.yml │ ├── tagging_test.rb │ ├── schema.rb │ ├── abstract_unit.rb │ ├── tag_test.rb │ └── acts_as_taggable_test.rb │ ├── init.rb │ ├── CHANGELOG │ ├── Rakefile │ ├── MIT-LICENSE │ └── README ├── script ├── about ├── plugin ├── runner ├── server ├── console ├── destroy ├── generate ├── breakpointer ├── process │ ├── reaper │ ├── spawner │ └── inspector └── performance │ ├── profiler │ └── benchmarker ├── test ├── fixtures │ ├── kites.yml │ └── tasks.yml ├── unit │ ├── kite_test.rb │ └── task_test.rb ├── functional │ ├── home_controller_test.rb │ ├── kite_controller_test.rb │ └── search_controller_test.rb └── test_helper.rb ├── doc └── README_FOR_APP ├── README.textile ├── Rakefile ├── db ├── migrate │ ├── 002_create_tasks.rb │ ├── 001_create_kites.rb │ └── 003_add_taggable.rb └── schema.rb └── lib ├── kites ├── metacritic.kite ├── play.kite ├── boots.kite ├── hmv.kite ├── virgin.kite ├── amazon.kite └── currency.kite └── tasks ├── kite.rake └── capistrano.rake /config/deploy.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/kite/_divider.rhtml: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | log/* 3 | tmp/* 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /app/helpers/home_helper.rb: -------------------------------------------------------------------------------- 1 | module HomeHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/kite/view.rhtml: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'kite' %> -------------------------------------------------------------------------------- /app/helpers/search_helper.rb: -------------------------------------------------------------------------------- 1 | module SearchHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/search/_result.rhtml: -------------------------------------------------------------------------------- 1 |
  • <%= result.first %>: <%= result.last %>
  • -------------------------------------------------------------------------------- /public/images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/kite/master/public/images/logo.gif -------------------------------------------------------------------------------- /public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/kite/master/public/images/rails.png -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb: -------------------------------------------------------------------------------- 1 | module TagCountsExtension 2 | end 3 | -------------------------------------------------------------------------------- /app/views/kite/_task.rhtml: -------------------------------------------------------------------------------- 1 |

    <%= h task.name %>

    2 |
    <%= h task.kite.name %> <%= h task.name %> query
    -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file -------------------------------------------------------------------------------- /script/about: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/about' -------------------------------------------------------------------------------- /script/plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/plugin' -------------------------------------------------------------------------------- /script/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/runner' -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/server' -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/console' -------------------------------------------------------------------------------- /script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/destroy' -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/generate' -------------------------------------------------------------------------------- /app/views/layouts/_footer.rhtml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/breakpointer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/breakpointer' -------------------------------------------------------------------------------- /script/process/reaper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/reaper' 4 | -------------------------------------------------------------------------------- /script/process/spawner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/spawner' 4 | -------------------------------------------------------------------------------- /test/fixtures/kites.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | one: 3 | id: 1 4 | two: 5 | id: 2 6 | -------------------------------------------------------------------------------- /test/fixtures/tasks.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | one: 3 | id: 1 4 | two: 5 | id: 2 6 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | layout 'standard' 3 | 4 | def index 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/home/index.rhtml: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'search/form' %> 2 |

    Top kites: <%= top_tasks %>

    3 | <%= render :partial => 'examples' %> -------------------------------------------------------------------------------- /app/views/kite/by_tag.rhtml: -------------------------------------------------------------------------------- 1 |

    Kite tag search

    2 | 3 | <%= render :partial => 'kite', :collection => @kites, :spacer_template => 'divider' %> -------------------------------------------------------------------------------- /script/process/inspector: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/inspector' 4 | -------------------------------------------------------------------------------- /app/views/kite/index.rhtml: -------------------------------------------------------------------------------- 1 |

    Kite command list

    2 | 3 | <%= render :partial => 'kite', :collection => @kites, :spacer_template => 'divider' %> -------------------------------------------------------------------------------- /script/performance/profiler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/profiler' 4 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :posts 3 | has_many :photos 4 | end 5 | -------------------------------------------------------------------------------- /script/performance/benchmarker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/benchmarker' 4 | -------------------------------------------------------------------------------- /app/models/kite.rb: -------------------------------------------------------------------------------- 1 | class Kite < ActiveRecord::Base 2 | acts_as_taggable 3 | 4 | has_many :tasks 5 | has_one :default_task, :class_name => 'Task' 6 | end 7 | -------------------------------------------------------------------------------- /app/views/layouts/_header.rhtml: -------------------------------------------------------------------------------- 1 | 2 | <%= link_to 'search', search_form_url %> | <%= link_to 'list', kites_url %> -------------------------------------------------------------------------------- /app/views/search/_form.rhtml: -------------------------------------------------------------------------------- 1 | <% form_tag search_form_url do -%> 2 | Look up: <%= text_field_tag 'q', params[:q] %> 3 | <%= submit_tag 'Search' %> 4 | <% end -%> 5 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | jonathan: 2 | id: 1 3 | name: Jonathan 4 | 5 | sam: 6 | id: 2 7 | name: Sam 8 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb: -------------------------------------------------------------------------------- 1 | class Tagging < ActiveRecord::Base 2 | belongs_to :tag 3 | belongs_to :taggable, :polymorphic => true 4 | end 5 | -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /app/helpers/kite_helper.rb: -------------------------------------------------------------------------------- 1 | module KiteHelper 2 | def tag_links(kite) 3 | kite.tags.collect do |tag| 4 | link_to(tag.name, kites_by_tag_url(:tag => tag.name)) 5 | end.join 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake appdoc" to generate API documentation for your models and controllers. -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | acts_as_taggable 3 | 4 | belongs_to :user 5 | 6 | validates_presence_of :text 7 | end 8 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/lib/acts_as_taggable' 2 | 3 | require File.dirname(__FILE__) + '/lib/tagging' 4 | require File.dirname(__FILE__) + '/lib/tag' 5 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb: -------------------------------------------------------------------------------- 1 | class Photo < ActiveRecord::Base 2 | acts_as_taggable 3 | 4 | belongs_to :user 5 | end 6 | 7 | class SpecialPhoto < Photo 8 | end 9 | -------------------------------------------------------------------------------- /app/views/kite/_kite.rhtml: -------------------------------------------------------------------------------- 1 |

    Kite: <%= h(kite.name) %>

    2 | 3 |

    <%= h(kite.description) %>

    4 | 5 | <%= render :partial => 'task', :collection => kite.tasks %> 6 | 7 |

    Tags: <%= tag_links(kite) %>

    8 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG: -------------------------------------------------------------------------------- 1 | [21 Feb 2007] 2 | 3 | * Use scoping instead of TagCountsExtension [Michael Schuerig] 4 | 5 | [7 Jan 2007] 6 | 7 | * Add :match_all to find_tagged_with [Michael Sheakoski] 8 | -------------------------------------------------------------------------------- /test/unit/kite_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class KiteTest < Test::Unit::TestCase 4 | fixtures :kites 5 | 6 | # Replace this with your real tests. 7 | def test_truth 8 | assert true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/unit/task_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class TaskTest < Test::Unit::TestCase 4 | fixtures :tasks 5 | 6 | # Replace this with your real tests. 7 | def test_truth 8 | assert true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/database.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | :adapter: mysql 3 | :host: localhost 4 | :username: rails 5 | :password: 6 | :database: rails_plugin_test 7 | 8 | sqlite3: 9 | :adapter: sqlite3 10 | :database: ':memory:' 11 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h3. Kite 2 | 3 | Kite is a mobile web search experiment that uses a DSL to define small scrapers, called 'Kites', that can find product information from web pages. 4 | 5 | Kite was originally released in March 2007, and has since been discontinued and open sourced. 6 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Methods added to this helper will be available to all templates in the application. 2 | module ApplicationHelper 3 | def top_tasks 4 | Task.find(:all, :limit => 5).collect { |task| "#{task.kite.name} #{task.name}" }.join(', ') 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/search/index.rhtml: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'search/form' %> 2 | <% if @results.size == 0 -%> 3 |

    There were no matches for your query.

    4 | <% else -%> 5 |

    Results

    6 | 9 | <% end -%> -------------------------------------------------------------------------------- /app/views/home/_examples.rhtml: -------------------------------------------------------------------------------- 1 |

    Examples:

    2 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml: -------------------------------------------------------------------------------- 1 | good: 2 | id: 1 3 | name: Very good 4 | 5 | bad: 6 | id: 2 7 | name: Bad 8 | 9 | nature: 10 | id: 3 11 | name: Nature 12 | 13 | question: 14 | id: 4 15 | name: Question 16 | 17 | animal: 18 | id: 5 19 | name: Crazy animal -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require(File.join(File.dirname(__FILE__), 'config', 'boot')) 5 | 6 | require 'rake' 7 | require 'rake/testtask' 8 | require 'rake/rdoctask' 9 | 10 | require 'tasks/rails' 11 | -------------------------------------------------------------------------------- /app/controllers/application.rb: -------------------------------------------------------------------------------- 1 | # Filters added to this controller apply to all controllers in the application. 2 | # Likewise, all the methods added will be available for all controllers. 3 | 4 | class ApplicationController < ActionController::Base 5 | # Pick a unique cookie name to distinguish our session data from others' 6 | # session :session_key => '_kite_session_id' 7 | session nil 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/kite_controller.rb: -------------------------------------------------------------------------------- 1 | class KiteController < ApplicationController 2 | layout 'standard' 3 | 4 | def index 5 | @kites = Kite.find(:all, :order => 'namespace, name') 6 | end 7 | 8 | def view 9 | @kite = Kite.find_by_name_and_namespace(params[:name], 'System') 10 | end 11 | 12 | def by_tag 13 | @kites = Kite.find_tagged_with(params[:tag]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/002_create_tasks.rb: -------------------------------------------------------------------------------- 1 | class CreateTasks < ActiveRecord::Migration 2 | def self.up 3 | create_table :tasks do |t| 4 | t.column :name, :string 5 | t.column :kite_id, :integer 6 | t.column :description, :text 7 | t.column :created_at, :datetime 8 | t.column :updated_at, :datetime 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :tasks 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/kites/metacritic.kite: -------------------------------------------------------------------------------- 1 | description 'Metacritic search' 2 | name 'metacritic' 3 | default 'score' 4 | tags 'ratings' 5 | hits 5 6 | 7 | site do 8 | home 'http://www.metacritic.com' 9 | search 'http://www.metacritic.com/search/process?sb=0&tfs=all&ts=%s&ty=0&x=0&y=0' 10 | end 11 | 12 | task 'score', :args => 'query' do 13 | get(site.search, query) 14 | save for_each('td#rightcolumn p', :find_first => ['a b', 'span']) 15 | end 16 | -------------------------------------------------------------------------------- /lib/kites/play.kite: -------------------------------------------------------------------------------- 1 | description 'Play.com search' 2 | name 'play' 3 | default 'price_search' 4 | tags 'shops' 5 | hits 5 6 | 7 | site do 8 | home 'http://play.com/' 9 | search 'http://play.com/Search.aspx?searchtype=allproducts&searchstring=%s&page=search&pa=search&go.x=0&go.y=0' 10 | end 11 | 12 | task 'price', :args => 'query' do 13 | get(site.search, query) 14 | save for_each('div.info', :find_first => ['h5', 'h6']) 15 | end 16 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | class TaggingTest < Test::Unit::TestCase 4 | fixtures :tags, :taggings, :posts 5 | 6 | def test_tag 7 | assert_equal tags(:good), taggings(:jonathan_sky_good).tag 8 | end 9 | 10 | def test_taggable 11 | assert_equal posts(:jonathan_sky), taggings(:jonathan_sky_good).taggable 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /public/stylesheets/all.css: -------------------------------------------------------------------------------- 1 | body { font-family: arial, sans-serif; font-size: 80% } 2 | h1, h2, h3, h4 { margin: 10px 0 } 3 | h1 { font-size: 1.5em } 4 | h2 { font-size: 1.2em } 5 | p { font-size: 1em; } 6 | ul.examples { list-style-type: none; padding: 0 } 7 | ul.examples li { margin-bottom: 3px } 8 | ul.examples li span { font-family: courier, fixed; background-color: #ffc } 9 | img#Logo { float: left; margin-right: 1em } 10 | p#Footer { font-size: 1em; color: #777; margin-top: 2em; } -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml: -------------------------------------------------------------------------------- 1 | jonathan_dog: 2 | id: 1 3 | user_id: 1 4 | title: A small dog 5 | 6 | jonathan_questioning_dog: 7 | id: 2 8 | user_id: 1 9 | title: What does this dog want? 10 | 11 | jonathan_bad_cat: 12 | id: 3 13 | user_id: 1 14 | title: Bad cat 15 | 16 | sam_flower: 17 | id: 4 18 | user_id: 2 19 | title: Flower 20 | 21 | sam_sky: 22 | id: 5 23 | user_id: 2 24 | title: Sky 25 | -------------------------------------------------------------------------------- /db/migrate/001_create_kites.rb: -------------------------------------------------------------------------------- 1 | class CreateKites < ActiveRecord::Migration 2 | def self.up 3 | create_table :kites do |t| 4 | t.column :name, :string 5 | t.column :namespace, :string 6 | t.column :script, :text 7 | t.column :default_task_id, :integer 8 | t.column :description, :text 9 | t.column :created_at, :datetime 10 | t.column :updated_at, :datetime 11 | end 12 | end 13 | 14 | def self.down 15 | drop_table :kites 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/search_controller.rb: -------------------------------------------------------------------------------- 1 | class SearchController < ApplicationController 2 | layout 'standard' 3 | 4 | def index 5 | @results = [] 6 | 7 | unless params[:q] 8 | redirect_to :controller => 'home' 9 | else 10 | begin 11 | task, search = Task.parse_and_load(params[:q]) 12 | @results = KiteParser.new(task.kite.script).send(task.name, search) 13 | rescue 14 | flash[:error] = 'Command not found.' 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /public/dispatch.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch -------------------------------------------------------------------------------- /public/dispatch.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch -------------------------------------------------------------------------------- /db/migrate/003_add_taggable.rb: -------------------------------------------------------------------------------- 1 | class AddTaggable < ActiveRecord::Migration 2 | def self.up 3 | create_table :tags, :force => true do |t| 4 | t.column :name, :string 5 | end 6 | 7 | create_table :taggings, :force => true do |t| 8 | t.column :tag_id, :integer 9 | t.column :taggable_id, :integer 10 | t.column :taggable_type, :string 11 | t.column :created_at, :datetime 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table "tags" 17 | drop_table "taggings" 18 | end 19 | end -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml: -------------------------------------------------------------------------------- 1 | jonathan_sky: 2 | id: 1 3 | user_id: 1 4 | text: The sky is particularly blue today 5 | 6 | jonathan_grass: 7 | id: 2 8 | user_id: 1 9 | text: The grass seems very green 10 | 11 | jonathan_rain: 12 | id: 3 13 | user_id: 1 14 | text: Why does the rain fall? 15 | 16 | sam_ground: 17 | id: 4 18 | user_id: 2 19 | text: The ground is looking too brown 20 | 21 | sam_flowers: 22 | id: 5 23 | user_id: 2 24 | text: Why are the flowers dead? -------------------------------------------------------------------------------- /lib/kites/boots.kite: -------------------------------------------------------------------------------- 1 | description 'Boots search' 2 | name 'boots' 3 | default 'price_search' 4 | tags 'shops' 5 | hits 5 6 | 7 | site do 8 | home 'http://www.boots.com' 9 | search 'http://www.boots.com/guidedsearch/newsearch.jsp?searchArea=1&searchTerm=%s&Go.x=0&Go.y=0&uri=%2Fonlineexperience%2Fflexible_template_2006_publish.jsp&classificationId=1043920&contentId=&articleId=&N=0&Ntk=all&Nty=1' 10 | end 11 | 12 | task 'price', :args => 'query' do 13 | get(site.search, query) 14 | save for_each('ul.searchResults', :find_first => ['td a.productLink', 'td p']) 15 | end 16 | -------------------------------------------------------------------------------- /test/functional/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require 'home_controller' 3 | 4 | # Re-raise errors caught by the controller. 5 | class HomeController; def rescue_action(e) raise e end; end 6 | 7 | class HomeControllerTest < Test::Unit::TestCase 8 | def setup 9 | @controller = HomeController.new 10 | @request = ActionController::TestRequest.new 11 | @response = ActionController::TestResponse.new 12 | end 13 | 14 | # Replace this with your real tests. 15 | def test_truth 16 | assert true 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/functional/kite_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require 'kite_controller' 3 | 4 | # Re-raise errors caught by the controller. 5 | class KiteController; def rescue_action(e) raise e end; end 6 | 7 | class KiteControllerTest < Test::Unit::TestCase 8 | def setup 9 | @controller = KiteController.new 10 | @request = ActionController::TestRequest.new 11 | @response = ActionController::TestResponse.new 12 | end 13 | 14 | # Replace this with your real tests. 15 | def test_truth 16 | assert true 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/functional/search_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require 'search_controller' 3 | 4 | # Re-raise errors caught by the controller. 5 | class SearchController; def rescue_action(e) raise e end; end 6 | 7 | class SearchControllerTest < Test::Unit::TestCase 8 | def setup 9 | @controller = SearchController.new 10 | @request = ActionController::TestRequest.new 11 | @response = ActionController::TestResponse.new 12 | end 13 | 14 | # Replace this with your real tests. 15 | def test_truth 16 | assert true 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/layouts/standard.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kite: Mobile search 6 | 7 | 8 | 9 | <%= stylesheet_link_tag 'all', :media => 'all' %> 10 | 11 | 12 | <%= render :partial => 'layouts/header' %> 13 | <%= yield %> 14 | <%= render :partial => 'layouts/footer' %> 15 | 16 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: mysql 3 | database: kite_development 4 | username: root 5 | password: 6 | host: localhost 7 | 8 | # Warning: The database defined as 'test' will be erased and 9 | # re-generated from your development database when you run 'rake'. 10 | # Do not set this db to the same as development or production. 11 | test: 12 | adapter: mysql 13 | database: kite_test 14 | username: root 15 | password: 16 | host: localhost 17 | 18 | production: 19 | adapter: mysql 20 | database: kite_production 21 | username: kite 22 | password: password 23 | host: localhost 24 | 25 | -------------------------------------------------------------------------------- /app/models/task.rb: -------------------------------------------------------------------------------- 1 | class Task < ActiveRecord::Base 2 | belongs_to :kite 3 | 4 | def self.parse_and_load(query) 5 | invoker = query.split(' ').first.strip 6 | task_name = query.split(' ')[1].strip 7 | query = query.gsub(invoker, '').gsub(task_name, '').strip 8 | 9 | if kite = Kite.find_by_name_and_namespace(invoker, 'System') 10 | task = Task.find_by_name_and_kite_id(task_name, kite.id) 11 | task = kite.default_task unless task 12 | return [task, query] 13 | elsif task = Task.find_by_name(invoker) 14 | return [task, query] 15 | else 16 | return nil 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the acts_as_taggable_on_steroids plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the acts_as_taggable_on_steroids plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'Acts As Taggable On Steroids' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end 23 | -------------------------------------------------------------------------------- /lib/kites/hmv.kite: -------------------------------------------------------------------------------- 1 | # Define this kite's site 2 | name 'hmv' 3 | description 'HMV.co.uk search' 4 | default 'price_search' 5 | tags 'shops' 6 | hits 5 7 | 8 | # Record URLs used by this site 9 | site do 10 | home 'http://www.hmv.co.uk' 11 | search 'http://www.hmv.co.uk/hmvweb/simpleSearch.do?pGroupID=-1&simpleSearchString=%s' 12 | end 13 | 14 | # Define tasks to be performed on this site 15 | task 'price', :args => 'query' do 16 | # All tasks should usually 'get' something. The results are stored internally for find_ operations 17 | get(site.search, query) 18 | 19 | # save adds the results to the result set, for_each loops through items found with a selector, and then searches within those results 20 | save for_each('div.results-pane', :find_first => ['h2', 'p.price']) 21 | end 22 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define :version => 0 do 2 | create_table :tags, :force => true do |t| 3 | t.column :name, :string 4 | end 5 | 6 | create_table :taggings, :force => true do |t| 7 | t.column :tag_id, :integer 8 | t.column :taggable_id, :integer 9 | t.column :taggable_type, :string 10 | t.column :created_at, :datetime 11 | end 12 | 13 | create_table :users, :force => true do |t| 14 | t.column :name, :string 15 | end 16 | 17 | create_table :posts, :force => true do |t| 18 | t.column :text, :text 19 | t.column :user_id, :integer 20 | end 21 | 22 | create_table :photos, :force => true do |t| 23 | t.column :title, :string 24 | t.column :user_id, :integer 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The production environment is meant for finished, "live" apps. 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Use a different logger for distributed setups 8 | # config.logger = SyslogLogger.new 9 | 10 | # Full error reports are disabled and caching is turned on 11 | config.action_controller.consider_all_requests_local = false 12 | config.action_controller.perform_caching = true 13 | 14 | # Enable serving of images, stylesheets, and javascripts from an asset server 15 | # config.action_controller.asset_host = "http://assets.example.com" 16 | 17 | # Disable delivery errors, bad email addresses will be ignored 18 | # config.action_mailer.raise_delivery_errors = false 19 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # In the development environment your application's code is reloaded on 4 | # every request. This slows down response time but is perfect for development 5 | # since you don't have to restart the webserver when you make code changes. 6 | config.cache_classes = false 7 | 8 | # Log error messages when you accidentally call methods on nil. 9 | config.whiny_nils = true 10 | 11 | # Show full error reports and disable caching 12 | config.action_controller.consider_all_requests_local = true 13 | config.action_controller.perform_caching = false 14 | config.action_view.cache_template_extensions = false 15 | config.action_view.debug_rjs = true 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | -------------------------------------------------------------------------------- /lib/kites/virgin.kite: -------------------------------------------------------------------------------- 1 | description 'Virgin Megastore search' 2 | name 'virgin' 3 | default 'price_search' 4 | tags 'shops' 5 | hits 5 6 | 7 | site do 8 | home 'http://www.virginmegastores.co.uk' 9 | search 'http://www.virginmegastores.co.uk/bin/venda?ex=co_wizr-xapian&threshold=50&bsref=virgin&searchfld=&searchpage=0&searchinvt=1&searchstry=0&searchlike=1&itemsperpage=10&srchopt=V%2CD&carryfields=V%2CD&SYNALL=1&V=&searchex=%s&searchsubmit.x=0&searchsubmit.y=0' 10 | end 11 | 12 | task 'price', :args => 'query' do 13 | get(site.search, query) 14 | save for_each('div.details', :find_first => ['li a', 'div.prices span.sell']) 15 | end 16 | 17 | # task 'price_search', :args => 'query' do 18 | # get(site.search, @args.query) 19 | # 20 | # find_links_to site.item do |item| 21 | # get item 22 | # collect_result([replace_tags(find_first('h1'), ' '), find_first('span.sellnew')]) 23 | # end 24 | # end -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | config.cache_classes = true 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.action_controller.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Tell ActionMailer not to deliver emails to the real world. 17 | # The :test delivery method accumulates sent emails in the 18 | # ActionMailer::Base.deliveries array. 19 | config.action_mailer.delivery_method = :test -------------------------------------------------------------------------------- /lib/kites/amazon.kite: -------------------------------------------------------------------------------- 1 | description 'Amazon search' 2 | name 'amazon' 3 | default 'price_search' 4 | tags 'shops' 5 | hits 5 6 | 7 | site do 8 | home 'http://amazon.co.uk/' 9 | search 'http://amazon.co.uk/s/ref=nb_ss_w_h_/026-3775253-3228404?url=search-alias%3Daps&field-keywords=%s&Go.x=0&Go.y=0&Go=Go' 10 | end 11 | 12 | task 'price', :args => 'query' do 13 | get(site.search, query) 14 | 15 | # Get sale price items 16 | for_each('table.searchresults td', :find_first => ['span.srTitle', 'span.saleprice']).each do |item| 17 | collect_result item if item[1] 18 | end 19 | 20 | # Get 'other' items 21 | for_each('table.searchresults td', :find_first => ['span.srTitle', 'span.otherprice']).each do |item| 22 | collect_result item if item[1] 23 | end 24 | 25 | # Get normal items 26 | for_each('table.searchresults td', :find_first => ['span.srTitle', 'span.sr_price']).each do |item| 27 | collect_result item if item[1] 28 | end 29 | end -------------------------------------------------------------------------------- /public/dispatch.fcgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # You may specify the path to the FastCGI crash log (a log of unhandled 4 | # exceptions which forced the FastCGI instance to exit, great for debugging) 5 | # and the number of requests to process before running garbage collection. 6 | # 7 | # By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log 8 | # and the GC period is nil (turned off). A reasonable number of requests 9 | # could range from 10-100 depending on the memory footprint of your app. 10 | # 11 | # Example: 12 | # # Default log path, normal GC behavior. 13 | # RailsFCGIHandler.process! 14 | # 15 | # # Default log path, 50 requests between GC. 16 | # RailsFCGIHandler.process! nil, 50 17 | # 18 | # # Custom log path, normal GC behavior. 19 | # RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' 20 | # 21 | require File.dirname(__FILE__) + "/../config/environment" 22 | require 'fcgi_handler' 23 | 24 | RailsFCGIHandler.process! 25 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ActiveRecord::Base 2 | has_many :taggings 3 | 4 | def self.parse(list) 5 | tags = [] 6 | 7 | return tags if list.blank? 8 | list = list.dup 9 | 10 | # Parse the quoted tags 11 | list.gsub!(/"(.*?)"\s*,?\s*/) { tags << $1; "" } 12 | 13 | # Strip whitespace and remove blank tags 14 | (tags + list.split(',')).map!(&:strip).delete_if(&:blank?) 15 | end 16 | 17 | # A list of all the objects tagged with this tag 18 | def tagged 19 | taggings.collect(&:taggable) 20 | end 21 | 22 | # Tag a taggable with this tag 23 | def tag(taggable) 24 | Tagging.create :tag_id => id, :taggable => taggable 25 | taggings.reset 26 | end 27 | 28 | def ==(object) 29 | super || (object.is_a?(Tag) && name == object.name) 30 | end 31 | 32 | def to_s 33 | name 34 | end 35 | 36 | def count 37 | read_attribute(:count).to_i 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | We're sorry, but something went wrong 9 | 21 | 22 | 23 | 24 | 25 |
    26 |

    We're sorry, but something went wrong.

    27 |

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

    28 |
    29 | 30 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | The page you were looking for doesn't exist (404) 9 | 21 | 22 | 23 | 24 | 25 |
    26 |

    The page you were looking for doesn't exist.

    27 |

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

    28 |
    29 | 30 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Jonathan Viney 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is autogenerated. Instead of editing this file, please use the 2 | # migrations feature of ActiveRecord to incrementally modify your database, and 3 | # then regenerate this schema definition. 4 | 5 | ActiveRecord::Schema.define(:version => 3) do 6 | 7 | create_table "kites", :force => true do |t| 8 | t.column "name", :string 9 | t.column "namespace", :string 10 | t.column "script", :text 11 | t.column "default_task_id", :integer 12 | t.column "description", :text 13 | t.column "created_at", :datetime 14 | t.column "updated_at", :datetime 15 | end 16 | 17 | create_table "taggings", :force => true do |t| 18 | t.column "tag_id", :integer 19 | t.column "taggable_id", :integer 20 | t.column "taggable_type", :string 21 | t.column "created_at", :datetime 22 | end 23 | 24 | create_table "tags", :force => true do |t| 25 | t.column "name", :string 26 | end 27 | 28 | create_table "tasks", :force => true do |t| 29 | t.column "name", :string 30 | t.column "kite_id", :integer 31 | t.column "description", :text 32 | t.column "created_at", :datetime 33 | t.column "updated_at", :datetime 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/kites/currency.kite: -------------------------------------------------------------------------------- 1 | description 'Currency conversions' 2 | name 'currency' 3 | default 'convert' 4 | tags 'money' 5 | hits 5 6 | 7 | currency_map = { 8 | 'JPY' => ['jpy', 'yen', '¥'], 9 | 'USD' => ['dollar', 'usd', 'us dollar'], 10 | 'GBP' => ['pounds sterling', 'pound sterling', '£', 'pound'] 11 | } 12 | 13 | site do 14 | currency_conversion 'http://www.ecb.int/stats/exchange/eurofxref/html/index.en.html' 15 | end 16 | 17 | task 'convert', :args => ['from_name', 'to_name', 'amount'] do 18 | currency_data = get_data 19 | 20 | from_value = currency_data.find { |data| match_currency(data[0], from_name) }[1].to_f 21 | to_value = currency_data.find { |data| match_currency(data[0], to_name) }[1].to_f 22 | 23 | save ((1 / from_value) * amount.to_f) / (1 / to_value) 24 | end 25 | 26 | task 'private get_data' do 27 | get site.currency_conversion 28 | find 'table.tablestats tbody tr' do |row| 29 | collect_result find_text(['td[@headers="aa"]', 'td[@headers="ac"]'], row) 30 | end 31 | end 32 | 33 | task 'private match_currency', :args => ['currency_name', 'query'] do 34 | match = if currency_name == query.upcase.singularize 35 | true 36 | else 37 | if currency_map.include? currency_name 38 | currency_map[currency_name].find { |currency_alias| query.singularize.downcase == currency_alias } 39 | end 40 | end 41 | 42 | save match 43 | end -------------------------------------------------------------------------------- /lib/tasks/kite.rake: -------------------------------------------------------------------------------- 1 | require RAILS_ROOT + '/config/environment' 2 | 3 | namespace :kite do 4 | desc 'Imports all standard kites' 5 | task :import do 6 | Dir.glob(RAILS_ROOT + '/lib/kites/*.kite').each do |file_name| 7 | p "Importing #{file_name}" 8 | 9 | script = File.open(file_name).read 10 | k = KiteParser.new(script) 11 | 12 | kite = if kite = Kite.find_by_name_and_namespace(k._name, 'System') 13 | kite.update_attributes(:name => k._name, 14 | :namespace => 'System', 15 | :script => script, 16 | :description => k._description) 17 | kite 18 | else 19 | Kite.create( 20 | :name => k._name, 21 | :namespace => 'System', 22 | :script => script, 23 | :description => k._description) 24 | end 25 | 26 | kite.write_tags k._tags 27 | 28 | k.tasks.each do |name, t| 29 | name = name.to_s 30 | 31 | next if t[:private] 32 | 33 | task = if task = Task.find_by_name_and_kite_id(name, kite.id) 34 | task.update_attributes(:kite => kite, :name => name) 35 | task 36 | else 37 | Task.create(:kite => kite, :name => name) 38 | end 39 | 40 | if task.name == k._default.to_s 41 | kite.update_attribute(:default_task_id, task.id) 42 | end 43 | end 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | # General Apache options 2 | AddHandler fastcgi-script .fcgi 3 | AddHandler cgi-script .cgi 4 | Options +FollowSymLinks +ExecCGI 5 | 6 | # If you don't want Rails to look in certain directories, 7 | # use the following rewrite rules so that Apache won't rewrite certain requests 8 | # 9 | # Example: 10 | # RewriteCond %{REQUEST_URI} ^/notrails.* 11 | # RewriteRule .* - [L] 12 | 13 | # Redirect all requests not available on the filesystem to Rails 14 | # By default the cgi dispatcher is used which is very slow 15 | # 16 | # For better performance replace the dispatcher with the fastcgi one 17 | # 18 | # Example: 19 | # RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] 20 | RewriteEngine On 21 | 22 | # If your Rails application is accessed via an Alias directive, 23 | # then you MUST also set the RewriteBase in this htaccess file. 24 | # 25 | # Example: 26 | # Alias /myrailsapp /path/to/myrailsapp/public 27 | # RewriteBase /myrailsapp 28 | 29 | RewriteRule ^$ index.html [QSA] 30 | RewriteRule ^([^.]+)$ $1.html [QSA] 31 | RewriteCond %{REQUEST_FILENAME} !-f 32 | RewriteRule ^(.*)$ dispatch.cgi [QSA,L] 33 | 34 | # In case Rails experiences terminal errors 35 | # Instead of displaying this message you can supply a file here which will be rendered instead 36 | # 37 | # Example: 38 | # ErrorDocument 500 /500.html 39 | 40 | ErrorDocument 500 "

    Application error

    Rails application failed to start properly" -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | 4 | # Sample of regular route: 5 | # map.connect 'products/:id', :controller => 'catalog', :action => 'view' 6 | # Keep in mind you can assign values other than :controller and :action 7 | 8 | # Sample of named route: 9 | # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' 10 | # This route can be invoked with purchase_url(:id => product.id) 11 | 12 | # You can have the root of your site routed by hooking up '' 13 | # -- just remember to delete public/index.html. 14 | # map.connect '', :controller => "welcome" 15 | 16 | map.home '', :controller => 'home' 17 | map.search_form 'search', :controller => 'search' 18 | map.search 'search/:q', :controller => 'search' 19 | 20 | map.kites 'kites', :controller => 'kite' 21 | map.kite 'kite/:name', :controller => 'kite', :action => 'view' 22 | map.kites_by_tag 'kites/:tag', :controller => 'kite', :action => 'by_tag' 23 | 24 | # Allow downloading Web Service WSDL as a file with an extension 25 | # instead of a file named 'wsdl' 26 | map.connect ':controller/service.wsdl', :action => 'wsdl' 27 | 28 | # Install the default route as the lowest priority. 29 | map.connect ':controller/:action/:id.:format' 30 | map.connect ':controller/:action/:id' 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path(File.dirname(__FILE__) + "/../config/environment") 3 | require 'test_help' 4 | 5 | class Test::Unit::TestCase 6 | # Transactional fixtures accelerate your tests by wrapping each test method 7 | # in a transaction that's rolled back on completion. This ensures that the 8 | # test database remains unchanged so your fixtures don't have to be reloaded 9 | # between every test method. Fewer database queries means faster tests. 10 | # 11 | # Read Mike Clark's excellent walkthrough at 12 | # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting 13 | # 14 | # Every Active Record database supports transactions except MyISAM tables 15 | # in MySQL. Turn off transactional fixtures in this case; however, if you 16 | # don't care one way or the other, switching from MyISAM to InnoDB tables 17 | # is recommended. 18 | self.use_transactional_fixtures = true 19 | 20 | # Instantiated fixtures are slow, but give you @david where otherwise you 21 | # would need people(:david). If you don't want to migrate your existing 22 | # test cases which use the @david style and don't mind the speed hit (each 23 | # instantiated fixtures translates to a database query per test method), 24 | # then set this back to true. 25 | self.use_instantiated_fixtures = false 26 | 27 | # Add more helper methods to be used by all tests here... 28 | end 29 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb 2 | 3 | unless defined?(RAILS_ROOT) 4 | root_path = File.join(File.dirname(__FILE__), '..') 5 | 6 | unless RUBY_PLATFORM =~ /mswin32/ 7 | require 'pathname' 8 | root_path = Pathname.new(root_path).cleanpath(true).to_s 9 | end 10 | 11 | RAILS_ROOT = root_path 12 | end 13 | 14 | unless defined?(Rails::Initializer) 15 | if File.directory?("#{RAILS_ROOT}/vendor/rails") 16 | require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" 17 | else 18 | require 'rubygems' 19 | 20 | environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join 21 | environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/ 22 | rails_gem_version = $1 23 | 24 | if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version 25 | # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems 26 | rails_gem = Gem.cache.search('rails', "~>#{version}.0").sort_by { |g| g.version.version }.last 27 | 28 | if rails_gem 29 | gem "rails", "=#{rails_gem.version.version}" 30 | require rails_gem.full_gem_path + '/lib/initializer' 31 | else 32 | STDERR.puts %(Cannot find gem for Rails ~>#{version}.0: 33 | Install the missing gem with 'gem install -v=#{version} rails', or 34 | change environment.rb to define RAILS_GEM_VERSION with your desired version. 35 | ) 36 | exit 1 37 | end 38 | else 39 | gem "rails" 40 | require 'initializer' 41 | end 42 | end 43 | 44 | Rails::Initializer.run(:set_load_path) 45 | end -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/README: -------------------------------------------------------------------------------- 1 | = acts_as_taggable_on_steroids 2 | 3 | If you find this plugin useful, please consider a donation to show your support! 4 | 5 | http://www.paypal.com/cgi-bin/webscr?cmd=_send-money 6 | 7 | Email address: jonathan.viney@gmail.com 8 | 9 | == Instructions 10 | 11 | This plugin is based on acts_as_taggable by DHH but includes extras 12 | such as tests, smarter tag assignment, and tag cloud calculations. 13 | 14 | Thanks to www.fanacious.com for allowing this plugin to be released. Please check out 15 | their site to show your support. 16 | 17 | == Resources 18 | 19 | Install 20 | * script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids 21 | 22 | == Usage 23 | 24 | === Basic tagging 25 | 26 | Using the examples from the tests, let's suppose we have users that have many posts and we want those 27 | posts to be able to be tagged by the user. 28 | 29 | As usual, we add +acts_as_taggable+ to the Post class: 30 | 31 | class Post < ActiveRecord::Base 32 | acts_as_taggable 33 | 34 | belongs_to :user 35 | end 36 | 37 | We can now use the tagging methods provided by acts_as_taggable, tag_list and tag_list=. Both these 38 | methods work like regular attribute accessors. 39 | 40 | p = Post.find(:first) 41 | p.tag_list # "" 42 | p.tag_list = "Funny, Silly" 43 | p.save 44 | p.reload.tag_list # "Funny, Silly" 45 | 46 | === Finding tagged objects 47 | 48 | To retrieve objects tagged with a certain tag, use find_tagged_with. 49 | 50 | Post.find_tagged_with('Funny, Silly') 51 | 52 | By default, find_tagged_with will find objects that have any of the given tags. To 53 | find only objects that are tagged with all the given tags, use match_all. 54 | 55 | Post.find_tagged_with('Funny, Silly', :match_all => true) 56 | 57 | === Tag cloud calculations 58 | 59 | To construct tag clouds, the frequency of each tag needs to be calculated. 60 | Because we specified +acts_as_taggable+ on the Post class, we can 61 | get a calculation of all the tag counts by using Post.tag_counts. But what if we wanted a tag count for 62 | an single user's posts? To achieve this we call tag_counts on the association: 63 | 64 | User.find(:first).posts.tag_counts 65 | 66 | 67 | Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com 68 | 69 | == Credits 70 | 71 | www.fanacious.com 72 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | begin 4 | require File.dirname(__FILE__) + '/../../../../config/boot' 5 | require 'active_record' 6 | rescue LoadError 7 | require 'rubygems' 8 | require_gem 'activerecord' 9 | end 10 | 11 | require 'active_record/fixtures' 12 | 13 | require File.dirname(__FILE__) + '/../lib/acts_as_taggable' 14 | require File.dirname(__FILE__) + '/../lib/tag' 15 | require File.dirname(__FILE__) + '/../lib/tagging' 16 | 17 | ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log') 18 | config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) 19 | ActiveRecord::Base.configurations = config 20 | ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'mysql']) 21 | 22 | load(File.dirname(__FILE__) + '/schema.rb') 23 | 24 | Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + '/fixtures/' 25 | $LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path) 26 | 27 | class Test::Unit::TestCase #:nodoc: 28 | def create_fixtures(*table_names) 29 | if block_given? 30 | Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield } 31 | else 32 | Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) 33 | end 34 | end 35 | 36 | # Turn off transactional fixtures if you're working with MyISAM tables in MySQL 37 | self.use_transactional_fixtures = true 38 | 39 | # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david) 40 | self.use_instantiated_fixtures = false 41 | 42 | def assert_equivalent(expected, actual, message = nil) 43 | if expected.first.is_a?(ActiveRecord::Base) 44 | assert_equal expected.sort_by(&:id), actual.sort_by(&:id), message 45 | else 46 | assert_equal expected.sort, actual.sort, message 47 | end 48 | end 49 | 50 | def assert_tag_counts(tags, expected_values) 51 | # Map the tag fixture names to real tag names 52 | expected_values = expected_values.inject({}) do |hash, (tag, count)| 53 | hash[tags(tag).name] = count 54 | hash 55 | end 56 | 57 | tags.each do |tag| 58 | value = expected_values.delete(tag.name) 59 | assert_not_nil value, "Expected count for #{tag.name} was not provided" if value.nil? 60 | assert_equal value, tag.count, "Expected value of #{value} for #{tag.name}, but was #{tag.count}" 61 | end 62 | 63 | unless expected_values.empty? 64 | assert false, "The following tag counts were not present: #{expected_values.inspect}" 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your web server when you modify this file. 2 | 3 | # Uncomment below to force Rails into production mode when 4 | # you don't control web/app server and can't set it the proper way 5 | # ENV['RAILS_ENV'] ||= 'production' 6 | 7 | # Specifies gem version of Rails to use when vendor/rails is not present 8 | RAILS_GEM_VERSION = '1.2.2' unless defined? RAILS_GEM_VERSION 9 | 10 | # Bootstrap the Rails environment, frameworks, and default configuration 11 | require File.join(File.dirname(__FILE__), 'boot') 12 | 13 | Rails::Initializer.run do |config| 14 | # Settings in config/environments/* take precedence over those specified here 15 | 16 | # Skip frameworks you're not going to use (only works if using vendor/rails) 17 | # config.frameworks -= [ :action_web_service, :action_mailer ] 18 | 19 | # Only load the plugins named here, by default all plugins in vendor/plugins are loaded 20 | # config.plugins = %W( exception_notification ssl_requirement ) 21 | 22 | # Add additional load paths for your own custom dirs 23 | # config.load_paths += %W( #{RAILS_ROOT}/extras ) 24 | 25 | # Force all environments to use the same logger level 26 | # (by default production uses :info, the others :debug) 27 | # config.log_level = :debug 28 | 29 | # Use the database for sessions instead of the file system 30 | # (create the session table with 'rake db:sessions:create') 31 | # config.action_controller.session_store = :active_record_store 32 | 33 | # Use SQL instead of Active Record's schema dumper when creating the test database. 34 | # This is necessary if your schema can't be completely dumped by the schema dumper, 35 | # like if you have constraints or database-specific column types 36 | # config.active_record.schema_format = :sql 37 | 38 | # Activate observers that should always be running 39 | # config.active_record.observers = :cacher, :garbage_collector 40 | 41 | # Make Active Record use UTC-base instead of local time 42 | # config.active_record.default_timezone = :utc 43 | 44 | # See Rails::Configuration for more options 45 | end 46 | 47 | # Add new inflection rules using the following format 48 | # (all these examples are active by default): 49 | # Inflector.inflections do |inflect| 50 | # inflect.plural /^(ox)$/i, '\1en' 51 | # inflect.singular /^(ox)en/i, '\1' 52 | # inflect.irregular 'person', 'people' 53 | # inflect.uncountable %w( fish sheep ) 54 | # end 55 | 56 | # Add new mime types for use in respond_to blocks: 57 | # Mime::Type.register "text/richtext", :rtf 58 | # Mime::Type.register "application/x-mobile", :mobile 59 | 60 | # Include your application configuration below -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | class TagTest < Test::Unit::TestCase 4 | fixtures :tags, :taggings, :users, :photos, :posts 5 | 6 | def test_taggings 7 | assert_equal [taggings(:jonathan_sky_good), taggings(:sam_flowers_good), taggings(:sam_flower_good)], tags(:good).taggings 8 | assert_equal [taggings(:sam_ground_bad), taggings(:jonathan_bad_cat_bad)], tags(:bad).taggings 9 | end 10 | 11 | def test_tagged 12 | assert_equal [posts(:jonathan_sky), posts(:sam_flowers), photos(:sam_flower)], tags(:good).tagged 13 | assert_equal [posts(:sam_ground), photos(:jonathan_bad_cat)], tags(:bad).tagged 14 | end 15 | 16 | def test_tag 17 | assert !tags(:good).tagged.include?(posts(:jonathan_grass)) 18 | tags(:good).tag(posts(:jonathan_grass)) 19 | assert tags(:good).tagged.include?(posts(:jonathan_grass)) 20 | end 21 | 22 | def test_parse_leaves_string_unchanged 23 | tags = '"One ", Two' 24 | original = tags.dup 25 | Tag.parse(tags) 26 | assert_equal tags, original 27 | end 28 | 29 | def test_parse_single_tag 30 | assert_equal %w{Fun}, Tag.parse("Fun") 31 | assert_equal %w{Fun}, Tag.parse('"Fun"') 32 | end 33 | 34 | def test_parse_blank 35 | assert_equal [], Tag.parse(nil) 36 | assert_equal [], Tag.parse("") 37 | end 38 | 39 | def test_parse_single_quoted_tag 40 | assert_equal ['with, comma'], Tag.parse('"with, comma"') 41 | end 42 | 43 | def test_spaces_do_not_delineate 44 | assert_equal ['A B', 'C'], Tag.parse('A B, C') 45 | end 46 | 47 | def test_parse_multiple_tags 48 | assert_equivalent %w{Alpha Beta Delta Gamma}, Tag.parse("Alpha, Beta, Gamma, Delta").sort 49 | end 50 | 51 | def test_parse_multiple_tags_with_quotes 52 | assert_equivalent %w{Alpha Beta Delta Gamma}, Tag.parse('Alpha, "Beta", Gamma , "Delta"').sort 53 | end 54 | 55 | def test_parse_multiple_tags_with_quote_and_commas 56 | assert_equivalent ['Alpha, Beta', 'Delta', 'Gamma, something'], Tag.parse('"Alpha, Beta", Delta, "Gamma, something"') 57 | end 58 | 59 | def test_parse_removes_white_space 60 | assert_equivalent %w{Alpha Beta}, Tag.parse('" Alpha ", "Beta "') 61 | assert_equivalent %w{Alpha Beta}, Tag.parse(' Alpha, Beta ') 62 | end 63 | 64 | def test_to_s 65 | assert_equal tags(:good).name, tags(:good).to_s 66 | end 67 | 68 | def test_equality 69 | assert_equal tags(:good), tags(:good) 70 | assert_equal Tag.find(1), Tag.find(1) 71 | assert_equal Tag.new(:name => 'A'), Tag.new(:name => 'A') 72 | assert_not_equal Tag.new(:name => 'A'), Tag.new(:name => 'B') 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml: -------------------------------------------------------------------------------- 1 | jonathan_sky_good: 2 | id: 1 3 | tag_id: 1 4 | taggable_id: 1 5 | taggable_type: Post 6 | created_at: 2006-08-01 7 | 8 | jonathan_sky_nature: 9 | id: 2 10 | tag_id: 3 11 | taggable_id: 1 12 | taggable_type: Post 13 | created_at: 2006-08-02 14 | 15 | jonathan_grass_nature: 16 | id: 3 17 | tag_id: 3 18 | taggable_id: 2 19 | taggable_type: Post 20 | created_at: 2006-08-03 21 | 22 | jonathan_rain_question: 23 | id: 4 24 | tag_id: 4 25 | taggable_id: 3 26 | taggable_type: Post 27 | created_at: 2006-08-04 28 | 29 | jonathan_rain_nature: 30 | id: 5 31 | tag_id: 3 32 | taggable_id: 3 33 | taggable_type: Post 34 | created_at: 2006-08-05 35 | 36 | sam_ground_nature: 37 | id: 6 38 | tag_id: 3 39 | taggable_id: 4 40 | taggable_type: Post 41 | created_at: 2006-08-06 42 | 43 | sam_ground_bad: 44 | id: 7 45 | tag_id: 2 46 | taggable_id: 4 47 | taggable_type: Post 48 | created_at: 2006-08-07 49 | 50 | sam_flowers_good: 51 | id: 8 52 | tag_id: 1 53 | taggable_id: 5 54 | taggable_type: Post 55 | created_at: 2006-08-08 56 | 57 | sam_flowers_nature: 58 | id: 9 59 | tag_id: 3 60 | taggable_id: 5 61 | taggable_type: Post 62 | created_at: 2006-08-09 63 | 64 | 65 | jonathan_dog_animal: 66 | id: 10 67 | tag_id: 5 68 | taggable_id: 1 69 | taggable_type: Photo 70 | created_at: 2006-08-10 71 | 72 | jonathan_dog_nature: 73 | id: 11 74 | tag_id: 3 75 | taggable_id: 1 76 | taggable_type: Photo 77 | created_at: 2006-08-11 78 | 79 | jonathan_questioning_dog_animal: 80 | id: 12 81 | tag_id: 5 82 | taggable_id: 2 83 | taggable_type: Photo 84 | created_at: 2006-08-12 85 | 86 | jonathan_questioning_dog_question: 87 | id: 13 88 | tag_id: 4 89 | taggable_id: 2 90 | taggable_type: Photo 91 | created_at: 2006-08-13 92 | 93 | jonathan_bad_cat_bad: 94 | id: 14 95 | tag_id: 2 96 | taggable_id: 3 97 | taggable_type: Photo 98 | created_at: 2006-08-14 99 | 100 | jonathan_bad_cat_animal: 101 | id: 15 102 | tag_id: 5 103 | taggable_id: 3 104 | taggable_type: Photo 105 | created_at: 2006-08-15 106 | 107 | sam_flower_nature: 108 | id: 16 109 | tag_id: 3 110 | taggable_id: 4 111 | taggable_type: Photo 112 | created_at: 2006-08-16 113 | 114 | sam_flower_good: 115 | id: 17 116 | tag_id: 1 117 | taggable_id: 4 118 | taggable_type: Photo 119 | created_at: 2006-08-17 120 | 121 | sam_sky_nature: 122 | id: 18 123 | tag_id: 3 124 | taggable_id: 5 125 | taggable_type: Photo 126 | created_at: 2006-08-18 127 | -------------------------------------------------------------------------------- /lib/tasks/capistrano.rake: -------------------------------------------------------------------------------- 1 | # ============================================================================= 2 | # A set of rake tasks for invoking the Capistrano automation utility. 3 | # ============================================================================= 4 | 5 | # Invoke the given actions via Capistrano 6 | def cap(*parameters) 7 | begin 8 | require 'rubygems' 9 | rescue LoadError 10 | # no rubygems to load, so we fail silently 11 | end 12 | 13 | require 'capistrano/cli' 14 | 15 | STDERR.puts "Capistrano/Rake integration is deprecated." 16 | STDERR.puts "Please invoke the 'cap' command directly: `cap #{parameters.join(" ")}'" 17 | 18 | Capistrano::CLI.new(parameters.map { |param| param.to_s }).execute! 19 | end 20 | 21 | namespace :remote do 22 | desc "Removes unused releases from the releases directory." 23 | task(:cleanup) { cap :cleanup } 24 | 25 | desc "Used only for deploying when the spinner isn't running." 26 | task(:cold_deploy) { cap :cold_deploy } 27 | 28 | desc "A macro-task that updates the code, fixes the symlink, and restarts the application servers." 29 | task(:deploy) { cap :deploy } 30 | 31 | desc "Similar to deploy, but it runs the migrate task on the new release before updating the symlink." 32 | task(:deploy_with_migrations) { cap :deploy_with_migrations } 33 | 34 | desc "Displays the diff between HEAD and what was last deployed." 35 | task(:diff_from_last_deploy) { cap :diff_from_last_deploy } 36 | 37 | desc "Disable the web server by writing a \"maintenance.html\" file to the web servers." 38 | task(:disable_web) { cap :disable_web } 39 | 40 | desc "Re-enable the web server by deleting any \"maintenance.html\" file." 41 | task(:enable_web) { cap :enable_web } 42 | 43 | desc "A simple task for performing one-off commands that may not require a full task to be written for them." 44 | task(:invoke) { cap :invoke } 45 | 46 | desc "Run the migrate rake task." 47 | task(:migrate) { cap :migrate } 48 | 49 | desc "Restart the FCGI processes on the app server." 50 | task(:restart) { cap :restart } 51 | 52 | desc "A macro-task that rolls back the code and restarts the application servers." 53 | task(:rollback) { cap :rollback } 54 | 55 | desc "Rollback the latest checked-out version to the previous one by fixing the symlinks and deleting the current release from all servers." 56 | task(:rollback_code) { cap :rollback_code } 57 | 58 | desc "Sets group permissions on checkout." 59 | task(:set_permissions) { cap :set_permissions } 60 | 61 | desc "Set up the expected application directory structure on all boxes" 62 | task(:setup) { cap :setup } 63 | 64 | desc "Begin an interactive Capistrano session." 65 | task(:shell) { cap :shell } 66 | 67 | desc "Enumerate and describe every available task." 68 | task(:show_tasks) { cap :show_tasks, '-q' } 69 | 70 | desc "Start the spinner daemon for the application (requires script/spin)." 71 | task(:spinner) { cap :spinner } 72 | 73 | desc "Update the 'current' symlink to point to the latest version of the application's code." 74 | task(:symlink) { cap :symlink } 75 | 76 | desc "Updates the code and fixes the symlink under a transaction" 77 | task(:update) { cap :update } 78 | 79 | desc "Update all servers with the latest release of the source code." 80 | task(:update_code) { cap :update_code } 81 | 82 | desc "Update the currently released version of the software directly via an SCM update operation" 83 | task(:update_current) { cap :update_current } 84 | 85 | desc "Execute a specific action using capistrano" 86 | task :exec do 87 | unless ENV['ACTION'] 88 | raise "Please specify an action (or comma separated list of actions) via the ACTION environment variable" 89 | end 90 | 91 | actions = ENV['ACTION'].split(",") 92 | actions.concat(ENV['PARAMS'].split(" ")) if ENV['PARAMS'] 93 | 94 | cap(*actions) 95 | end 96 | end 97 | 98 | desc "Push the latest revision into production (delegates to remote:deploy)" 99 | task :deploy => "remote:deploy" 100 | 101 | desc "Rollback to the release before the current release in production (delegates to remote:rollback)" 102 | task :rollback => "remote:rollback" 103 | -------------------------------------------------------------------------------- /app/models/kite_parser.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'hpricot' 3 | require 'ostruct' 4 | 5 | class KiteParser 6 | attr_accessor :_description, :_name, :_default, :_tags, :_hits 7 | attr_reader :site, :data, :tasks 8 | 9 | def initialize(script = nil) 10 | @hits = 10 11 | @data = '' 12 | @tasks = {} 13 | @results = [] 14 | @trace = [] 15 | @site = OpenStruct.new() 16 | @args = {} 17 | load(script) if script 18 | end 19 | 20 | def load(script) 21 | instance_eval script 22 | end 23 | 24 | def task(name, options = {}, &block) 25 | method_name = name.to_s.gsub(/^private\s+/, '').to_sym 26 | 27 | @tasks[method_name] = {} 28 | @tasks[method_name][:args] = options[:args] if options[:args] 29 | @tasks[method_name][:private] = name.match(/^private/) ? true : false 30 | 31 | define_method(method_name) do |*args| 32 | @results = [] 33 | @trace.push current_method 34 | 35 | @tasks[method_name][:args].each_with_index do |param_name, index| 36 | unless @args[method_name] 37 | klass = Struct.new(*@tasks[method_name][:args].collect { |p| p.to_sym }) unless @args[method_name] 38 | @args[method_name] = klass.new 39 | end 40 | @args[method_name].send(param_name + '=', args[index]) 41 | end 42 | 43 | instance_eval &block 44 | 45 | @trace.pop 46 | @results 47 | end 48 | end 49 | 50 | def params(sym) 51 | @args[@current_method].send(sym) 52 | end 53 | 54 | def site(&block) 55 | if block 56 | @defining_site = true 57 | instance_eval &block 58 | @defining_site = false 59 | end 60 | return @site 61 | end 62 | 63 | def get(url, *args) 64 | url = insert_url_params(url, args.first) if args.first 65 | @data = Hpricot open(url) 66 | end 67 | 68 | # This works as an iterator when a block is passed 69 | def find_links_to(url, &block) 70 | url, selector = if url.kind_of? Array 71 | [url[1], url[0]] 72 | else 73 | [url, 'a'] 74 | end 75 | 76 | links = (@data/selector).find_all do |link| 77 | link[:href].match(strip_url_params(url)) if link and link[:href] 78 | end 79 | 80 | links = links.compact.collect { |link| [link[:href], link.inner_text] }.uniq[0..@_hits - 1] 81 | 82 | links.collect do |link| 83 | yield link[0] 84 | end 85 | end 86 | 87 | def for_each(selector, options = {}, &block) 88 | results = [] 89 | 90 | (@data/selector).collect do |item| 91 | items = if options[:find_first].kind_of? Array 92 | options[:find_first].collect do |sub_selector| 93 | result = find_first(sub_selector, item) 94 | replace_tags result if result 95 | end.compact 96 | elsif options[:find_first] 97 | find_first(options[:find_first], item) 98 | else 99 | replace_tags item.inner_text 100 | end 101 | 102 | items.empty? ? nil : items 103 | end.compact.uniq[0..@_hits - 1] 104 | end 105 | 106 | # Searches the last page fetched with get and returns html in tags using Hpricot 107 | def find_first(selector, data) 108 | data = @data unless data 109 | results = (data/selector) 110 | if results and results.first 111 | results.first.inner_html.strip 112 | else 113 | nil 114 | end 115 | end 116 | 117 | def find(selector, data = nil, &block) 118 | data = @data unless data 119 | 120 | if selector.kind_of? Array 121 | return selector.collect { |s| find(s, data, &block) } 122 | end 123 | 124 | (data/selector).collect do |elem| 125 | if block 126 | yield elem 127 | else 128 | elem.inner_text 129 | end 130 | end 131 | end 132 | 133 | def find_text(selector, data = nil, &block) 134 | data = @data unless data 135 | 136 | if selector.kind_of? Array 137 | return selector.collect { |s| find_text(s, data, &block).first } 138 | end 139 | 140 | (data/selector).collect do |elem| 141 | if block 142 | yield elem.inner_text 143 | else 144 | elem.inner_text 145 | end 146 | end 147 | end 148 | 149 | def collect_result(data) 150 | @results.push data 151 | end 152 | 153 | def save(data) 154 | @results = data 155 | end 156 | 157 | def replace_tags(text, replace = '') 158 | text.gsub(/(<[^>]+?>| )/, replace).strip.gsub(/[\s\t]+/, ' ').gsub(/\243/, '£') 159 | end 160 | 161 | private 162 | 163 | def metaclass 164 | class << self; self; end 165 | end 166 | 167 | def method_missing(sym, *args) 168 | if @defining_site 169 | @site.send(sym.to_s + '=', args[0]) 170 | elsif [:description, :name, :default, :tags, :hits].include? sym 171 | send('_' + sym.to_s + '=', args[0]) 172 | elsif respond_to? sym 173 | send(sym, args) 174 | elsif @args[@trace.last] and @args[@trace.last].members.include? sym.to_s 175 | return @args[@trace.last].send(sym) 176 | else 177 | p @args[@trace.last].members 178 | p sym.to_s + ' not found' 179 | super 180 | end 181 | end 182 | 183 | def define_method(name, &block) 184 | metaclass.send(:define_method, name, &block) 185 | end 186 | 187 | def strip_url_params(url) 188 | url.gsub(/%s/, '') 189 | end 190 | 191 | def insert_url_params(url, *args) 192 | args.each do |arg| 193 | arg ||= '' 194 | url.gsub!(/%s/, CGI.escape(arg)) 195 | end 196 | 197 | return url 198 | end 199 | 200 | def current_method 201 | caller[0].sub(/.*`([^']+)'/, '\1').to_sym 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Acts #:nodoc: 3 | module Taggable #:nodoc: 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | end 7 | 8 | module ClassMethods 9 | def acts_as_taggable(options = {}) 10 | has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag 11 | has_many :tags, :through => :taggings 12 | 13 | after_save :save_tags 14 | 15 | include ActiveRecord::Acts::Taggable::InstanceMethods 16 | extend ActiveRecord::Acts::Taggable::SingletonMethods 17 | 18 | alias_method :reload_without_tag_list, :reload 19 | alias_method :reload, :reload_with_tag_list 20 | end 21 | end 22 | 23 | module SingletonMethods 24 | # Pass either a tag string, or an array of strings or tags 25 | def find_tagged_with(tags, options = {}) 26 | tags = Tag.parse(tags) if tags.is_a?(String) 27 | return [] if tags.empty? 28 | tags.map!(&:to_s) 29 | 30 | conditions = sanitize_sql(['tags.name IN (?)', tags]) 31 | conditions << " AND #{sanitize_sql(options.delete(:conditions))}" if options[:conditions] 32 | 33 | group = "taggings.taggable_id HAVING COUNT(taggings.taggable_id) = #{tags.size}" if options.delete(:match_all) 34 | 35 | find(:all, { :select => "DISTINCT #{table_name}.*", 36 | :joins => "LEFT OUTER JOIN taggings ON taggings.taggable_id = #{table_name}.#{primary_key} AND taggings.taggable_type = '#{name}' " + 37 | "LEFT OUTER JOIN tags ON tags.id = taggings.tag_id", 38 | :conditions => conditions, 39 | :group => group }.merge(options)) 40 | end 41 | 42 | # Options: 43 | # :start_at - Restrict the tags to those created after a certain time 44 | # :end_at - Restrict the tags to those created before a certain time 45 | # :conditions - A piece of SQL conditions to add to the query 46 | # :limit - The maximum number of tags to return 47 | # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' 48 | # :at_least - Exclude tags with a frequency less than the given value 49 | # :at_most - Exclude tags with a frequency greater then the given value 50 | def tag_counts(options = {}) 51 | options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit 52 | 53 | scope = scope(:find) 54 | start_at = sanitize_sql(['taggings.created_at >= ?', options[:start_at]]) if options[:start_at] 55 | end_at = sanitize_sql(['taggings.created_at <= ?', options[:end_at]]) if options[:end_at] 56 | 57 | conditions = [ 58 | "taggings.taggable_type = '#{name}'", 59 | options[:conditions], 60 | scope && scope[:conditions], 61 | start_at, 62 | end_at 63 | ] 64 | conditions = conditions.compact.join(' and ') 65 | 66 | at_least = sanitize_sql(['count >= ?', options[:at_least]]) if options[:at_least] 67 | at_most = sanitize_sql(['count <= ?', options[:at_most]]) if options[:at_most] 68 | having = [at_least, at_most].compact.join(' and ') 69 | group_by = 'tags.id having count(*) > 0' 70 | group_by << " and #{having}" unless having.blank? 71 | 72 | Tag.find(:all, 73 | :select => 'tags.id, tags.name, COUNT(*) AS count', 74 | :joins => "LEFT OUTER JOIN taggings ON tags.id = taggings.tag_id LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = taggings.taggable_id", 75 | :conditions => conditions, 76 | :group => group_by, 77 | :order => options[:order], 78 | :limit => options[:limit] 79 | ) 80 | end 81 | end 82 | 83 | module InstanceMethods 84 | attr_writer :tag_list 85 | 86 | def tag_list 87 | defined?(@tag_list) ? @tag_list : read_tags 88 | end 89 | 90 | def save_tags 91 | if defined?(@tag_list) 92 | write_tags(@tag_list) 93 | remove_tag_list 94 | end 95 | end 96 | 97 | def write_tags(list) 98 | new_tag_names = Tag.parse(list).uniq 99 | old_tagging_ids = [] 100 | 101 | Tag.transaction do 102 | taggings.each do |tagging| 103 | index = new_tag_names.index(tagging.tag.name) 104 | index ? new_tag_names.delete_at(index) : old_tagging_ids << tagging.id 105 | end 106 | 107 | Tagging.delete_all(['id in (?)', old_tagging_ids]) if old_tagging_ids.any? 108 | 109 | # Create any new tags/taggings 110 | new_tag_names.each do |name| 111 | Tag.find_or_create_by_name(name).tag(self) 112 | end 113 | 114 | taggings.reset 115 | tags.reset 116 | end 117 | true 118 | end 119 | 120 | def read_tags 121 | tags.collect do |tag| 122 | tag.name.include?(',') ? "\"#{tag.name}\"" : tag.name 123 | end.join(', ') 124 | end 125 | 126 | def reload_with_tag_list(*args) 127 | remove_tag_list 128 | reload_without_tag_list(*args) 129 | end 130 | 131 | private 132 | def remove_tag_list 133 | remove_instance_variable(:@tag_list) if defined?(@tag_list) 134 | end 135 | end 136 | end 137 | end 138 | end 139 | 140 | ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable) 141 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | class ActsAsTaggableOnSteroidsTest < Test::Unit::TestCase 4 | fixtures :tags, :taggings, :posts, :users, :photos 5 | 6 | def test_find_tagged_with 7 | assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with('"Very good"') 8 | assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with(['Very good']) 9 | assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with([tags(:good)]) 10 | 11 | assert_equivalent [photos(:jonathan_dog), photos(:sam_flower), photos(:sam_sky)], Photo.find_tagged_with('Nature') 12 | assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with(['Nature']) 13 | assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with([tags(:nature)]) 14 | 15 | assert_equivalent [photos(:jonathan_bad_cat), photos(:jonathan_dog), photos(:jonathan_questioning_dog)], Photo.find_tagged_with('"Crazy animal" Bad') 16 | assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with(['Crazy animal', 'Bad']) 17 | assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with([tags(:animal), tags(:bad)]) 18 | end 19 | 20 | def test_find_tagged_with_nonexistant_tags 21 | assert_equal [], Post.find_tagged_with('ABCDEFG') 22 | assert_equal [], Photo.find_tagged_with(['HIJKLM']) 23 | assert_equal [], Photo.find_tagged_with([Tag.new(:name => 'unsaved tag')]) 24 | end 25 | 26 | def test_find_tagged_with_matching_all_tags 27 | assert_equivalent [photos(:jonathan_dog)], Photo.find_tagged_with('Crazy animal, "Nature"', :match_all => true) 28 | assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with(['Very good', 'Nature'], :match_all => true) 29 | end 30 | 31 | def test_basic_tag_counts_on_class 32 | assert_tag_counts Post.tag_counts, :good => 2, :nature => 5, :question => 1, :bad => 1 33 | assert_tag_counts Photo.tag_counts, :good => 1, :nature => 3, :question => 1, :bad => 1, :animal => 3 34 | end 35 | 36 | def test_tag_counts_on_class_with_date_conditions 37 | assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 4)), :good => 1, :nature => 3, :question => 1, :bad => 1 38 | assert_tag_counts Post.tag_counts(:end_at => Date.new(2006, 8, 6)), :good => 1, :nature => 4, :question => 1 39 | assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 5), :end_at => Date.new(2006, 8, 8)), :good => 1, :nature => 2, :bad => 1 40 | 41 | assert_tag_counts Photo.tag_counts(:start_at => Date.new(2006, 8, 12), :end_at => Date.new(2006, 8, 17)), :good => 1, :nature => 1, :bad => 1, :question => 1, :animal => 2 42 | end 43 | 44 | def test_tag_counts_on_class_with_frequencies 45 | assert_tag_counts Photo.tag_counts(:at_least => 2), :nature => 3, :animal => 3 46 | assert_tag_counts Photo.tag_counts(:at_most => 2), :good => 1, :question => 1, :bad => 1 47 | end 48 | 49 | def test_tag_counts_with_limit 50 | assert_equal 2, Photo.tag_counts(:limit => 2).size 51 | assert_equal 1, Post.tag_counts(:at_least => 4, :limit => 2).size 52 | end 53 | 54 | def test_tag_counts_with_limit_and_order 55 | assert_equal [tags(:nature), tags(:good)], Post.tag_counts(:order => 'count desc', :limit => 2) 56 | end 57 | 58 | def test_tag_counts_on_association 59 | assert_tag_counts users(:jonathan).posts.tag_counts, :good => 1, :nature => 3, :question => 1 60 | assert_tag_counts users(:sam).posts.tag_counts, :good => 1, :nature => 2, :bad => 1 61 | 62 | assert_tag_counts users(:jonathan).photos.tag_counts, :animal => 3, :nature => 1, :question => 1, :bad => 1 63 | assert_tag_counts users(:sam).photos.tag_counts, :nature => 2, :good => 1 64 | end 65 | 66 | def test_tag_counts_on_association_with_options 67 | assert_equal [], users(:jonathan).posts.tag_counts(:conditions => '1=0') 68 | assert_tag_counts users(:jonathan).posts.tag_counts(:at_most => 2), :good => 1, :question => 1 69 | end 70 | 71 | def test_tag_list 72 | assert_equivalent Tag.parse('"Very good", Nature'), Tag.parse(posts(:jonathan_sky).tag_list) 73 | assert_equivalent Tag.parse('Bad, "Crazy animal"'), Tag.parse(photos(:jonathan_bad_cat).tag_list) 74 | end 75 | 76 | def test_reassign_tag_list 77 | assert_equivalent Tag.parse('Nature, Question'), Tag.parse(posts(:jonathan_rain).tag_list) 78 | assert posts(:jonathan_rain).update_attributes(:tag_list => posts(:jonathan_rain).tag_list) 79 | assert_equivalent Tag.parse('Nature, Question'), Tag.parse(posts(:jonathan_rain).tag_list) 80 | end 81 | 82 | def test_assign_new_tags 83 | assert_equivalent Tag.parse('"Very good", Nature'), Tag.parse(posts(:jonathan_sky).tag_list) 84 | assert posts(:jonathan_sky).update_attributes(:tag_list => "#{posts(:jonathan_sky).tag_list}, One, Two") 85 | assert_equivalent Tag.parse('"Very good", Nature, One, Two'), Tag.parse(posts(:jonathan_sky).tag_list) 86 | end 87 | 88 | def test_duplicate_tags_ignored 89 | assert posts(:jonathan_sky).update_attributes(:tag_list => "Test, Test") 90 | assert_equal "Test", posts(:jonathan_sky).tag_list 91 | assert posts(:jonathan_sky).update_attributes(:tag_list => "Test, Test, Test") 92 | assert_equal "Test", posts(:jonathan_sky).reload.tag_list 93 | end 94 | 95 | def test_remove_tag 96 | assert_equivalent Tag.parse('"Very good", Nature'), Tag.parse(posts(:jonathan_sky).tag_list) 97 | assert posts(:jonathan_sky).update_attributes(:tag_list => "Nature") 98 | assert_equivalent Tag.parse('Nature'), Tag.parse(posts(:jonathan_sky).tag_list) 99 | end 100 | 101 | def test_remove_and_add_tag 102 | assert_equivalent Tag.parse('"Very good", Nature'), Tag.parse(posts(:jonathan_sky).tag_list) 103 | assert posts(:jonathan_sky).update_attributes(:tag_list => "Nature, Beautiful") 104 | assert_equivalent Tag.parse('Nature, Beautiful'), Tag.parse(posts(:jonathan_sky).tag_list) 105 | end 106 | 107 | def test_tags_not_saved_if_validation_fails 108 | assert_equivalent Tag.parse('"Very good", Nature'), Tag.parse(posts(:jonathan_sky).tag_list) 109 | assert !posts(:jonathan_sky).update_attributes(:tag_list => "One Two", :text => "") 110 | assert_equivalent Tag.parse('"Very good", Nature'), Tag.parse(Post.find(posts(:jonathan_sky).id).tag_list) 111 | end 112 | 113 | def test_tag_list_accessors_on_new_record 114 | p = Post.new(:text => 'Test') 115 | 116 | assert_equal "", p.tag_list 117 | p.tag_list = "One, Two" 118 | assert_equal "One, Two", p.tag_list 119 | end 120 | 121 | def test_read_tag_list_with_commas 122 | assert ["Question, Crazy animal", "Crazy animal, Question"].include?(photos(:jonathan_questioning_dog).tag_list) 123 | end 124 | 125 | def test_clear_tag_list_with_nil 126 | p = photos(:jonathan_questioning_dog) 127 | 128 | assert !p.tag_list.blank? 129 | assert p.update_attributes(:tag_list => nil) 130 | assert p.tag_list.blank? 131 | 132 | assert p.reload.tag_list.blank? 133 | assert Photo.find(p.id).tag_list.blank? 134 | end 135 | 136 | def test_clear_tag_list_with_string 137 | p = photos(:jonathan_questioning_dog) 138 | 139 | assert !p.tag_list.blank? 140 | assert p.update_attributes(:tag_list => ' ') 141 | assert p.tag_list.blank? 142 | 143 | assert p.reload.tag_list.blank? 144 | assert Photo.find(p.id).tag_list.blank? 145 | end 146 | 147 | def test_tag_list_reset_on_reload 148 | p = photos(:jonathan_questioning_dog) 149 | assert !p.tag_list.blank? 150 | p.tag_list = nil 151 | assert p.tag_list.blank? 152 | assert !p.reload.tag_list.blank? 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /public/javascripts/controls.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 2 | // (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan) 3 | // (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com) 4 | // Contributors: 5 | // Richard Livsey 6 | // Rahul Bhargava 7 | // Rob Wills 8 | // 9 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 10 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 11 | 12 | // Autocompleter.Base handles all the autocompletion functionality 13 | // that's independent of the data source for autocompletion. This 14 | // includes drawing the autocompletion menu, observing keyboard 15 | // and mouse events, and similar. 16 | // 17 | // Specific autocompleters need to provide, at the very least, 18 | // a getUpdatedChoices function that will be invoked every time 19 | // the text inside the monitored textbox changes. This method 20 | // should get the text for which to provide autocompletion by 21 | // invoking this.getToken(), NOT by directly accessing 22 | // this.element.value. This is to allow incremental tokenized 23 | // autocompletion. Specific auto-completion logic (AJAX, etc) 24 | // belongs in getUpdatedChoices. 25 | // 26 | // Tokenized incremental autocompletion is enabled automatically 27 | // when an autocompleter is instantiated with the 'tokens' option 28 | // in the options parameter, e.g.: 29 | // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); 30 | // will incrementally autocomplete with a comma as the token. 31 | // Additionally, ',' in the above example can be replaced with 32 | // a token array, e.g. { tokens: [',', '\n'] } which 33 | // enables autocompletion on multiple tokens. This is most 34 | // useful when one of the tokens is \n (a newline), as it 35 | // allows smart autocompletion after linebreaks. 36 | 37 | if(typeof Effect == 'undefined') 38 | throw("controls.js requires including script.aculo.us' effects.js library"); 39 | 40 | var Autocompleter = {} 41 | Autocompleter.Base = function() {}; 42 | Autocompleter.Base.prototype = { 43 | baseInitialize: function(element, update, options) { 44 | this.element = $(element); 45 | this.update = $(update); 46 | this.hasFocus = false; 47 | this.changed = false; 48 | this.active = false; 49 | this.index = 0; 50 | this.entryCount = 0; 51 | 52 | if(this.setOptions) 53 | this.setOptions(options); 54 | else 55 | this.options = options || {}; 56 | 57 | this.options.paramName = this.options.paramName || this.element.name; 58 | this.options.tokens = this.options.tokens || []; 59 | this.options.frequency = this.options.frequency || 0.4; 60 | this.options.minChars = this.options.minChars || 1; 61 | this.options.onShow = this.options.onShow || 62 | function(element, update){ 63 | if(!update.style.position || update.style.position=='absolute') { 64 | update.style.position = 'absolute'; 65 | Position.clone(element, update, { 66 | setHeight: false, 67 | offsetTop: element.offsetHeight 68 | }); 69 | } 70 | Effect.Appear(update,{duration:0.15}); 71 | }; 72 | this.options.onHide = this.options.onHide || 73 | function(element, update){ new Effect.Fade(update,{duration:0.15}) }; 74 | 75 | if(typeof(this.options.tokens) == 'string') 76 | this.options.tokens = new Array(this.options.tokens); 77 | 78 | this.observer = null; 79 | 80 | this.element.setAttribute('autocomplete','off'); 81 | 82 | Element.hide(this.update); 83 | 84 | Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); 85 | Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); 86 | }, 87 | 88 | show: function() { 89 | if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); 90 | if(!this.iefix && 91 | (navigator.appVersion.indexOf('MSIE')>0) && 92 | (navigator.userAgent.indexOf('Opera')<0) && 93 | (Element.getStyle(this.update, 'position')=='absolute')) { 94 | new Insertion.After(this.update, 95 | ''); 98 | this.iefix = $(this.update.id+'_iefix'); 99 | } 100 | if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); 101 | }, 102 | 103 | fixIEOverlapping: function() { 104 | Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); 105 | this.iefix.style.zIndex = 1; 106 | this.update.style.zIndex = 2; 107 | Element.show(this.iefix); 108 | }, 109 | 110 | hide: function() { 111 | this.stopIndicator(); 112 | if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); 113 | if(this.iefix) Element.hide(this.iefix); 114 | }, 115 | 116 | startIndicator: function() { 117 | if(this.options.indicator) Element.show(this.options.indicator); 118 | }, 119 | 120 | stopIndicator: function() { 121 | if(this.options.indicator) Element.hide(this.options.indicator); 122 | }, 123 | 124 | onKeyPress: function(event) { 125 | if(this.active) 126 | switch(event.keyCode) { 127 | case Event.KEY_TAB: 128 | case Event.KEY_RETURN: 129 | this.selectEntry(); 130 | Event.stop(event); 131 | case Event.KEY_ESC: 132 | this.hide(); 133 | this.active = false; 134 | Event.stop(event); 135 | return; 136 | case Event.KEY_LEFT: 137 | case Event.KEY_RIGHT: 138 | return; 139 | case Event.KEY_UP: 140 | this.markPrevious(); 141 | this.render(); 142 | if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); 143 | return; 144 | case Event.KEY_DOWN: 145 | this.markNext(); 146 | this.render(); 147 | if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); 148 | return; 149 | } 150 | else 151 | if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 152 | (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; 153 | 154 | this.changed = true; 155 | this.hasFocus = true; 156 | 157 | if(this.observer) clearTimeout(this.observer); 158 | this.observer = 159 | setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 160 | }, 161 | 162 | activate: function() { 163 | this.changed = false; 164 | this.hasFocus = true; 165 | this.getUpdatedChoices(); 166 | }, 167 | 168 | onHover: function(event) { 169 | var element = Event.findElement(event, 'LI'); 170 | if(this.index != element.autocompleteIndex) 171 | { 172 | this.index = element.autocompleteIndex; 173 | this.render(); 174 | } 175 | Event.stop(event); 176 | }, 177 | 178 | onClick: function(event) { 179 | var element = Event.findElement(event, 'LI'); 180 | this.index = element.autocompleteIndex; 181 | this.selectEntry(); 182 | this.hide(); 183 | }, 184 | 185 | onBlur: function(event) { 186 | // needed to make click events working 187 | setTimeout(this.hide.bind(this), 250); 188 | this.hasFocus = false; 189 | this.active = false; 190 | }, 191 | 192 | render: function() { 193 | if(this.entryCount > 0) { 194 | for (var i = 0; i < this.entryCount; i++) 195 | this.index==i ? 196 | Element.addClassName(this.getEntry(i),"selected") : 197 | Element.removeClassName(this.getEntry(i),"selected"); 198 | 199 | if(this.hasFocus) { 200 | this.show(); 201 | this.active = true; 202 | } 203 | } else { 204 | this.active = false; 205 | this.hide(); 206 | } 207 | }, 208 | 209 | markPrevious: function() { 210 | if(this.index > 0) this.index-- 211 | else this.index = this.entryCount-1; 212 | this.getEntry(this.index).scrollIntoView(true); 213 | }, 214 | 215 | markNext: function() { 216 | if(this.index < this.entryCount-1) this.index++ 217 | else this.index = 0; 218 | this.getEntry(this.index).scrollIntoView(false); 219 | }, 220 | 221 | getEntry: function(index) { 222 | return this.update.firstChild.childNodes[index]; 223 | }, 224 | 225 | getCurrentEntry: function() { 226 | return this.getEntry(this.index); 227 | }, 228 | 229 | selectEntry: function() { 230 | this.active = false; 231 | this.updateElement(this.getCurrentEntry()); 232 | }, 233 | 234 | updateElement: function(selectedElement) { 235 | if (this.options.updateElement) { 236 | this.options.updateElement(selectedElement); 237 | return; 238 | } 239 | var value = ''; 240 | if (this.options.select) { 241 | var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; 242 | if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); 243 | } else 244 | value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); 245 | 246 | var lastTokenPos = this.findLastToken(); 247 | if (lastTokenPos != -1) { 248 | var newValue = this.element.value.substr(0, lastTokenPos + 1); 249 | var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); 250 | if (whitespace) 251 | newValue += whitespace[0]; 252 | this.element.value = newValue + value; 253 | } else { 254 | this.element.value = value; 255 | } 256 | this.element.focus(); 257 | 258 | if (this.options.afterUpdateElement) 259 | this.options.afterUpdateElement(this.element, selectedElement); 260 | }, 261 | 262 | updateChoices: function(choices) { 263 | if(!this.changed && this.hasFocus) { 264 | this.update.innerHTML = choices; 265 | Element.cleanWhitespace(this.update); 266 | Element.cleanWhitespace(this.update.down()); 267 | 268 | if(this.update.firstChild && this.update.down().childNodes) { 269 | this.entryCount = 270 | this.update.down().childNodes.length; 271 | for (var i = 0; i < this.entryCount; i++) { 272 | var entry = this.getEntry(i); 273 | entry.autocompleteIndex = i; 274 | this.addObservers(entry); 275 | } 276 | } else { 277 | this.entryCount = 0; 278 | } 279 | 280 | this.stopIndicator(); 281 | this.index = 0; 282 | 283 | if(this.entryCount==1 && this.options.autoSelect) { 284 | this.selectEntry(); 285 | this.hide(); 286 | } else { 287 | this.render(); 288 | } 289 | } 290 | }, 291 | 292 | addObservers: function(element) { 293 | Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); 294 | Event.observe(element, "click", this.onClick.bindAsEventListener(this)); 295 | }, 296 | 297 | onObserverEvent: function() { 298 | this.changed = false; 299 | if(this.getToken().length>=this.options.minChars) { 300 | this.startIndicator(); 301 | this.getUpdatedChoices(); 302 | } else { 303 | this.active = false; 304 | this.hide(); 305 | } 306 | }, 307 | 308 | getToken: function() { 309 | var tokenPos = this.findLastToken(); 310 | if (tokenPos != -1) 311 | var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); 312 | else 313 | var ret = this.element.value; 314 | 315 | return /\n/.test(ret) ? '' : ret; 316 | }, 317 | 318 | findLastToken: function() { 319 | var lastTokenPos = -1; 320 | 321 | for (var i=0; i lastTokenPos) 324 | lastTokenPos = thisTokenPos; 325 | } 326 | return lastTokenPos; 327 | } 328 | } 329 | 330 | Ajax.Autocompleter = Class.create(); 331 | Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { 332 | initialize: function(element, update, url, options) { 333 | this.baseInitialize(element, update, options); 334 | this.options.asynchronous = true; 335 | this.options.onComplete = this.onComplete.bind(this); 336 | this.options.defaultParams = this.options.parameters || null; 337 | this.url = url; 338 | }, 339 | 340 | getUpdatedChoices: function() { 341 | entry = encodeURIComponent(this.options.paramName) + '=' + 342 | encodeURIComponent(this.getToken()); 343 | 344 | this.options.parameters = this.options.callback ? 345 | this.options.callback(this.element, entry) : entry; 346 | 347 | if(this.options.defaultParams) 348 | this.options.parameters += '&' + this.options.defaultParams; 349 | 350 | new Ajax.Request(this.url, this.options); 351 | }, 352 | 353 | onComplete: function(request) { 354 | this.updateChoices(request.responseText); 355 | } 356 | 357 | }); 358 | 359 | // The local array autocompleter. Used when you'd prefer to 360 | // inject an array of autocompletion options into the page, rather 361 | // than sending out Ajax queries, which can be quite slow sometimes. 362 | // 363 | // The constructor takes four parameters. The first two are, as usual, 364 | // the id of the monitored textbox, and id of the autocompletion menu. 365 | // The third is the array you want to autocomplete from, and the fourth 366 | // is the options block. 367 | // 368 | // Extra local autocompletion options: 369 | // - choices - How many autocompletion choices to offer 370 | // 371 | // - partialSearch - If false, the autocompleter will match entered 372 | // text only at the beginning of strings in the 373 | // autocomplete array. Defaults to true, which will 374 | // match text at the beginning of any *word* in the 375 | // strings in the autocomplete array. If you want to 376 | // search anywhere in the string, additionally set 377 | // the option fullSearch to true (default: off). 378 | // 379 | // - fullSsearch - Search anywhere in autocomplete array strings. 380 | // 381 | // - partialChars - How many characters to enter before triggering 382 | // a partial match (unlike minChars, which defines 383 | // how many characters are required to do any match 384 | // at all). Defaults to 2. 385 | // 386 | // - ignoreCase - Whether to ignore case when autocompleting. 387 | // Defaults to true. 388 | // 389 | // It's possible to pass in a custom function as the 'selector' 390 | // option, if you prefer to write your own autocompletion logic. 391 | // In that case, the other options above will not apply unless 392 | // you support them. 393 | 394 | Autocompleter.Local = Class.create(); 395 | Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { 396 | initialize: function(element, update, array, options) { 397 | this.baseInitialize(element, update, options); 398 | this.options.array = array; 399 | }, 400 | 401 | getUpdatedChoices: function() { 402 | this.updateChoices(this.options.selector(this)); 403 | }, 404 | 405 | setOptions: function(options) { 406 | this.options = Object.extend({ 407 | choices: 10, 408 | partialSearch: true, 409 | partialChars: 2, 410 | ignoreCase: true, 411 | fullSearch: false, 412 | selector: function(instance) { 413 | var ret = []; // Beginning matches 414 | var partial = []; // Inside matches 415 | var entry = instance.getToken(); 416 | var count = 0; 417 | 418 | for (var i = 0; i < instance.options.array.length && 419 | ret.length < instance.options.choices ; i++) { 420 | 421 | var elem = instance.options.array[i]; 422 | var foundPos = instance.options.ignoreCase ? 423 | elem.toLowerCase().indexOf(entry.toLowerCase()) : 424 | elem.indexOf(entry); 425 | 426 | while (foundPos != -1) { 427 | if (foundPos == 0 && elem.length != entry.length) { 428 | ret.push("
  • " + elem.substr(0, entry.length) + "" + 429 | elem.substr(entry.length) + "
  • "); 430 | break; 431 | } else if (entry.length >= instance.options.partialChars && 432 | instance.options.partialSearch && foundPos != -1) { 433 | if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { 434 | partial.push("
  • " + elem.substr(0, foundPos) + "" + 435 | elem.substr(foundPos, entry.length) + "" + elem.substr( 436 | foundPos + entry.length) + "
  • "); 437 | break; 438 | } 439 | } 440 | 441 | foundPos = instance.options.ignoreCase ? 442 | elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 443 | elem.indexOf(entry, foundPos + 1); 444 | 445 | } 446 | } 447 | if (partial.length) 448 | ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) 449 | return ""; 450 | } 451 | }, options || {}); 452 | } 453 | }); 454 | 455 | // AJAX in-place editor 456 | // 457 | // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor 458 | 459 | // Use this if you notice weird scrolling problems on some browsers, 460 | // the DOM might be a bit confused when this gets called so do this 461 | // waits 1 ms (with setTimeout) until it does the activation 462 | Field.scrollFreeActivate = function(field) { 463 | setTimeout(function() { 464 | Field.activate(field); 465 | }, 1); 466 | } 467 | 468 | Ajax.InPlaceEditor = Class.create(); 469 | Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; 470 | Ajax.InPlaceEditor.prototype = { 471 | initialize: function(element, url, options) { 472 | this.url = url; 473 | this.element = $(element); 474 | 475 | this.options = Object.extend({ 476 | paramName: "value", 477 | okButton: true, 478 | okText: "ok", 479 | cancelLink: true, 480 | cancelText: "cancel", 481 | savingText: "Saving...", 482 | clickToEditText: "Click to edit", 483 | okText: "ok", 484 | rows: 1, 485 | onComplete: function(transport, element) { 486 | new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); 487 | }, 488 | onFailure: function(transport) { 489 | alert("Error communicating with the server: " + transport.responseText.stripTags()); 490 | }, 491 | callback: function(form) { 492 | return Form.serialize(form); 493 | }, 494 | handleLineBreaks: true, 495 | loadingText: 'Loading...', 496 | savingClassName: 'inplaceeditor-saving', 497 | loadingClassName: 'inplaceeditor-loading', 498 | formClassName: 'inplaceeditor-form', 499 | highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, 500 | highlightendcolor: "#FFFFFF", 501 | externalControl: null, 502 | submitOnBlur: false, 503 | ajaxOptions: {}, 504 | evalScripts: false 505 | }, options || {}); 506 | 507 | if(!this.options.formId && this.element.id) { 508 | this.options.formId = this.element.id + "-inplaceeditor"; 509 | if ($(this.options.formId)) { 510 | // there's already a form with that name, don't specify an id 511 | this.options.formId = null; 512 | } 513 | } 514 | 515 | if (this.options.externalControl) { 516 | this.options.externalControl = $(this.options.externalControl); 517 | } 518 | 519 | this.originalBackground = Element.getStyle(this.element, 'background-color'); 520 | if (!this.originalBackground) { 521 | this.originalBackground = "transparent"; 522 | } 523 | 524 | this.element.title = this.options.clickToEditText; 525 | 526 | this.onclickListener = this.enterEditMode.bindAsEventListener(this); 527 | this.mouseoverListener = this.enterHover.bindAsEventListener(this); 528 | this.mouseoutListener = this.leaveHover.bindAsEventListener(this); 529 | Event.observe(this.element, 'click', this.onclickListener); 530 | Event.observe(this.element, 'mouseover', this.mouseoverListener); 531 | Event.observe(this.element, 'mouseout', this.mouseoutListener); 532 | if (this.options.externalControl) { 533 | Event.observe(this.options.externalControl, 'click', this.onclickListener); 534 | Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); 535 | Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); 536 | } 537 | }, 538 | enterEditMode: function(evt) { 539 | if (this.saving) return; 540 | if (this.editing) return; 541 | this.editing = true; 542 | this.onEnterEditMode(); 543 | if (this.options.externalControl) { 544 | Element.hide(this.options.externalControl); 545 | } 546 | Element.hide(this.element); 547 | this.createForm(); 548 | this.element.parentNode.insertBefore(this.form, this.element); 549 | if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField); 550 | // stop the event to avoid a page refresh in Safari 551 | if (evt) { 552 | Event.stop(evt); 553 | } 554 | return false; 555 | }, 556 | createForm: function() { 557 | this.form = document.createElement("form"); 558 | this.form.id = this.options.formId; 559 | Element.addClassName(this.form, this.options.formClassName) 560 | this.form.onsubmit = this.onSubmit.bind(this); 561 | 562 | this.createEditField(); 563 | 564 | if (this.options.textarea) { 565 | var br = document.createElement("br"); 566 | this.form.appendChild(br); 567 | } 568 | 569 | if (this.options.okButton) { 570 | okButton = document.createElement("input"); 571 | okButton.type = "submit"; 572 | okButton.value = this.options.okText; 573 | okButton.className = 'editor_ok_button'; 574 | this.form.appendChild(okButton); 575 | } 576 | 577 | if (this.options.cancelLink) { 578 | cancelLink = document.createElement("a"); 579 | cancelLink.href = "#"; 580 | cancelLink.appendChild(document.createTextNode(this.options.cancelText)); 581 | cancelLink.onclick = this.onclickCancel.bind(this); 582 | cancelLink.className = 'editor_cancel'; 583 | this.form.appendChild(cancelLink); 584 | } 585 | }, 586 | hasHTMLLineBreaks: function(string) { 587 | if (!this.options.handleLineBreaks) return false; 588 | return string.match(/
    /i); 589 | }, 590 | convertHTMLLineBreaks: function(string) { 591 | return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); 592 | }, 593 | createEditField: function() { 594 | var text; 595 | if(this.options.loadTextURL) { 596 | text = this.options.loadingText; 597 | } else { 598 | text = this.getText(); 599 | } 600 | 601 | var obj = this; 602 | 603 | if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { 604 | this.options.textarea = false; 605 | var textField = document.createElement("input"); 606 | textField.obj = this; 607 | textField.type = "text"; 608 | textField.name = this.options.paramName; 609 | textField.value = text; 610 | textField.style.backgroundColor = this.options.highlightcolor; 611 | textField.className = 'editor_field'; 612 | var size = this.options.size || this.options.cols || 0; 613 | if (size != 0) textField.size = size; 614 | if (this.options.submitOnBlur) 615 | textField.onblur = this.onSubmit.bind(this); 616 | this.editField = textField; 617 | } else { 618 | this.options.textarea = true; 619 | var textArea = document.createElement("textarea"); 620 | textArea.obj = this; 621 | textArea.name = this.options.paramName; 622 | textArea.value = this.convertHTMLLineBreaks(text); 623 | textArea.rows = this.options.rows; 624 | textArea.cols = this.options.cols || 40; 625 | textArea.className = 'editor_field'; 626 | if (this.options.submitOnBlur) 627 | textArea.onblur = this.onSubmit.bind(this); 628 | this.editField = textArea; 629 | } 630 | 631 | if(this.options.loadTextURL) { 632 | this.loadExternalText(); 633 | } 634 | this.form.appendChild(this.editField); 635 | }, 636 | getText: function() { 637 | return this.element.innerHTML; 638 | }, 639 | loadExternalText: function() { 640 | Element.addClassName(this.form, this.options.loadingClassName); 641 | this.editField.disabled = true; 642 | new Ajax.Request( 643 | this.options.loadTextURL, 644 | Object.extend({ 645 | asynchronous: true, 646 | onComplete: this.onLoadedExternalText.bind(this) 647 | }, this.options.ajaxOptions) 648 | ); 649 | }, 650 | onLoadedExternalText: function(transport) { 651 | Element.removeClassName(this.form, this.options.loadingClassName); 652 | this.editField.disabled = false; 653 | this.editField.value = transport.responseText.stripTags(); 654 | Field.scrollFreeActivate(this.editField); 655 | }, 656 | onclickCancel: function() { 657 | this.onComplete(); 658 | this.leaveEditMode(); 659 | return false; 660 | }, 661 | onFailure: function(transport) { 662 | this.options.onFailure(transport); 663 | if (this.oldInnerHTML) { 664 | this.element.innerHTML = this.oldInnerHTML; 665 | this.oldInnerHTML = null; 666 | } 667 | return false; 668 | }, 669 | onSubmit: function() { 670 | // onLoading resets these so we need to save them away for the Ajax call 671 | var form = this.form; 672 | var value = this.editField.value; 673 | 674 | // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... 675 | // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... 676 | // to be displayed indefinitely 677 | this.onLoading(); 678 | 679 | if (this.options.evalScripts) { 680 | new Ajax.Request( 681 | this.url, Object.extend({ 682 | parameters: this.options.callback(form, value), 683 | onComplete: this.onComplete.bind(this), 684 | onFailure: this.onFailure.bind(this), 685 | asynchronous:true, 686 | evalScripts:true 687 | }, this.options.ajaxOptions)); 688 | } else { 689 | new Ajax.Updater( 690 | { success: this.element, 691 | // don't update on failure (this could be an option) 692 | failure: null }, 693 | this.url, Object.extend({ 694 | parameters: this.options.callback(form, value), 695 | onComplete: this.onComplete.bind(this), 696 | onFailure: this.onFailure.bind(this) 697 | }, this.options.ajaxOptions)); 698 | } 699 | // stop the event to avoid a page refresh in Safari 700 | if (arguments.length > 1) { 701 | Event.stop(arguments[0]); 702 | } 703 | return false; 704 | }, 705 | onLoading: function() { 706 | this.saving = true; 707 | this.removeForm(); 708 | this.leaveHover(); 709 | this.showSaving(); 710 | }, 711 | showSaving: function() { 712 | this.oldInnerHTML = this.element.innerHTML; 713 | this.element.innerHTML = this.options.savingText; 714 | Element.addClassName(this.element, this.options.savingClassName); 715 | this.element.style.backgroundColor = this.originalBackground; 716 | Element.show(this.element); 717 | }, 718 | removeForm: function() { 719 | if(this.form) { 720 | if (this.form.parentNode) Element.remove(this.form); 721 | this.form = null; 722 | } 723 | }, 724 | enterHover: function() { 725 | if (this.saving) return; 726 | this.element.style.backgroundColor = this.options.highlightcolor; 727 | if (this.effect) { 728 | this.effect.cancel(); 729 | } 730 | Element.addClassName(this.element, this.options.hoverClassName) 731 | }, 732 | leaveHover: function() { 733 | if (this.options.backgroundColor) { 734 | this.element.style.backgroundColor = this.oldBackground; 735 | } 736 | Element.removeClassName(this.element, this.options.hoverClassName) 737 | if (this.saving) return; 738 | this.effect = new Effect.Highlight(this.element, { 739 | startcolor: this.options.highlightcolor, 740 | endcolor: this.options.highlightendcolor, 741 | restorecolor: this.originalBackground 742 | }); 743 | }, 744 | leaveEditMode: function() { 745 | Element.removeClassName(this.element, this.options.savingClassName); 746 | this.removeForm(); 747 | this.leaveHover(); 748 | this.element.style.backgroundColor = this.originalBackground; 749 | Element.show(this.element); 750 | if (this.options.externalControl) { 751 | Element.show(this.options.externalControl); 752 | } 753 | this.editing = false; 754 | this.saving = false; 755 | this.oldInnerHTML = null; 756 | this.onLeaveEditMode(); 757 | }, 758 | onComplete: function(transport) { 759 | this.leaveEditMode(); 760 | this.options.onComplete.bind(this)(transport, this.element); 761 | }, 762 | onEnterEditMode: function() {}, 763 | onLeaveEditMode: function() {}, 764 | dispose: function() { 765 | if (this.oldInnerHTML) { 766 | this.element.innerHTML = this.oldInnerHTML; 767 | } 768 | this.leaveEditMode(); 769 | Event.stopObserving(this.element, 'click', this.onclickListener); 770 | Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); 771 | Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); 772 | if (this.options.externalControl) { 773 | Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); 774 | Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); 775 | Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); 776 | } 777 | } 778 | }; 779 | 780 | Ajax.InPlaceCollectionEditor = Class.create(); 781 | Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); 782 | Object.extend(Ajax.InPlaceCollectionEditor.prototype, { 783 | createEditField: function() { 784 | if (!this.cached_selectTag) { 785 | var selectTag = document.createElement("select"); 786 | var collection = this.options.collection || []; 787 | var optionTag; 788 | collection.each(function(e,i) { 789 | optionTag = document.createElement("option"); 790 | optionTag.value = (e instanceof Array) ? e[0] : e; 791 | if((typeof this.options.value == 'undefined') && 792 | ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true; 793 | if(this.options.value==optionTag.value) optionTag.selected = true; 794 | optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); 795 | selectTag.appendChild(optionTag); 796 | }.bind(this)); 797 | this.cached_selectTag = selectTag; 798 | } 799 | 800 | this.editField = this.cached_selectTag; 801 | if(this.options.loadTextURL) this.loadExternalText(); 802 | this.form.appendChild(this.editField); 803 | this.options.callback = function(form, value) { 804 | return "value=" + encodeURIComponent(value); 805 | } 806 | } 807 | }); 808 | 809 | // Delayed observer, like Form.Element.Observer, 810 | // but waits for delay after last key input 811 | // Ideal for live-search fields 812 | 813 | Form.Element.DelayedObserver = Class.create(); 814 | Form.Element.DelayedObserver.prototype = { 815 | initialize: function(element, delay, callback) { 816 | this.delay = delay || 0.5; 817 | this.element = $(element); 818 | this.callback = callback; 819 | this.timer = null; 820 | this.lastValue = $F(this.element); 821 | Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); 822 | }, 823 | delayedListener: function(event) { 824 | if(this.lastValue == $F(this.element)) return; 825 | if(this.timer) clearTimeout(this.timer); 826 | this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); 827 | this.lastValue = $F(this.element); 828 | }, 829 | onTimerEvent: function() { 830 | this.timer = null; 831 | this.callback(this.element, $F(this.element)); 832 | } 833 | }; 834 | -------------------------------------------------------------------------------- /public/javascripts/dragdrop.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 2 | // (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) 3 | // 4 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 5 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 6 | 7 | if(typeof Effect == 'undefined') 8 | throw("dragdrop.js requires including script.aculo.us' effects.js library"); 9 | 10 | var Droppables = { 11 | drops: [], 12 | 13 | remove: function(element) { 14 | this.drops = this.drops.reject(function(d) { return d.element==$(element) }); 15 | }, 16 | 17 | add: function(element) { 18 | element = $(element); 19 | var options = Object.extend({ 20 | greedy: true, 21 | hoverclass: null, 22 | tree: false 23 | }, arguments[1] || {}); 24 | 25 | // cache containers 26 | if(options.containment) { 27 | options._containers = []; 28 | var containment = options.containment; 29 | if((typeof containment == 'object') && 30 | (containment.constructor == Array)) { 31 | containment.each( function(c) { options._containers.push($(c)) }); 32 | } else { 33 | options._containers.push($(containment)); 34 | } 35 | } 36 | 37 | if(options.accept) options.accept = [options.accept].flatten(); 38 | 39 | Element.makePositioned(element); // fix IE 40 | options.element = element; 41 | 42 | this.drops.push(options); 43 | }, 44 | 45 | findDeepestChild: function(drops) { 46 | deepest = drops[0]; 47 | 48 | for (i = 1; i < drops.length; ++i) 49 | if (Element.isParent(drops[i].element, deepest.element)) 50 | deepest = drops[i]; 51 | 52 | return deepest; 53 | }, 54 | 55 | isContained: function(element, drop) { 56 | var containmentNode; 57 | if(drop.tree) { 58 | containmentNode = element.treeNode; 59 | } else { 60 | containmentNode = element.parentNode; 61 | } 62 | return drop._containers.detect(function(c) { return containmentNode == c }); 63 | }, 64 | 65 | isAffected: function(point, element, drop) { 66 | return ( 67 | (drop.element!=element) && 68 | ((!drop._containers) || 69 | this.isContained(element, drop)) && 70 | ((!drop.accept) || 71 | (Element.classNames(element).detect( 72 | function(v) { return drop.accept.include(v) } ) )) && 73 | Position.within(drop.element, point[0], point[1]) ); 74 | }, 75 | 76 | deactivate: function(drop) { 77 | if(drop.hoverclass) 78 | Element.removeClassName(drop.element, drop.hoverclass); 79 | this.last_active = null; 80 | }, 81 | 82 | activate: function(drop) { 83 | if(drop.hoverclass) 84 | Element.addClassName(drop.element, drop.hoverclass); 85 | this.last_active = drop; 86 | }, 87 | 88 | show: function(point, element) { 89 | if(!this.drops.length) return; 90 | var affected = []; 91 | 92 | if(this.last_active) this.deactivate(this.last_active); 93 | this.drops.each( function(drop) { 94 | if(Droppables.isAffected(point, element, drop)) 95 | affected.push(drop); 96 | }); 97 | 98 | if(affected.length>0) { 99 | drop = Droppables.findDeepestChild(affected); 100 | Position.within(drop.element, point[0], point[1]); 101 | if(drop.onHover) 102 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); 103 | 104 | Droppables.activate(drop); 105 | } 106 | }, 107 | 108 | fire: function(event, element) { 109 | if(!this.last_active) return; 110 | Position.prepare(); 111 | 112 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) 113 | if (this.last_active.onDrop) 114 | this.last_active.onDrop(element, this.last_active.element, event); 115 | }, 116 | 117 | reset: function() { 118 | if(this.last_active) 119 | this.deactivate(this.last_active); 120 | } 121 | } 122 | 123 | var Draggables = { 124 | drags: [], 125 | observers: [], 126 | 127 | register: function(draggable) { 128 | if(this.drags.length == 0) { 129 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 130 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this); 131 | this.eventKeypress = this.keyPress.bindAsEventListener(this); 132 | 133 | Event.observe(document, "mouseup", this.eventMouseUp); 134 | Event.observe(document, "mousemove", this.eventMouseMove); 135 | Event.observe(document, "keypress", this.eventKeypress); 136 | } 137 | this.drags.push(draggable); 138 | }, 139 | 140 | unregister: function(draggable) { 141 | this.drags = this.drags.reject(function(d) { return d==draggable }); 142 | if(this.drags.length == 0) { 143 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 144 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 145 | Event.stopObserving(document, "keypress", this.eventKeypress); 146 | } 147 | }, 148 | 149 | activate: function(draggable) { 150 | if(draggable.options.delay) { 151 | this._timeout = setTimeout(function() { 152 | Draggables._timeout = null; 153 | window.focus(); 154 | Draggables.activeDraggable = draggable; 155 | }.bind(this), draggable.options.delay); 156 | } else { 157 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari 158 | this.activeDraggable = draggable; 159 | } 160 | }, 161 | 162 | deactivate: function() { 163 | this.activeDraggable = null; 164 | }, 165 | 166 | updateDrag: function(event) { 167 | if(!this.activeDraggable) return; 168 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 169 | // Mozilla-based browsers fire successive mousemove events with 170 | // the same coordinates, prevent needless redrawing (moz bug?) 171 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; 172 | this._lastPointer = pointer; 173 | 174 | this.activeDraggable.updateDrag(event, pointer); 175 | }, 176 | 177 | endDrag: function(event) { 178 | if(this._timeout) { 179 | clearTimeout(this._timeout); 180 | this._timeout = null; 181 | } 182 | if(!this.activeDraggable) return; 183 | this._lastPointer = null; 184 | this.activeDraggable.endDrag(event); 185 | this.activeDraggable = null; 186 | }, 187 | 188 | keyPress: function(event) { 189 | if(this.activeDraggable) 190 | this.activeDraggable.keyPress(event); 191 | }, 192 | 193 | addObserver: function(observer) { 194 | this.observers.push(observer); 195 | this._cacheObserverCallbacks(); 196 | }, 197 | 198 | removeObserver: function(element) { // element instead of observer fixes mem leaks 199 | this.observers = this.observers.reject( function(o) { return o.element==element }); 200 | this._cacheObserverCallbacks(); 201 | }, 202 | 203 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' 204 | if(this[eventName+'Count'] > 0) 205 | this.observers.each( function(o) { 206 | if(o[eventName]) o[eventName](eventName, draggable, event); 207 | }); 208 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event); 209 | }, 210 | 211 | _cacheObserverCallbacks: function() { 212 | ['onStart','onEnd','onDrag'].each( function(eventName) { 213 | Draggables[eventName+'Count'] = Draggables.observers.select( 214 | function(o) { return o[eventName]; } 215 | ).length; 216 | }); 217 | } 218 | } 219 | 220 | /*--------------------------------------------------------------------------*/ 221 | 222 | var Draggable = Class.create(); 223 | Draggable._dragging = {}; 224 | 225 | Draggable.prototype = { 226 | initialize: function(element) { 227 | var defaults = { 228 | handle: false, 229 | reverteffect: function(element, top_offset, left_offset) { 230 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; 231 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, 232 | queue: {scope:'_draggable', position:'end'} 233 | }); 234 | }, 235 | endeffect: function(element) { 236 | var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0; 237 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 238 | queue: {scope:'_draggable', position:'end'}, 239 | afterFinish: function(){ 240 | Draggable._dragging[element] = false 241 | } 242 | }); 243 | }, 244 | zindex: 1000, 245 | revert: false, 246 | scroll: false, 247 | scrollSensitivity: 20, 248 | scrollSpeed: 15, 249 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } 250 | delay: 0 251 | }; 252 | 253 | if(!arguments[1] || typeof arguments[1].endeffect == 'undefined') 254 | Object.extend(defaults, { 255 | starteffect: function(element) { 256 | element._opacity = Element.getOpacity(element); 257 | Draggable._dragging[element] = true; 258 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 259 | } 260 | }); 261 | 262 | var options = Object.extend(defaults, arguments[1] || {}); 263 | 264 | this.element = $(element); 265 | 266 | if(options.handle && (typeof options.handle == 'string')) 267 | this.handle = this.element.down('.'+options.handle, 0); 268 | 269 | if(!this.handle) this.handle = $(options.handle); 270 | if(!this.handle) this.handle = this.element; 271 | 272 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { 273 | options.scroll = $(options.scroll); 274 | this._isScrollChild = Element.childOf(this.element, options.scroll); 275 | } 276 | 277 | Element.makePositioned(this.element); // fix IE 278 | 279 | this.delta = this.currentDelta(); 280 | this.options = options; 281 | this.dragging = false; 282 | 283 | this.eventMouseDown = this.initDrag.bindAsEventListener(this); 284 | Event.observe(this.handle, "mousedown", this.eventMouseDown); 285 | 286 | Draggables.register(this); 287 | }, 288 | 289 | destroy: function() { 290 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); 291 | Draggables.unregister(this); 292 | }, 293 | 294 | currentDelta: function() { 295 | return([ 296 | parseInt(Element.getStyle(this.element,'left') || '0'), 297 | parseInt(Element.getStyle(this.element,'top') || '0')]); 298 | }, 299 | 300 | initDrag: function(event) { 301 | if(typeof Draggable._dragging[this.element] != 'undefined' && 302 | Draggable._dragging[this.element]) return; 303 | if(Event.isLeftClick(event)) { 304 | // abort on form elements, fixes a Firefox issue 305 | var src = Event.element(event); 306 | if(src.tagName && ( 307 | src.tagName=='INPUT' || 308 | src.tagName=='SELECT' || 309 | src.tagName=='OPTION' || 310 | src.tagName=='BUTTON' || 311 | src.tagName=='TEXTAREA')) return; 312 | 313 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 314 | var pos = Position.cumulativeOffset(this.element); 315 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); 316 | 317 | Draggables.activate(this); 318 | Event.stop(event); 319 | } 320 | }, 321 | 322 | startDrag: function(event) { 323 | this.dragging = true; 324 | 325 | if(this.options.zindex) { 326 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 327 | this.element.style.zIndex = this.options.zindex; 328 | } 329 | 330 | if(this.options.ghosting) { 331 | this._clone = this.element.cloneNode(true); 332 | Position.absolutize(this.element); 333 | this.element.parentNode.insertBefore(this._clone, this.element); 334 | } 335 | 336 | if(this.options.scroll) { 337 | if (this.options.scroll == window) { 338 | var where = this._getWindowScroll(this.options.scroll); 339 | this.originalScrollLeft = where.left; 340 | this.originalScrollTop = where.top; 341 | } else { 342 | this.originalScrollLeft = this.options.scroll.scrollLeft; 343 | this.originalScrollTop = this.options.scroll.scrollTop; 344 | } 345 | } 346 | 347 | Draggables.notify('onStart', this, event); 348 | 349 | if(this.options.starteffect) this.options.starteffect(this.element); 350 | }, 351 | 352 | updateDrag: function(event, pointer) { 353 | if(!this.dragging) this.startDrag(event); 354 | Position.prepare(); 355 | Droppables.show(pointer, this.element); 356 | Draggables.notify('onDrag', this, event); 357 | 358 | this.draw(pointer); 359 | if(this.options.change) this.options.change(this); 360 | 361 | if(this.options.scroll) { 362 | this.stopScrolling(); 363 | 364 | var p; 365 | if (this.options.scroll == window) { 366 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } 367 | } else { 368 | p = Position.page(this.options.scroll); 369 | p[0] += this.options.scroll.scrollLeft + Position.deltaX; 370 | p[1] += this.options.scroll.scrollTop + Position.deltaY; 371 | p.push(p[0]+this.options.scroll.offsetWidth); 372 | p.push(p[1]+this.options.scroll.offsetHeight); 373 | } 374 | var speed = [0,0]; 375 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); 376 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); 377 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); 378 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); 379 | this.startScrolling(speed); 380 | } 381 | 382 | // fix AppleWebKit rendering 383 | if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 384 | 385 | Event.stop(event); 386 | }, 387 | 388 | finishDrag: function(event, success) { 389 | this.dragging = false; 390 | 391 | if(this.options.ghosting) { 392 | Position.relativize(this.element); 393 | Element.remove(this._clone); 394 | this._clone = null; 395 | } 396 | 397 | if(success) Droppables.fire(event, this.element); 398 | Draggables.notify('onEnd', this, event); 399 | 400 | var revert = this.options.revert; 401 | if(revert && typeof revert == 'function') revert = revert(this.element); 402 | 403 | var d = this.currentDelta(); 404 | if(revert && this.options.reverteffect) { 405 | this.options.reverteffect(this.element, 406 | d[1]-this.delta[1], d[0]-this.delta[0]); 407 | } else { 408 | this.delta = d; 409 | } 410 | 411 | if(this.options.zindex) 412 | this.element.style.zIndex = this.originalZ; 413 | 414 | if(this.options.endeffect) 415 | this.options.endeffect(this.element); 416 | 417 | Draggables.deactivate(this); 418 | Droppables.reset(); 419 | }, 420 | 421 | keyPress: function(event) { 422 | if(event.keyCode!=Event.KEY_ESC) return; 423 | this.finishDrag(event, false); 424 | Event.stop(event); 425 | }, 426 | 427 | endDrag: function(event) { 428 | if(!this.dragging) return; 429 | this.stopScrolling(); 430 | this.finishDrag(event, true); 431 | Event.stop(event); 432 | }, 433 | 434 | draw: function(point) { 435 | var pos = Position.cumulativeOffset(this.element); 436 | if(this.options.ghosting) { 437 | var r = Position.realOffset(this.element); 438 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; 439 | } 440 | 441 | var d = this.currentDelta(); 442 | pos[0] -= d[0]; pos[1] -= d[1]; 443 | 444 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { 445 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; 446 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; 447 | } 448 | 449 | var p = [0,1].map(function(i){ 450 | return (point[i]-pos[i]-this.offset[i]) 451 | }.bind(this)); 452 | 453 | if(this.options.snap) { 454 | if(typeof this.options.snap == 'function') { 455 | p = this.options.snap(p[0],p[1],this); 456 | } else { 457 | if(this.options.snap instanceof Array) { 458 | p = p.map( function(v, i) { 459 | return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) 460 | } else { 461 | p = p.map( function(v) { 462 | return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) 463 | } 464 | }} 465 | 466 | var style = this.element.style; 467 | if((!this.options.constraint) || (this.options.constraint=='horizontal')) 468 | style.left = p[0] + "px"; 469 | if((!this.options.constraint) || (this.options.constraint=='vertical')) 470 | style.top = p[1] + "px"; 471 | 472 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering 473 | }, 474 | 475 | stopScrolling: function() { 476 | if(this.scrollInterval) { 477 | clearInterval(this.scrollInterval); 478 | this.scrollInterval = null; 479 | Draggables._lastScrollPointer = null; 480 | } 481 | }, 482 | 483 | startScrolling: function(speed) { 484 | if(!(speed[0] || speed[1])) return; 485 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; 486 | this.lastScrolled = new Date(); 487 | this.scrollInterval = setInterval(this.scroll.bind(this), 10); 488 | }, 489 | 490 | scroll: function() { 491 | var current = new Date(); 492 | var delta = current - this.lastScrolled; 493 | this.lastScrolled = current; 494 | if(this.options.scroll == window) { 495 | with (this._getWindowScroll(this.options.scroll)) { 496 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) { 497 | var d = delta / 1000; 498 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); 499 | } 500 | } 501 | } else { 502 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; 503 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; 504 | } 505 | 506 | Position.prepare(); 507 | Droppables.show(Draggables._lastPointer, this.element); 508 | Draggables.notify('onDrag', this); 509 | if (this._isScrollChild) { 510 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); 511 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; 512 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; 513 | if (Draggables._lastScrollPointer[0] < 0) 514 | Draggables._lastScrollPointer[0] = 0; 515 | if (Draggables._lastScrollPointer[1] < 0) 516 | Draggables._lastScrollPointer[1] = 0; 517 | this.draw(Draggables._lastScrollPointer); 518 | } 519 | 520 | if(this.options.change) this.options.change(this); 521 | }, 522 | 523 | _getWindowScroll: function(w) { 524 | var T, L, W, H; 525 | with (w.document) { 526 | if (w.document.documentElement && documentElement.scrollTop) { 527 | T = documentElement.scrollTop; 528 | L = documentElement.scrollLeft; 529 | } else if (w.document.body) { 530 | T = body.scrollTop; 531 | L = body.scrollLeft; 532 | } 533 | if (w.innerWidth) { 534 | W = w.innerWidth; 535 | H = w.innerHeight; 536 | } else if (w.document.documentElement && documentElement.clientWidth) { 537 | W = documentElement.clientWidth; 538 | H = documentElement.clientHeight; 539 | } else { 540 | W = body.offsetWidth; 541 | H = body.offsetHeight 542 | } 543 | } 544 | return { top: T, left: L, width: W, height: H }; 545 | } 546 | } 547 | 548 | /*--------------------------------------------------------------------------*/ 549 | 550 | var SortableObserver = Class.create(); 551 | SortableObserver.prototype = { 552 | initialize: function(element, observer) { 553 | this.element = $(element); 554 | this.observer = observer; 555 | this.lastValue = Sortable.serialize(this.element); 556 | }, 557 | 558 | onStart: function() { 559 | this.lastValue = Sortable.serialize(this.element); 560 | }, 561 | 562 | onEnd: function() { 563 | Sortable.unmark(); 564 | if(this.lastValue != Sortable.serialize(this.element)) 565 | this.observer(this.element) 566 | } 567 | } 568 | 569 | var Sortable = { 570 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, 571 | 572 | sortables: {}, 573 | 574 | _findRootElement: function(element) { 575 | while (element.tagName != "BODY") { 576 | if(element.id && Sortable.sortables[element.id]) return element; 577 | element = element.parentNode; 578 | } 579 | }, 580 | 581 | options: function(element) { 582 | element = Sortable._findRootElement($(element)); 583 | if(!element) return; 584 | return Sortable.sortables[element.id]; 585 | }, 586 | 587 | destroy: function(element){ 588 | var s = Sortable.options(element); 589 | 590 | if(s) { 591 | Draggables.removeObserver(s.element); 592 | s.droppables.each(function(d){ Droppables.remove(d) }); 593 | s.draggables.invoke('destroy'); 594 | 595 | delete Sortable.sortables[s.element.id]; 596 | } 597 | }, 598 | 599 | create: function(element) { 600 | element = $(element); 601 | var options = Object.extend({ 602 | element: element, 603 | tag: 'li', // assumes li children, override with tag: 'tagname' 604 | dropOnEmpty: false, 605 | tree: false, 606 | treeTag: 'ul', 607 | overlap: 'vertical', // one of 'vertical', 'horizontal' 608 | constraint: 'vertical', // one of 'vertical', 'horizontal', false 609 | containment: element, // also takes array of elements (or id's); or false 610 | handle: false, // or a CSS class 611 | only: false, 612 | delay: 0, 613 | hoverclass: null, 614 | ghosting: false, 615 | scroll: false, 616 | scrollSensitivity: 20, 617 | scrollSpeed: 15, 618 | format: this.SERIALIZE_RULE, 619 | onChange: Prototype.emptyFunction, 620 | onUpdate: Prototype.emptyFunction 621 | }, arguments[1] || {}); 622 | 623 | // clear any old sortable with same element 624 | this.destroy(element); 625 | 626 | // build options for the draggables 627 | var options_for_draggable = { 628 | revert: true, 629 | scroll: options.scroll, 630 | scrollSpeed: options.scrollSpeed, 631 | scrollSensitivity: options.scrollSensitivity, 632 | delay: options.delay, 633 | ghosting: options.ghosting, 634 | constraint: options.constraint, 635 | handle: options.handle }; 636 | 637 | if(options.starteffect) 638 | options_for_draggable.starteffect = options.starteffect; 639 | 640 | if(options.reverteffect) 641 | options_for_draggable.reverteffect = options.reverteffect; 642 | else 643 | if(options.ghosting) options_for_draggable.reverteffect = function(element) { 644 | element.style.top = 0; 645 | element.style.left = 0; 646 | }; 647 | 648 | if(options.endeffect) 649 | options_for_draggable.endeffect = options.endeffect; 650 | 651 | if(options.zindex) 652 | options_for_draggable.zindex = options.zindex; 653 | 654 | // build options for the droppables 655 | var options_for_droppable = { 656 | overlap: options.overlap, 657 | containment: options.containment, 658 | tree: options.tree, 659 | hoverclass: options.hoverclass, 660 | onHover: Sortable.onHover 661 | } 662 | 663 | var options_for_tree = { 664 | onHover: Sortable.onEmptyHover, 665 | overlap: options.overlap, 666 | containment: options.containment, 667 | hoverclass: options.hoverclass 668 | } 669 | 670 | // fix for gecko engine 671 | Element.cleanWhitespace(element); 672 | 673 | options.draggables = []; 674 | options.droppables = []; 675 | 676 | // drop on empty handling 677 | if(options.dropOnEmpty || options.tree) { 678 | Droppables.add(element, options_for_tree); 679 | options.droppables.push(element); 680 | } 681 | 682 | (this.findElements(element, options) || []).each( function(e) { 683 | // handles are per-draggable 684 | var handle = options.handle ? 685 | $(e).down('.'+options.handle,0) : e; 686 | options.draggables.push( 687 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); 688 | Droppables.add(e, options_for_droppable); 689 | if(options.tree) e.treeNode = element; 690 | options.droppables.push(e); 691 | }); 692 | 693 | if(options.tree) { 694 | (Sortable.findTreeElements(element, options) || []).each( function(e) { 695 | Droppables.add(e, options_for_tree); 696 | e.treeNode = element; 697 | options.droppables.push(e); 698 | }); 699 | } 700 | 701 | // keep reference 702 | this.sortables[element.id] = options; 703 | 704 | // for onupdate 705 | Draggables.addObserver(new SortableObserver(element, options.onUpdate)); 706 | 707 | }, 708 | 709 | // return all suitable-for-sortable elements in a guaranteed order 710 | findElements: function(element, options) { 711 | return Element.findChildren( 712 | element, options.only, options.tree ? true : false, options.tag); 713 | }, 714 | 715 | findTreeElements: function(element, options) { 716 | return Element.findChildren( 717 | element, options.only, options.tree ? true : false, options.treeTag); 718 | }, 719 | 720 | onHover: function(element, dropon, overlap) { 721 | if(Element.isParent(dropon, element)) return; 722 | 723 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { 724 | return; 725 | } else if(overlap>0.5) { 726 | Sortable.mark(dropon, 'before'); 727 | if(dropon.previousSibling != element) { 728 | var oldParentNode = element.parentNode; 729 | element.style.visibility = "hidden"; // fix gecko rendering 730 | dropon.parentNode.insertBefore(element, dropon); 731 | if(dropon.parentNode!=oldParentNode) 732 | Sortable.options(oldParentNode).onChange(element); 733 | Sortable.options(dropon.parentNode).onChange(element); 734 | } 735 | } else { 736 | Sortable.mark(dropon, 'after'); 737 | var nextElement = dropon.nextSibling || null; 738 | if(nextElement != element) { 739 | var oldParentNode = element.parentNode; 740 | element.style.visibility = "hidden"; // fix gecko rendering 741 | dropon.parentNode.insertBefore(element, nextElement); 742 | if(dropon.parentNode!=oldParentNode) 743 | Sortable.options(oldParentNode).onChange(element); 744 | Sortable.options(dropon.parentNode).onChange(element); 745 | } 746 | } 747 | }, 748 | 749 | onEmptyHover: function(element, dropon, overlap) { 750 | var oldParentNode = element.parentNode; 751 | var droponOptions = Sortable.options(dropon); 752 | 753 | if(!Element.isParent(dropon, element)) { 754 | var index; 755 | 756 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); 757 | var child = null; 758 | 759 | if(children) { 760 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); 761 | 762 | for (index = 0; index < children.length; index += 1) { 763 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { 764 | offset -= Element.offsetSize (children[index], droponOptions.overlap); 765 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { 766 | child = index + 1 < children.length ? children[index + 1] : null; 767 | break; 768 | } else { 769 | child = children[index]; 770 | break; 771 | } 772 | } 773 | } 774 | 775 | dropon.insertBefore(element, child); 776 | 777 | Sortable.options(oldParentNode).onChange(element); 778 | droponOptions.onChange(element); 779 | } 780 | }, 781 | 782 | unmark: function() { 783 | if(Sortable._marker) Sortable._marker.hide(); 784 | }, 785 | 786 | mark: function(dropon, position) { 787 | // mark on ghosting only 788 | var sortable = Sortable.options(dropon.parentNode); 789 | if(sortable && !sortable.ghosting) return; 790 | 791 | if(!Sortable._marker) { 792 | Sortable._marker = 793 | ($('dropmarker') || Element.extend(document.createElement('DIV'))). 794 | hide().addClassName('dropmarker').setStyle({position:'absolute'}); 795 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); 796 | } 797 | var offsets = Position.cumulativeOffset(dropon); 798 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); 799 | 800 | if(position=='after') 801 | if(sortable.overlap == 'horizontal') 802 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); 803 | else 804 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); 805 | 806 | Sortable._marker.show(); 807 | }, 808 | 809 | _tree: function(element, options, parent) { 810 | var children = Sortable.findElements(element, options) || []; 811 | 812 | for (var i = 0; i < children.length; ++i) { 813 | var match = children[i].id.match(options.format); 814 | 815 | if (!match) continue; 816 | 817 | var child = { 818 | id: encodeURIComponent(match ? match[1] : null), 819 | element: element, 820 | parent: parent, 821 | children: [], 822 | position: parent.children.length, 823 | container: $(children[i]).down(options.treeTag) 824 | } 825 | 826 | /* Get the element containing the children and recurse over it */ 827 | if (child.container) 828 | this._tree(child.container, options, child) 829 | 830 | parent.children.push (child); 831 | } 832 | 833 | return parent; 834 | }, 835 | 836 | tree: function(element) { 837 | element = $(element); 838 | var sortableOptions = this.options(element); 839 | var options = Object.extend({ 840 | tag: sortableOptions.tag, 841 | treeTag: sortableOptions.treeTag, 842 | only: sortableOptions.only, 843 | name: element.id, 844 | format: sortableOptions.format 845 | }, arguments[1] || {}); 846 | 847 | var root = { 848 | id: null, 849 | parent: null, 850 | children: [], 851 | container: element, 852 | position: 0 853 | } 854 | 855 | return Sortable._tree(element, options, root); 856 | }, 857 | 858 | /* Construct a [i] index for a particular node */ 859 | _constructIndex: function(node) { 860 | var index = ''; 861 | do { 862 | if (node.id) index = '[' + node.position + ']' + index; 863 | } while ((node = node.parent) != null); 864 | return index; 865 | }, 866 | 867 | sequence: function(element) { 868 | element = $(element); 869 | var options = Object.extend(this.options(element), arguments[1] || {}); 870 | 871 | return $(this.findElements(element, options) || []).map( function(item) { 872 | return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; 873 | }); 874 | }, 875 | 876 | setSequence: function(element, new_sequence) { 877 | element = $(element); 878 | var options = Object.extend(this.options(element), arguments[2] || {}); 879 | 880 | var nodeMap = {}; 881 | this.findElements(element, options).each( function(n) { 882 | if (n.id.match(options.format)) 883 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; 884 | n.parentNode.removeChild(n); 885 | }); 886 | 887 | new_sequence.each(function(ident) { 888 | var n = nodeMap[ident]; 889 | if (n) { 890 | n[1].appendChild(n[0]); 891 | delete nodeMap[ident]; 892 | } 893 | }); 894 | }, 895 | 896 | serialize: function(element) { 897 | element = $(element); 898 | var options = Object.extend(Sortable.options(element), arguments[1] || {}); 899 | var name = encodeURIComponent( 900 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); 901 | 902 | if (options.tree) { 903 | return Sortable.tree(element, arguments[1]).children.map( function (item) { 904 | return [name + Sortable._constructIndex(item) + "[id]=" + 905 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); 906 | }).flatten().join('&'); 907 | } else { 908 | return Sortable.sequence(element, arguments[1]).map( function(item) { 909 | return name + "[]=" + encodeURIComponent(item); 910 | }).join('&'); 911 | } 912 | } 913 | } 914 | 915 | // Returns true if child is contained within element 916 | Element.isParent = function(child, element) { 917 | if (!child.parentNode || child == element) return false; 918 | if (child.parentNode == element) return true; 919 | return Element.isParent(child.parentNode, element); 920 | } 921 | 922 | Element.findChildren = function(element, only, recursive, tagName) { 923 | if(!element.hasChildNodes()) return null; 924 | tagName = tagName.toUpperCase(); 925 | if(only) only = [only].flatten(); 926 | var elements = []; 927 | $A(element.childNodes).each( function(e) { 928 | if(e.tagName && e.tagName.toUpperCase()==tagName && 929 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) 930 | elements.push(e); 931 | if(recursive) { 932 | var grandchildren = Element.findChildren(e, only, recursive, tagName); 933 | if(grandchildren) elements.push(grandchildren); 934 | } 935 | }); 936 | 937 | return (elements.length>0 ? elements.flatten() : []); 938 | } 939 | 940 | Element.offsetSize = function (element, type) { 941 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; 942 | } 943 | -------------------------------------------------------------------------------- /public/javascripts/effects.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 2 | // Contributors: 3 | // Justin Palmer (http://encytemedia.com/) 4 | // Mark Pilgrim (http://diveintomark.org/) 5 | // Martin Bialasinki 6 | // 7 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 8 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 9 | 10 | // converts rgb() and #xxx to #xxxxxx format, 11 | // returns self (or first argument) if not convertable 12 | String.prototype.parseColor = function() { 13 | var color = '#'; 14 | if(this.slice(0,4) == 'rgb(') { 15 | var cols = this.slice(4,this.length-1).split(','); 16 | var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); 17 | } else { 18 | if(this.slice(0,1) == '#') { 19 | if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); 20 | if(this.length==7) color = this.toLowerCase(); 21 | } 22 | } 23 | return(color.length==7 ? color : (arguments[0] || this)); 24 | } 25 | 26 | /*--------------------------------------------------------------------------*/ 27 | 28 | Element.collectTextNodes = function(element) { 29 | return $A($(element).childNodes).collect( function(node) { 30 | return (node.nodeType==3 ? node.nodeValue : 31 | (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); 32 | }).flatten().join(''); 33 | } 34 | 35 | Element.collectTextNodesIgnoreClass = function(element, className) { 36 | return $A($(element).childNodes).collect( function(node) { 37 | return (node.nodeType==3 ? node.nodeValue : 38 | ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 39 | Element.collectTextNodesIgnoreClass(node, className) : '')); 40 | }).flatten().join(''); 41 | } 42 | 43 | Element.setContentZoom = function(element, percent) { 44 | element = $(element); 45 | element.setStyle({fontSize: (percent/100) + 'em'}); 46 | if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 47 | return element; 48 | } 49 | 50 | Element.getOpacity = function(element){ 51 | element = $(element); 52 | var opacity; 53 | if (opacity = element.getStyle('opacity')) 54 | return parseFloat(opacity); 55 | if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) 56 | if(opacity[1]) return parseFloat(opacity[1]) / 100; 57 | return 1.0; 58 | } 59 | 60 | Element.setOpacity = function(element, value){ 61 | element= $(element); 62 | if (value == 1){ 63 | element.setStyle({ opacity: 64 | (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 65 | 0.999999 : 1.0 }); 66 | if(/MSIE/.test(navigator.userAgent) && !window.opera) 67 | element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); 68 | } else { 69 | if(value < 0.00001) value = 0; 70 | element.setStyle({opacity: value}); 71 | if(/MSIE/.test(navigator.userAgent) && !window.opera) 72 | element.setStyle( 73 | { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + 74 | 'alpha(opacity='+value*100+')' }); 75 | } 76 | return element; 77 | } 78 | 79 | Element.getInlineOpacity = function(element){ 80 | return $(element).style.opacity || ''; 81 | } 82 | 83 | Element.forceRerendering = function(element) { 84 | try { 85 | element = $(element); 86 | var n = document.createTextNode(' '); 87 | element.appendChild(n); 88 | element.removeChild(n); 89 | } catch(e) { } 90 | }; 91 | 92 | /*--------------------------------------------------------------------------*/ 93 | 94 | Array.prototype.call = function() { 95 | var args = arguments; 96 | this.each(function(f){ f.apply(this, args) }); 97 | } 98 | 99 | /*--------------------------------------------------------------------------*/ 100 | 101 | var Effect = { 102 | _elementDoesNotExistError: { 103 | name: 'ElementDoesNotExistError', 104 | message: 'The specified DOM element does not exist, but is required for this effect to operate' 105 | }, 106 | tagifyText: function(element) { 107 | if(typeof Builder == 'undefined') 108 | throw("Effect.tagifyText requires including script.aculo.us' builder.js library"); 109 | 110 | var tagifyStyle = 'position:relative'; 111 | if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1'; 112 | 113 | element = $(element); 114 | $A(element.childNodes).each( function(child) { 115 | if(child.nodeType==3) { 116 | child.nodeValue.toArray().each( function(character) { 117 | element.insertBefore( 118 | Builder.node('span',{style: tagifyStyle}, 119 | character == ' ' ? String.fromCharCode(160) : character), 120 | child); 121 | }); 122 | Element.remove(child); 123 | } 124 | }); 125 | }, 126 | multiple: function(element, effect) { 127 | var elements; 128 | if(((typeof element == 'object') || 129 | (typeof element == 'function')) && 130 | (element.length)) 131 | elements = element; 132 | else 133 | elements = $(element).childNodes; 134 | 135 | var options = Object.extend({ 136 | speed: 0.1, 137 | delay: 0.0 138 | }, arguments[2] || {}); 139 | var masterDelay = options.delay; 140 | 141 | $A(elements).each( function(element, index) { 142 | new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); 143 | }); 144 | }, 145 | PAIRS: { 146 | 'slide': ['SlideDown','SlideUp'], 147 | 'blind': ['BlindDown','BlindUp'], 148 | 'appear': ['Appear','Fade'] 149 | }, 150 | toggle: function(element, effect) { 151 | element = $(element); 152 | effect = (effect || 'appear').toLowerCase(); 153 | var options = Object.extend({ 154 | queue: { position:'end', scope:(element.id || 'global'), limit: 1 } 155 | }, arguments[2] || {}); 156 | Effect[element.visible() ? 157 | Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); 158 | } 159 | }; 160 | 161 | var Effect2 = Effect; // deprecated 162 | 163 | /* ------------- transitions ------------- */ 164 | 165 | Effect.Transitions = { 166 | linear: Prototype.K, 167 | sinoidal: function(pos) { 168 | return (-Math.cos(pos*Math.PI)/2) + 0.5; 169 | }, 170 | reverse: function(pos) { 171 | return 1-pos; 172 | }, 173 | flicker: function(pos) { 174 | return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; 175 | }, 176 | wobble: function(pos) { 177 | return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; 178 | }, 179 | pulse: function(pos, pulses) { 180 | pulses = pulses || 5; 181 | return ( 182 | Math.round((pos % (1/pulses)) * pulses) == 0 ? 183 | ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : 184 | 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) 185 | ); 186 | }, 187 | none: function(pos) { 188 | return 0; 189 | }, 190 | full: function(pos) { 191 | return 1; 192 | } 193 | }; 194 | 195 | /* ------------- core effects ------------- */ 196 | 197 | Effect.ScopedQueue = Class.create(); 198 | Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { 199 | initialize: function() { 200 | this.effects = []; 201 | this.interval = null; 202 | }, 203 | _each: function(iterator) { 204 | this.effects._each(iterator); 205 | }, 206 | add: function(effect) { 207 | var timestamp = new Date().getTime(); 208 | 209 | var position = (typeof effect.options.queue == 'string') ? 210 | effect.options.queue : effect.options.queue.position; 211 | 212 | switch(position) { 213 | case 'front': 214 | // move unstarted effects after this effect 215 | this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { 216 | e.startOn += effect.finishOn; 217 | e.finishOn += effect.finishOn; 218 | }); 219 | break; 220 | case 'with-last': 221 | timestamp = this.effects.pluck('startOn').max() || timestamp; 222 | break; 223 | case 'end': 224 | // start effect after last queued effect has finished 225 | timestamp = this.effects.pluck('finishOn').max() || timestamp; 226 | break; 227 | } 228 | 229 | effect.startOn += timestamp; 230 | effect.finishOn += timestamp; 231 | 232 | if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) 233 | this.effects.push(effect); 234 | 235 | if(!this.interval) 236 | this.interval = setInterval(this.loop.bind(this), 40); 237 | }, 238 | remove: function(effect) { 239 | this.effects = this.effects.reject(function(e) { return e==effect }); 240 | if(this.effects.length == 0) { 241 | clearInterval(this.interval); 242 | this.interval = null; 243 | } 244 | }, 245 | loop: function() { 246 | var timePos = new Date().getTime(); 247 | this.effects.invoke('loop', timePos); 248 | } 249 | }); 250 | 251 | Effect.Queues = { 252 | instances: $H(), 253 | get: function(queueName) { 254 | if(typeof queueName != 'string') return queueName; 255 | 256 | if(!this.instances[queueName]) 257 | this.instances[queueName] = new Effect.ScopedQueue(); 258 | 259 | return this.instances[queueName]; 260 | } 261 | } 262 | Effect.Queue = Effect.Queues.get('global'); 263 | 264 | Effect.DefaultOptions = { 265 | transition: Effect.Transitions.sinoidal, 266 | duration: 1.0, // seconds 267 | fps: 25.0, // max. 25fps due to Effect.Queue implementation 268 | sync: false, // true for combining 269 | from: 0.0, 270 | to: 1.0, 271 | delay: 0.0, 272 | queue: 'parallel' 273 | } 274 | 275 | Effect.Base = function() {}; 276 | Effect.Base.prototype = { 277 | position: null, 278 | start: function(options) { 279 | this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); 280 | this.currentFrame = 0; 281 | this.state = 'idle'; 282 | this.startOn = this.options.delay*1000; 283 | this.finishOn = this.startOn + (this.options.duration*1000); 284 | this.event('beforeStart'); 285 | if(!this.options.sync) 286 | Effect.Queues.get(typeof this.options.queue == 'string' ? 287 | 'global' : this.options.queue.scope).add(this); 288 | }, 289 | loop: function(timePos) { 290 | if(timePos >= this.startOn) { 291 | if(timePos >= this.finishOn) { 292 | this.render(1.0); 293 | this.cancel(); 294 | this.event('beforeFinish'); 295 | if(this.finish) this.finish(); 296 | this.event('afterFinish'); 297 | return; 298 | } 299 | var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); 300 | var frame = Math.round(pos * this.options.fps * this.options.duration); 301 | if(frame > this.currentFrame) { 302 | this.render(pos); 303 | this.currentFrame = frame; 304 | } 305 | } 306 | }, 307 | render: function(pos) { 308 | if(this.state == 'idle') { 309 | this.state = 'running'; 310 | this.event('beforeSetup'); 311 | if(this.setup) this.setup(); 312 | this.event('afterSetup'); 313 | } 314 | if(this.state == 'running') { 315 | if(this.options.transition) pos = this.options.transition(pos); 316 | pos *= (this.options.to-this.options.from); 317 | pos += this.options.from; 318 | this.position = pos; 319 | this.event('beforeUpdate'); 320 | if(this.update) this.update(pos); 321 | this.event('afterUpdate'); 322 | } 323 | }, 324 | cancel: function() { 325 | if(!this.options.sync) 326 | Effect.Queues.get(typeof this.options.queue == 'string' ? 327 | 'global' : this.options.queue.scope).remove(this); 328 | this.state = 'finished'; 329 | }, 330 | event: function(eventName) { 331 | if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); 332 | if(this.options[eventName]) this.options[eventName](this); 333 | }, 334 | inspect: function() { 335 | return '#'; 336 | } 337 | } 338 | 339 | Effect.Parallel = Class.create(); 340 | Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { 341 | initialize: function(effects) { 342 | this.effects = effects || []; 343 | this.start(arguments[1]); 344 | }, 345 | update: function(position) { 346 | this.effects.invoke('render', position); 347 | }, 348 | finish: function(position) { 349 | this.effects.each( function(effect) { 350 | effect.render(1.0); 351 | effect.cancel(); 352 | effect.event('beforeFinish'); 353 | if(effect.finish) effect.finish(position); 354 | effect.event('afterFinish'); 355 | }); 356 | } 357 | }); 358 | 359 | Effect.Event = Class.create(); 360 | Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), { 361 | initialize: function() { 362 | var options = Object.extend({ 363 | duration: 0 364 | }, arguments[0] || {}); 365 | this.start(options); 366 | }, 367 | update: Prototype.emptyFunction 368 | }); 369 | 370 | Effect.Opacity = Class.create(); 371 | Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { 372 | initialize: function(element) { 373 | this.element = $(element); 374 | if(!this.element) throw(Effect._elementDoesNotExistError); 375 | // make this work on IE on elements without 'layout' 376 | if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout)) 377 | this.element.setStyle({zoom: 1}); 378 | var options = Object.extend({ 379 | from: this.element.getOpacity() || 0.0, 380 | to: 1.0 381 | }, arguments[1] || {}); 382 | this.start(options); 383 | }, 384 | update: function(position) { 385 | this.element.setOpacity(position); 386 | } 387 | }); 388 | 389 | Effect.Move = Class.create(); 390 | Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { 391 | initialize: function(element) { 392 | this.element = $(element); 393 | if(!this.element) throw(Effect._elementDoesNotExistError); 394 | var options = Object.extend({ 395 | x: 0, 396 | y: 0, 397 | mode: 'relative' 398 | }, arguments[1] || {}); 399 | this.start(options); 400 | }, 401 | setup: function() { 402 | // Bug in Opera: Opera returns the "real" position of a static element or 403 | // relative element that does not have top/left explicitly set. 404 | // ==> Always set top and left for position relative elements in your stylesheets 405 | // (to 0 if you do not need them) 406 | this.element.makePositioned(); 407 | this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); 408 | this.originalTop = parseFloat(this.element.getStyle('top') || '0'); 409 | if(this.options.mode == 'absolute') { 410 | // absolute movement, so we need to calc deltaX and deltaY 411 | this.options.x = this.options.x - this.originalLeft; 412 | this.options.y = this.options.y - this.originalTop; 413 | } 414 | }, 415 | update: function(position) { 416 | this.element.setStyle({ 417 | left: Math.round(this.options.x * position + this.originalLeft) + 'px', 418 | top: Math.round(this.options.y * position + this.originalTop) + 'px' 419 | }); 420 | } 421 | }); 422 | 423 | // for backwards compatibility 424 | Effect.MoveBy = function(element, toTop, toLeft) { 425 | return new Effect.Move(element, 426 | Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); 427 | }; 428 | 429 | Effect.Scale = Class.create(); 430 | Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { 431 | initialize: function(element, percent) { 432 | this.element = $(element); 433 | if(!this.element) throw(Effect._elementDoesNotExistError); 434 | var options = Object.extend({ 435 | scaleX: true, 436 | scaleY: true, 437 | scaleContent: true, 438 | scaleFromCenter: false, 439 | scaleMode: 'box', // 'box' or 'contents' or {} with provided values 440 | scaleFrom: 100.0, 441 | scaleTo: percent 442 | }, arguments[2] || {}); 443 | this.start(options); 444 | }, 445 | setup: function() { 446 | this.restoreAfterFinish = this.options.restoreAfterFinish || false; 447 | this.elementPositioning = this.element.getStyle('position'); 448 | 449 | this.originalStyle = {}; 450 | ['top','left','width','height','fontSize'].each( function(k) { 451 | this.originalStyle[k] = this.element.style[k]; 452 | }.bind(this)); 453 | 454 | this.originalTop = this.element.offsetTop; 455 | this.originalLeft = this.element.offsetLeft; 456 | 457 | var fontSize = this.element.getStyle('font-size') || '100%'; 458 | ['em','px','%','pt'].each( function(fontSizeType) { 459 | if(fontSize.indexOf(fontSizeType)>0) { 460 | this.fontSize = parseFloat(fontSize); 461 | this.fontSizeType = fontSizeType; 462 | } 463 | }.bind(this)); 464 | 465 | this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; 466 | 467 | this.dims = null; 468 | if(this.options.scaleMode=='box') 469 | this.dims = [this.element.offsetHeight, this.element.offsetWidth]; 470 | if(/^content/.test(this.options.scaleMode)) 471 | this.dims = [this.element.scrollHeight, this.element.scrollWidth]; 472 | if(!this.dims) 473 | this.dims = [this.options.scaleMode.originalHeight, 474 | this.options.scaleMode.originalWidth]; 475 | }, 476 | update: function(position) { 477 | var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); 478 | if(this.options.scaleContent && this.fontSize) 479 | this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); 480 | this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); 481 | }, 482 | finish: function(position) { 483 | if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle); 484 | }, 485 | setDimensions: function(height, width) { 486 | var d = {}; 487 | if(this.options.scaleX) d.width = Math.round(width) + 'px'; 488 | if(this.options.scaleY) d.height = Math.round(height) + 'px'; 489 | if(this.options.scaleFromCenter) { 490 | var topd = (height - this.dims[0])/2; 491 | var leftd = (width - this.dims[1])/2; 492 | if(this.elementPositioning == 'absolute') { 493 | if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; 494 | if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; 495 | } else { 496 | if(this.options.scaleY) d.top = -topd + 'px'; 497 | if(this.options.scaleX) d.left = -leftd + 'px'; 498 | } 499 | } 500 | this.element.setStyle(d); 501 | } 502 | }); 503 | 504 | Effect.Highlight = Class.create(); 505 | Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { 506 | initialize: function(element) { 507 | this.element = $(element); 508 | if(!this.element) throw(Effect._elementDoesNotExistError); 509 | var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); 510 | this.start(options); 511 | }, 512 | setup: function() { 513 | // Prevent executing on elements not in the layout flow 514 | if(this.element.getStyle('display')=='none') { this.cancel(); return; } 515 | // Disable background image during the effect 516 | this.oldStyle = { 517 | backgroundImage: this.element.getStyle('background-image') }; 518 | this.element.setStyle({backgroundImage: 'none'}); 519 | if(!this.options.endcolor) 520 | this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); 521 | if(!this.options.restorecolor) 522 | this.options.restorecolor = this.element.getStyle('background-color'); 523 | // init color calculations 524 | this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); 525 | 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)); 526 | }, 527 | update: function(position) { 528 | this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ 529 | return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); 530 | }, 531 | finish: function() { 532 | this.element.setStyle(Object.extend(this.oldStyle, { 533 | backgroundColor: this.options.restorecolor 534 | })); 535 | } 536 | }); 537 | 538 | Effect.ScrollTo = Class.create(); 539 | Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { 540 | initialize: function(element) { 541 | this.element = $(element); 542 | this.start(arguments[1] || {}); 543 | }, 544 | setup: function() { 545 | Position.prepare(); 546 | var offsets = Position.cumulativeOffset(this.element); 547 | if(this.options.offset) offsets[1] += this.options.offset; 548 | var max = window.innerHeight ? 549 | window.height - window.innerHeight : 550 | document.body.scrollHeight - 551 | (document.documentElement.clientHeight ? 552 | document.documentElement.clientHeight : document.body.clientHeight); 553 | this.scrollStart = Position.deltaY; 554 | this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; 555 | }, 556 | update: function(position) { 557 | Position.prepare(); 558 | window.scrollTo(Position.deltaX, 559 | this.scrollStart + (position*this.delta)); 560 | } 561 | }); 562 | 563 | /* ------------- combination effects ------------- */ 564 | 565 | Effect.Fade = function(element) { 566 | element = $(element); 567 | var oldOpacity = element.getInlineOpacity(); 568 | var options = Object.extend({ 569 | from: element.getOpacity() || 1.0, 570 | to: 0.0, 571 | afterFinishInternal: function(effect) { 572 | if(effect.options.to!=0) return; 573 | effect.element.hide().setStyle({opacity: oldOpacity}); 574 | }}, arguments[1] || {}); 575 | return new Effect.Opacity(element,options); 576 | } 577 | 578 | Effect.Appear = function(element) { 579 | element = $(element); 580 | var options = Object.extend({ 581 | from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), 582 | to: 1.0, 583 | // force Safari to render floated elements properly 584 | afterFinishInternal: function(effect) { 585 | effect.element.forceRerendering(); 586 | }, 587 | beforeSetup: function(effect) { 588 | effect.element.setOpacity(effect.options.from).show(); 589 | }}, arguments[1] || {}); 590 | return new Effect.Opacity(element,options); 591 | } 592 | 593 | Effect.Puff = function(element) { 594 | element = $(element); 595 | var oldStyle = { 596 | opacity: element.getInlineOpacity(), 597 | position: element.getStyle('position'), 598 | top: element.style.top, 599 | left: element.style.left, 600 | width: element.style.width, 601 | height: element.style.height 602 | }; 603 | return new Effect.Parallel( 604 | [ new Effect.Scale(element, 200, 605 | { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 606 | new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 607 | Object.extend({ duration: 1.0, 608 | beforeSetupInternal: function(effect) { 609 | Position.absolutize(effect.effects[0].element) 610 | }, 611 | afterFinishInternal: function(effect) { 612 | effect.effects[0].element.hide().setStyle(oldStyle); } 613 | }, arguments[1] || {}) 614 | ); 615 | } 616 | 617 | Effect.BlindUp = function(element) { 618 | element = $(element); 619 | element.makeClipping(); 620 | return new Effect.Scale(element, 0, 621 | Object.extend({ scaleContent: false, 622 | scaleX: false, 623 | restoreAfterFinish: true, 624 | afterFinishInternal: function(effect) { 625 | effect.element.hide().undoClipping(); 626 | } 627 | }, arguments[1] || {}) 628 | ); 629 | } 630 | 631 | Effect.BlindDown = function(element) { 632 | element = $(element); 633 | var elementDimensions = element.getDimensions(); 634 | return new Effect.Scale(element, 100, Object.extend({ 635 | scaleContent: false, 636 | scaleX: false, 637 | scaleFrom: 0, 638 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 639 | restoreAfterFinish: true, 640 | afterSetup: function(effect) { 641 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 642 | }, 643 | afterFinishInternal: function(effect) { 644 | effect.element.undoClipping(); 645 | } 646 | }, arguments[1] || {})); 647 | } 648 | 649 | Effect.SwitchOff = function(element) { 650 | element = $(element); 651 | var oldOpacity = element.getInlineOpacity(); 652 | return new Effect.Appear(element, Object.extend({ 653 | duration: 0.4, 654 | from: 0, 655 | transition: Effect.Transitions.flicker, 656 | afterFinishInternal: function(effect) { 657 | new Effect.Scale(effect.element, 1, { 658 | duration: 0.3, scaleFromCenter: true, 659 | scaleX: false, scaleContent: false, restoreAfterFinish: true, 660 | beforeSetup: function(effect) { 661 | effect.element.makePositioned().makeClipping(); 662 | }, 663 | afterFinishInternal: function(effect) { 664 | effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); 665 | } 666 | }) 667 | } 668 | }, arguments[1] || {})); 669 | } 670 | 671 | Effect.DropOut = function(element) { 672 | element = $(element); 673 | var oldStyle = { 674 | top: element.getStyle('top'), 675 | left: element.getStyle('left'), 676 | opacity: element.getInlineOpacity() }; 677 | return new Effect.Parallel( 678 | [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 679 | new Effect.Opacity(element, { sync: true, to: 0.0 }) ], 680 | Object.extend( 681 | { duration: 0.5, 682 | beforeSetup: function(effect) { 683 | effect.effects[0].element.makePositioned(); 684 | }, 685 | afterFinishInternal: function(effect) { 686 | effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); 687 | } 688 | }, arguments[1] || {})); 689 | } 690 | 691 | Effect.Shake = function(element) { 692 | element = $(element); 693 | var oldStyle = { 694 | top: element.getStyle('top'), 695 | left: element.getStyle('left') }; 696 | return new Effect.Move(element, 697 | { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { 698 | new Effect.Move(effect.element, 699 | { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { 700 | new Effect.Move(effect.element, 701 | { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { 702 | new Effect.Move(effect.element, 703 | { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { 704 | new Effect.Move(effect.element, 705 | { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { 706 | new Effect.Move(effect.element, 707 | { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { 708 | effect.element.undoPositioned().setStyle(oldStyle); 709 | }}) }}) }}) }}) }}) }}); 710 | } 711 | 712 | Effect.SlideDown = function(element) { 713 | element = $(element).cleanWhitespace(); 714 | // SlideDown need to have the content of the element wrapped in a container element with fixed height! 715 | var oldInnerBottom = element.down().getStyle('bottom'); 716 | var elementDimensions = element.getDimensions(); 717 | return new Effect.Scale(element, 100, Object.extend({ 718 | scaleContent: false, 719 | scaleX: false, 720 | scaleFrom: window.opera ? 0 : 1, 721 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 722 | restoreAfterFinish: true, 723 | afterSetup: function(effect) { 724 | effect.element.makePositioned(); 725 | effect.element.down().makePositioned(); 726 | if(window.opera) effect.element.setStyle({top: ''}); 727 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 728 | }, 729 | afterUpdateInternal: function(effect) { 730 | effect.element.down().setStyle({bottom: 731 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 732 | }, 733 | afterFinishInternal: function(effect) { 734 | effect.element.undoClipping().undoPositioned(); 735 | effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } 736 | }, arguments[1] || {}) 737 | ); 738 | } 739 | 740 | Effect.SlideUp = function(element) { 741 | element = $(element).cleanWhitespace(); 742 | var oldInnerBottom = element.down().getStyle('bottom'); 743 | return new Effect.Scale(element, window.opera ? 0 : 1, 744 | Object.extend({ scaleContent: false, 745 | scaleX: false, 746 | scaleMode: 'box', 747 | scaleFrom: 100, 748 | restoreAfterFinish: true, 749 | beforeStartInternal: function(effect) { 750 | effect.element.makePositioned(); 751 | effect.element.down().makePositioned(); 752 | if(window.opera) effect.element.setStyle({top: ''}); 753 | effect.element.makeClipping().show(); 754 | }, 755 | afterUpdateInternal: function(effect) { 756 | effect.element.down().setStyle({bottom: 757 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 758 | }, 759 | afterFinishInternal: function(effect) { 760 | effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom}); 761 | effect.element.down().undoPositioned(); 762 | } 763 | }, arguments[1] || {}) 764 | ); 765 | } 766 | 767 | // Bug in opera makes the TD containing this element expand for a instance after finish 768 | Effect.Squish = function(element) { 769 | return new Effect.Scale(element, window.opera ? 1 : 0, { 770 | restoreAfterFinish: true, 771 | beforeSetup: function(effect) { 772 | effect.element.makeClipping(); 773 | }, 774 | afterFinishInternal: function(effect) { 775 | effect.element.hide().undoClipping(); 776 | } 777 | }); 778 | } 779 | 780 | Effect.Grow = function(element) { 781 | element = $(element); 782 | var options = Object.extend({ 783 | direction: 'center', 784 | moveTransition: Effect.Transitions.sinoidal, 785 | scaleTransition: Effect.Transitions.sinoidal, 786 | opacityTransition: Effect.Transitions.full 787 | }, arguments[1] || {}); 788 | var oldStyle = { 789 | top: element.style.top, 790 | left: element.style.left, 791 | height: element.style.height, 792 | width: element.style.width, 793 | opacity: element.getInlineOpacity() }; 794 | 795 | var dims = element.getDimensions(); 796 | var initialMoveX, initialMoveY; 797 | var moveX, moveY; 798 | 799 | switch (options.direction) { 800 | case 'top-left': 801 | initialMoveX = initialMoveY = moveX = moveY = 0; 802 | break; 803 | case 'top-right': 804 | initialMoveX = dims.width; 805 | initialMoveY = moveY = 0; 806 | moveX = -dims.width; 807 | break; 808 | case 'bottom-left': 809 | initialMoveX = moveX = 0; 810 | initialMoveY = dims.height; 811 | moveY = -dims.height; 812 | break; 813 | case 'bottom-right': 814 | initialMoveX = dims.width; 815 | initialMoveY = dims.height; 816 | moveX = -dims.width; 817 | moveY = -dims.height; 818 | break; 819 | case 'center': 820 | initialMoveX = dims.width / 2; 821 | initialMoveY = dims.height / 2; 822 | moveX = -dims.width / 2; 823 | moveY = -dims.height / 2; 824 | break; 825 | } 826 | 827 | return new Effect.Move(element, { 828 | x: initialMoveX, 829 | y: initialMoveY, 830 | duration: 0.01, 831 | beforeSetup: function(effect) { 832 | effect.element.hide().makeClipping().makePositioned(); 833 | }, 834 | afterFinishInternal: function(effect) { 835 | new Effect.Parallel( 836 | [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), 837 | new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), 838 | new Effect.Scale(effect.element, 100, { 839 | scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 840 | sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) 841 | ], Object.extend({ 842 | beforeSetup: function(effect) { 843 | effect.effects[0].element.setStyle({height: '0px'}).show(); 844 | }, 845 | afterFinishInternal: function(effect) { 846 | effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 847 | } 848 | }, options) 849 | ) 850 | } 851 | }); 852 | } 853 | 854 | Effect.Shrink = function(element) { 855 | element = $(element); 856 | var options = Object.extend({ 857 | direction: 'center', 858 | moveTransition: Effect.Transitions.sinoidal, 859 | scaleTransition: Effect.Transitions.sinoidal, 860 | opacityTransition: Effect.Transitions.none 861 | }, arguments[1] || {}); 862 | var oldStyle = { 863 | top: element.style.top, 864 | left: element.style.left, 865 | height: element.style.height, 866 | width: element.style.width, 867 | opacity: element.getInlineOpacity() }; 868 | 869 | var dims = element.getDimensions(); 870 | var moveX, moveY; 871 | 872 | switch (options.direction) { 873 | case 'top-left': 874 | moveX = moveY = 0; 875 | break; 876 | case 'top-right': 877 | moveX = dims.width; 878 | moveY = 0; 879 | break; 880 | case 'bottom-left': 881 | moveX = 0; 882 | moveY = dims.height; 883 | break; 884 | case 'bottom-right': 885 | moveX = dims.width; 886 | moveY = dims.height; 887 | break; 888 | case 'center': 889 | moveX = dims.width / 2; 890 | moveY = dims.height / 2; 891 | break; 892 | } 893 | 894 | return new Effect.Parallel( 895 | [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), 896 | new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), 897 | new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) 898 | ], Object.extend({ 899 | beforeStartInternal: function(effect) { 900 | effect.effects[0].element.makePositioned().makeClipping(); 901 | }, 902 | afterFinishInternal: function(effect) { 903 | effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } 904 | }, options) 905 | ); 906 | } 907 | 908 | Effect.Pulsate = function(element) { 909 | element = $(element); 910 | var options = arguments[1] || {}; 911 | var oldOpacity = element.getInlineOpacity(); 912 | var transition = options.transition || Effect.Transitions.sinoidal; 913 | var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; 914 | reverser.bind(transition); 915 | return new Effect.Opacity(element, 916 | Object.extend(Object.extend({ duration: 2.0, from: 0, 917 | afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } 918 | }, options), {transition: reverser})); 919 | } 920 | 921 | Effect.Fold = function(element) { 922 | element = $(element); 923 | var oldStyle = { 924 | top: element.style.top, 925 | left: element.style.left, 926 | width: element.style.width, 927 | height: element.style.height }; 928 | element.makeClipping(); 929 | return new Effect.Scale(element, 5, Object.extend({ 930 | scaleContent: false, 931 | scaleX: false, 932 | afterFinishInternal: function(effect) { 933 | new Effect.Scale(element, 1, { 934 | scaleContent: false, 935 | scaleY: false, 936 | afterFinishInternal: function(effect) { 937 | effect.element.hide().undoClipping().setStyle(oldStyle); 938 | } }); 939 | }}, arguments[1] || {})); 940 | }; 941 | 942 | Effect.Morph = Class.create(); 943 | Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), { 944 | initialize: function(element) { 945 | this.element = $(element); 946 | if(!this.element) throw(Effect._elementDoesNotExistError); 947 | var options = Object.extend({ 948 | style: '' 949 | }, arguments[1] || {}); 950 | this.start(options); 951 | }, 952 | setup: function(){ 953 | function parseColor(color){ 954 | if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; 955 | color = color.parseColor(); 956 | return $R(0,2).map(function(i){ 957 | return parseInt( color.slice(i*2+1,i*2+3), 16 ) 958 | }); 959 | } 960 | this.transforms = this.options.style.parseStyle().map(function(property){ 961 | var originalValue = this.element.getStyle(property[0]); 962 | return $H({ 963 | style: property[0], 964 | originalValue: property[1].unit=='color' ? 965 | parseColor(originalValue) : parseFloat(originalValue || 0), 966 | targetValue: property[1].unit=='color' ? 967 | parseColor(property[1].value) : property[1].value, 968 | unit: property[1].unit 969 | }); 970 | }.bind(this)).reject(function(transform){ 971 | return ( 972 | (transform.originalValue == transform.targetValue) || 973 | ( 974 | transform.unit != 'color' && 975 | (isNaN(transform.originalValue) || isNaN(transform.targetValue)) 976 | ) 977 | ) 978 | }); 979 | }, 980 | update: function(position) { 981 | var style = $H(), value = null; 982 | this.transforms.each(function(transform){ 983 | value = transform.unit=='color' ? 984 | $R(0,2).inject('#',function(m,v,i){ 985 | return m+(Math.round(transform.originalValue[i]+ 986 | (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) : 987 | transform.originalValue + Math.round( 988 | ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit; 989 | style[transform.style] = value; 990 | }); 991 | this.element.setStyle(style); 992 | } 993 | }); 994 | 995 | Effect.Transform = Class.create(); 996 | Object.extend(Effect.Transform.prototype, { 997 | initialize: function(tracks){ 998 | this.tracks = []; 999 | this.options = arguments[1] || {}; 1000 | this.addTracks(tracks); 1001 | }, 1002 | addTracks: function(tracks){ 1003 | tracks.each(function(track){ 1004 | var data = $H(track).values().first(); 1005 | this.tracks.push($H({ 1006 | ids: $H(track).keys().first(), 1007 | effect: Effect.Morph, 1008 | options: { style: data } 1009 | })); 1010 | }.bind(this)); 1011 | return this; 1012 | }, 1013 | play: function(){ 1014 | return new Effect.Parallel( 1015 | this.tracks.map(function(track){ 1016 | var elements = [$(track.ids) || $$(track.ids)].flatten(); 1017 | return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) }); 1018 | }).flatten(), 1019 | this.options 1020 | ); 1021 | } 1022 | }); 1023 | 1024 | Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage', 1025 | 'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle', 1026 | 'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth', 1027 | 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor', 1028 | 'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content', 1029 | 'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction', 1030 | 'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch', 1031 | 'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight', 1032 | 'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight', 1033 | 'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity', 1034 | 'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY', 1035 | 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore', 1036 | 'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes', 1037 | 'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress', 1038 | 'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top', 1039 | 'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows', 1040 | 'width', 'wordSpacing', 'zIndex']; 1041 | 1042 | Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; 1043 | 1044 | String.prototype.parseStyle = function(){ 1045 | var element = Element.extend(document.createElement('div')); 1046 | element.innerHTML = '

    '; 1047 | var style = element.down().style, styleRules = $H(); 1048 | 1049 | Element.CSS_PROPERTIES.each(function(property){ 1050 | if(style[property]) styleRules[property] = style[property]; 1051 | }); 1052 | 1053 | var result = $H(); 1054 | 1055 | styleRules.each(function(pair){ 1056 | var property = pair[0], value = pair[1], unit = null; 1057 | 1058 | if(value.parseColor('#zzzzzz') != '#zzzzzz') { 1059 | value = value.parseColor(); 1060 | unit = 'color'; 1061 | } else if(Element.CSS_LENGTH.test(value)) 1062 | var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/), 1063 | value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null; 1064 | 1065 | result[property.underscore().dasherize()] = $H({ value:value, unit:unit }); 1066 | }.bind(this)); 1067 | 1068 | return result; 1069 | }; 1070 | 1071 | Element.morph = function(element, style) { 1072 | new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {})); 1073 | return element; 1074 | }; 1075 | 1076 | ['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', 1077 | 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( 1078 | function(f) { Element.Methods[f] = Element[f]; } 1079 | ); 1080 | 1081 | Element.Methods.visualEffect = function(element, effect, options) { 1082 | s = effect.gsub(/_/, '-').camelize(); 1083 | effect_class = s.charAt(0).toUpperCase() + s.substring(1); 1084 | new Effect[effect_class](element, options); 1085 | return $(element); 1086 | }; 1087 | 1088 | Element.addMethods(); --------------------------------------------------------------------------------