├── spec ├── dummy │ ├── public │ │ ├── favicon.ico │ │ ├── stylesheets │ │ │ └── .gitkeep │ │ ├── javascripts │ │ │ ├── application.js │ │ │ ├── rails.js │ │ │ ├── dragdrop.js │ │ │ ├── controls.js │ │ │ └── effects.js │ │ ├── 422.html │ │ ├── 404.html │ │ └── 500.html │ ├── app │ │ ├── views │ │ │ ├── notes │ │ │ │ └── index.html.erb │ │ │ ├── partial_demos │ │ │ │ ├── easy.html.erb │ │ │ │ └── with_variable.html.erb │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ ├── helpers │ │ │ ├── notes_helper.rb │ │ │ └── application_helper.rb │ │ ├── models │ │ │ └── note.rb │ │ ├── test_views │ │ │ ├── eco_views │ │ │ │ ├── partial_demos │ │ │ │ │ ├── _easy_partial.html.eco │ │ │ │ │ └── _with_variable_partial.html.eco │ │ │ │ ├── notes │ │ │ │ │ ├── _note.html.eco │ │ │ │ │ └── show.html.eco │ │ │ │ └── navigation_demos │ │ │ │ │ ├── sample_nav_bar.html.eco │ │ │ │ │ ├── override_nav_bar.html.eco │ │ │ │ │ └── routed_nav_bar.html.eco │ │ │ └── coffeekup_views │ │ │ │ ├── partial_demos │ │ │ │ ├── _easy_partial.html.coffeekup │ │ │ │ └── _with_variable_partial.html.coffeekup │ │ │ │ ├── notes │ │ │ │ ├── _note.html.coffeekup │ │ │ │ └── show.html.coffeekup │ │ │ │ └── navigation_demos │ │ │ │ ├── sample_nav_bar.html.coffeekup │ │ │ │ ├── routed_nav_bar.html.coffeekup │ │ │ │ └── override_nav_bar.html.coffeekup │ │ ├── cubes │ │ │ └── note_cube.rb │ │ └── controllers │ │ │ ├── partial_demos_controller.rb │ │ │ ├── navigation_demos_controller.rb │ │ │ ├── notes_controller.rb │ │ │ └── application_controller.rb │ ├── config │ │ ├── routes.rb │ │ ├── environment.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── initializers │ │ │ ├── mime_types.rb │ │ │ ├── inflections.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── session_store.rb │ │ │ └── secret_token.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ └── application.rb │ ├── config.ru │ ├── Rakefile │ ├── db │ │ ├── migrate │ │ │ ├── 20110703014136_add_rating.rb │ │ │ └── 20110531164457_create_notes.rb │ │ └── schema.rb │ └── script │ │ └── rails ├── javascripts │ ├── helpers │ │ └── SpecHelper.js │ ├── support │ │ ├── jasmine_config.rb │ │ ├── jasmine_runner.rb │ │ └── jasmine.yml │ ├── path-helper-spec.coffee │ ├── form-tag-spec.coffee │ ├── path-helper-spec.js │ └── form-tag-spec.js ├── integration │ ├── coffeekup_renderer_spec.rb │ ├── eco_renderer_spec.rb │ ├── navigation_spec.rb │ └── template_renderer_group.rb ├── to_ice_spec.rb ├── base_cube_spec.rb ├── spec_helper.rb └── cube_spec.rb ├── init.rb ├── lib ├── ice │ ├── railtie.rb │ ├── cube_helpers.rb │ ├── cubeable.rb │ ├── generated_helpers.rb │ ├── cube_association.rb │ ├── handlers │ │ ├── eco │ │ │ └── handler.rb │ │ ├── base.rb │ │ └── coffeekup │ │ │ └── handler.rb │ └── base_cube.rb └── ice.rb ├── .gitignore ├── Gemfile ├── js ├── lib │ ├── coffeekup-path-helper.coffee │ ├── coffeekup-path-helper.js │ ├── eco-path-helper.coffee │ ├── eco-path-helper.js │ ├── form-tag-inputs.coffee │ └── form-tag-inputs.js ├── Cakefile └── coffeekup.js ├── ice.gemspec ├── Rakefile ├── MIT-LICENSE ├── Gemfile.lock └── README.markdown /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/SpecHelper.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notes/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render @notes %> -------------------------------------------------------------------------------- /spec/dummy/app/helpers/notes_helper.rb: -------------------------------------------------------------------------------- 1 | module NotesHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/models/note.rb: -------------------------------------------------------------------------------- 1 | class Note < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/views/partial_demos/easy.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "easy_partial" %> -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/test_views/eco_views/partial_demos/_easy_partial.html.eco: -------------------------------------------------------------------------------- 1 | Hello From Partial -------------------------------------------------------------------------------- /spec/dummy/app/cubes/note_cube.rb: -------------------------------------------------------------------------------- 1 | class NoteCube < Ice::BaseCube 2 | revealing :name, :data 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/test_views/eco_views/partial_demos/_with_variable_partial.html.eco: -------------------------------------------------------------------------------- 1 | Hello From <%= @variable %> -------------------------------------------------------------------------------- /spec/dummy/app/test_views/coffeekup_views/partial_demos/_easy_partial.html.coffeekup: -------------------------------------------------------------------------------- 1 | p -> "Hello From Partial" -------------------------------------------------------------------------------- /spec/dummy/app/test_views/coffeekup_views/partial_demos/_with_variable_partial.html.coffeekup: -------------------------------------------------------------------------------- 1 | p -> "Hello From #{variable}" -------------------------------------------------------------------------------- /spec/dummy/app/views/partial_demos/with_variable.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "with_variable_partial", :variable => "Variable" %> -------------------------------------------------------------------------------- /spec/dummy/app/test_views/coffeekup_views/notes/_note.html.coffeekup: -------------------------------------------------------------------------------- 1 | frameset -> 2 | legend -> @note.name 3 | p -> @note.data -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | resources :notes 3 | match ':controller/:action' 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/test_views/eco_views/notes/_note.html.eco: -------------------------------------------------------------------------------- 1 | 2 | <%= @note.name %> 3 | <%= @note.data %> 4 | -------------------------------------------------------------------------------- /spec/dummy/app/test_views/coffeekup_views/navigation_demos/sample_nav_bar.html.coffeekup: -------------------------------------------------------------------------------- 1 | navBar (o)=> 2 | o.linkTo("Bar", "/foo") 3 | o.linkTo("http://ludicast.com") -------------------------------------------------------------------------------- /spec/dummy/app/controllers/partial_demos_controller.rb: -------------------------------------------------------------------------------- 1 | class PartialDemosController < ApplicationController 2 | def easy 3 | 4 | end 5 | def with_variable 6 | 7 | end 8 | end -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/dummy/app/test_views/eco_views/navigation_demos/sample_nav_bar.html.eco: -------------------------------------------------------------------------------- 1 | <%= navBar {}, (bar)=> %> 2 | <%= bar.linkTo("Bar", "/foo") %> 3 | <%= bar.linkTo("http://ludicast.com") %> 4 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/app/test_views/coffeekup_views/notes/show.html.coffeekup: -------------------------------------------------------------------------------- 1 | p -> 2 | b -> "Name:" 3 | @note.name 4 | 5 | p -> 6 | b -> "Data:" 7 | @note.data 8 | 9 | p -> linkTo "All Notes", notesPath() -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/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 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'ice' 2 | require 'ice/cubeable' 3 | require 'ice/cube_association' 4 | require 'ice/base_cube' 5 | 6 | require 'rails' 7 | require 'active_model/serialization' 8 | 9 | require 'ice/railtie' if defined?(Rails) -------------------------------------------------------------------------------- /spec/dummy/app/test_views/eco_views/notes/show.html.eco: -------------------------------------------------------------------------------- 1 |

2 | Name: 3 | <%= @note.name %> 4 |

5 | 6 |

7 | Data: 8 | <%= @note.data %> 9 |

10 | <%- linkTo("All Notes", notesPath()) %> -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy/app/test_views/coffeekup_views/navigation_demos/routed_nav_bar.html.coffeekup: -------------------------------------------------------------------------------- 1 | navBar (o) => 2 | o.linkTo("All Notes", notesPath()) 3 | o.linkTo("New Note", newNotePath()) 4 | o.linkTo("Note Details", notePath(@note)) 5 | o.linkTo("Edit This Note", editNotePath(@note)) -------------------------------------------------------------------------------- /spec/dummy/app/test_views/eco_views/navigation_demos/override_nav_bar.html.eco: -------------------------------------------------------------------------------- 1 | <% opts = navPrefix:'
', navPostfix: '
', linkPrefix: '', link_Postfix: '' %> 2 | <%= navBar opts, (bar)=> %> 3 | <%= bar.linkTo("Bar", "/foo") %> 4 | <%= bar.linkTo("http://ludicast.com") %> 5 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /spec/dummy/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.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/ice/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/serialization' 2 | require 'action_view' 3 | 4 | module Ice 5 | class Railtie < Rails::Railtie 6 | initializer "ice.configure_rails_initialization" do 7 | 8 | end 9 | end 10 | 11 | end 12 | 13 | ActiveModel::Serialization.send(:include, Ice::Cubeable) -------------------------------------------------------------------------------- /spec/dummy/app/test_views/eco_views/navigation_demos/routed_nav_bar.html.eco: -------------------------------------------------------------------------------- 1 | <%= navBar {}, (bar)=> %> 2 | <%= bar.linkTo("All Notes", notesPath()) %> 3 | <%= bar.linkTo("New Note", newNotePath()) %> 4 | <%= bar.linkTo("Note Details", notePath(@note)) %> 5 | <%= bar.linkTo("Edit This Note", editNotePath(@note)) %> 6 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20110703014136_add_rating.rb: -------------------------------------------------------------------------------- 1 | class AddRating < ActiveRecord::Migration 2 | def self.up 3 | change_table :notes do |t| 4 | t.string :secret_data, :default => "Secret Data" 5 | end 6 | end 7 | 8 | def self.down 9 | remove_column :notes, :secret_data 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/integration/coffeekup_renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'integration/template_renderer_group' 3 | 4 | describe "Coffeekup Renderer" do 5 | include Capybara::DSL 6 | before(:all) do 7 | set_js_handler(:coffeekup) 8 | end 9 | 10 | it_should_behave_like "Template Renderer" 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20110531164457_create_notes.rb: -------------------------------------------------------------------------------- 1 | class CreateNotes < ActiveRecord::Migration 2 | def self.up 3 | create_table :notes do |t| 4 | t.string :name 5 | t.text :data 6 | 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :notes 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | .idea 22 | 23 | ## PROJECT::SPECIFIC 24 | spec/dummy/db/*.sqlite3 25 | spec/dummy/log/*.log 26 | 27 | *.gem 28 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/navigation_demos_controller.rb: -------------------------------------------------------------------------------- 1 | class NavigationDemosController < ApplicationController 2 | def sample_nav_bar 3 | 4 | end 5 | def override_nav_bar 6 | 7 | end 8 | def routed_nav_bar 9 | @note = Note.last || Note.create!(:name => "yoo #{rand(100)}", :data => "wee #{rand(200)}") 10 | end 11 | 12 | end -------------------------------------------------------------------------------- /spec/dummy/app/test_views/coffeekup_views/navigation_demos/override_nav_bar.html.coffeekup: -------------------------------------------------------------------------------- 1 | opts = 2 | navWrap: (innerFunc)-> 3 | div -> 4 | innerFunc() 5 | linkWrap: (innerFunc) -> 6 | span -> innerFunc() 7 | 8 | navBar opts, (bar)=> 9 | bar.linkTo("Bar", "/foo") 10 | bar.linkTo("http://ludicast.com") -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "rails", "3.0.7" 4 | gem 'rake', '0.8.7' 5 | gem "capybara", ">= 0.4.0" 6 | gem "sqlite3" 7 | 8 | gem "rspec-rails", ">= 2.0.0.beta" 9 | 10 | gem "therubyracer", "0.9.2" 11 | gem "informal" 12 | 13 | gem "jasmine" 14 | 15 | gem "eco" 16 | # To use debugger (ruby-debug for Ruby 1.8.7+, ruby-debug19 for Ruby 1.9.2+) 17 | # gem 'ruby-debug' 18 | # gem 'ruby-debug19' 19 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/to_ice_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | describe "to_ice" do 4 | context "when there exists a active model without a to_ice method on it" do 5 | before do 6 | @my_class = Class.new do 7 | include ActiveModel::Serialization 8 | end 9 | end 10 | 11 | specify { 12 | expect { @my_class.new.to_ice }.to raise_error "Cannot find Cube class for model that you are calling to_ice on." } 13 | 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /lib/ice/cube_helpers.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | def to_ice 3 | nil 4 | end 5 | end 6 | 7 | [FalseClass, TrueClass, Numeric, String].each do |cls| 8 | cls.class_eval do 9 | def to_ice 10 | self 11 | end 12 | end 13 | end 14 | 15 | class Array 16 | def to_ice 17 | map &:to_ice 18 | end 19 | end 20 | 21 | class Hash 22 | def to_ice 23 | res = {} 24 | each_pair do |key,value| 25 | res[key] = value.to_ice 26 | end 27 | res 28 | end 29 | end -------------------------------------------------------------------------------- /js/lib/coffeekup-path-helper.coffee: -------------------------------------------------------------------------------- 1 | linkTo = (label, link, opts) -> 2 | if (! link) 3 | link = label 4 | a href:link, -> label 5 | 6 | navBar = (args...)-> 7 | if args.length == 2 8 | opts = args.shift() 9 | else 10 | opts = {} 11 | f = args[0] 12 | 13 | navWrap = opts.navWrap || ul 14 | linkWrap = opts.linkWrap || li 15 | 16 | methods = 17 | linkTo: (name, href)-> 18 | if (! href) 19 | href = name 20 | linkWrap -> linkTo name, href 21 | 22 | navWrap -> 23 | f(methods) -------------------------------------------------------------------------------- /js/Cakefile: -------------------------------------------------------------------------------- 1 | 2 | {spawn, exec} = require 'child_process' 3 | sys = require 'sys' 4 | 5 | task 'assets:watch', 'Watch source files and build JS', (options) -> 6 | runCommand = (name, args...) -> 7 | proc = spawn name, args 8 | proc.stderr.on 'data', (buffer) -> console.log buffer.toString() 9 | proc.stdout.on 'data', (buffer) -> console.log buffer.toString() 10 | proc.on 'exit', (status) -> process.exit(1) if status isnt 0 11 | 12 | runCommand 'coffee', '-wcb', '../spec/javascripts', 'lib' -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = '9ecfa9b4cd177011873f76d0c1803a86f38ca612e113f61a29e7289dd67dc6792811723039c70ae2245c50a2e6935acbcb8326ee37c24f35545448705bec72d8' 8 | -------------------------------------------------------------------------------- /lib/ice/cubeable.rb: -------------------------------------------------------------------------------- 1 | module Ice 2 | module Cubeable 3 | def get_cube_class(class_obj) 4 | begin 5 | cube_string = class_obj.to_s + "Cube" 6 | cube_string.constantize 7 | rescue 8 | get_cube_class class_obj.superclass 9 | end 10 | end 11 | 12 | def to_ice 13 | begin 14 | cube_class = get_cube_class self.class 15 | cube_class.new self 16 | rescue 17 | raise "Cannot find Cube class for model that you are calling to_ice on." 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | pool: 5 22 | timeout: 5000 23 | -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine_config.rb: -------------------------------------------------------------------------------- 1 | module Jasmine 2 | class Config 3 | 4 | # Add your overrides or custom config code here 5 | 6 | end 7 | end 8 | 9 | 10 | # Note - this is necessary for rspec2, which has removed the backtrace 11 | module Jasmine 12 | class SpecBuilder 13 | def declare_spec(parent, spec) 14 | me = self 15 | example_name = spec["name"] 16 | @spec_ids << spec["id"] 17 | backtrace = @example_locations[parent.description + " " + example_name] 18 | parent.it example_name, {} do 19 | me.report_spec(spec["id"]) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/notes_controller.rb: -------------------------------------------------------------------------------- 1 | class NotesController < ApplicationController 2 | respond_to :html, :ice 3 | 4 | before_filter do 5 | perform_caching = false 6 | # puts "hola" 7 | # puts methods 8 | # puts methods.grep /view/ 9 | # puts methods.grep /cache/ 10 | # puts methods.grep /hand/ 11 | end 12 | def show 13 | @note = Note.find(params[:id]) 14 | respond_with(@note) do |format| 15 | format.ice { render :text => @note.to_ice.to_json } 16 | end 17 | end 18 | def index 19 | @notes = Note.all 20 | respond_with(@notes) do |format| 21 | format.ice { render :text => @notes.to_ice.to_json } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /ice.gemspec: -------------------------------------------------------------------------------- 1 | # Provide a simple gemspec so you can easily use your enginex 2 | # project in your rails apps through git. 3 | Gem::Specification.new do |s| 4 | s.name = "ice" 5 | s.summary = %q{User templates written in javascript} 6 | s.authors = ["Nate Kidwell"] 7 | s.date = %q{2011-07-03} 8 | s.description = %q{User templates written in javascript} 9 | s.email = %q{nate@ludicast.com} 10 | s.files = Dir["{app,lib,config,js}/**/*"] + ["MIT-LICENSE", "Rakefile", "Gemfile", "README.markdown"] 11 | s.version = "0.5.1" 12 | s.add_dependency("eco", '>= 1.0.0') 13 | s.add_dependency("therubyracer", '>= 0.9.1') 14 | s.rdoc_options = ["--charset=UTF-8"] 15 | s.homepage = %q{http://github.com/ludicast/ice} 16 | end 17 | -------------------------------------------------------------------------------- /lib/ice/generated_helpers.rb: -------------------------------------------------------------------------------- 1 | module Ice 2 | module GeneratedHelpers 3 | def self.get_routes 4 | coffeescript = "" 5 | Ice::BaseCube.subclasses.map(&:name).each do |cube_model_name| 6 | model_name = cube_model_name.sub(/Cube/, "") 7 | name = model_name[0].downcase + model_name[1..-1] 8 | 9 | coffeescript << <<-COFFEESCRIPT 10 | 11 | edit#{model_name}Path = (object)-> 12 | "/#{name.tableize}/" + object.id + "/edit" 13 | 14 | new#{model_name}Path = ()-> 15 | "/#{name.tableize}/new" 16 | 17 | #{name}Path = (object)-> 18 | "/#{name.tableize}/" + object.id 19 | 20 | #{name.pluralize}Path = ()-> 21 | "/#{name.tableize}" 22 | 23 | COFFEESCRIPT 24 | end 25 | coffeescript 26 | end 27 | 28 | end 29 | end -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

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

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/ice.rb: -------------------------------------------------------------------------------- 1 | require 'ice' 2 | require 'ice/cubeable' 3 | require 'ice/cube_association' 4 | require 'ice/base_cube' 5 | 6 | require 'ice/railtie' 7 | require 'ice/cube_helpers' 8 | require 'rails' 9 | 10 | require 'ice/handlers/eco/handler' 11 | require 'ice/handlers/coffeekup/handler' 12 | 13 | IceJavascriptHelpers = [] 14 | IceCoffeescriptHelpers = [] 15 | 16 | ActionView::Template.register_template_handler :coffeekup, Ice::Handlers::Coffeekup 17 | ActionView::Template.register_template_handler :eco, Ice::Handlers::Eco 18 | 19 | require "action_controller" 20 | Mime::Type.register "text/ice", :ice 21 | 22 | ActionController::Renderers.add :ice do |object, options| 23 | puts "oootototOOOOOOOOOO" 24 | puts "rendering with #{object}" 25 | puts "rendering to #{object.to_ice.to_json}" 26 | self.send_data object.to_ice.to_json, :type => :ice 27 | end -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

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

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

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

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rake' 10 | require 'rake/rdoctask' 11 | 12 | require 'rspec/core' 13 | require 'rspec/core/rake_task' 14 | 15 | RSpec::Core::RakeTask.new(:spec) 16 | 17 | task :default => :spec 18 | 19 | Rake::RDocTask.new(:rdoc) do |rdoc| 20 | rdoc.rdoc_dir = 'rdoc' 21 | rdoc.title = 'Ice' 22 | rdoc.options << '--line-numbers' << '--inline-source' 23 | rdoc.rdoc_files.include('README.rdoc') 24 | rdoc.rdoc_files.include('lib/**/*.rb') 25 | end 26 | 27 | begin 28 | require 'jasmine' 29 | load 'jasmine/tasks/jasmine.rake' 30 | rescue LoadError 31 | task :jasmine do 32 | abort "Jasmine is not available. In order to run jasmine, you must: (sudo) gem install jasmine" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ice/cube_association.rb: -------------------------------------------------------------------------------- 1 | module Ice 2 | module CubeAssociation 3 | def belongs_to(*args) 4 | args.each do |sym| 5 | belongs_to = %{ 6 | def #{sym} 7 | @source.#{sym}.to_ice 8 | end 9 | def #{sym}_id 10 | @source.#{sym}_id 11 | end 12 | } 13 | class_eval belongs_to 14 | end 15 | end 16 | 17 | def has_many(*args) 18 | args.each do |sym| 19 | has_many = %{ 20 | def #{sym} 21 | @source.#{sym}.map(&:to_ice) 22 | end 23 | def has_#{sym} 24 | ! @source.#{sym}.empty? 25 | end 26 | def num_#{sym} 27 | @source.#{sym}.count 28 | end 29 | def #{sym.to_s.singularize}_ids 30 | @source.#{sym.to_s.singularize}_ids 31 | end 32 | } 33 | class_eval has_many 34 | end 35 | 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /spec/base_cube_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | class FooClassCube < Ice::BaseCube 4 | revealing :first, :second 5 | end 6 | 7 | class FooClass 8 | include Ice::Cubeable 9 | 10 | def first 11 | "primero" 12 | end 13 | 14 | def second 15 | @second ||= SecondClass.new 16 | end 17 | end 18 | 19 | class SecondClass 20 | def to_ice 21 | "segundo" 22 | end 23 | 24 | end 25 | 26 | describe "BaseCube" do 27 | context "a cubeable class" do 28 | it "should automatically to_ice the cube_class" do 29 | FooClass.new.to_ice.class.should == FooClassCube 30 | end 31 | 32 | it "should retrieve revealed properties" do 33 | FooClass.new.to_ice.first.should == "primero" 34 | end 35 | 36 | it "should map revealed properties via to_ice" do 37 | FooClass.new.to_ice.second.should == "segundo" 38 | end 39 | 40 | end 41 | 42 | 43 | 44 | end -------------------------------------------------------------------------------- /js/lib/coffeekup-path-helper.js: -------------------------------------------------------------------------------- 1 | var linkTo, navBar; 2 | var __slice = Array.prototype.slice; 3 | linkTo = function(label, link, opts) { 4 | if (!link) { 5 | link = label; 6 | } 7 | return a({ 8 | href: link 9 | }, function() { 10 | return label; 11 | }); 12 | }; 13 | navBar = function() { 14 | var args, f, linkWrap, methods, navWrap, opts; 15 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 16 | if (args.length === 2) { 17 | opts = args.shift(); 18 | } else { 19 | opts = {}; 20 | } 21 | f = args[0]; 22 | navWrap = opts.navWrap || ul; 23 | linkWrap = opts.linkWrap || li; 24 | methods = { 25 | linkTo: function(name, href) { 26 | if (!href) { 27 | href = name; 28 | } 29 | return linkWrap(function() { 30 | return linkTo(name, href); 31 | }); 32 | } 33 | }; 34 | return navWrap(function() { 35 | return f(methods); 36 | }); 37 | }; -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine_runner.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(ENV['JASMINE_GEM_PATH']) if ENV['JASMINE_GEM_PATH'] # for gem testing purposes 2 | 3 | require 'rubygems' 4 | require 'jasmine' 5 | jasmine_config_overrides = File.expand_path(File.join(File.dirname(__FILE__), 'jasmine_config.rb')) 6 | require jasmine_config_overrides if File.exist?(jasmine_config_overrides) 7 | if Jasmine::rspec2? 8 | require 'rspec' 9 | else 10 | require 'spec' 11 | end 12 | 13 | jasmine_config = Jasmine::Config.new 14 | spec_builder = Jasmine::SpecBuilder.new(jasmine_config) 15 | 16 | should_stop = false 17 | 18 | if Jasmine::rspec2? 19 | RSpec.configuration.after(:suite) do 20 | spec_builder.stop if should_stop 21 | end 22 | else 23 | Spec::Runner.configure do |config| 24 | config.after(:suite) do 25 | spec_builder.stop if should_stop 26 | end 27 | end 28 | end 29 | 30 | spec_builder.start 31 | should_stop = true 32 | spec_builder.declare_suites -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | 4 | def self.parent_prefixes 5 | @parent_prefixes ||= begin 6 | parent_controller = superclass 7 | prefixes = [] 8 | 9 | until parent_controller.abstract? 10 | prefixes << parent_controller.controller_path 11 | parent_controller = parent_controller.superclass 12 | end 13 | 14 | prefixes 15 | end 16 | end 17 | 18 | def _prefixes 19 | @_prefixes ||= begin 20 | parent_prefixes = self.class.parent_prefixes 21 | parent_prefixes.dup.unshift(controller_path) 22 | end 23 | end 24 | 25 | def lookup_contextoos 26 | puts ">>>>>>>LOOKUp<<<<<< #{self.class._view_paths} ::: #{@_lookup_context}" 27 | # @_lookup_context ||= 28 | ActionView::LookupContext.new(self.class._view_paths, {}) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/ice/handlers/eco/handler.rb: -------------------------------------------------------------------------------- 1 | require "ice/handlers/base" 2 | require 'eco' 3 | require 'v8' 4 | 5 | module Ice 6 | module Handlers 7 | module Eco 8 | def self.convert_template(template_text, vars = {}) 9 | Base.convert_template(template_text) do |context| 10 | helpers = "#{File.dirname(__FILE__)}/../../../../js/lib/eco-path-helper.js" 11 | 12 | context.eval(open(helpers).read) 13 | context.eval(::Eco::Source.combined_contents) 14 | template = context["eco"]["compile"].call(template_text) 15 | template.call(vars.to_ice) 16 | end 17 | end 18 | 19 | def self.call(template) 20 | <<-ECO 21 | template_source = <<-ECO_TEMPLATE 22 | #{template.source} 23 | ECO_TEMPLATE 24 | 25 | #{Base.variables} 26 | 27 | Ice::Handlers::Eco.convert_template(template_source, variables.merge(local_assigns)) 28 | ECO 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /js/lib/eco-path-helper.coffee: -------------------------------------------------------------------------------- 1 | safe = (value)-> 2 | result = new String(value) 3 | result.ecoSafe = true 4 | result 5 | 6 | linkTo = (label, link, opts) -> 7 | if (! link) 8 | link = label 9 | '' + label + '' 10 | 11 | navBar = (options, yield)-> 12 | config = try 13 | NavBarConfig 14 | catch error 15 | {} 16 | config["linkPrefix"] ||= "
  • " 17 | config["linkPostfix"] ||= "
  • " 18 | config["navPrefix"] ||= "" 20 | 21 | linkPrefix = ()-> options["linkPrefix"] || config["linkPrefix"] 22 | linkPostfix = ()-> options["linkPostfix"] || config["linkPostfix"] 23 | navPrefix = ()-> options["navPrefix"] || config["navPrefix"] 24 | navPostfix = ()-> options["navPostfix"] || config["navPostfix"] 25 | bar = 26 | linkTo: (label, link = null) => 27 | safe "#{linkPrefix()}#{linkTo label, link}#{linkPostfix()}" 28 | 29 | links = yield(bar) 30 | safe "#{navPrefix()}#{links}#{navPostfix}" -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended to check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(:version => 20110703014136) do 14 | 15 | create_table "notes", :force => true do |t| 16 | t.string "name" 17 | t.text "data" 18 | t.datetime "created_at" 19 | t.datetime "updated_at" 20 | t.string "secret_data", :default => "Secret Data" 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 YOURNAME 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 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 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.consider_all_requests_local = true 14 | config.action_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | 23 | # Only use best-standards-support built into browsers 24 | config.action_dispatch.best_standards_support = :builtin 25 | end 26 | 27 | -------------------------------------------------------------------------------- /spec/integration/eco_renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'integration/template_renderer_group' 3 | 4 | describe "Eco Renderer" do 5 | include Capybara::DSL 6 | before(:all) do 7 | set_js_handler(:eco) 8 | end 9 | 10 | it_should_behave_like "Template Renderer" 11 | 12 | it "allows class-wide javascript overrides" do 13 | javascript = <<-JAVASCRIPT 14 | NavBarConfig = { 15 | navPrefix: "
    ", 16 | navPostFix: "
    ", 17 | linkPrefix: "", 18 | linkPostFix: "" 19 | }; 20 | JAVASCRIPT 21 | 22 | mock_out_enumerable_each IceJavascriptHelpers, javascript 23 | visit "/navigation_demos/sample_nav_bar" 24 | page.should have_xpath('//div/span/a') 25 | end 26 | 27 | it "allows class-wide coffeescript overrides" do 28 | coffeescript = <<-COFFEESCRIPT 29 | NavBarConfig = 30 | navPrefix: "
    ", 31 | navPostFix: "
    ", 32 | linkPrefix: "", 33 | linkPostFix: "" 34 | COFFEESCRIPT 35 | 36 | mock_out_enumerable_each IceCoffeescriptHelpers, coffeescript 37 | visit "/navigation_demos/sample_nav_bar" 38 | page.should have_xpath('//div/span/a') 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/integration/navigation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'integration/template_renderer_group' 3 | 4 | describe "Ice Format" do 5 | include Capybara::DSL 6 | 7 | context "with existing note" do 8 | 9 | before do 10 | @note = Note.create! :name => "Name #{rand(9999)}", :data => "Data #{rand(999)}" 11 | end 12 | 13 | it "is formatted in show" do 14 | visit note_path(@note.id, :format => :ice) 15 | headers['Content-Type'].should match(/ice/) 16 | retrieved_note = JSON::parser.new(page.source).parse 17 | retrieved_note["id"].should == @note.id 18 | end 19 | 20 | it "is formatted in index" do 21 | visit (notes_path :format => :ice) 22 | headers['Content-Type'].should match(/ice/) 23 | notes = JSON::parser.new(page.source).parse 24 | notes.last["id"].should == @note.id 25 | end 26 | 27 | it "skips fields" do 28 | visit note_path(@note.id, :format => :ice) 29 | puts page.source 30 | page.source.should_not match(/Secret Data/) 31 | @note.to_json.should match(/Secret Data/) 32 | end 33 | 34 | protected 35 | 36 | def headers 37 | page.response_headers 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/ice/handlers/base.rb: -------------------------------------------------------------------------------- 1 | require "ice/generated_helpers" 2 | 3 | module Ice 4 | module Handlers 5 | module Base 6 | def self.variables 7 | <<-VARIABLES 8 | variable_names = controller.instance_variable_names 9 | variable_names -= %w[@template] 10 | if controller.respond_to?(:protected_instance_variables) 11 | variable_names -= controller.protected_instance_variables 12 | end 13 | 14 | variables = {} 15 | variable_names.each do |name| 16 | variables[name.sub(/^@/, "")] = controller.instance_variable_get(name) 17 | end 18 | VARIABLES 19 | end 20 | 21 | def self.convert_template(template_text) 22 | V8::C::Locker() do 23 | context = V8::Context.new 24 | 25 | IceJavascriptHelpers.each do |helper| 26 | context.eval(helper) 27 | end 28 | IceCoffeescriptHelpers.each do |helper| 29 | context.eval CoffeeScript.compile(helper, :bare => true) 30 | end 31 | 32 | context.eval CoffeeScript.compile(GeneratedHelpers.get_routes, :bare => true) 33 | yield context 34 | 35 | end 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /lib/ice/handlers/coffeekup/handler.rb: -------------------------------------------------------------------------------- 1 | require "ice/handlers/base" 2 | require 'v8' 3 | 4 | module Ice 5 | module Handlers 6 | module Coffeekup 7 | def self.convert_template(template_text, vars = {}) 8 | Base.convert_template(template_text) do |context| 9 | coffeescript_file = "#{File.dirname(__FILE__)}/../../../../js/coffee-script.js" 10 | coffeekup_file = "#{File.dirname(__FILE__)}/../../../../js/coffeekup.js" 11 | 12 | context.eval(open(coffeescript_file).read) 13 | context.eval(open(coffeekup_file).read) 14 | 15 | coffeekup_helpers_file = "#{File.dirname(__FILE__)}/../../../../js/lib/coffeekup-path-helper.coffee" 16 | combo = open(coffeekup_helpers_file).read + "\n" + template_text.sub(/^(\s)*/, "") 17 | template = context["coffeekup"]["compile"].call(combo) 18 | template.call({context: vars.to_ice}) 19 | end 20 | end 21 | 22 | def self.call(template) 23 | <<-COFFEEKUP 24 | template_source = <<-COFFEEKUP_TEMPLATE 25 | #{template.source} 26 | COFFEEKUP_TEMPLATE 27 | #{Base.variables} 28 | 29 | Ice::Handlers::Coffeekup.convert_template(template_source, variables.merge(local_assigns)) 30 | COFFEEKUP 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ice/base_cube.rb: -------------------------------------------------------------------------------- 1 | module Ice 2 | class BaseCube 3 | extend Ice::CubeAssociation 4 | attr_reader :source 5 | 6 | def to_ice 7 | self 8 | end 9 | 10 | def id 11 | @source.id 12 | end 13 | 14 | def self.attribute_names 15 | @@attribute_names ||= [] 16 | end 17 | 18 | def self.revealing(* attributes) 19 | attribute_names.concat(attributes) 20 | 21 | attributes.each do |attr| 22 | define_method attr.to_sym do 23 | @source.send(attr).to_ice 24 | end 25 | end 26 | end 27 | 28 | 29 | def to_hash 30 | if self.class.attribute_names.count > 0 31 | hash = {} 32 | ([:id, :created_at, :updated_at] + 33 | self.class.attribute_names).each do |method| 34 | if @source.respond_to? method 35 | hash[method] = source.send(method) 36 | end 37 | end 38 | hash 39 | else 40 | @hash ||= @source.serializable_hash.to_ice 41 | end 42 | end 43 | 44 | def to_json 45 | to_hash.to_json 46 | end 47 | 48 | 49 | def initialize(source) 50 | @source = source 51 | 52 | unless self.class.attribute_names.count > 0 53 | to_hash.each_key do |key| 54 | unless self.respond_to? key.to_sym 55 | self.class.send :define_method, key.to_sym do 56 | @source.send(key.to_sym) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | 63 | end 64 | end -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine.yml: -------------------------------------------------------------------------------- 1 | # src_files 2 | # 3 | # Return an array of filepaths relative to src_dir to include before jasmine specs. 4 | # Default: [] 5 | # 6 | # EXAMPLE: 7 | # 8 | # src_files: 9 | # - lib/source1.js 10 | # - lib/source2.js 11 | # - dist/**/*.js 12 | # 13 | src_files: 14 | - js/lib/*.js 15 | 16 | # stylesheets 17 | # 18 | # Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. 19 | # Default: [] 20 | # 21 | # EXAMPLE: 22 | # 23 | # stylesheets: 24 | # - css/style.css 25 | # - stylesheets/*.css 26 | # 27 | stylesheets: 28 | 29 | # helpers 30 | # 31 | # Return an array of filepaths relative to spec_dir to include before jasmine specs. 32 | # Default: ["helpers/**/*.js"] 33 | # 34 | # EXAMPLE: 35 | # 36 | # helpers: 37 | # - helpers/**/*.js 38 | # 39 | helpers: 40 | 41 | # spec_files 42 | # 43 | # Return an array of filepaths relative to spec_dir to include. 44 | # Default: ["**/*[sS]pec.js"] 45 | # 46 | # EXAMPLE: 47 | # 48 | # spec_files: 49 | # - **/*[sS]pec.js 50 | # 51 | spec_files: 52 | 53 | # src_dir 54 | # 55 | # Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. 56 | # Default: project root 57 | # 58 | # EXAMPLE: 59 | # 60 | # src_dir: public 61 | # 62 | src_dir: 63 | 64 | # spec_dir 65 | # 66 | # Spec directory path. Your spec_files must be returned relative to this path. 67 | # Default: spec/javascripts 68 | # 69 | # EXAMPLE: 70 | # 71 | # spec_dir: spec/javascripts 72 | # 73 | spec_dir: 74 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | end 36 | -------------------------------------------------------------------------------- /js/lib/eco-path-helper.js: -------------------------------------------------------------------------------- 1 | var linkTo, navBar, safe; 2 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 3 | safe = function(value) { 4 | var result; 5 | result = new String(value); 6 | result.ecoSafe = true; 7 | return result; 8 | }; 9 | linkTo = function(label, link, opts) { 10 | if (!link) { 11 | link = label; 12 | } 13 | return '' + label + ''; 14 | }; 15 | navBar = function(options, yield) { 16 | var bar, config, linkPostfix, linkPrefix, links, navPostfix, navPrefix; 17 | config = (function() { 18 | try { 19 | return NavBarConfig; 20 | } catch (error) { 21 | return {}; 22 | } 23 | })(); 24 | config["linkPrefix"] || (config["linkPrefix"] = "
  • "); 25 | config["linkPostfix"] || (config["linkPostfix"] = "
  • "); 26 | config["navPrefix"] || (config["navPrefix"] = ""); 28 | linkPrefix = function() { 29 | return options["linkPrefix"] || config["linkPrefix"]; 30 | }; 31 | linkPostfix = function() { 32 | return options["linkPostfix"] || config["linkPostfix"]; 33 | }; 34 | navPrefix = function() { 35 | return options["navPrefix"] || config["navPrefix"]; 36 | }; 37 | navPostfix = function() { 38 | return options["navPostfix"] || config["navPostfix"]; 39 | }; 40 | bar = { 41 | linkTo: __bind(function(label, link) { 42 | if (link == null) { 43 | link = null; 44 | } 45 | return safe("" + (linkPrefix()) + (linkTo(label, link)) + (linkPostfix())); 46 | }, this) 47 | }; 48 | links = yield(bar); 49 | return safe("" + (navPrefix()) + links + navPostfix); 50 | }; -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | end 50 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "active_model/railtie" 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_view/railtie" 7 | require "action_mailer/railtie" 8 | 9 | Bundler.require 10 | require "ice" 11 | 12 | module Dummy 13 | class Application < Rails::Application 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | # Custom directories with classes and modules you want to be autoloadable. 19 | config.autoload_paths += %W(#{config.root}/app/cubes) 20 | 21 | # Only load the plugins named here, in the order given (default is alphabetical). 22 | # :all can be used as a placeholder for all plugins not explicitly named. 23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 24 | 25 | # Activate observers that should always be running. 26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 27 | 28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 30 | # config.time_zone = 'Central Time (US & Canada)' 31 | 32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 34 | # config.i18n.default_locale = :de 35 | 36 | # JavaScript files you want as :defaults (application.js is always included). 37 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 38 | 39 | # Configure the default encoding used in templates for Ruby 1.9. 40 | config.encoding = "utf-8" 41 | 42 | # Configure sensitive parameters which will be filtered from the log file. 43 | config.filter_parameters += [:password] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/integration/template_renderer_group.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "Template Renderer" do 2 | it "should show the show page" do 3 | @note = Note.create! :name => "note name", :data => "data goes here" 4 | visit note_path(@note) 5 | page.should have_content(@note.name) 6 | page.should have_content(@note.data) 7 | end 8 | 9 | it "should let us have a mini partial" do 10 | visit "/partial_demos/easy" 11 | page.should have_content("Hello From Partial") 12 | end 13 | 14 | it "should let us have a partial with variables" do 15 | visit "/partial_demos/with_variable" 16 | page.should have_content("Hello From Variable") 17 | end 18 | 19 | it "should let us have a partial with with instance variables" do 20 | @note = Note.create! :name => "note name", :data => "data goes here" 21 | visit "/notes" 22 | page.should have_content(@note.name) 23 | page.should have_content(@note.data) 24 | end 25 | 26 | it "has path" do 27 | @note = Note.create! :name => "note name", :data => "data goes here" 28 | visit note_path(@note) 29 | page.should have_xpath('//a[@href="/notes"]') 30 | end 31 | 32 | it "parses navbar" do 33 | visit "/navigation_demos/sample_nav_bar" 34 | page.should have_xpath('//a[@href="/foo"]') 35 | end 36 | 37 | it "parses navbar" do 38 | visit "/navigation_demos/override_nav_bar" 39 | page.should have_xpath('//div/span/a') 40 | end 41 | 42 | it "parses navbar" do 43 | visit "/navigation_demos/routed_nav_bar" 44 | note = Note.create! :name => "Another Note", :data => "More Note Data" 45 | page.should have_selector('a', :href => note_path(note)) 46 | page.should have_selector('a', :href => edit_note_path(note)) 47 | page.should have_selector('a', :href => notes_path) 48 | page.should have_selector('a', :href => new_note_path) 49 | end 50 | end 51 | 52 | def set_js_handler(type) 53 | ApplicationController.class_eval %{ 54 | def lookup_context 55 | ActionView::LookupContext.new( [ 56 | ActionView::FileSystemResolver.new("/Users/natekidwell/RubymineProjects/ice/spec/dummy/app/test_views/#{type}_views"), 57 | self.class._view_paths[0] 58 | ], {}) 59 | end 60 | } 61 | end -------------------------------------------------------------------------------- /js/lib/form-tag-inputs.coffee: -------------------------------------------------------------------------------- 1 | humanize = (name) -> 2 | match = name.match(/(.*)_id$/) 3 | if match 4 | name = match[1] 5 | name.split('_').join(' ') 6 | 7 | getTypeValue = (type, opts) -> 8 | switch type 9 | when "disabled" 10 | if opts[type] then "disabled" else "" 11 | when "checked" then (opts[type] ? "checked" : "") 12 | else opts[type] 13 | 14 | getAttributeString = (type, opts) -> 15 | (opts && opts[type] && type + "=\"" + getTypeValue(type,opts) + "\" ") || "" 16 | 17 | getSizeString = (opts) -> 18 | getAttributeString('size', opts) 19 | 20 | getClassString = (opts) -> 21 | getAttributeString('class', opts) 22 | 23 | getDisabledString = (opts) -> 24 | getAttributeString('disabled', opts) 25 | 26 | getCheckedString = (opts) -> 27 | getAttributeString('checked', opts) 28 | 29 | getMaxlengthString = (opts) -> 30 | getAttributeString('maxlength', opts) 31 | 32 | labelTag = (name, opts) -> 33 | label = if typeof opts == 'string' then opts else humanize(name) 34 | classString = getClassString(opts) 35 | "" 36 | 37 | class BaseInputTag 38 | constructor: (@tagType) -> 39 | 40 | render: -> 41 | "" 42 | 43 | setOpts: (opts) -> 44 | @classString = getClassString opts 45 | @sizeString = getSizeString opts 46 | @disabledString = getDisabledString(opts) 47 | @maxlengthString = getMaxlengthString(opts) 48 | 49 | 50 | passwordFieldTag = (name) -> 51 | tag = new BaseInputTag("password") 52 | tag.name = name 53 | tag.value = ((typeof arguments[1] == 'string') && "value=\"" + arguments[1] + "\" ") || "" 54 | opts = arguments[2] || arguments[1] 55 | tag.setOpts(opts) 56 | tag.checkedString = "" 57 | tag.render() 58 | 59 | checkBoxTag = (name) -> 60 | tag = new BaseInputTag("checkbox") 61 | tag.name = name 62 | tag.value = "value=\"" + (((typeof arguments[1] == 'string') && arguments[1]) || 1) + "\" " 63 | tag.checkedString = if arguments[2] is true then "checked=\"checked\" " else "" 64 | opts = arguments[2] || arguments[1] 65 | tag.setOpts(opts) 66 | tag.render() -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Envinronment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 5 | require "rails/test_help" 6 | require "rspec/rails" 7 | 8 | Note.delete_all 9 | 10 | ActionMailer::Base.delivery_method = :test 11 | ActionMailer::Base.perform_deliveries = true 12 | ActionMailer::Base.default_url_options[:host] = "test.com" 13 | 14 | require File.dirname(__FILE__) + "/../lib/ice" 15 | require "informal" 16 | 17 | require "capybara/rails" 18 | Rails.backtrace_cleaner.remove_silencers! 19 | Capybara.default_driver = :rack_test 20 | Capybara.default_selector = :css 21 | 22 | # Run any available migration 23 | ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__) 24 | 25 | # Load support files 26 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 27 | 28 | RSpec.configure do |config| 29 | # Remove this line if you don't want RSpec's should and should_not 30 | # methods or matchers 31 | require 'rspec/expectations' 32 | config.include RSpec::Matchers 33 | 34 | # == Mock Framework 35 | config.mock_with :rspec 36 | end 37 | 38 | 39 | class FooClassCube < Ice::BaseCube 40 | revealing :first, :second 41 | end 42 | 43 | class FooClass 44 | include Ice::Cubeable 45 | include ActiveModel::Serialization 46 | 47 | attr_accessor :attributes 48 | 49 | def initialize(attributes = {}) 50 | @attributes = attributes 51 | end 52 | 53 | def first 54 | "primero" 55 | end 56 | 57 | def second 58 | @second ||= SecondClass.new 59 | end 60 | end 61 | 62 | class SecondClass 63 | def to_ice 64 | "segundo" 65 | end 66 | end 67 | 68 | 69 | describe "BaseCube" do 70 | context "a cubeable class" do 71 | it "should automatically to_ice the cube_class" do 72 | FooClass.new.to_ice.class.should == FooClassCube 73 | end 74 | 75 | it "should retrieve revealed properties" do 76 | FooClass.new.to_ice.first.should == "primero" 77 | end 78 | 79 | it "should map revealed properties via to_ice" do 80 | FooClass.new.to_ice.second.should == "segundo" 81 | end 82 | end 83 | end 84 | 85 | def mock_out_enumerable_each(object, *items) 86 | block = lambda {|block| items.each{|n| block.call(n)}} 87 | object.stub!(:each).and_return(&block) 88 | end -------------------------------------------------------------------------------- /spec/cube_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | class ParentObj 4 | def to_ice 5 | "parent" 6 | end 7 | end 8 | 9 | class TagObj 10 | def to_ice 11 | @name 12 | end 13 | 14 | def initialize(name) 15 | @name = name 16 | end 17 | 18 | end 19 | 20 | 21 | class ChildModel 22 | def parent 23 | @parent ||= ParentObj.new 24 | end 25 | 26 | def parent_id 27 | 15 28 | end 29 | 30 | def tags 31 | @tags ||= [TagObj.new("tag1"), TagObj.new("tag2")] 32 | end 33 | 34 | def tag_ids 35 | [1, 2] 36 | end 37 | 38 | def children 39 | [] 40 | end 41 | 42 | end 43 | 44 | 45 | class BaseCubeWithBelongsTo 46 | extend Ice::CubeAssociation 47 | 48 | def initialize 49 | @source = ChildModel.new 50 | end 51 | 52 | belongs_to :parent 53 | end 54 | 55 | class BaseCubeWithHasMany 56 | extend Ice::CubeAssociation 57 | 58 | def initialize 59 | @source = ChildModel.new 60 | end 61 | has_many :tags 62 | has_many :children 63 | end 64 | 65 | describe "Cube" do 66 | 67 | context "which has associations" do 68 | context "when belongs to an item" do 69 | 70 | it "should delegate object calls to its source object" do 71 | cube = BaseCubeWithBelongsTo.new 72 | cube.parent.should == "parent" 73 | end 74 | 75 | it "should delegate id calls to its source object" do 76 | cube = BaseCubeWithBelongsTo.new 77 | cube.parent_id.should == 15 78 | end 79 | 80 | end 81 | 82 | 83 | 84 | context "when has many of an item" do 85 | 86 | context "for populated collection" do 87 | it "should delegate object calls to its source object" do 88 | cube = BaseCubeWithHasMany.new 89 | cube.tags.should == ["tag1", "tag2"] 90 | end 91 | 92 | it "should return true from has" do 93 | cube = BaseCubeWithHasMany.new 94 | cube.has_tags.should == true 95 | end 96 | 97 | it "should return tag count" do 98 | cube = BaseCubeWithHasMany.new 99 | cube.num_tags.should == 2 100 | end 101 | 102 | it "should delegate id calls to its source object" do 103 | cube = BaseCubeWithHasMany.new 104 | cube.tag_ids.should == [1, 2] 105 | end 106 | end 107 | 108 | context "for empty collection" do 109 | it "should return false from has" do 110 | cube = BaseCubeWithHasMany.new 111 | cube.has_children.should == false 112 | end 113 | end 114 | end 115 | end 116 | end 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /spec/javascripts/path-helper-spec.coffee: -------------------------------------------------------------------------------- 1 | describe "NavBar", -> 2 | 3 | describe "by default", -> 4 | beforeEach -> 5 | @bar = new NavBar() 6 | 7 | it "should generate list by default", -> 8 | (expect (@bar.open() + @bar.close())).toEqual "" 9 | 10 | it "should generate list with internal links", -> 11 | links = (@bar.open() + @bar.linkTo("ff") + @bar.linkTo("aa") + @bar.close()) 12 | (expect links).toEqual "" 13 | 14 | it "should take optional titles", -> 15 | links = (@bar.open() + @bar.linkTo("ff", "aa") + @bar.close()) 16 | (expect links).toEqual "" 17 | 18 | describe "with options", -> 19 | beforeEach -> 20 | opts = 21 | navOpen: "
    " 22 | navClose: "
    " 23 | linkWrapper: (link) -> "" + link + "" 24 | @bar = new NavBar(opts) 25 | 26 | it "should generate list with wrappers", -> 27 | links = (@bar.open() + @bar.linkTo("ff") + @bar.close()) 28 | (expect links).toEqual "
    ff
    " 29 | 30 | describe "with separator", -> 31 | beforeEach -> 32 | @separator = " --- " 33 | separatorObject = 34 | separator: @separator 35 | @bar = new NavBar(separatorObject) 36 | 37 | it "should not separate single links", -> 38 | links = (@bar.open() + @bar.linkTo("ff") + @bar.close()) 39 | (expect links).toEqual "" 40 | 41 | it "should separate multiple links", -> 42 | links = (@bar.open() + @bar.linkTo("ff") + @bar.linkTo("aa") + @bar.close()) 43 | (expect links).toEqual "" 44 | 45 | it "should not display for missing links", -> 46 | @bar.navOpen = "
    " 47 | @bar.navClose = "
    " 48 | @bar.linkWrapper = (link)-> 49 | if link.match /aa/ 50 | "" 51 | else 52 | link 53 | 54 | links = (@bar.open() + @bar.linkTo("ff") + @bar.linkTo("aa") + @bar.linkTo("gg") + @bar.close()) 55 | (expect links).toEqual "
    ff" + @separator + "gg
    " 56 | 57 | describe "with class-wide options", -> 58 | beforeEach -> 59 | NavBar.defaultOptions = 60 | navOpen: "
    " 61 | navClose: "
    " 62 | linkWrapper: (link) -> "" + link + "" 63 | @bar = new NavBar() 64 | 65 | it "should generate list with wrappers", -> 66 | links = (@bar.open() + @bar.linkTo("ff") + @bar.close()) 67 | (expect links).toEqual "
    ff
    " 68 | 69 | afterEach -> 70 | NavBar.defaultOptions = {} -------------------------------------------------------------------------------- /spec/javascripts/form-tag-spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Form Builder Tags", -> 2 | describe "labelTag", -> 3 | it "assigns default value", -> 4 | tag = labelTag('name') 5 | (expect tag).toEqual '' 6 | 7 | it "assigns humanized default value", -> 8 | tag = labelTag 'supervising_boss_id' 9 | (expect tag).toEqual '' 10 | 11 | it "allows alternative value", -> 12 | tag = labelTag 'name', 'Your Name' 13 | (expect tag).toEqual '' 14 | 15 | it "allows class to be assigned", -> 16 | tag = labelTag 'name', 'class':'small_label' 17 | (expect tag).toEqual '' 18 | 19 | describe "for passwordFieldTag", -> 20 | it "should generate regular password tag", -> 21 | tag = passwordFieldTag('pass') 22 | (expect tag).toEqual '' 23 | 24 | it "should have alternate value", -> 25 | tag = passwordFieldTag('secret', 'Your secret here') 26 | (expect tag).toEqual '' 27 | 28 | it "should take class", -> 29 | tag = passwordFieldTag('masked', {'class':'masked_input_field'}) 30 | (expect tag).toEqual '' 31 | 32 | it "should take size", -> 33 | tag = passwordFieldTag('token','', {size:15}) 34 | (expect tag).toEqual '' 35 | 36 | it "should take maxlength", -> 37 | tag = passwordFieldTag('key',{maxlength:16}) 38 | (expect tag).toEqual '' 39 | 40 | it "should take disabled option", -> 41 | tag = passwordFieldTag('confirm_pass',{disabled:true}) 42 | (expect tag).toEqual '' 43 | 44 | it "should take multiple options", -> 45 | tag = passwordFieldTag('pin','1234',{maxlength:4,size:6, 'class':'pin-input'}) 46 | (expect tag).toEqual '' 47 | 48 | describe "for checkBoxTag", -> 49 | 50 | it "should generate basic checkbox", -> 51 | tag = checkBoxTag('accept') 52 | (expect tag).toEqual '' 53 | 54 | it "should take alternate values", -> 55 | tag = checkBoxTag('rock', 'rock music') 56 | (expect tag).toEqual '' 57 | 58 | it "should take parameter for checked", -> 59 | tag = checkBoxTag('receive_email', 'yes', true) 60 | (expect tag).toEqual '' 61 | -------------------------------------------------------------------------------- /js/lib/form-tag-inputs.js: -------------------------------------------------------------------------------- 1 | var BaseInputTag, checkBoxTag, getAttributeString, getCheckedString, getClassString, getDisabledString, getMaxlengthString, getSizeString, getTypeValue, humanize, labelTag, passwordFieldTag; 2 | humanize = function(name) { 3 | var match; 4 | match = name.match(/(.*)_id$/); 5 | if (match) { 6 | name = match[1]; 7 | } 8 | return name.split('_').join(' '); 9 | }; 10 | getTypeValue = function(type, opts) { 11 | var _ref; 12 | switch (type) { 13 | case "disabled": 14 | if (opts[type]) { 15 | return "disabled"; 16 | } else { 17 | return ""; 18 | } 19 | break; 20 | case "checked": 21 | return (_ref = opts[type]) != null ? _ref : { 22 | "checked": "" 23 | }; 24 | break; 25 | default: 26 | return opts[type]; 27 | } 28 | }; 29 | getAttributeString = function(type, opts) { 30 | return (opts && opts[type] && type + "=\"" + getTypeValue(type, opts) + "\" ") || ""; 31 | }; 32 | getSizeString = function(opts) { 33 | return getAttributeString('size', opts); 34 | }; 35 | getClassString = function(opts) { 36 | return getAttributeString('class', opts); 37 | }; 38 | getDisabledString = function(opts) { 39 | return getAttributeString('disabled', opts); 40 | }; 41 | getCheckedString = function(opts) { 42 | return getAttributeString('checked', opts); 43 | }; 44 | getMaxlengthString = function(opts) { 45 | return getAttributeString('maxlength', opts); 46 | }; 47 | labelTag = function(name, opts) { 48 | var classString, label; 49 | label = typeof opts === 'string' ? opts : humanize(name); 50 | classString = getClassString(opts); 51 | return ""; 52 | }; 53 | BaseInputTag = (function() { 54 | function BaseInputTag(tagType) { 55 | this.tagType = tagType; 56 | } 57 | BaseInputTag.prototype.render = function() { 58 | return ""; 59 | }; 60 | BaseInputTag.prototype.setOpts = function(opts) { 61 | this.classString = getClassString(opts); 62 | this.sizeString = getSizeString(opts); 63 | this.disabledString = getDisabledString(opts); 64 | return this.maxlengthString = getMaxlengthString(opts); 65 | }; 66 | return BaseInputTag; 67 | })(); 68 | passwordFieldTag = function(name) { 69 | var opts, tag; 70 | tag = new BaseInputTag("password"); 71 | tag.name = name; 72 | tag.value = ((typeof arguments[1] === 'string') && "value=\"" + arguments[1] + "\" ") || ""; 73 | opts = arguments[2] || arguments[1]; 74 | tag.setOpts(opts); 75 | tag.checkedString = ""; 76 | return tag.render(); 77 | }; 78 | checkBoxTag = function(name) { 79 | var opts, tag; 80 | tag = new BaseInputTag("checkbox"); 81 | tag.name = name; 82 | tag.value = "value=\"" + (((typeof arguments[1] === 'string') && arguments[1]) || 1) + "\" "; 83 | tag.checkedString = arguments[2] === true ? "checked=\"checked\" " : ""; 84 | opts = arguments[2] || arguments[1]; 85 | tag.setOpts(opts); 86 | return tag.render(); 87 | }; -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | abstract (1.0.0) 5 | actionmailer (3.0.7) 6 | actionpack (= 3.0.7) 7 | mail (~> 2.2.15) 8 | actionpack (3.0.7) 9 | activemodel (= 3.0.7) 10 | activesupport (= 3.0.7) 11 | builder (~> 2.1.2) 12 | erubis (~> 2.6.6) 13 | i18n (~> 0.5.0) 14 | rack (~> 1.2.1) 15 | rack-mount (~> 0.6.14) 16 | rack-test (~> 0.5.7) 17 | tzinfo (~> 0.3.23) 18 | activemodel (3.0.7) 19 | activesupport (= 3.0.7) 20 | builder (~> 2.1.2) 21 | i18n (~> 0.5.0) 22 | activerecord (3.0.7) 23 | activemodel (= 3.0.7) 24 | activesupport (= 3.0.7) 25 | arel (~> 2.0.2) 26 | tzinfo (~> 0.3.23) 27 | activeresource (3.0.7) 28 | activemodel (= 3.0.7) 29 | activesupport (= 3.0.7) 30 | activesupport (3.0.7) 31 | arel (2.0.10) 32 | builder (2.1.2) 33 | capybara (1.0.0) 34 | mime-types (>= 1.16) 35 | nokogiri (>= 1.3.3) 36 | rack (>= 1.0.0) 37 | rack-test (>= 0.5.4) 38 | selenium-webdriver (~> 0.2.0) 39 | xpath (~> 0.1.4) 40 | childprocess (0.1.9) 41 | ffi (~> 1.0.6) 42 | coffee-script (2.2.0) 43 | coffee-script-source 44 | execjs 45 | coffee-script-source (1.1.1) 46 | diff-lcs (1.1.2) 47 | eco (1.0.0) 48 | coffee-script 49 | eco-source 50 | execjs 51 | eco-source (1.1.0.rc.1) 52 | erubis (2.6.6) 53 | abstract (>= 1.0.0) 54 | execjs (1.2.0) 55 | multi_json (~> 1.0) 56 | ffi (1.0.9) 57 | i18n (0.5.0) 58 | informal (0.1.0) 59 | activemodel (~> 3.0) 60 | jasmine (1.0.2.1) 61 | json_pure (>= 1.4.3) 62 | rack (>= 1.1) 63 | rspec (>= 1.3.1) 64 | selenium-webdriver (>= 0.1.3) 65 | json_pure (1.5.3) 66 | libv8 (3.3.10.2) 67 | mail (2.2.19) 68 | activesupport (>= 2.3.6) 69 | i18n (>= 0.4.0) 70 | mime-types (~> 1.16) 71 | treetop (~> 1.4.8) 72 | mime-types (1.16) 73 | multi_json (1.0.3) 74 | nokogiri (1.4.6) 75 | polyglot (0.3.1) 76 | rack (1.2.3) 77 | rack-mount (0.6.14) 78 | rack (>= 1.0.0) 79 | rack-test (0.5.7) 80 | rack (>= 1.0) 81 | rails (3.0.7) 82 | actionmailer (= 3.0.7) 83 | actionpack (= 3.0.7) 84 | activerecord (= 3.0.7) 85 | activeresource (= 3.0.7) 86 | activesupport (= 3.0.7) 87 | bundler (~> 1.0) 88 | railties (= 3.0.7) 89 | railties (3.0.7) 90 | actionpack (= 3.0.7) 91 | activesupport (= 3.0.7) 92 | rake (>= 0.8.7) 93 | thor (~> 0.14.4) 94 | rake (0.8.7) 95 | rspec (2.6.0) 96 | rspec-core (~> 2.6.0) 97 | rspec-expectations (~> 2.6.0) 98 | rspec-mocks (~> 2.6.0) 99 | rspec-core (2.6.4) 100 | rspec-expectations (2.6.0) 101 | diff-lcs (~> 1.1.2) 102 | rspec-mocks (2.6.0) 103 | rspec-rails (2.6.1) 104 | actionpack (~> 3.0) 105 | activesupport (~> 3.0) 106 | railties (~> 3.0) 107 | rspec (~> 2.6.0) 108 | rubyzip (0.9.4) 109 | selenium-webdriver (0.2.2) 110 | childprocess (>= 0.1.9) 111 | ffi (>= 1.0.7) 112 | json_pure 113 | rubyzip 114 | sqlite3 (1.3.3) 115 | therubyracer (0.9.2) 116 | libv8 (~> 3.3.10) 117 | thor (0.14.6) 118 | treetop (1.4.9) 119 | polyglot (>= 0.3.1) 120 | tzinfo (0.3.28) 121 | xpath (0.1.4) 122 | nokogiri (~> 1.3) 123 | 124 | PLATFORMS 125 | ruby 126 | 127 | DEPENDENCIES 128 | capybara (>= 0.4.0) 129 | eco 130 | informal 131 | jasmine 132 | rails (= 3.0.7) 133 | rake (= 0.8.7) 134 | rspec-rails (>= 2.0.0.beta) 135 | sqlite3 136 | therubyracer (= 0.9.2) 137 | -------------------------------------------------------------------------------- /spec/javascripts/path-helper-spec.js: -------------------------------------------------------------------------------- 1 | describe("NavBar", function() { 2 | describe("by default", function() { 3 | beforeEach(function() { 4 | return this.bar = new NavBar(); 5 | }); 6 | it("should generate list by default", function() { 7 | return (expect(this.bar.open() + this.bar.close())).toEqual(""); 8 | }); 9 | it("should generate list with internal links", function() { 10 | var links; 11 | links = this.bar.open() + this.bar.linkTo("ff") + this.bar.linkTo("aa") + this.bar.close(); 12 | return (expect(links)).toEqual(""); 13 | }); 14 | return it("should take optional titles", function() { 15 | var links; 16 | links = this.bar.open() + this.bar.linkTo("ff", "aa") + this.bar.close(); 17 | return (expect(links)).toEqual(""); 18 | }); 19 | }); 20 | describe("with options", function() { 21 | beforeEach(function() { 22 | var opts; 23 | opts = { 24 | navOpen: "
    ", 25 | navClose: "
    ", 26 | linkWrapper: function(link) { 27 | return "" + link + ""; 28 | } 29 | }; 30 | return this.bar = new NavBar(opts); 31 | }); 32 | return it("should generate list with wrappers", function() { 33 | var links; 34 | links = this.bar.open() + this.bar.linkTo("ff") + this.bar.close(); 35 | return (expect(links)).toEqual("
    ff
    "); 36 | }); 37 | }); 38 | describe("with separator", function() { 39 | beforeEach(function() { 40 | var separatorObject; 41 | this.separator = " --- "; 42 | separatorObject = { 43 | separator: this.separator 44 | }; 45 | return this.bar = new NavBar(separatorObject); 46 | }); 47 | it("should not separate single links", function() { 48 | var links; 49 | links = this.bar.open() + this.bar.linkTo("ff") + this.bar.close(); 50 | return (expect(links)).toEqual(""); 51 | }); 52 | it("should separate multiple links", function() { 53 | var links; 54 | links = this.bar.open() + this.bar.linkTo("ff") + this.bar.linkTo("aa") + this.bar.close(); 55 | return (expect(links)).toEqual(""); 56 | }); 57 | return it("should not display for missing links", function() { 58 | var links; 59 | this.bar.navOpen = "
    "; 60 | this.bar.navClose = "
    "; 61 | this.bar.linkWrapper = function(link) { 62 | if (link.match(/aa/)) { 63 | return ""; 64 | } else { 65 | return link; 66 | } 67 | }; 68 | links = this.bar.open() + this.bar.linkTo("ff") + this.bar.linkTo("aa") + this.bar.linkTo("gg") + this.bar.close(); 69 | return (expect(links)).toEqual("
    ff" + this.separator + "gg
    "); 70 | }); 71 | }); 72 | return describe("with class-wide options", function() { 73 | beforeEach(function() { 74 | NavBar.defaultOptions = { 75 | navOpen: "
    ", 76 | navClose: "
    ", 77 | linkWrapper: function(link) { 78 | return "" + link + ""; 79 | } 80 | }; 81 | return this.bar = new NavBar(); 82 | }); 83 | it("should generate list with wrappers", function() { 84 | var links; 85 | links = this.bar.open() + this.bar.linkTo("ff") + this.bar.close(); 86 | return (expect(links)).toEqual("
    ff
    "); 87 | }); 88 | return afterEach(function() { 89 | return NavBar.defaultOptions = {}; 90 | }); 91 | }); 92 | }); -------------------------------------------------------------------------------- /spec/javascripts/form-tag-spec.js: -------------------------------------------------------------------------------- 1 | describe("Form Builder Tags", function() { 2 | describe("labelTag", function() { 3 | it("assigns default value", function() { 4 | var tag; 5 | tag = labelTag('name'); 6 | return (expect(tag)).toEqual(''); 7 | }); 8 | it("assigns humanized default value", function() { 9 | var tag; 10 | tag = labelTag('supervising_boss_id'); 11 | return (expect(tag)).toEqual(''); 12 | }); 13 | it("allows alternative value", function() { 14 | var tag; 15 | tag = labelTag('name', 'Your Name'); 16 | return (expect(tag)).toEqual(''); 17 | }); 18 | return it("allows class to be assigned", function() { 19 | var tag; 20 | tag = labelTag('name', { 21 | 'class': 'small_label' 22 | }); 23 | return (expect(tag)).toEqual(''); 24 | }); 25 | }); 26 | describe("for passwordFieldTag", function() { 27 | it("should generate regular password tag", function() { 28 | var tag; 29 | tag = passwordFieldTag('pass'); 30 | return (expect(tag)).toEqual(''); 31 | }); 32 | it("should have alternate value", function() { 33 | var tag; 34 | tag = passwordFieldTag('secret', 'Your secret here'); 35 | return (expect(tag)).toEqual(''); 36 | }); 37 | it("should take class", function() { 38 | var tag; 39 | tag = passwordFieldTag('masked', { 40 | 'class': 'masked_input_field' 41 | }); 42 | return (expect(tag)).toEqual(''); 43 | }); 44 | it("should take size", function() { 45 | var tag; 46 | tag = passwordFieldTag('token', '', { 47 | size: 15 48 | }); 49 | return (expect(tag)).toEqual(''); 50 | }); 51 | it("should take maxlength", function() { 52 | var tag; 53 | tag = passwordFieldTag('key', { 54 | maxlength: 16 55 | }); 56 | return (expect(tag)).toEqual(''); 57 | }); 58 | it("should take disabled option", function() { 59 | var tag; 60 | tag = passwordFieldTag('confirm_pass', { 61 | disabled: true 62 | }); 63 | return (expect(tag)).toEqual(''); 64 | }); 65 | return it("should take multiple options", function() { 66 | var tag; 67 | tag = passwordFieldTag('pin', '1234', { 68 | maxlength: 4, 69 | size: 6, 70 | 'class': 'pin-input' 71 | }); 72 | return (expect(tag)).toEqual(''); 73 | }); 74 | }); 75 | return describe("for checkBoxTag", function() { 76 | it("should generate basic checkbox", function() { 77 | var tag; 78 | tag = checkBoxTag('accept'); 79 | return (expect(tag)).toEqual(''); 80 | }); 81 | it("should take alternate values", function() { 82 | var tag; 83 | tag = checkBoxTag('rock', 'rock music'); 84 | return (expect(tag)).toEqual(''); 85 | }); 86 | return it("should take parameter for checked", function() { 87 | var tag; 88 | tag = checkBoxTag('receive_email', 'yes', true); 89 | return (expect(tag)).toEqual(''); 90 | }); 91 | }); 92 | }); -------------------------------------------------------------------------------- /spec/dummy/public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Technique from Juriy Zaytsev 3 | // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ 4 | function isEventSupported(eventName) { 5 | var el = document.createElement('div'); 6 | eventName = 'on' + eventName; 7 | var isSupported = (eventName in el); 8 | if (!isSupported) { 9 | el.setAttribute(eventName, 'return;'); 10 | isSupported = typeof el[eventName] == 'function'; 11 | } 12 | el = null; 13 | return isSupported; 14 | } 15 | 16 | function isForm(element) { 17 | return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM' 18 | } 19 | 20 | function isInput(element) { 21 | if (Object.isElement(element)) { 22 | var name = element.nodeName.toUpperCase() 23 | return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA' 24 | } 25 | else return false 26 | } 27 | 28 | var submitBubbles = isEventSupported('submit'), 29 | changeBubbles = isEventSupported('change') 30 | 31 | if (!submitBubbles || !changeBubbles) { 32 | // augment the Event.Handler class to observe custom events when needed 33 | Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( 34 | function(init, element, eventName, selector, callback) { 35 | init(element, eventName, selector, callback) 36 | // is the handler being attached to an element that doesn't support this event? 37 | if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || 38 | (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { 39 | // "submit" => "emulated:submit" 40 | this.eventName = 'emulated:' + this.eventName 41 | } 42 | } 43 | ) 44 | } 45 | 46 | if (!submitBubbles) { 47 | // discover forms on the page by observing focus events which always bubble 48 | document.on('focusin', 'form', function(focusEvent, form) { 49 | // special handler for the real "submit" event (one-time operation) 50 | if (!form.retrieve('emulated:submit')) { 51 | form.on('submit', function(submitEvent) { 52 | var emulated = form.fire('emulated:submit', submitEvent, true) 53 | // if custom event received preventDefault, cancel the real one too 54 | if (emulated.returnValue === false) submitEvent.preventDefault() 55 | }) 56 | form.store('emulated:submit', true) 57 | } 58 | }) 59 | } 60 | 61 | if (!changeBubbles) { 62 | // discover form inputs on the page 63 | document.on('focusin', 'input, select, texarea', function(focusEvent, input) { 64 | // special handler for real "change" events 65 | if (!input.retrieve('emulated:change')) { 66 | input.on('change', function(changeEvent) { 67 | input.fire('emulated:change', changeEvent, true) 68 | }) 69 | input.store('emulated:change', true) 70 | } 71 | }) 72 | } 73 | 74 | function handleRemote(element) { 75 | var method, url, params; 76 | 77 | var event = element.fire("ajax:before"); 78 | if (event.stopped) return false; 79 | 80 | if (element.tagName.toLowerCase() === 'form') { 81 | method = element.readAttribute('method') || 'post'; 82 | url = element.readAttribute('action'); 83 | params = element.serialize(); 84 | } else { 85 | method = element.readAttribute('data-method') || 'get'; 86 | url = element.readAttribute('href'); 87 | params = {}; 88 | } 89 | 90 | new Ajax.Request(url, { 91 | method: method, 92 | parameters: params, 93 | evalScripts: true, 94 | 95 | onComplete: function(request) { element.fire("ajax:complete", request); }, 96 | onSuccess: function(request) { element.fire("ajax:success", request); }, 97 | onFailure: function(request) { element.fire("ajax:failure", request); } 98 | }); 99 | 100 | element.fire("ajax:after"); 101 | } 102 | 103 | function handleMethod(element) { 104 | var method = element.readAttribute('data-method'), 105 | url = element.readAttribute('href'), 106 | csrf_param = $$('meta[name=csrf-param]')[0], 107 | csrf_token = $$('meta[name=csrf-token]')[0]; 108 | 109 | var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); 110 | element.parentNode.insert(form); 111 | 112 | if (method !== 'post') { 113 | var field = new Element('input', { type: 'hidden', name: '_method', value: method }); 114 | form.insert(field); 115 | } 116 | 117 | if (csrf_param) { 118 | var param = csrf_param.readAttribute('content'), 119 | token = csrf_token.readAttribute('content'), 120 | field = new Element('input', { type: 'hidden', name: param, value: token }); 121 | form.insert(field); 122 | } 123 | 124 | form.submit(); 125 | } 126 | 127 | 128 | document.on("click", "*[data-confirm]", function(event, element) { 129 | var message = element.readAttribute('data-confirm'); 130 | if (!confirm(message)) event.stop(); 131 | }); 132 | 133 | document.on("click", "a[data-remote]", function(event, element) { 134 | if (event.stopped) return; 135 | handleRemote(element); 136 | event.stop(); 137 | }); 138 | 139 | document.on("click", "a[data-method]", function(event, element) { 140 | if (event.stopped) return; 141 | handleMethod(element); 142 | event.stop(); 143 | }); 144 | 145 | document.on("submit", function(event) { 146 | var element = event.findElement(), 147 | message = element.readAttribute('data-confirm'); 148 | if (message && !confirm(message)) { 149 | event.stop(); 150 | return false; 151 | } 152 | 153 | var inputs = element.select("input[type=submit][data-disable-with]"); 154 | inputs.each(function(input) { 155 | input.disabled = true; 156 | input.writeAttribute('data-original-value', input.value); 157 | input.value = input.readAttribute('data-disable-with'); 158 | }); 159 | 160 | var element = event.findElement("form[data-remote]"); 161 | if (element) { 162 | handleRemote(element); 163 | event.stop(); 164 | } 165 | }); 166 | 167 | document.on("ajax:after", "form", function(event, element) { 168 | var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); 169 | inputs.each(function(input) { 170 | input.value = input.readAttribute('data-original-value'); 171 | input.removeAttribute('data-original-value'); 172 | input.disabled = false; 173 | }); 174 | }); 175 | 176 | Ajax.Responders.register({ 177 | onCreate: function(request) { 178 | var csrf_meta_tag = $$('meta[name=csrf-token]')[0]; 179 | 180 | if (csrf_meta_tag) { 181 | var header = 'X-CSRF-Token', 182 | token = csrf_meta_tag.readAttribute('content'); 183 | 184 | if (!request.options.requestHeaders) { 185 | request.options.requestHeaders = {}; 186 | } 187 | request.options.requestHeaders[header] = token; 188 | } 189 | } 190 | }); 191 | })(); 192 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | #Ice Template System 2 | 3 | The Ice system for templating allows people to serve Javascript/Coffeescript templates from Rails applications. Its approach is similar to that taken by [Liquid](http://github.com/tobi/liquid) and some other template systems. 4 | 5 | Using Javascript/Coffeescript has a few nice aspects 6 | 7 | * It is easy to store these templates in your database. Therefore, your users can upload templates that are evaluated and served, but do not execute malicious code. 8 | * By using Javascript/Coffeescript, you have the advantage of running your views on both the server and the browser (reducing code duplication). 9 | * Also, thanks to Javascript/Coffeescript, your users have the ability to modify the templates in exciting ways, using their own code libraries. 10 | 11 | Ice builds upon [The Ruby Racer](http://github.com/cowboyd/therubyracer) (written by Charles Lowell). This gem lets you use Google's V8 Javascript engine to execute your templates inside a sandbox. 12 | 13 | Ice allows you to write your templates in one of two formats. 14 | 15 | * [Eco](https://github.com/sstephenson/ruby-eco) (written by Sam Stephenson). This gem allows you to use Coffeescript with HTML in an ERB-ish fashion. 16 | * [CoffeeKup](http://coffeekup.org/) (written by Maurice Machado). This library uses Coffeescript itself to define your templates in a way reminiscent of Markaby and Haml. 17 | 18 | You can then write Eco templates like: 19 | 20 | 21 | 22 | <% for user in @users %> 23 | 24 | 25 | 26 | <% end %> 27 |
    NameEmail
    <%= user.name %><%= mailTo(user.email) %>
    28 | 29 | Eco-formatted files may also exist in your views directory, provided they have a .eco extension. An example of how this works is in [this blog post](http://ludicast.com/articles/3-ice-templates). 30 | 31 | Also, the templates may be compiled on demand with the method: 32 | 33 | Ice::Handlers::Eco.convert_template(template_text, variables) 34 | 35 | The CoffeeKup equivalent to the above Eco template is: 36 | 37 | table -> 38 | tr -> 39 | th -> "Name" 40 | th -> "Email" 41 | for user in @users -> 42 | tr -> 43 | td -> user.name 44 | td -> mailTo(user.email) 45 | 46 | Similarly, these CoffeeKup files may exist on your filesystem provided they have a .coffeekup extension. A demo of this is in [this screencast](http://vimeo.com/25907220/). 47 | 48 | And you'd compile them on demand with: 49 | 50 | Ice::Handlers::CoffeeKup.convert_template(template_text, variables) 51 | 52 | Eventually I'd like to bring in other JS template libraries, but Eco and CoffeeKup should suffice for now. If you like Erb, use Eco. If you like Haml, use CoffeeKup. 53 | 54 | ## Installation 55 | 56 | Ice is currently being developed only for Rails 3. Simply add to your Gemfile 57 | 58 | gem 'ice' 59 | 60 | Ice is undergoing *very* active development so be sure to either use the most recent gem, or pull from master. 61 | 62 | ## to_ice 63 | 64 | Every object is revealed to the templates via its to_ice method. This helps sanitize the objects that are passed into Ice, so people editing the template only have access to a limited subset of the data. This prevents people from adding code like: 65 | 66 |

    Hi, <%= User.delete_all %>

    67 | 68 | Instances of some classes like String and Numeric just return themselves as the result of to_ice. Hashes and Arrays run to_ice recursively on their members. 69 | 70 | If you want an object to map to a different representation, simply define a to_ice object that returns whatever object you want to represent it within the Ice template. These objects are referred to as "Cubes", and are equivalent to "Drops" for those used to Liquid. 71 | 72 | ## ActiveModel and to_ice 73 | 74 | To make life easy, since most complex objects passed to the templates will be classes including ActiveModel::Serializable, the default to_ice behaviour of these classes is to pass itself in to a class with the same name, but followed by the word "Cube". 75 | 76 | Therefore calling to_ice on an instance of a User class will invoke 77 | 78 | UserCube.new self 79 | 80 | ## BaseCube Class 81 | 82 | You can have your cubes inherit from our Ice::BaseCube class. Your cubes inheriting from it can then determine what additional attributes they want to reveal. For example 83 | 84 | class BookCube < Ice::BaseCube 85 | revealing :title, :author_id, :genre_id 86 | 87 | def reviewer_names 88 | @source.reviewers.map(&:name) 89 | end 90 | end 91 | 92 | would provide a cube with access to the title, author_id and genre properties of the underlying ActiveModel. In addition, it exposes a reviewer_names function that uses the @source instance variable to get at the record which is being filtered. Note that if no call to `revealing` occurs, the cube generates a mapping for the `@source` object's serializable `attributes`. 93 | 94 | These cubes also have simple belongs_to and has_many associations, so you can write things like: 95 | 96 | class ArticleCube < Ice::BaseCube 97 | has_many :comments, :tags 98 | belongs_to :author, :section 99 | end 100 | 101 | This generates association helper functions such as comment_ids, num_comments, has_comments, comments, author_id, and author. 102 | 103 | Note that the results of all associations and revealed functions are also sanitized via to_ice. 104 | 105 | ## Partials 106 | 107 | Partials may now be written in Eco or CoffeeKup, and included in ERB (and other) templates. 108 | 109 | ## Helpers 110 | 111 | Two global arrays exist named `IceJavascriptHelpers` and `IceCoffeescriptHelpers`. If you add to those arrays strings of Javascript or Coffeescript, those strings will be included in your views. These string are also compiled in the case of Coffeescript. 112 | 113 | This is slightly hackish, so expect this approach to shortly be replaced with a better one. But it is a quick way to add helpers to Ice. 114 | 115 | ## NavBar 116 | 117 | To make it easier to generate links, we added a `navBar` helper. For Eco templates it appears as: 118 | 119 | <%= navBar (bar) => %> 120 | <%= bar.linkTo("Bar", "/foo") %> 121 | <%= bar.linkTo("http://ludicast.com") %> 122 | <% end %> 123 | 124 | and in CoffeeKup the navBar is written as: 125 | 126 | navBar (o)=> 127 | o.linkTo("Bar", "/foo") 128 | o.linkTo("http://ludicast.com") 129 | 130 | In either case this generates the following html 131 | 132 | 136 | 137 | The `navBar` helper also takes options so if the Eco above was instead instantiated with: 138 | 139 | <% opts = nav_prefix:'
    ', nav_postfix: '
    ', link_prefix: '', link_postfix: '' %> 140 | <%= navBar opts, (bar)=> %> 141 | 142 | it would generate 143 | 144 |
    145 | Bar 146 | http://ludicast.com 147 |
    148 | 149 | Also, if you want to make a site-wide change to the default NavBar settings, all you need to do is add these options to the NavBarConfig class like 150 | 151 | coffeescript = <<-COFFEESCRIPT 152 | NavBarConfig = 153 | navPrefix: "
    ", 154 | navPostFix: "
    ", 155 | linkPrefix: "", 156 | linkPostFix: "" 157 | COFFEESCRIPT 158 | IceCoffeescriptHelpers << coffeescript 159 | 160 | Then all links will generate with these options, unless overridden in the values passed in to `navBar`. 161 | 162 | ## Routes 163 | 164 | Assuming that all your cubes are models that you are exposing to your app, we add to Ice routing helpers for every class inheriting from BaseCube. Therefore, if you have a cube class named `NoteCube`, you will have the following helper methods available: 165 | 166 | newNotePath 167 | notesPath 168 | notePath(@note) 169 | editNotePath(@note) 170 | 171 | which are converted to the appropriate paths. 172 | 173 | Note that some people might claim that it is insecure to expose your resources like this, but that probably should be dealt with on a case-by-case basis. Besides, the fact that you are exposing these resources as cubes means that you are, well, already exposing these resources. 174 | 175 | ## Note on Patches/Pull Requests 176 | 177 | * Fork the project. 178 | * Make your feature addition or bug fix. 179 | * Add spec for it. This is important so I don't break it in a future version unintentionally. In fact, try to write your specs in a test-first manner. 180 | * Commit 181 | * Send me a pull request. 182 | 183 | ## Todo 184 | 185 | * Add in form builders (from clots project) 186 | * Use [Moneta](http://github.com/wycats/moneta) for caching autogenerated javascript files. 187 | * Allowing Ice to render partials 188 | * Allowing Ice to serve as Rails layout files. 189 | 190 | ## Copyright 191 | 192 | MIT Licence. See MIT-LICENSE file for details. -------------------------------------------------------------------------------- /js/coffeekup.js: -------------------------------------------------------------------------------- 1 | var window = {}; 2 | var cache, coffee, coffeekup, skeleton, support, tags; 3 | var __hasProp = Object.prototype.hasOwnProperty; 4 | if (typeof window !== "undefined" && window !== null) { 5 | coffeekup = (window.CoffeeKup = {}); 6 | coffee = (typeof CoffeeScript !== "undefined" && CoffeeScript !== null) ? CoffeeScript : null; 7 | } else { 8 | coffeekup = exports; 9 | coffee = require('coffee-script'); 10 | } 11 | coffeekup.version = '0.2.0'; 12 | skeleton = function(ck_options) { 13 | var ck_buffer, ck_doctypes, ck_esc, ck_indent, ck_render_attrs, ck_repeat, ck_self_closing, ck_tabs, ck_tag, coffeescript, comment, doctype, h, tag, text; 14 | ck_options = (typeof ck_options !== "undefined" && ck_options !== null) ? ck_options : {}; 15 | ck_options.context = (typeof ck_options.context !== "undefined" && ck_options.context !== null) ? ck_options.context : {}; 16 | ck_options.locals = (typeof ck_options.locals !== "undefined" && ck_options.locals !== null) ? ck_options.locals : {}; 17 | ck_options.format = (typeof ck_options.format !== "undefined" && ck_options.format !== null) ? ck_options.format : false; 18 | ck_options.autoescape = (typeof ck_options.autoescape !== "undefined" && ck_options.autoescape !== null) ? ck_options.autoescape : false; 19 | ck_buffer = []; 20 | ck_render_attrs = function(obj) { 21 | var _ref, k, str, v; 22 | str = ''; 23 | _ref = obj; 24 | for (k in _ref) { 25 | if (!__hasProp.call(_ref, k)) continue; 26 | v = _ref[k]; 27 | str += (" " + (k) + "=\"" + (ck_esc(v)) + "\""); 28 | } 29 | return str; 30 | }; 31 | ck_doctypes = { 32 | '5': '', 33 | 'xml': '', 34 | 'default': '', 35 | 'transitional': '', 36 | 'strict': '', 37 | 'frameset': '', 38 | '1.1': '', 39 | 'basic': '', 40 | 'mobile': '' 41 | }; 42 | ck_self_closing = ['area', 'base', 'basefont', 'br', 'hr', 'img', 'input', 'link', 'meta']; 43 | ck_esc = function(txt) { 44 | return ck_options.autoescape ? h(txt) : String(txt); 45 | }; 46 | ck_tabs = 0; 47 | ck_repeat = function(string, count) { 48 | return Array(count + 1).join(string); 49 | }; 50 | ck_indent = function() { 51 | if (ck_options.format) { 52 | return text(ck_repeat(' ', ck_tabs)); 53 | } 54 | }; 55 | h = function(txt) { 56 | return String(txt).replace(/&(?!\w+;)/g, '&').replace(//g, '>').replace(/"/g, '"'); 57 | }; 58 | doctype = function(type) { 59 | type = (typeof type !== "undefined" && type !== null) ? type : 5; 60 | text(ck_doctypes[type]); 61 | if (ck_options.format) { 62 | return text('\n'); 63 | } 64 | }; 65 | text = function(txt) { 66 | ck_buffer.push(String(txt)); 67 | return null; 68 | }; 69 | comment = function(cmt) { 70 | text(""); 71 | if (ck_options.format) { 72 | return text('\n'); 73 | } 74 | }; 75 | tag = function() { 76 | var name; 77 | name = arguments[0]; 78 | delete arguments[0]; 79 | return ck_tag(name, arguments); 80 | }; 81 | ck_tag = function(name, opts) { 82 | var _i, _len, _ref, o, result; 83 | ck_indent(); 84 | text("<" + (name)); 85 | _ref = opts; 86 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 87 | o = _ref[_i]; 88 | if (typeof o === 'object') { 89 | text(ck_render_attrs(o)); 90 | } 91 | } 92 | if ((function(){ for (var _i=0, _len=ck_self_closing.length; _i<_len; _i++) { if (ck_self_closing[_i] === name) return true; } return false; }).call(this)) { 93 | text(' />'); 94 | if (ck_options.format) { 95 | text('\n'); 96 | } 97 | } else { 98 | text('>'); 99 | _ref = opts; 100 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 101 | o = _ref[_i]; 102 | switch (typeof o) { 103 | case 'string': 104 | case 'number': 105 | text(ck_esc(o)); 106 | break; 107 | case 'function': 108 | if (ck_options.format) { 109 | text('\n'); 110 | } 111 | ck_tabs++; 112 | result = o.call(ck_options.context); 113 | if (typeof result === 'string') { 114 | ck_indent(); 115 | text(ck_esc(result)); 116 | if (ck_options.format) { 117 | text('\n'); 118 | } 119 | } 120 | ck_tabs--; 121 | ck_indent(); 122 | break; 123 | } 124 | } 125 | text(""); 126 | if (ck_options.format) { 127 | text('\n'); 128 | } 129 | } 130 | return null; 131 | }; 132 | coffeescript = function(code) { 133 | return script(";(" + (code) + ")();"); 134 | }; 135 | return null; 136 | }; 137 | support = 'var __slice = Array.prototype.slice;\nvar __hasProp = Object.prototype.hasOwnProperty;\nvar __bind = function(func, context) {return function(){ return func.apply(context, arguments); };};'; 138 | skeleton = String(skeleton).replace('function (ck_options) {', '').replace(/return null;\s*\}$/, ''); 139 | skeleton = support + skeleton; 140 | tags = 'a|abbr|acronym|address|applet|area|article|aside|audio|b|base|basefont|bdo|big|blockquote|body|br|button|canvas|caption|center|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|dir|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hgroup|hr|html|i|iframe|img|input|ins|keygen|kbd|label|legend|li|link|map|mark|menu|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|s|samp|script|section|select|small|source|span|strike|strong|style|sub|summary|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|u|ul|video|xmp'.split('|'); 141 | coffeekup.compile = function(template, options) { 142 | var _i, _len, _ref, code, k, t, tags_here, v; 143 | options = (typeof options !== "undefined" && options !== null) ? options : {}; 144 | options.locals = (typeof options.locals !== "undefined" && options.locals !== null) ? options.locals : {}; 145 | if (typeof template === 'function') { 146 | template = String(template); 147 | } else if (typeof template === 'string' && (typeof coffee !== "undefined" && coffee !== null)) { 148 | template = coffee.compile(template, { 149 | 'noWrap': 'noWrap' 150 | }); 151 | template = ("function(){" + (template) + "}"); 152 | } 153 | tags_here = []; 154 | _ref = tags; 155 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 156 | t = _ref[_i]; 157 | if (template.indexOf(t) > -1) { 158 | tags_here.push(t); 159 | } 160 | } 161 | code = skeleton.replace(', text;', ", text, " + (tags_here.join(',')) + ";"); 162 | code += 'var arrayCreator = Array;'; 163 | _ref = tags_here; 164 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 165 | t = _ref[_i]; 166 | code += ("" + (t) + " = function(){return ck_tag('" + (t) + "', arguments)};"); 167 | } 168 | _ref = options.locals; 169 | for (k in _ref) { 170 | if (!__hasProp.call(_ref, k)) continue; 171 | v = _ref[k]; 172 | if (typeof v === 'function') { 173 | code += ("var " + (k) + " = " + (v) + ";"); 174 | } else { 175 | code += ("var " + (k) + " = " + (JSON.stringify(v)) + ";"); 176 | } 177 | } 178 | if (options.dynamic_locals) { 179 | code += 'with(ck_options.locals){'; 180 | } 181 | code += ("(" + (template) + ").call(ck_options.context);"); 182 | if (options.dynamic_locals) { 183 | code += '}'; 184 | } 185 | code += "return ck_buffer.join('');"; 186 | return new Function('ck_options', code); 187 | }; 188 | cache = {}; 189 | coffeekup.render = function(template, options) { 190 | var _ref, tpl; 191 | options = (typeof options !== "undefined" && options !== null) ? options : {}; 192 | options.context = (typeof options.context !== "undefined" && options.context !== null) ? options.context : {}; 193 | options.locals = (typeof options.locals !== "undefined" && options.locals !== null) ? options.locals : {}; 194 | options.cache = (typeof options.cache !== "undefined" && options.cache !== null) ? options.cache : true; 195 | if (typeof (_ref = options.locals.body) !== "undefined" && _ref !== null) { 196 | options.context.body = options.locals.body; 197 | delete options.locals.body; 198 | } 199 | if (options.cache && (typeof (_ref = cache[template]) !== "undefined" && _ref !== null)) { 200 | tpl = cache[template]; 201 | } else if (options.cache) { 202 | tpl = (cache[template] = coffeekup.compile(template, options)); 203 | } else { 204 | tpl = coffeekup.compile(template, options); 205 | } 206 | return tpl(options); 207 | }; 208 | if (!(typeof window !== "undefined" && window !== null)) { 209 | coffeekup.adapters = { 210 | simple: function(template, data) { 211 | return coffeekup.render(template, { 212 | context: data 213 | }); 214 | } 215 | }; 216 | coffeekup.adapters.meryl = coffeekup.adapters.simple; 217 | } 218 | -------------------------------------------------------------------------------- /spec/dummy/public/javascripts/dragdrop.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 6 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 7 | 8 | if(Object.isUndefined(Effect)) 9 | throw("dragdrop.js requires including script.aculo.us' effects.js library"); 10 | 11 | var Droppables = { 12 | drops: [], 13 | 14 | remove: function(element) { 15 | this.drops = this.drops.reject(function(d) { return d.element==$(element) }); 16 | }, 17 | 18 | add: function(element) { 19 | element = $(element); 20 | var options = Object.extend({ 21 | greedy: true, 22 | hoverclass: null, 23 | tree: false 24 | }, arguments[1] || { }); 25 | 26 | // cache containers 27 | if(options.containment) { 28 | options._containers = []; 29 | var containment = options.containment; 30 | if(Object.isArray(containment)) { 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 drop, affected = []; 91 | 92 | this.drops.each( function(drop) { 93 | if(Droppables.isAffected(point, element, drop)) 94 | affected.push(drop); 95 | }); 96 | 97 | if(affected.length>0) 98 | drop = Droppables.findDeepestChild(affected); 99 | 100 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); 101 | if (drop) { 102 | Position.within(drop.element, point[0], point[1]); 103 | if(drop.onHover) 104 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); 105 | 106 | if (drop != this.last_active) Droppables.activate(drop); 107 | } 108 | }, 109 | 110 | fire: function(event, element) { 111 | if(!this.last_active) return; 112 | Position.prepare(); 113 | 114 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) 115 | if (this.last_active.onDrop) { 116 | this.last_active.onDrop(element, this.last_active.element, event); 117 | return true; 118 | } 119 | }, 120 | 121 | reset: function() { 122 | if(this.last_active) 123 | this.deactivate(this.last_active); 124 | } 125 | }; 126 | 127 | var Draggables = { 128 | drags: [], 129 | observers: [], 130 | 131 | register: function(draggable) { 132 | if(this.drags.length == 0) { 133 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 134 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this); 135 | this.eventKeypress = this.keyPress.bindAsEventListener(this); 136 | 137 | Event.observe(document, "mouseup", this.eventMouseUp); 138 | Event.observe(document, "mousemove", this.eventMouseMove); 139 | Event.observe(document, "keypress", this.eventKeypress); 140 | } 141 | this.drags.push(draggable); 142 | }, 143 | 144 | unregister: function(draggable) { 145 | this.drags = this.drags.reject(function(d) { return d==draggable }); 146 | if(this.drags.length == 0) { 147 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 148 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 149 | Event.stopObserving(document, "keypress", this.eventKeypress); 150 | } 151 | }, 152 | 153 | activate: function(draggable) { 154 | if(draggable.options.delay) { 155 | this._timeout = setTimeout(function() { 156 | Draggables._timeout = null; 157 | window.focus(); 158 | Draggables.activeDraggable = draggable; 159 | }.bind(this), draggable.options.delay); 160 | } else { 161 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari 162 | this.activeDraggable = draggable; 163 | } 164 | }, 165 | 166 | deactivate: function() { 167 | this.activeDraggable = null; 168 | }, 169 | 170 | updateDrag: function(event) { 171 | if(!this.activeDraggable) return; 172 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 173 | // Mozilla-based browsers fire successive mousemove events with 174 | // the same coordinates, prevent needless redrawing (moz bug?) 175 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; 176 | this._lastPointer = pointer; 177 | 178 | this.activeDraggable.updateDrag(event, pointer); 179 | }, 180 | 181 | endDrag: function(event) { 182 | if(this._timeout) { 183 | clearTimeout(this._timeout); 184 | this._timeout = null; 185 | } 186 | if(!this.activeDraggable) return; 187 | this._lastPointer = null; 188 | this.activeDraggable.endDrag(event); 189 | this.activeDraggable = null; 190 | }, 191 | 192 | keyPress: function(event) { 193 | if(this.activeDraggable) 194 | this.activeDraggable.keyPress(event); 195 | }, 196 | 197 | addObserver: function(observer) { 198 | this.observers.push(observer); 199 | this._cacheObserverCallbacks(); 200 | }, 201 | 202 | removeObserver: function(element) { // element instead of observer fixes mem leaks 203 | this.observers = this.observers.reject( function(o) { return o.element==element }); 204 | this._cacheObserverCallbacks(); 205 | }, 206 | 207 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' 208 | if(this[eventName+'Count'] > 0) 209 | this.observers.each( function(o) { 210 | if(o[eventName]) o[eventName](eventName, draggable, event); 211 | }); 212 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event); 213 | }, 214 | 215 | _cacheObserverCallbacks: function() { 216 | ['onStart','onEnd','onDrag'].each( function(eventName) { 217 | Draggables[eventName+'Count'] = Draggables.observers.select( 218 | function(o) { return o[eventName]; } 219 | ).length; 220 | }); 221 | } 222 | }; 223 | 224 | /*--------------------------------------------------------------------------*/ 225 | 226 | var Draggable = Class.create({ 227 | initialize: function(element) { 228 | var defaults = { 229 | handle: false, 230 | reverteffect: function(element, top_offset, left_offset) { 231 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; 232 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, 233 | queue: {scope:'_draggable', position:'end'} 234 | }); 235 | }, 236 | endeffect: function(element) { 237 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; 238 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 239 | queue: {scope:'_draggable', position:'end'}, 240 | afterFinish: function(){ 241 | Draggable._dragging[element] = false 242 | } 243 | }); 244 | }, 245 | zindex: 1000, 246 | revert: false, 247 | quiet: false, 248 | scroll: false, 249 | scrollSensitivity: 20, 250 | scrollSpeed: 15, 251 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } 252 | delay: 0 253 | }; 254 | 255 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) 256 | Object.extend(defaults, { 257 | starteffect: function(element) { 258 | element._opacity = Element.getOpacity(element); 259 | Draggable._dragging[element] = true; 260 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 261 | } 262 | }); 263 | 264 | var options = Object.extend(defaults, arguments[1] || { }); 265 | 266 | this.element = $(element); 267 | 268 | if(options.handle && Object.isString(options.handle)) 269 | this.handle = this.element.down('.'+options.handle, 0); 270 | 271 | if(!this.handle) this.handle = $(options.handle); 272 | if(!this.handle) this.handle = this.element; 273 | 274 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { 275 | options.scroll = $(options.scroll); 276 | this._isScrollChild = Element.childOf(this.element, options.scroll); 277 | } 278 | 279 | Element.makePositioned(this.element); // fix IE 280 | 281 | this.options = options; 282 | this.dragging = false; 283 | 284 | this.eventMouseDown = this.initDrag.bindAsEventListener(this); 285 | Event.observe(this.handle, "mousedown", this.eventMouseDown); 286 | 287 | Draggables.register(this); 288 | }, 289 | 290 | destroy: function() { 291 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); 292 | Draggables.unregister(this); 293 | }, 294 | 295 | currentDelta: function() { 296 | return([ 297 | parseInt(Element.getStyle(this.element,'left') || '0'), 298 | parseInt(Element.getStyle(this.element,'top') || '0')]); 299 | }, 300 | 301 | initDrag: function(event) { 302 | if(!Object.isUndefined(Draggable._dragging[this.element]) && 303 | Draggable._dragging[this.element]) return; 304 | if(Event.isLeftClick(event)) { 305 | // abort on form elements, fixes a Firefox issue 306 | var src = Event.element(event); 307 | if((tag_name = src.tagName.toUpperCase()) && ( 308 | tag_name=='INPUT' || 309 | tag_name=='SELECT' || 310 | tag_name=='OPTION' || 311 | tag_name=='BUTTON' || 312 | tag_name=='TEXTAREA')) return; 313 | 314 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 315 | var pos = this.element.cumulativeOffset(); 316 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); 317 | 318 | Draggables.activate(this); 319 | Event.stop(event); 320 | } 321 | }, 322 | 323 | startDrag: function(event) { 324 | this.dragging = true; 325 | if(!this.delta) 326 | this.delta = this.currentDelta(); 327 | 328 | if(this.options.zindex) { 329 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 330 | this.element.style.zIndex = this.options.zindex; 331 | } 332 | 333 | if(this.options.ghosting) { 334 | this._clone = this.element.cloneNode(true); 335 | this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); 336 | if (!this._originallyAbsolute) 337 | Position.absolutize(this.element); 338 | this.element.parentNode.insertBefore(this._clone, this.element); 339 | } 340 | 341 | if(this.options.scroll) { 342 | if (this.options.scroll == window) { 343 | var where = this._getWindowScroll(this.options.scroll); 344 | this.originalScrollLeft = where.left; 345 | this.originalScrollTop = where.top; 346 | } else { 347 | this.originalScrollLeft = this.options.scroll.scrollLeft; 348 | this.originalScrollTop = this.options.scroll.scrollTop; 349 | } 350 | } 351 | 352 | Draggables.notify('onStart', this, event); 353 | 354 | if(this.options.starteffect) this.options.starteffect(this.element); 355 | }, 356 | 357 | updateDrag: function(event, pointer) { 358 | if(!this.dragging) this.startDrag(event); 359 | 360 | if(!this.options.quiet){ 361 | Position.prepare(); 362 | Droppables.show(pointer, this.element); 363 | } 364 | 365 | Draggables.notify('onDrag', this, event); 366 | 367 | this.draw(pointer); 368 | if(this.options.change) this.options.change(this); 369 | 370 | if(this.options.scroll) { 371 | this.stopScrolling(); 372 | 373 | var p; 374 | if (this.options.scroll == window) { 375 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } 376 | } else { 377 | p = Position.page(this.options.scroll); 378 | p[0] += this.options.scroll.scrollLeft + Position.deltaX; 379 | p[1] += this.options.scroll.scrollTop + Position.deltaY; 380 | p.push(p[0]+this.options.scroll.offsetWidth); 381 | p.push(p[1]+this.options.scroll.offsetHeight); 382 | } 383 | var speed = [0,0]; 384 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); 385 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); 386 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); 387 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); 388 | this.startScrolling(speed); 389 | } 390 | 391 | // fix AppleWebKit rendering 392 | if(Prototype.Browser.WebKit) window.scrollBy(0,0); 393 | 394 | Event.stop(event); 395 | }, 396 | 397 | finishDrag: function(event, success) { 398 | this.dragging = false; 399 | 400 | if(this.options.quiet){ 401 | Position.prepare(); 402 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 403 | Droppables.show(pointer, this.element); 404 | } 405 | 406 | if(this.options.ghosting) { 407 | if (!this._originallyAbsolute) 408 | Position.relativize(this.element); 409 | delete this._originallyAbsolute; 410 | Element.remove(this._clone); 411 | this._clone = null; 412 | } 413 | 414 | var dropped = false; 415 | if(success) { 416 | dropped = Droppables.fire(event, this.element); 417 | if (!dropped) dropped = false; 418 | } 419 | if(dropped && this.options.onDropped) this.options.onDropped(this.element); 420 | Draggables.notify('onEnd', this, event); 421 | 422 | var revert = this.options.revert; 423 | if(revert && Object.isFunction(revert)) revert = revert(this.element); 424 | 425 | var d = this.currentDelta(); 426 | if(revert && this.options.reverteffect) { 427 | if (dropped == 0 || revert != 'failure') 428 | this.options.reverteffect(this.element, 429 | d[1]-this.delta[1], d[0]-this.delta[0]); 430 | } else { 431 | this.delta = d; 432 | } 433 | 434 | if(this.options.zindex) 435 | this.element.style.zIndex = this.originalZ; 436 | 437 | if(this.options.endeffect) 438 | this.options.endeffect(this.element); 439 | 440 | Draggables.deactivate(this); 441 | Droppables.reset(); 442 | }, 443 | 444 | keyPress: function(event) { 445 | if(event.keyCode!=Event.KEY_ESC) return; 446 | this.finishDrag(event, false); 447 | Event.stop(event); 448 | }, 449 | 450 | endDrag: function(event) { 451 | if(!this.dragging) return; 452 | this.stopScrolling(); 453 | this.finishDrag(event, true); 454 | Event.stop(event); 455 | }, 456 | 457 | draw: function(point) { 458 | var pos = this.element.cumulativeOffset(); 459 | if(this.options.ghosting) { 460 | var r = Position.realOffset(this.element); 461 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; 462 | } 463 | 464 | var d = this.currentDelta(); 465 | pos[0] -= d[0]; pos[1] -= d[1]; 466 | 467 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { 468 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; 469 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; 470 | } 471 | 472 | var p = [0,1].map(function(i){ 473 | return (point[i]-pos[i]-this.offset[i]) 474 | }.bind(this)); 475 | 476 | if(this.options.snap) { 477 | if(Object.isFunction(this.options.snap)) { 478 | p = this.options.snap(p[0],p[1],this); 479 | } else { 480 | if(Object.isArray(this.options.snap)) { 481 | p = p.map( function(v, i) { 482 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); 483 | } else { 484 | p = p.map( function(v) { 485 | return (v/this.options.snap).round()*this.options.snap }.bind(this)); 486 | } 487 | }} 488 | 489 | var style = this.element.style; 490 | if((!this.options.constraint) || (this.options.constraint=='horizontal')) 491 | style.left = p[0] + "px"; 492 | if((!this.options.constraint) || (this.options.constraint=='vertical')) 493 | style.top = p[1] + "px"; 494 | 495 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering 496 | }, 497 | 498 | stopScrolling: function() { 499 | if(this.scrollInterval) { 500 | clearInterval(this.scrollInterval); 501 | this.scrollInterval = null; 502 | Draggables._lastScrollPointer = null; 503 | } 504 | }, 505 | 506 | startScrolling: function(speed) { 507 | if(!(speed[0] || speed[1])) return; 508 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; 509 | this.lastScrolled = new Date(); 510 | this.scrollInterval = setInterval(this.scroll.bind(this), 10); 511 | }, 512 | 513 | scroll: function() { 514 | var current = new Date(); 515 | var delta = current - this.lastScrolled; 516 | this.lastScrolled = current; 517 | if(this.options.scroll == window) { 518 | with (this._getWindowScroll(this.options.scroll)) { 519 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) { 520 | var d = delta / 1000; 521 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); 522 | } 523 | } 524 | } else { 525 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; 526 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; 527 | } 528 | 529 | Position.prepare(); 530 | Droppables.show(Draggables._lastPointer, this.element); 531 | Draggables.notify('onDrag', this); 532 | if (this._isScrollChild) { 533 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); 534 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; 535 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; 536 | if (Draggables._lastScrollPointer[0] < 0) 537 | Draggables._lastScrollPointer[0] = 0; 538 | if (Draggables._lastScrollPointer[1] < 0) 539 | Draggables._lastScrollPointer[1] = 0; 540 | this.draw(Draggables._lastScrollPointer); 541 | } 542 | 543 | if(this.options.change) this.options.change(this); 544 | }, 545 | 546 | _getWindowScroll: function(w) { 547 | var T, L, W, H; 548 | with (w.document) { 549 | if (w.document.documentElement && documentElement.scrollTop) { 550 | T = documentElement.scrollTop; 551 | L = documentElement.scrollLeft; 552 | } else if (w.document.body) { 553 | T = body.scrollTop; 554 | L = body.scrollLeft; 555 | } 556 | if (w.innerWidth) { 557 | W = w.innerWidth; 558 | H = w.innerHeight; 559 | } else if (w.document.documentElement && documentElement.clientWidth) { 560 | W = documentElement.clientWidth; 561 | H = documentElement.clientHeight; 562 | } else { 563 | W = body.offsetWidth; 564 | H = body.offsetHeight; 565 | } 566 | } 567 | return { top: T, left: L, width: W, height: H }; 568 | } 569 | }); 570 | 571 | Draggable._dragging = { }; 572 | 573 | /*--------------------------------------------------------------------------*/ 574 | 575 | var SortableObserver = Class.create({ 576 | initialize: function(element, observer) { 577 | this.element = $(element); 578 | this.observer = observer; 579 | this.lastValue = Sortable.serialize(this.element); 580 | }, 581 | 582 | onStart: function() { 583 | this.lastValue = Sortable.serialize(this.element); 584 | }, 585 | 586 | onEnd: function() { 587 | Sortable.unmark(); 588 | if(this.lastValue != Sortable.serialize(this.element)) 589 | this.observer(this.element) 590 | } 591 | }); 592 | 593 | var Sortable = { 594 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, 595 | 596 | sortables: { }, 597 | 598 | _findRootElement: function(element) { 599 | while (element.tagName.toUpperCase() != "BODY") { 600 | if(element.id && Sortable.sortables[element.id]) return element; 601 | element = element.parentNode; 602 | } 603 | }, 604 | 605 | options: function(element) { 606 | element = Sortable._findRootElement($(element)); 607 | if(!element) return; 608 | return Sortable.sortables[element.id]; 609 | }, 610 | 611 | destroy: function(element){ 612 | element = $(element); 613 | var s = Sortable.sortables[element.id]; 614 | 615 | if(s) { 616 | Draggables.removeObserver(s.element); 617 | s.droppables.each(function(d){ Droppables.remove(d) }); 618 | s.draggables.invoke('destroy'); 619 | 620 | delete Sortable.sortables[s.element.id]; 621 | } 622 | }, 623 | 624 | create: function(element) { 625 | element = $(element); 626 | var options = Object.extend({ 627 | element: element, 628 | tag: 'li', // assumes li children, override with tag: 'tagname' 629 | dropOnEmpty: false, 630 | tree: false, 631 | treeTag: 'ul', 632 | overlap: 'vertical', // one of 'vertical', 'horizontal' 633 | constraint: 'vertical', // one of 'vertical', 'horizontal', false 634 | containment: element, // also takes array of elements (or id's); or false 635 | handle: false, // or a CSS class 636 | only: false, 637 | delay: 0, 638 | hoverclass: null, 639 | ghosting: false, 640 | quiet: false, 641 | scroll: false, 642 | scrollSensitivity: 20, 643 | scrollSpeed: 15, 644 | format: this.SERIALIZE_RULE, 645 | 646 | // these take arrays of elements or ids and can be 647 | // used for better initialization performance 648 | elements: false, 649 | handles: false, 650 | 651 | onChange: Prototype.emptyFunction, 652 | onUpdate: Prototype.emptyFunction 653 | }, arguments[1] || { }); 654 | 655 | // clear any old sortable with same element 656 | this.destroy(element); 657 | 658 | // build options for the draggables 659 | var options_for_draggable = { 660 | revert: true, 661 | quiet: options.quiet, 662 | scroll: options.scroll, 663 | scrollSpeed: options.scrollSpeed, 664 | scrollSensitivity: options.scrollSensitivity, 665 | delay: options.delay, 666 | ghosting: options.ghosting, 667 | constraint: options.constraint, 668 | handle: options.handle }; 669 | 670 | if(options.starteffect) 671 | options_for_draggable.starteffect = options.starteffect; 672 | 673 | if(options.reverteffect) 674 | options_for_draggable.reverteffect = options.reverteffect; 675 | else 676 | if(options.ghosting) options_for_draggable.reverteffect = function(element) { 677 | element.style.top = 0; 678 | element.style.left = 0; 679 | }; 680 | 681 | if(options.endeffect) 682 | options_for_draggable.endeffect = options.endeffect; 683 | 684 | if(options.zindex) 685 | options_for_draggable.zindex = options.zindex; 686 | 687 | // build options for the droppables 688 | var options_for_droppable = { 689 | overlap: options.overlap, 690 | containment: options.containment, 691 | tree: options.tree, 692 | hoverclass: options.hoverclass, 693 | onHover: Sortable.onHover 694 | }; 695 | 696 | var options_for_tree = { 697 | onHover: Sortable.onEmptyHover, 698 | overlap: options.overlap, 699 | containment: options.containment, 700 | hoverclass: options.hoverclass 701 | }; 702 | 703 | // fix for gecko engine 704 | Element.cleanWhitespace(element); 705 | 706 | options.draggables = []; 707 | options.droppables = []; 708 | 709 | // drop on empty handling 710 | if(options.dropOnEmpty || options.tree) { 711 | Droppables.add(element, options_for_tree); 712 | options.droppables.push(element); 713 | } 714 | 715 | (options.elements || this.findElements(element, options) || []).each( function(e,i) { 716 | var handle = options.handles ? $(options.handles[i]) : 717 | (options.handle ? $(e).select('.' + options.handle)[0] : e); 718 | options.draggables.push( 719 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); 720 | Droppables.add(e, options_for_droppable); 721 | if(options.tree) e.treeNode = element; 722 | options.droppables.push(e); 723 | }); 724 | 725 | if(options.tree) { 726 | (Sortable.findTreeElements(element, options) || []).each( function(e) { 727 | Droppables.add(e, options_for_tree); 728 | e.treeNode = element; 729 | options.droppables.push(e); 730 | }); 731 | } 732 | 733 | // keep reference 734 | this.sortables[element.identify()] = options; 735 | 736 | // for onupdate 737 | Draggables.addObserver(new SortableObserver(element, options.onUpdate)); 738 | 739 | }, 740 | 741 | // return all suitable-for-sortable elements in a guaranteed order 742 | findElements: function(element, options) { 743 | return Element.findChildren( 744 | element, options.only, options.tree ? true : false, options.tag); 745 | }, 746 | 747 | findTreeElements: function(element, options) { 748 | return Element.findChildren( 749 | element, options.only, options.tree ? true : false, options.treeTag); 750 | }, 751 | 752 | onHover: function(element, dropon, overlap) { 753 | if(Element.isParent(dropon, element)) return; 754 | 755 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { 756 | return; 757 | } else if(overlap>0.5) { 758 | Sortable.mark(dropon, 'before'); 759 | if(dropon.previousSibling != element) { 760 | var oldParentNode = element.parentNode; 761 | element.style.visibility = "hidden"; // fix gecko rendering 762 | dropon.parentNode.insertBefore(element, dropon); 763 | if(dropon.parentNode!=oldParentNode) 764 | Sortable.options(oldParentNode).onChange(element); 765 | Sortable.options(dropon.parentNode).onChange(element); 766 | } 767 | } else { 768 | Sortable.mark(dropon, 'after'); 769 | var nextElement = dropon.nextSibling || null; 770 | if(nextElement != element) { 771 | var oldParentNode = element.parentNode; 772 | element.style.visibility = "hidden"; // fix gecko rendering 773 | dropon.parentNode.insertBefore(element, nextElement); 774 | if(dropon.parentNode!=oldParentNode) 775 | Sortable.options(oldParentNode).onChange(element); 776 | Sortable.options(dropon.parentNode).onChange(element); 777 | } 778 | } 779 | }, 780 | 781 | onEmptyHover: function(element, dropon, overlap) { 782 | var oldParentNode = element.parentNode; 783 | var droponOptions = Sortable.options(dropon); 784 | 785 | if(!Element.isParent(dropon, element)) { 786 | var index; 787 | 788 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); 789 | var child = null; 790 | 791 | if(children) { 792 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); 793 | 794 | for (index = 0; index < children.length; index += 1) { 795 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { 796 | offset -= Element.offsetSize (children[index], droponOptions.overlap); 797 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { 798 | child = index + 1 < children.length ? children[index + 1] : null; 799 | break; 800 | } else { 801 | child = children[index]; 802 | break; 803 | } 804 | } 805 | } 806 | 807 | dropon.insertBefore(element, child); 808 | 809 | Sortable.options(oldParentNode).onChange(element); 810 | droponOptions.onChange(element); 811 | } 812 | }, 813 | 814 | unmark: function() { 815 | if(Sortable._marker) Sortable._marker.hide(); 816 | }, 817 | 818 | mark: function(dropon, position) { 819 | // mark on ghosting only 820 | var sortable = Sortable.options(dropon.parentNode); 821 | if(sortable && !sortable.ghosting) return; 822 | 823 | if(!Sortable._marker) { 824 | Sortable._marker = 825 | ($('dropmarker') || Element.extend(document.createElement('DIV'))). 826 | hide().addClassName('dropmarker').setStyle({position:'absolute'}); 827 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); 828 | } 829 | var offsets = dropon.cumulativeOffset(); 830 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); 831 | 832 | if(position=='after') 833 | if(sortable.overlap == 'horizontal') 834 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); 835 | else 836 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); 837 | 838 | Sortable._marker.show(); 839 | }, 840 | 841 | _tree: function(element, options, parent) { 842 | var children = Sortable.findElements(element, options) || []; 843 | 844 | for (var i = 0; i < children.length; ++i) { 845 | var match = children[i].id.match(options.format); 846 | 847 | if (!match) continue; 848 | 849 | var child = { 850 | id: encodeURIComponent(match ? match[1] : null), 851 | element: element, 852 | parent: parent, 853 | children: [], 854 | position: parent.children.length, 855 | container: $(children[i]).down(options.treeTag) 856 | }; 857 | 858 | /* Get the element containing the children and recurse over it */ 859 | if (child.container) 860 | this._tree(child.container, options, child); 861 | 862 | parent.children.push (child); 863 | } 864 | 865 | return parent; 866 | }, 867 | 868 | tree: function(element) { 869 | element = $(element); 870 | var sortableOptions = this.options(element); 871 | var options = Object.extend({ 872 | tag: sortableOptions.tag, 873 | treeTag: sortableOptions.treeTag, 874 | only: sortableOptions.only, 875 | name: element.id, 876 | format: sortableOptions.format 877 | }, arguments[1] || { }); 878 | 879 | var root = { 880 | id: null, 881 | parent: null, 882 | children: [], 883 | container: element, 884 | position: 0 885 | }; 886 | 887 | return Sortable._tree(element, options, root); 888 | }, 889 | 890 | /* Construct a [i] index for a particular node */ 891 | _constructIndex: function(node) { 892 | var index = ''; 893 | do { 894 | if (node.id) index = '[' + node.position + ']' + index; 895 | } while ((node = node.parent) != null); 896 | return index; 897 | }, 898 | 899 | sequence: function(element) { 900 | element = $(element); 901 | var options = Object.extend(this.options(element), arguments[1] || { }); 902 | 903 | return $(this.findElements(element, options) || []).map( function(item) { 904 | return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; 905 | }); 906 | }, 907 | 908 | setSequence: function(element, new_sequence) { 909 | element = $(element); 910 | var options = Object.extend(this.options(element), arguments[2] || { }); 911 | 912 | var nodeMap = { }; 913 | this.findElements(element, options).each( function(n) { 914 | if (n.id.match(options.format)) 915 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; 916 | n.parentNode.removeChild(n); 917 | }); 918 | 919 | new_sequence.each(function(ident) { 920 | var n = nodeMap[ident]; 921 | if (n) { 922 | n[1].appendChild(n[0]); 923 | delete nodeMap[ident]; 924 | } 925 | }); 926 | }, 927 | 928 | serialize: function(element) { 929 | element = $(element); 930 | var options = Object.extend(Sortable.options(element), arguments[1] || { }); 931 | var name = encodeURIComponent( 932 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); 933 | 934 | if (options.tree) { 935 | return Sortable.tree(element, arguments[1]).children.map( function (item) { 936 | return [name + Sortable._constructIndex(item) + "[id]=" + 937 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); 938 | }).flatten().join('&'); 939 | } else { 940 | return Sortable.sequence(element, arguments[1]).map( function(item) { 941 | return name + "[]=" + encodeURIComponent(item); 942 | }).join('&'); 943 | } 944 | } 945 | }; 946 | 947 | // Returns true if child is contained within element 948 | Element.isParent = function(child, element) { 949 | if (!child.parentNode || child == element) return false; 950 | if (child.parentNode == element) return true; 951 | return Element.isParent(child.parentNode, element); 952 | }; 953 | 954 | Element.findChildren = function(element, only, recursive, tagName) { 955 | if(!element.hasChildNodes()) return null; 956 | tagName = tagName.toUpperCase(); 957 | if(only) only = [only].flatten(); 958 | var elements = []; 959 | $A(element.childNodes).each( function(e) { 960 | if(e.tagName && e.tagName.toUpperCase()==tagName && 961 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) 962 | elements.push(e); 963 | if(recursive) { 964 | var grandchildren = Element.findChildren(e, only, recursive, tagName); 965 | if(grandchildren) elements.push(grandchildren); 966 | } 967 | }); 968 | 969 | return (elements.length>0 ? elements.flatten() : []); 970 | }; 971 | 972 | Element.offsetSize = function (element, type) { 973 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; 974 | }; -------------------------------------------------------------------------------- /spec/dummy/public/javascripts/controls.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan) 5 | // (c) 2005-2009 Jon Tirsen (http://www.tirsen.com) 6 | // Contributors: 7 | // Richard Livsey 8 | // Rahul Bhargava 9 | // Rob Wills 10 | // 11 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 12 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 13 | 14 | // Autocompleter.Base handles all the autocompletion functionality 15 | // that's independent of the data source for autocompletion. This 16 | // includes drawing the autocompletion menu, observing keyboard 17 | // and mouse events, and similar. 18 | // 19 | // Specific autocompleters need to provide, at the very least, 20 | // a getUpdatedChoices function that will be invoked every time 21 | // the text inside the monitored textbox changes. This method 22 | // should get the text for which to provide autocompletion by 23 | // invoking this.getToken(), NOT by directly accessing 24 | // this.element.value. This is to allow incremental tokenized 25 | // autocompletion. Specific auto-completion logic (AJAX, etc) 26 | // belongs in getUpdatedChoices. 27 | // 28 | // Tokenized incremental autocompletion is enabled automatically 29 | // when an autocompleter is instantiated with the 'tokens' option 30 | // in the options parameter, e.g.: 31 | // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); 32 | // will incrementally autocomplete with a comma as the token. 33 | // Additionally, ',' in the above example can be replaced with 34 | // a token array, e.g. { tokens: [',', '\n'] } which 35 | // enables autocompletion on multiple tokens. This is most 36 | // useful when one of the tokens is \n (a newline), as it 37 | // allows smart autocompletion after linebreaks. 38 | 39 | if(typeof Effect == 'undefined') 40 | throw("controls.js requires including script.aculo.us' effects.js library"); 41 | 42 | var Autocompleter = { }; 43 | Autocompleter.Base = Class.create({ 44 | baseInitialize: function(element, update, options) { 45 | element = $(element); 46 | this.element = element; 47 | this.update = $(update); 48 | this.hasFocus = false; 49 | this.changed = false; 50 | this.active = false; 51 | this.index = 0; 52 | this.entryCount = 0; 53 | this.oldElementValue = this.element.value; 54 | 55 | if(this.setOptions) 56 | this.setOptions(options); 57 | else 58 | this.options = options || { }; 59 | 60 | this.options.paramName = this.options.paramName || this.element.name; 61 | this.options.tokens = this.options.tokens || []; 62 | this.options.frequency = this.options.frequency || 0.4; 63 | this.options.minChars = this.options.minChars || 1; 64 | this.options.onShow = this.options.onShow || 65 | function(element, update){ 66 | if(!update.style.position || update.style.position=='absolute') { 67 | update.style.position = 'absolute'; 68 | Position.clone(element, update, { 69 | setHeight: false, 70 | offsetTop: element.offsetHeight 71 | }); 72 | } 73 | Effect.Appear(update,{duration:0.15}); 74 | }; 75 | this.options.onHide = this.options.onHide || 76 | function(element, update){ new Effect.Fade(update,{duration:0.15}) }; 77 | 78 | if(typeof(this.options.tokens) == 'string') 79 | this.options.tokens = new Array(this.options.tokens); 80 | // Force carriage returns as token delimiters anyway 81 | if (!this.options.tokens.include('\n')) 82 | this.options.tokens.push('\n'); 83 | 84 | this.observer = null; 85 | 86 | this.element.setAttribute('autocomplete','off'); 87 | 88 | Element.hide(this.update); 89 | 90 | Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); 91 | Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); 92 | }, 93 | 94 | show: function() { 95 | if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); 96 | if(!this.iefix && 97 | (Prototype.Browser.IE) && 98 | (Element.getStyle(this.update, 'position')=='absolute')) { 99 | new Insertion.After(this.update, 100 | ''); 103 | this.iefix = $(this.update.id+'_iefix'); 104 | } 105 | if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); 106 | }, 107 | 108 | fixIEOverlapping: function() { 109 | Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); 110 | this.iefix.style.zIndex = 1; 111 | this.update.style.zIndex = 2; 112 | Element.show(this.iefix); 113 | }, 114 | 115 | hide: function() { 116 | this.stopIndicator(); 117 | if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); 118 | if(this.iefix) Element.hide(this.iefix); 119 | }, 120 | 121 | startIndicator: function() { 122 | if(this.options.indicator) Element.show(this.options.indicator); 123 | }, 124 | 125 | stopIndicator: function() { 126 | if(this.options.indicator) Element.hide(this.options.indicator); 127 | }, 128 | 129 | onKeyPress: function(event) { 130 | if(this.active) 131 | switch(event.keyCode) { 132 | case Event.KEY_TAB: 133 | case Event.KEY_RETURN: 134 | this.selectEntry(); 135 | Event.stop(event); 136 | case Event.KEY_ESC: 137 | this.hide(); 138 | this.active = false; 139 | Event.stop(event); 140 | return; 141 | case Event.KEY_LEFT: 142 | case Event.KEY_RIGHT: 143 | return; 144 | case Event.KEY_UP: 145 | this.markPrevious(); 146 | this.render(); 147 | Event.stop(event); 148 | return; 149 | case Event.KEY_DOWN: 150 | this.markNext(); 151 | this.render(); 152 | Event.stop(event); 153 | return; 154 | } 155 | else 156 | if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 157 | (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; 158 | 159 | this.changed = true; 160 | this.hasFocus = true; 161 | 162 | if(this.observer) clearTimeout(this.observer); 163 | this.observer = 164 | setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 165 | }, 166 | 167 | activate: function() { 168 | this.changed = false; 169 | this.hasFocus = true; 170 | this.getUpdatedChoices(); 171 | }, 172 | 173 | onHover: function(event) { 174 | var element = Event.findElement(event, 'LI'); 175 | if(this.index != element.autocompleteIndex) 176 | { 177 | this.index = element.autocompleteIndex; 178 | this.render(); 179 | } 180 | Event.stop(event); 181 | }, 182 | 183 | onClick: function(event) { 184 | var element = Event.findElement(event, 'LI'); 185 | this.index = element.autocompleteIndex; 186 | this.selectEntry(); 187 | this.hide(); 188 | }, 189 | 190 | onBlur: function(event) { 191 | // needed to make click events working 192 | setTimeout(this.hide.bind(this), 250); 193 | this.hasFocus = false; 194 | this.active = false; 195 | }, 196 | 197 | render: function() { 198 | if(this.entryCount > 0) { 199 | for (var i = 0; i < this.entryCount; i++) 200 | this.index==i ? 201 | Element.addClassName(this.getEntry(i),"selected") : 202 | Element.removeClassName(this.getEntry(i),"selected"); 203 | if(this.hasFocus) { 204 | this.show(); 205 | this.active = true; 206 | } 207 | } else { 208 | this.active = false; 209 | this.hide(); 210 | } 211 | }, 212 | 213 | markPrevious: function() { 214 | if(this.index > 0) this.index--; 215 | else this.index = this.entryCount-1; 216 | this.getEntry(this.index).scrollIntoView(true); 217 | }, 218 | 219 | markNext: function() { 220 | if(this.index < this.entryCount-1) this.index++; 221 | else this.index = 0; 222 | this.getEntry(this.index).scrollIntoView(false); 223 | }, 224 | 225 | getEntry: function(index) { 226 | return this.update.firstChild.childNodes[index]; 227 | }, 228 | 229 | getCurrentEntry: function() { 230 | return this.getEntry(this.index); 231 | }, 232 | 233 | selectEntry: function() { 234 | this.active = false; 235 | this.updateElement(this.getCurrentEntry()); 236 | }, 237 | 238 | updateElement: function(selectedElement) { 239 | if (this.options.updateElement) { 240 | this.options.updateElement(selectedElement); 241 | return; 242 | } 243 | var value = ''; 244 | if (this.options.select) { 245 | var nodes = $(selectedElement).select('.' + this.options.select) || []; 246 | if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); 247 | } else 248 | value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); 249 | 250 | var bounds = this.getTokenBounds(); 251 | if (bounds[0] != -1) { 252 | var newValue = this.element.value.substr(0, bounds[0]); 253 | var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); 254 | if (whitespace) 255 | newValue += whitespace[0]; 256 | this.element.value = newValue + value + this.element.value.substr(bounds[1]); 257 | } else { 258 | this.element.value = value; 259 | } 260 | this.oldElementValue = this.element.value; 261 | this.element.focus(); 262 | 263 | if (this.options.afterUpdateElement) 264 | this.options.afterUpdateElement(this.element, selectedElement); 265 | }, 266 | 267 | updateChoices: function(choices) { 268 | if(!this.changed && this.hasFocus) { 269 | this.update.innerHTML = choices; 270 | Element.cleanWhitespace(this.update); 271 | Element.cleanWhitespace(this.update.down()); 272 | 273 | if(this.update.firstChild && this.update.down().childNodes) { 274 | this.entryCount = 275 | this.update.down().childNodes.length; 276 | for (var i = 0; i < this.entryCount; i++) { 277 | var entry = this.getEntry(i); 278 | entry.autocompleteIndex = i; 279 | this.addObservers(entry); 280 | } 281 | } else { 282 | this.entryCount = 0; 283 | } 284 | 285 | this.stopIndicator(); 286 | this.index = 0; 287 | 288 | if(this.entryCount==1 && this.options.autoSelect) { 289 | this.selectEntry(); 290 | this.hide(); 291 | } else { 292 | this.render(); 293 | } 294 | } 295 | }, 296 | 297 | addObservers: function(element) { 298 | Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); 299 | Event.observe(element, "click", this.onClick.bindAsEventListener(this)); 300 | }, 301 | 302 | onObserverEvent: function() { 303 | this.changed = false; 304 | this.tokenBounds = null; 305 | if(this.getToken().length>=this.options.minChars) { 306 | this.getUpdatedChoices(); 307 | } else { 308 | this.active = false; 309 | this.hide(); 310 | } 311 | this.oldElementValue = this.element.value; 312 | }, 313 | 314 | getToken: function() { 315 | var bounds = this.getTokenBounds(); 316 | return this.element.value.substring(bounds[0], bounds[1]).strip(); 317 | }, 318 | 319 | getTokenBounds: function() { 320 | if (null != this.tokenBounds) return this.tokenBounds; 321 | var value = this.element.value; 322 | if (value.strip().empty()) return [-1, 0]; 323 | var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); 324 | var offset = (diff == this.oldElementValue.length ? 1 : 0); 325 | var prevTokenPos = -1, nextTokenPos = value.length; 326 | var tp; 327 | for (var index = 0, l = this.options.tokens.length; index < l; ++index) { 328 | tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); 329 | if (tp > prevTokenPos) prevTokenPos = tp; 330 | tp = value.indexOf(this.options.tokens[index], diff + offset); 331 | if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; 332 | } 333 | return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); 334 | } 335 | }); 336 | 337 | Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { 338 | var boundary = Math.min(newS.length, oldS.length); 339 | for (var index = 0; index < boundary; ++index) 340 | if (newS[index] != oldS[index]) 341 | return index; 342 | return boundary; 343 | }; 344 | 345 | Ajax.Autocompleter = Class.create(Autocompleter.Base, { 346 | initialize: function(element, update, url, options) { 347 | this.baseInitialize(element, update, options); 348 | this.options.asynchronous = true; 349 | this.options.onComplete = this.onComplete.bind(this); 350 | this.options.defaultParams = this.options.parameters || null; 351 | this.url = url; 352 | }, 353 | 354 | getUpdatedChoices: function() { 355 | this.startIndicator(); 356 | 357 | var entry = encodeURIComponent(this.options.paramName) + '=' + 358 | encodeURIComponent(this.getToken()); 359 | 360 | this.options.parameters = this.options.callback ? 361 | this.options.callback(this.element, entry) : entry; 362 | 363 | if(this.options.defaultParams) 364 | this.options.parameters += '&' + this.options.defaultParams; 365 | 366 | new Ajax.Request(this.url, this.options); 367 | }, 368 | 369 | onComplete: function(request) { 370 | this.updateChoices(request.responseText); 371 | } 372 | }); 373 | 374 | // The local array autocompleter. Used when you'd prefer to 375 | // inject an array of autocompletion options into the page, rather 376 | // than sending out Ajax queries, which can be quite slow sometimes. 377 | // 378 | // The constructor takes four parameters. The first two are, as usual, 379 | // the id of the monitored textbox, and id of the autocompletion menu. 380 | // The third is the array you want to autocomplete from, and the fourth 381 | // is the options block. 382 | // 383 | // Extra local autocompletion options: 384 | // - choices - How many autocompletion choices to offer 385 | // 386 | // - partialSearch - If false, the autocompleter will match entered 387 | // text only at the beginning of strings in the 388 | // autocomplete array. Defaults to true, which will 389 | // match text at the beginning of any *word* in the 390 | // strings in the autocomplete array. If you want to 391 | // search anywhere in the string, additionally set 392 | // the option fullSearch to true (default: off). 393 | // 394 | // - fullSsearch - Search anywhere in autocomplete array strings. 395 | // 396 | // - partialChars - How many characters to enter before triggering 397 | // a partial match (unlike minChars, which defines 398 | // how many characters are required to do any match 399 | // at all). Defaults to 2. 400 | // 401 | // - ignoreCase - Whether to ignore case when autocompleting. 402 | // Defaults to true. 403 | // 404 | // It's possible to pass in a custom function as the 'selector' 405 | // option, if you prefer to write your own autocompletion logic. 406 | // In that case, the other options above will not apply unless 407 | // you support them. 408 | 409 | Autocompleter.Local = Class.create(Autocompleter.Base, { 410 | initialize: function(element, update, array, options) { 411 | this.baseInitialize(element, update, options); 412 | this.options.array = array; 413 | }, 414 | 415 | getUpdatedChoices: function() { 416 | this.updateChoices(this.options.selector(this)); 417 | }, 418 | 419 | setOptions: function(options) { 420 | this.options = Object.extend({ 421 | choices: 10, 422 | partialSearch: true, 423 | partialChars: 2, 424 | ignoreCase: true, 425 | fullSearch: false, 426 | selector: function(instance) { 427 | var ret = []; // Beginning matches 428 | var partial = []; // Inside matches 429 | var entry = instance.getToken(); 430 | var count = 0; 431 | 432 | for (var i = 0; i < instance.options.array.length && 433 | ret.length < instance.options.choices ; i++) { 434 | 435 | var elem = instance.options.array[i]; 436 | var foundPos = instance.options.ignoreCase ? 437 | elem.toLowerCase().indexOf(entry.toLowerCase()) : 438 | elem.indexOf(entry); 439 | 440 | while (foundPos != -1) { 441 | if (foundPos == 0 && elem.length != entry.length) { 442 | ret.push("
  • " + elem.substr(0, entry.length) + "" + 443 | elem.substr(entry.length) + "
  • "); 444 | break; 445 | } else if (entry.length >= instance.options.partialChars && 446 | instance.options.partialSearch && foundPos != -1) { 447 | if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { 448 | partial.push("
  • " + elem.substr(0, foundPos) + "" + 449 | elem.substr(foundPos, entry.length) + "" + elem.substr( 450 | foundPos + entry.length) + "
  • "); 451 | break; 452 | } 453 | } 454 | 455 | foundPos = instance.options.ignoreCase ? 456 | elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 457 | elem.indexOf(entry, foundPos + 1); 458 | 459 | } 460 | } 461 | if (partial.length) 462 | ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); 463 | return ""; 464 | } 465 | }, options || { }); 466 | } 467 | }); 468 | 469 | // AJAX in-place editor and collection editor 470 | // Full rewrite by Christophe Porteneuve (April 2007). 471 | 472 | // Use this if you notice weird scrolling problems on some browsers, 473 | // the DOM might be a bit confused when this gets called so do this 474 | // waits 1 ms (with setTimeout) until it does the activation 475 | Field.scrollFreeActivate = function(field) { 476 | setTimeout(function() { 477 | Field.activate(field); 478 | }, 1); 479 | }; 480 | 481 | Ajax.InPlaceEditor = Class.create({ 482 | initialize: function(element, url, options) { 483 | this.url = url; 484 | this.element = element = $(element); 485 | this.prepareOptions(); 486 | this._controls = { }; 487 | arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! 488 | Object.extend(this.options, options || { }); 489 | if (!this.options.formId && this.element.id) { 490 | this.options.formId = this.element.id + '-inplaceeditor'; 491 | if ($(this.options.formId)) 492 | this.options.formId = ''; 493 | } 494 | if (this.options.externalControl) 495 | this.options.externalControl = $(this.options.externalControl); 496 | if (!this.options.externalControl) 497 | this.options.externalControlOnly = false; 498 | this._originalBackground = this.element.getStyle('background-color') || 'transparent'; 499 | this.element.title = this.options.clickToEditText; 500 | this._boundCancelHandler = this.handleFormCancellation.bind(this); 501 | this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); 502 | this._boundFailureHandler = this.handleAJAXFailure.bind(this); 503 | this._boundSubmitHandler = this.handleFormSubmission.bind(this); 504 | this._boundWrapperHandler = this.wrapUp.bind(this); 505 | this.registerListeners(); 506 | }, 507 | checkForEscapeOrReturn: function(e) { 508 | if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; 509 | if (Event.KEY_ESC == e.keyCode) 510 | this.handleFormCancellation(e); 511 | else if (Event.KEY_RETURN == e.keyCode) 512 | this.handleFormSubmission(e); 513 | }, 514 | createControl: function(mode, handler, extraClasses) { 515 | var control = this.options[mode + 'Control']; 516 | var text = this.options[mode + 'Text']; 517 | if ('button' == control) { 518 | var btn = document.createElement('input'); 519 | btn.type = 'submit'; 520 | btn.value = text; 521 | btn.className = 'editor_' + mode + '_button'; 522 | if ('cancel' == mode) 523 | btn.onclick = this._boundCancelHandler; 524 | this._form.appendChild(btn); 525 | this._controls[mode] = btn; 526 | } else if ('link' == control) { 527 | var link = document.createElement('a'); 528 | link.href = '#'; 529 | link.appendChild(document.createTextNode(text)); 530 | link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; 531 | link.className = 'editor_' + mode + '_link'; 532 | if (extraClasses) 533 | link.className += ' ' + extraClasses; 534 | this._form.appendChild(link); 535 | this._controls[mode] = link; 536 | } 537 | }, 538 | createEditField: function() { 539 | var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); 540 | var fld; 541 | if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { 542 | fld = document.createElement('input'); 543 | fld.type = 'text'; 544 | var size = this.options.size || this.options.cols || 0; 545 | if (0 < size) fld.size = size; 546 | } else { 547 | fld = document.createElement('textarea'); 548 | fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); 549 | fld.cols = this.options.cols || 40; 550 | } 551 | fld.name = this.options.paramName; 552 | fld.value = text; // No HTML breaks conversion anymore 553 | fld.className = 'editor_field'; 554 | if (this.options.submitOnBlur) 555 | fld.onblur = this._boundSubmitHandler; 556 | this._controls.editor = fld; 557 | if (this.options.loadTextURL) 558 | this.loadExternalText(); 559 | this._form.appendChild(this._controls.editor); 560 | }, 561 | createForm: function() { 562 | var ipe = this; 563 | function addText(mode, condition) { 564 | var text = ipe.options['text' + mode + 'Controls']; 565 | if (!text || condition === false) return; 566 | ipe._form.appendChild(document.createTextNode(text)); 567 | }; 568 | this._form = $(document.createElement('form')); 569 | this._form.id = this.options.formId; 570 | this._form.addClassName(this.options.formClassName); 571 | this._form.onsubmit = this._boundSubmitHandler; 572 | this.createEditField(); 573 | if ('textarea' == this._controls.editor.tagName.toLowerCase()) 574 | this._form.appendChild(document.createElement('br')); 575 | if (this.options.onFormCustomization) 576 | this.options.onFormCustomization(this, this._form); 577 | addText('Before', this.options.okControl || this.options.cancelControl); 578 | this.createControl('ok', this._boundSubmitHandler); 579 | addText('Between', this.options.okControl && this.options.cancelControl); 580 | this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); 581 | addText('After', this.options.okControl || this.options.cancelControl); 582 | }, 583 | destroy: function() { 584 | if (this._oldInnerHTML) 585 | this.element.innerHTML = this._oldInnerHTML; 586 | this.leaveEditMode(); 587 | this.unregisterListeners(); 588 | }, 589 | enterEditMode: function(e) { 590 | if (this._saving || this._editing) return; 591 | this._editing = true; 592 | this.triggerCallback('onEnterEditMode'); 593 | if (this.options.externalControl) 594 | this.options.externalControl.hide(); 595 | this.element.hide(); 596 | this.createForm(); 597 | this.element.parentNode.insertBefore(this._form, this.element); 598 | if (!this.options.loadTextURL) 599 | this.postProcessEditField(); 600 | if (e) Event.stop(e); 601 | }, 602 | enterHover: function(e) { 603 | if (this.options.hoverClassName) 604 | this.element.addClassName(this.options.hoverClassName); 605 | if (this._saving) return; 606 | this.triggerCallback('onEnterHover'); 607 | }, 608 | getText: function() { 609 | return this.element.innerHTML.unescapeHTML(); 610 | }, 611 | handleAJAXFailure: function(transport) { 612 | this.triggerCallback('onFailure', transport); 613 | if (this._oldInnerHTML) { 614 | this.element.innerHTML = this._oldInnerHTML; 615 | this._oldInnerHTML = null; 616 | } 617 | }, 618 | handleFormCancellation: function(e) { 619 | this.wrapUp(); 620 | if (e) Event.stop(e); 621 | }, 622 | handleFormSubmission: function(e) { 623 | var form = this._form; 624 | var value = $F(this._controls.editor); 625 | this.prepareSubmission(); 626 | var params = this.options.callback(form, value) || ''; 627 | if (Object.isString(params)) 628 | params = params.toQueryParams(); 629 | params.editorId = this.element.id; 630 | if (this.options.htmlResponse) { 631 | var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); 632 | Object.extend(options, { 633 | parameters: params, 634 | onComplete: this._boundWrapperHandler, 635 | onFailure: this._boundFailureHandler 636 | }); 637 | new Ajax.Updater({ success: this.element }, this.url, options); 638 | } else { 639 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 640 | Object.extend(options, { 641 | parameters: params, 642 | onComplete: this._boundWrapperHandler, 643 | onFailure: this._boundFailureHandler 644 | }); 645 | new Ajax.Request(this.url, options); 646 | } 647 | if (e) Event.stop(e); 648 | }, 649 | leaveEditMode: function() { 650 | this.element.removeClassName(this.options.savingClassName); 651 | this.removeForm(); 652 | this.leaveHover(); 653 | this.element.style.backgroundColor = this._originalBackground; 654 | this.element.show(); 655 | if (this.options.externalControl) 656 | this.options.externalControl.show(); 657 | this._saving = false; 658 | this._editing = false; 659 | this._oldInnerHTML = null; 660 | this.triggerCallback('onLeaveEditMode'); 661 | }, 662 | leaveHover: function(e) { 663 | if (this.options.hoverClassName) 664 | this.element.removeClassName(this.options.hoverClassName); 665 | if (this._saving) return; 666 | this.triggerCallback('onLeaveHover'); 667 | }, 668 | loadExternalText: function() { 669 | this._form.addClassName(this.options.loadingClassName); 670 | this._controls.editor.disabled = true; 671 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 672 | Object.extend(options, { 673 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 674 | onComplete: Prototype.emptyFunction, 675 | onSuccess: function(transport) { 676 | this._form.removeClassName(this.options.loadingClassName); 677 | var text = transport.responseText; 678 | if (this.options.stripLoadedTextTags) 679 | text = text.stripTags(); 680 | this._controls.editor.value = text; 681 | this._controls.editor.disabled = false; 682 | this.postProcessEditField(); 683 | }.bind(this), 684 | onFailure: this._boundFailureHandler 685 | }); 686 | new Ajax.Request(this.options.loadTextURL, options); 687 | }, 688 | postProcessEditField: function() { 689 | var fpc = this.options.fieldPostCreation; 690 | if (fpc) 691 | $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); 692 | }, 693 | prepareOptions: function() { 694 | this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); 695 | Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); 696 | [this._extraDefaultOptions].flatten().compact().each(function(defs) { 697 | Object.extend(this.options, defs); 698 | }.bind(this)); 699 | }, 700 | prepareSubmission: function() { 701 | this._saving = true; 702 | this.removeForm(); 703 | this.leaveHover(); 704 | this.showSaving(); 705 | }, 706 | registerListeners: function() { 707 | this._listeners = { }; 708 | var listener; 709 | $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { 710 | listener = this[pair.value].bind(this); 711 | this._listeners[pair.key] = listener; 712 | if (!this.options.externalControlOnly) 713 | this.element.observe(pair.key, listener); 714 | if (this.options.externalControl) 715 | this.options.externalControl.observe(pair.key, listener); 716 | }.bind(this)); 717 | }, 718 | removeForm: function() { 719 | if (!this._form) return; 720 | this._form.remove(); 721 | this._form = null; 722 | this._controls = { }; 723 | }, 724 | showSaving: function() { 725 | this._oldInnerHTML = this.element.innerHTML; 726 | this.element.innerHTML = this.options.savingText; 727 | this.element.addClassName(this.options.savingClassName); 728 | this.element.style.backgroundColor = this._originalBackground; 729 | this.element.show(); 730 | }, 731 | triggerCallback: function(cbName, arg) { 732 | if ('function' == typeof this.options[cbName]) { 733 | this.options[cbName](this, arg); 734 | } 735 | }, 736 | unregisterListeners: function() { 737 | $H(this._listeners).each(function(pair) { 738 | if (!this.options.externalControlOnly) 739 | this.element.stopObserving(pair.key, pair.value); 740 | if (this.options.externalControl) 741 | this.options.externalControl.stopObserving(pair.key, pair.value); 742 | }.bind(this)); 743 | }, 744 | wrapUp: function(transport) { 745 | this.leaveEditMode(); 746 | // Can't use triggerCallback due to backward compatibility: requires 747 | // binding + direct element 748 | this._boundComplete(transport, this.element); 749 | } 750 | }); 751 | 752 | Object.extend(Ajax.InPlaceEditor.prototype, { 753 | dispose: Ajax.InPlaceEditor.prototype.destroy 754 | }); 755 | 756 | Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { 757 | initialize: function($super, element, url, options) { 758 | this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; 759 | $super(element, url, options); 760 | }, 761 | 762 | createEditField: function() { 763 | var list = document.createElement('select'); 764 | list.name = this.options.paramName; 765 | list.size = 1; 766 | this._controls.editor = list; 767 | this._collection = this.options.collection || []; 768 | if (this.options.loadCollectionURL) 769 | this.loadCollection(); 770 | else 771 | this.checkForExternalText(); 772 | this._form.appendChild(this._controls.editor); 773 | }, 774 | 775 | loadCollection: function() { 776 | this._form.addClassName(this.options.loadingClassName); 777 | this.showLoadingText(this.options.loadingCollectionText); 778 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 779 | Object.extend(options, { 780 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 781 | onComplete: Prototype.emptyFunction, 782 | onSuccess: function(transport) { 783 | var js = transport.responseText.strip(); 784 | if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check 785 | throw('Server returned an invalid collection representation.'); 786 | this._collection = eval(js); 787 | this.checkForExternalText(); 788 | }.bind(this), 789 | onFailure: this.onFailure 790 | }); 791 | new Ajax.Request(this.options.loadCollectionURL, options); 792 | }, 793 | 794 | showLoadingText: function(text) { 795 | this._controls.editor.disabled = true; 796 | var tempOption = this._controls.editor.firstChild; 797 | if (!tempOption) { 798 | tempOption = document.createElement('option'); 799 | tempOption.value = ''; 800 | this._controls.editor.appendChild(tempOption); 801 | tempOption.selected = true; 802 | } 803 | tempOption.update((text || '').stripScripts().stripTags()); 804 | }, 805 | 806 | checkForExternalText: function() { 807 | this._text = this.getText(); 808 | if (this.options.loadTextURL) 809 | this.loadExternalText(); 810 | else 811 | this.buildOptionList(); 812 | }, 813 | 814 | loadExternalText: function() { 815 | this.showLoadingText(this.options.loadingText); 816 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 817 | Object.extend(options, { 818 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 819 | onComplete: Prototype.emptyFunction, 820 | onSuccess: function(transport) { 821 | this._text = transport.responseText.strip(); 822 | this.buildOptionList(); 823 | }.bind(this), 824 | onFailure: this.onFailure 825 | }); 826 | new Ajax.Request(this.options.loadTextURL, options); 827 | }, 828 | 829 | buildOptionList: function() { 830 | this._form.removeClassName(this.options.loadingClassName); 831 | this._collection = this._collection.map(function(entry) { 832 | return 2 === entry.length ? entry : [entry, entry].flatten(); 833 | }); 834 | var marker = ('value' in this.options) ? this.options.value : this._text; 835 | var textFound = this._collection.any(function(entry) { 836 | return entry[0] == marker; 837 | }.bind(this)); 838 | this._controls.editor.update(''); 839 | var option; 840 | this._collection.each(function(entry, index) { 841 | option = document.createElement('option'); 842 | option.value = entry[0]; 843 | option.selected = textFound ? entry[0] == marker : 0 == index; 844 | option.appendChild(document.createTextNode(entry[1])); 845 | this._controls.editor.appendChild(option); 846 | }.bind(this)); 847 | this._controls.editor.disabled = false; 848 | Field.scrollFreeActivate(this._controls.editor); 849 | } 850 | }); 851 | 852 | //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** 853 | //**** This only exists for a while, in order to let **** 854 | //**** users adapt to the new API. Read up on the new **** 855 | //**** API and convert your code to it ASAP! **** 856 | 857 | Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { 858 | if (!options) return; 859 | function fallback(name, expr) { 860 | if (name in options || expr === undefined) return; 861 | options[name] = expr; 862 | }; 863 | fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : 864 | options.cancelLink == options.cancelButton == false ? false : undefined))); 865 | fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : 866 | options.okLink == options.okButton == false ? false : undefined))); 867 | fallback('highlightColor', options.highlightcolor); 868 | fallback('highlightEndColor', options.highlightendcolor); 869 | }; 870 | 871 | Object.extend(Ajax.InPlaceEditor, { 872 | DefaultOptions: { 873 | ajaxOptions: { }, 874 | autoRows: 3, // Use when multi-line w/ rows == 1 875 | cancelControl: 'link', // 'link'|'button'|false 876 | cancelText: 'cancel', 877 | clickToEditText: 'Click to edit', 878 | externalControl: null, // id|elt 879 | externalControlOnly: false, 880 | fieldPostCreation: 'activate', // 'activate'|'focus'|false 881 | formClassName: 'inplaceeditor-form', 882 | formId: null, // id|elt 883 | highlightColor: '#ffff99', 884 | highlightEndColor: '#ffffff', 885 | hoverClassName: '', 886 | htmlResponse: true, 887 | loadingClassName: 'inplaceeditor-loading', 888 | loadingText: 'Loading...', 889 | okControl: 'button', // 'link'|'button'|false 890 | okText: 'ok', 891 | paramName: 'value', 892 | rows: 1, // If 1 and multi-line, uses autoRows 893 | savingClassName: 'inplaceeditor-saving', 894 | savingText: 'Saving...', 895 | size: 0, 896 | stripLoadedTextTags: false, 897 | submitOnBlur: false, 898 | textAfterControls: '', 899 | textBeforeControls: '', 900 | textBetweenControls: '' 901 | }, 902 | DefaultCallbacks: { 903 | callback: function(form) { 904 | return Form.serialize(form); 905 | }, 906 | onComplete: function(transport, element) { 907 | // For backward compatibility, this one is bound to the IPE, and passes 908 | // the element directly. It was too often customized, so we don't break it. 909 | new Effect.Highlight(element, { 910 | startcolor: this.options.highlightColor, keepBackgroundImage: true }); 911 | }, 912 | onEnterEditMode: null, 913 | onEnterHover: function(ipe) { 914 | ipe.element.style.backgroundColor = ipe.options.highlightColor; 915 | if (ipe._effect) 916 | ipe._effect.cancel(); 917 | }, 918 | onFailure: function(transport, ipe) { 919 | alert('Error communication with the server: ' + transport.responseText.stripTags()); 920 | }, 921 | onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. 922 | onLeaveEditMode: null, 923 | onLeaveHover: function(ipe) { 924 | ipe._effect = new Effect.Highlight(ipe.element, { 925 | startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, 926 | restorecolor: ipe._originalBackground, keepBackgroundImage: true 927 | }); 928 | } 929 | }, 930 | Listeners: { 931 | click: 'enterEditMode', 932 | keydown: 'checkForEscapeOrReturn', 933 | mouseover: 'enterHover', 934 | mouseout: 'leaveHover' 935 | } 936 | }); 937 | 938 | Ajax.InPlaceCollectionEditor.DefaultOptions = { 939 | loadingCollectionText: 'Loading options...' 940 | }; 941 | 942 | // Delayed observer, like Form.Element.Observer, 943 | // but waits for delay after last key input 944 | // Ideal for live-search fields 945 | 946 | Form.Element.DelayedObserver = Class.create({ 947 | initialize: function(element, delay, callback) { 948 | this.delay = delay || 0.5; 949 | this.element = $(element); 950 | this.callback = callback; 951 | this.timer = null; 952 | this.lastValue = $F(this.element); 953 | Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); 954 | }, 955 | delayedListener: function(event) { 956 | if(this.lastValue == $F(this.element)) return; 957 | if(this.timer) clearTimeout(this.timer); 958 | this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); 959 | this.lastValue = $F(this.element); 960 | }, 961 | onTimerEvent: function() { 962 | this.timer = null; 963 | this.callback(this.element, $F(this.element)); 964 | } 965 | }); -------------------------------------------------------------------------------- /spec/dummy/public/javascripts/effects.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // Contributors: 5 | // Justin Palmer (http://encytemedia.com/) 6 | // Mark Pilgrim (http://diveintomark.org/) 7 | // Martin Bialasinki 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 | // converts rgb() and #xxx to #xxxxxx format, 13 | // returns self (or first argument) if not convertable 14 | String.prototype.parseColor = function() { 15 | var color = '#'; 16 | if (this.slice(0,4) == 'rgb(') { 17 | var cols = this.slice(4,this.length-1).split(','); 18 | var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); 19 | } else { 20 | if (this.slice(0,1) == '#') { 21 | if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); 22 | if (this.length==7) color = this.toLowerCase(); 23 | } 24 | } 25 | return (color.length==7 ? color : (arguments[0] || this)); 26 | }; 27 | 28 | /*--------------------------------------------------------------------------*/ 29 | 30 | Element.collectTextNodes = function(element) { 31 | return $A($(element).childNodes).collect( function(node) { 32 | return (node.nodeType==3 ? node.nodeValue : 33 | (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); 34 | }).flatten().join(''); 35 | }; 36 | 37 | Element.collectTextNodesIgnoreClass = function(element, className) { 38 | return $A($(element).childNodes).collect( function(node) { 39 | return (node.nodeType==3 ? node.nodeValue : 40 | ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 41 | Element.collectTextNodesIgnoreClass(node, className) : '')); 42 | }).flatten().join(''); 43 | }; 44 | 45 | Element.setContentZoom = function(element, percent) { 46 | element = $(element); 47 | element.setStyle({fontSize: (percent/100) + 'em'}); 48 | if (Prototype.Browser.WebKit) window.scrollBy(0,0); 49 | return element; 50 | }; 51 | 52 | Element.getInlineOpacity = function(element){ 53 | return $(element).style.opacity || ''; 54 | }; 55 | 56 | Element.forceRerendering = function(element) { 57 | try { 58 | element = $(element); 59 | var n = document.createTextNode(' '); 60 | element.appendChild(n); 61 | element.removeChild(n); 62 | } catch(e) { } 63 | }; 64 | 65 | /*--------------------------------------------------------------------------*/ 66 | 67 | var Effect = { 68 | _elementDoesNotExistError: { 69 | name: 'ElementDoesNotExistError', 70 | message: 'The specified DOM element does not exist, but is required for this effect to operate' 71 | }, 72 | Transitions: { 73 | linear: Prototype.K, 74 | sinoidal: function(pos) { 75 | return (-Math.cos(pos*Math.PI)/2) + .5; 76 | }, 77 | reverse: function(pos) { 78 | return 1-pos; 79 | }, 80 | flicker: function(pos) { 81 | var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; 82 | return pos > 1 ? 1 : pos; 83 | }, 84 | wobble: function(pos) { 85 | return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; 86 | }, 87 | pulse: function(pos, pulses) { 88 | return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; 89 | }, 90 | spring: function(pos) { 91 | return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); 92 | }, 93 | none: function(pos) { 94 | return 0; 95 | }, 96 | full: function(pos) { 97 | return 1; 98 | } 99 | }, 100 | DefaultOptions: { 101 | duration: 1.0, // seconds 102 | fps: 100, // 100= assume 66fps max. 103 | sync: false, // true for combining 104 | from: 0.0, 105 | to: 1.0, 106 | delay: 0.0, 107 | queue: 'parallel' 108 | }, 109 | tagifyText: function(element) { 110 | var tagifyStyle = 'position:relative'; 111 | if (Prototype.Browser.IE) 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 | new Element('span', {style: tagifyStyle}).update( 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 | Object.isFunction(element)) && 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, options) { 151 | element = $(element); 152 | effect = (effect || 'appear').toLowerCase(); 153 | 154 | return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({ 155 | queue: { position:'end', scope:(element.id || 'global'), limit: 1 } 156 | }, options || {})); 157 | } 158 | }; 159 | 160 | Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; 161 | 162 | /* ------------- core effects ------------- */ 163 | 164 | Effect.ScopedQueue = Class.create(Enumerable, { 165 | initialize: function() { 166 | this.effects = []; 167 | this.interval = null; 168 | }, 169 | _each: function(iterator) { 170 | this.effects._each(iterator); 171 | }, 172 | add: function(effect) { 173 | var timestamp = new Date().getTime(); 174 | 175 | var position = Object.isString(effect.options.queue) ? 176 | effect.options.queue : effect.options.queue.position; 177 | 178 | switch(position) { 179 | case 'front': 180 | // move unstarted effects after this effect 181 | this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { 182 | e.startOn += effect.finishOn; 183 | e.finishOn += effect.finishOn; 184 | }); 185 | break; 186 | case 'with-last': 187 | timestamp = this.effects.pluck('startOn').max() || timestamp; 188 | break; 189 | case 'end': 190 | // start effect after last queued effect has finished 191 | timestamp = this.effects.pluck('finishOn').max() || timestamp; 192 | break; 193 | } 194 | 195 | effect.startOn += timestamp; 196 | effect.finishOn += timestamp; 197 | 198 | if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) 199 | this.effects.push(effect); 200 | 201 | if (!this.interval) 202 | this.interval = setInterval(this.loop.bind(this), 15); 203 | }, 204 | remove: function(effect) { 205 | this.effects = this.effects.reject(function(e) { return e==effect }); 206 | if (this.effects.length == 0) { 207 | clearInterval(this.interval); 208 | this.interval = null; 209 | } 210 | }, 211 | loop: function() { 212 | var timePos = new Date().getTime(); 213 | for(var i=0, len=this.effects.length;i= this.startOn) { 274 | if (timePos >= this.finishOn) { 275 | this.render(1.0); 276 | this.cancel(); 277 | this.event('beforeFinish'); 278 | if (this.finish) this.finish(); 279 | this.event('afterFinish'); 280 | return; 281 | } 282 | var pos = (timePos - this.startOn) / this.totalTime, 283 | frame = (pos * this.totalFrames).round(); 284 | if (frame > this.currentFrame) { 285 | this.render(pos); 286 | this.currentFrame = frame; 287 | } 288 | } 289 | }, 290 | cancel: function() { 291 | if (!this.options.sync) 292 | Effect.Queues.get(Object.isString(this.options.queue) ? 293 | 'global' : this.options.queue.scope).remove(this); 294 | this.state = 'finished'; 295 | }, 296 | event: function(eventName) { 297 | if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); 298 | if (this.options[eventName]) this.options[eventName](this); 299 | }, 300 | inspect: function() { 301 | var data = $H(); 302 | for(property in this) 303 | if (!Object.isFunction(this[property])) data.set(property, this[property]); 304 | return '#'; 305 | } 306 | }); 307 | 308 | Effect.Parallel = Class.create(Effect.Base, { 309 | initialize: function(effects) { 310 | this.effects = effects || []; 311 | this.start(arguments[1]); 312 | }, 313 | update: function(position) { 314 | this.effects.invoke('render', position); 315 | }, 316 | finish: function(position) { 317 | this.effects.each( function(effect) { 318 | effect.render(1.0); 319 | effect.cancel(); 320 | effect.event('beforeFinish'); 321 | if (effect.finish) effect.finish(position); 322 | effect.event('afterFinish'); 323 | }); 324 | } 325 | }); 326 | 327 | Effect.Tween = Class.create(Effect.Base, { 328 | initialize: function(object, from, to) { 329 | object = Object.isString(object) ? $(object) : object; 330 | var args = $A(arguments), method = args.last(), 331 | options = args.length == 5 ? args[3] : null; 332 | this.method = Object.isFunction(method) ? method.bind(object) : 333 | Object.isFunction(object[method]) ? object[method].bind(object) : 334 | function(value) { object[method] = value }; 335 | this.start(Object.extend({ from: from, to: to }, options || { })); 336 | }, 337 | update: function(position) { 338 | this.method(position); 339 | } 340 | }); 341 | 342 | Effect.Event = Class.create(Effect.Base, { 343 | initialize: function() { 344 | this.start(Object.extend({ duration: 0 }, arguments[0] || { })); 345 | }, 346 | update: Prototype.emptyFunction 347 | }); 348 | 349 | Effect.Opacity = Class.create(Effect.Base, { 350 | initialize: function(element) { 351 | this.element = $(element); 352 | if (!this.element) throw(Effect._elementDoesNotExistError); 353 | // make this work on IE on elements without 'layout' 354 | if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) 355 | this.element.setStyle({zoom: 1}); 356 | var options = Object.extend({ 357 | from: this.element.getOpacity() || 0.0, 358 | to: 1.0 359 | }, arguments[1] || { }); 360 | this.start(options); 361 | }, 362 | update: function(position) { 363 | this.element.setOpacity(position); 364 | } 365 | }); 366 | 367 | Effect.Move = Class.create(Effect.Base, { 368 | initialize: function(element) { 369 | this.element = $(element); 370 | if (!this.element) throw(Effect._elementDoesNotExistError); 371 | var options = Object.extend({ 372 | x: 0, 373 | y: 0, 374 | mode: 'relative' 375 | }, arguments[1] || { }); 376 | this.start(options); 377 | }, 378 | setup: function() { 379 | this.element.makePositioned(); 380 | this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); 381 | this.originalTop = parseFloat(this.element.getStyle('top') || '0'); 382 | if (this.options.mode == 'absolute') { 383 | this.options.x = this.options.x - this.originalLeft; 384 | this.options.y = this.options.y - this.originalTop; 385 | } 386 | }, 387 | update: function(position) { 388 | this.element.setStyle({ 389 | left: (this.options.x * position + this.originalLeft).round() + 'px', 390 | top: (this.options.y * position + this.originalTop).round() + 'px' 391 | }); 392 | } 393 | }); 394 | 395 | // for backwards compatibility 396 | Effect.MoveBy = function(element, toTop, toLeft) { 397 | return new Effect.Move(element, 398 | Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); 399 | }; 400 | 401 | Effect.Scale = Class.create(Effect.Base, { 402 | initialize: function(element, percent) { 403 | this.element = $(element); 404 | if (!this.element) throw(Effect._elementDoesNotExistError); 405 | var options = Object.extend({ 406 | scaleX: true, 407 | scaleY: true, 408 | scaleContent: true, 409 | scaleFromCenter: false, 410 | scaleMode: 'box', // 'box' or 'contents' or { } with provided values 411 | scaleFrom: 100.0, 412 | scaleTo: percent 413 | }, arguments[2] || { }); 414 | this.start(options); 415 | }, 416 | setup: function() { 417 | this.restoreAfterFinish = this.options.restoreAfterFinish || false; 418 | this.elementPositioning = this.element.getStyle('position'); 419 | 420 | this.originalStyle = { }; 421 | ['top','left','width','height','fontSize'].each( function(k) { 422 | this.originalStyle[k] = this.element.style[k]; 423 | }.bind(this)); 424 | 425 | this.originalTop = this.element.offsetTop; 426 | this.originalLeft = this.element.offsetLeft; 427 | 428 | var fontSize = this.element.getStyle('font-size') || '100%'; 429 | ['em','px','%','pt'].each( function(fontSizeType) { 430 | if (fontSize.indexOf(fontSizeType)>0) { 431 | this.fontSize = parseFloat(fontSize); 432 | this.fontSizeType = fontSizeType; 433 | } 434 | }.bind(this)); 435 | 436 | this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; 437 | 438 | this.dims = null; 439 | if (this.options.scaleMode=='box') 440 | this.dims = [this.element.offsetHeight, this.element.offsetWidth]; 441 | if (/^content/.test(this.options.scaleMode)) 442 | this.dims = [this.element.scrollHeight, this.element.scrollWidth]; 443 | if (!this.dims) 444 | this.dims = [this.options.scaleMode.originalHeight, 445 | this.options.scaleMode.originalWidth]; 446 | }, 447 | update: function(position) { 448 | var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); 449 | if (this.options.scaleContent && this.fontSize) 450 | this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); 451 | this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); 452 | }, 453 | finish: function(position) { 454 | if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); 455 | }, 456 | setDimensions: function(height, width) { 457 | var d = { }; 458 | if (this.options.scaleX) d.width = width.round() + 'px'; 459 | if (this.options.scaleY) d.height = height.round() + 'px'; 460 | if (this.options.scaleFromCenter) { 461 | var topd = (height - this.dims[0])/2; 462 | var leftd = (width - this.dims[1])/2; 463 | if (this.elementPositioning == 'absolute') { 464 | if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; 465 | if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; 466 | } else { 467 | if (this.options.scaleY) d.top = -topd + 'px'; 468 | if (this.options.scaleX) d.left = -leftd + 'px'; 469 | } 470 | } 471 | this.element.setStyle(d); 472 | } 473 | }); 474 | 475 | Effect.Highlight = Class.create(Effect.Base, { 476 | initialize: function(element) { 477 | this.element = $(element); 478 | if (!this.element) throw(Effect._elementDoesNotExistError); 479 | var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); 480 | this.start(options); 481 | }, 482 | setup: function() { 483 | // Prevent executing on elements not in the layout flow 484 | if (this.element.getStyle('display')=='none') { this.cancel(); return; } 485 | // Disable background image during the effect 486 | this.oldStyle = { }; 487 | if (!this.options.keepBackgroundImage) { 488 | this.oldStyle.backgroundImage = this.element.getStyle('background-image'); 489 | this.element.setStyle({backgroundImage: 'none'}); 490 | } 491 | if (!this.options.endcolor) 492 | this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); 493 | if (!this.options.restorecolor) 494 | this.options.restorecolor = this.element.getStyle('background-color'); 495 | // init color calculations 496 | this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); 497 | 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)); 498 | }, 499 | update: function(position) { 500 | this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ 501 | return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); 502 | }, 503 | finish: function() { 504 | this.element.setStyle(Object.extend(this.oldStyle, { 505 | backgroundColor: this.options.restorecolor 506 | })); 507 | } 508 | }); 509 | 510 | Effect.ScrollTo = function(element) { 511 | var options = arguments[1] || { }, 512 | scrollOffsets = document.viewport.getScrollOffsets(), 513 | elementOffsets = $(element).cumulativeOffset(); 514 | 515 | if (options.offset) elementOffsets[1] += options.offset; 516 | 517 | return new Effect.Tween(null, 518 | scrollOffsets.top, 519 | elementOffsets[1], 520 | options, 521 | function(p){ scrollTo(scrollOffsets.left, p.round()); } 522 | ); 523 | }; 524 | 525 | /* ------------- combination effects ------------- */ 526 | 527 | Effect.Fade = function(element) { 528 | element = $(element); 529 | var oldOpacity = element.getInlineOpacity(); 530 | var options = Object.extend({ 531 | from: element.getOpacity() || 1.0, 532 | to: 0.0, 533 | afterFinishInternal: function(effect) { 534 | if (effect.options.to!=0) return; 535 | effect.element.hide().setStyle({opacity: oldOpacity}); 536 | } 537 | }, arguments[1] || { }); 538 | return new Effect.Opacity(element,options); 539 | }; 540 | 541 | Effect.Appear = function(element) { 542 | element = $(element); 543 | var options = Object.extend({ 544 | from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), 545 | to: 1.0, 546 | // force Safari to render floated elements properly 547 | afterFinishInternal: function(effect) { 548 | effect.element.forceRerendering(); 549 | }, 550 | beforeSetup: function(effect) { 551 | effect.element.setOpacity(effect.options.from).show(); 552 | }}, arguments[1] || { }); 553 | return new Effect.Opacity(element,options); 554 | }; 555 | 556 | Effect.Puff = function(element) { 557 | element = $(element); 558 | var oldStyle = { 559 | opacity: element.getInlineOpacity(), 560 | position: element.getStyle('position'), 561 | top: element.style.top, 562 | left: element.style.left, 563 | width: element.style.width, 564 | height: element.style.height 565 | }; 566 | return new Effect.Parallel( 567 | [ new Effect.Scale(element, 200, 568 | { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 569 | new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 570 | Object.extend({ duration: 1.0, 571 | beforeSetupInternal: function(effect) { 572 | Position.absolutize(effect.effects[0].element); 573 | }, 574 | afterFinishInternal: function(effect) { 575 | effect.effects[0].element.hide().setStyle(oldStyle); } 576 | }, arguments[1] || { }) 577 | ); 578 | }; 579 | 580 | Effect.BlindUp = function(element) { 581 | element = $(element); 582 | element.makeClipping(); 583 | return new Effect.Scale(element, 0, 584 | Object.extend({ scaleContent: false, 585 | scaleX: false, 586 | restoreAfterFinish: true, 587 | afterFinishInternal: function(effect) { 588 | effect.element.hide().undoClipping(); 589 | } 590 | }, arguments[1] || { }) 591 | ); 592 | }; 593 | 594 | Effect.BlindDown = function(element) { 595 | element = $(element); 596 | var elementDimensions = element.getDimensions(); 597 | return new Effect.Scale(element, 100, Object.extend({ 598 | scaleContent: false, 599 | scaleX: false, 600 | scaleFrom: 0, 601 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 602 | restoreAfterFinish: true, 603 | afterSetup: function(effect) { 604 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 605 | }, 606 | afterFinishInternal: function(effect) { 607 | effect.element.undoClipping(); 608 | } 609 | }, arguments[1] || { })); 610 | }; 611 | 612 | Effect.SwitchOff = function(element) { 613 | element = $(element); 614 | var oldOpacity = element.getInlineOpacity(); 615 | return new Effect.Appear(element, Object.extend({ 616 | duration: 0.4, 617 | from: 0, 618 | transition: Effect.Transitions.flicker, 619 | afterFinishInternal: function(effect) { 620 | new Effect.Scale(effect.element, 1, { 621 | duration: 0.3, scaleFromCenter: true, 622 | scaleX: false, scaleContent: false, restoreAfterFinish: true, 623 | beforeSetup: function(effect) { 624 | effect.element.makePositioned().makeClipping(); 625 | }, 626 | afterFinishInternal: function(effect) { 627 | effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); 628 | } 629 | }); 630 | } 631 | }, arguments[1] || { })); 632 | }; 633 | 634 | Effect.DropOut = function(element) { 635 | element = $(element); 636 | var oldStyle = { 637 | top: element.getStyle('top'), 638 | left: element.getStyle('left'), 639 | opacity: element.getInlineOpacity() }; 640 | return new Effect.Parallel( 641 | [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 642 | new Effect.Opacity(element, { sync: true, to: 0.0 }) ], 643 | Object.extend( 644 | { duration: 0.5, 645 | beforeSetup: function(effect) { 646 | effect.effects[0].element.makePositioned(); 647 | }, 648 | afterFinishInternal: function(effect) { 649 | effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); 650 | } 651 | }, arguments[1] || { })); 652 | }; 653 | 654 | Effect.Shake = function(element) { 655 | element = $(element); 656 | var options = Object.extend({ 657 | distance: 20, 658 | duration: 0.5 659 | }, arguments[1] || {}); 660 | var distance = parseFloat(options.distance); 661 | var split = parseFloat(options.duration) / 10.0; 662 | var oldStyle = { 663 | top: element.getStyle('top'), 664 | left: element.getStyle('left') }; 665 | return new Effect.Move(element, 666 | { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { 667 | new Effect.Move(effect.element, 668 | { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 669 | new Effect.Move(effect.element, 670 | { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 671 | new Effect.Move(effect.element, 672 | { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 673 | new Effect.Move(effect.element, 674 | { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 675 | new Effect.Move(effect.element, 676 | { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { 677 | effect.element.undoPositioned().setStyle(oldStyle); 678 | }}); }}); }}); }}); }}); }}); 679 | }; 680 | 681 | Effect.SlideDown = function(element) { 682 | element = $(element).cleanWhitespace(); 683 | // SlideDown need to have the content of the element wrapped in a container element with fixed height! 684 | var oldInnerBottom = element.down().getStyle('bottom'); 685 | var elementDimensions = element.getDimensions(); 686 | return new Effect.Scale(element, 100, Object.extend({ 687 | scaleContent: false, 688 | scaleX: false, 689 | scaleFrom: window.opera ? 0 : 1, 690 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 691 | restoreAfterFinish: true, 692 | afterSetup: function(effect) { 693 | effect.element.makePositioned(); 694 | effect.element.down().makePositioned(); 695 | if (window.opera) effect.element.setStyle({top: ''}); 696 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 697 | }, 698 | afterUpdateInternal: function(effect) { 699 | effect.element.down().setStyle({bottom: 700 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 701 | }, 702 | afterFinishInternal: function(effect) { 703 | effect.element.undoClipping().undoPositioned(); 704 | effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } 705 | }, arguments[1] || { }) 706 | ); 707 | }; 708 | 709 | Effect.SlideUp = function(element) { 710 | element = $(element).cleanWhitespace(); 711 | var oldInnerBottom = element.down().getStyle('bottom'); 712 | var elementDimensions = element.getDimensions(); 713 | return new Effect.Scale(element, window.opera ? 0 : 1, 714 | Object.extend({ scaleContent: false, 715 | scaleX: false, 716 | scaleMode: 'box', 717 | scaleFrom: 100, 718 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 719 | restoreAfterFinish: true, 720 | afterSetup: function(effect) { 721 | effect.element.makePositioned(); 722 | effect.element.down().makePositioned(); 723 | if (window.opera) effect.element.setStyle({top: ''}); 724 | effect.element.makeClipping().show(); 725 | }, 726 | afterUpdateInternal: function(effect) { 727 | effect.element.down().setStyle({bottom: 728 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 729 | }, 730 | afterFinishInternal: function(effect) { 731 | effect.element.hide().undoClipping().undoPositioned(); 732 | effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); 733 | } 734 | }, arguments[1] || { }) 735 | ); 736 | }; 737 | 738 | // Bug in opera makes the TD containing this element expand for a instance after finish 739 | Effect.Squish = function(element) { 740 | return new Effect.Scale(element, window.opera ? 1 : 0, { 741 | restoreAfterFinish: true, 742 | beforeSetup: function(effect) { 743 | effect.element.makeClipping(); 744 | }, 745 | afterFinishInternal: function(effect) { 746 | effect.element.hide().undoClipping(); 747 | } 748 | }); 749 | }; 750 | 751 | Effect.Grow = function(element) { 752 | element = $(element); 753 | var options = Object.extend({ 754 | direction: 'center', 755 | moveTransition: Effect.Transitions.sinoidal, 756 | scaleTransition: Effect.Transitions.sinoidal, 757 | opacityTransition: Effect.Transitions.full 758 | }, arguments[1] || { }); 759 | var oldStyle = { 760 | top: element.style.top, 761 | left: element.style.left, 762 | height: element.style.height, 763 | width: element.style.width, 764 | opacity: element.getInlineOpacity() }; 765 | 766 | var dims = element.getDimensions(); 767 | var initialMoveX, initialMoveY; 768 | var moveX, moveY; 769 | 770 | switch (options.direction) { 771 | case 'top-left': 772 | initialMoveX = initialMoveY = moveX = moveY = 0; 773 | break; 774 | case 'top-right': 775 | initialMoveX = dims.width; 776 | initialMoveY = moveY = 0; 777 | moveX = -dims.width; 778 | break; 779 | case 'bottom-left': 780 | initialMoveX = moveX = 0; 781 | initialMoveY = dims.height; 782 | moveY = -dims.height; 783 | break; 784 | case 'bottom-right': 785 | initialMoveX = dims.width; 786 | initialMoveY = dims.height; 787 | moveX = -dims.width; 788 | moveY = -dims.height; 789 | break; 790 | case 'center': 791 | initialMoveX = dims.width / 2; 792 | initialMoveY = dims.height / 2; 793 | moveX = -dims.width / 2; 794 | moveY = -dims.height / 2; 795 | break; 796 | } 797 | 798 | return new Effect.Move(element, { 799 | x: initialMoveX, 800 | y: initialMoveY, 801 | duration: 0.01, 802 | beforeSetup: function(effect) { 803 | effect.element.hide().makeClipping().makePositioned(); 804 | }, 805 | afterFinishInternal: function(effect) { 806 | new Effect.Parallel( 807 | [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), 808 | new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), 809 | new Effect.Scale(effect.element, 100, { 810 | scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 811 | sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) 812 | ], Object.extend({ 813 | beforeSetup: function(effect) { 814 | effect.effects[0].element.setStyle({height: '0px'}).show(); 815 | }, 816 | afterFinishInternal: function(effect) { 817 | effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 818 | } 819 | }, options) 820 | ); 821 | } 822 | }); 823 | }; 824 | 825 | Effect.Shrink = function(element) { 826 | element = $(element); 827 | var options = Object.extend({ 828 | direction: 'center', 829 | moveTransition: Effect.Transitions.sinoidal, 830 | scaleTransition: Effect.Transitions.sinoidal, 831 | opacityTransition: Effect.Transitions.none 832 | }, arguments[1] || { }); 833 | var oldStyle = { 834 | top: element.style.top, 835 | left: element.style.left, 836 | height: element.style.height, 837 | width: element.style.width, 838 | opacity: element.getInlineOpacity() }; 839 | 840 | var dims = element.getDimensions(); 841 | var moveX, moveY; 842 | 843 | switch (options.direction) { 844 | case 'top-left': 845 | moveX = moveY = 0; 846 | break; 847 | case 'top-right': 848 | moveX = dims.width; 849 | moveY = 0; 850 | break; 851 | case 'bottom-left': 852 | moveX = 0; 853 | moveY = dims.height; 854 | break; 855 | case 'bottom-right': 856 | moveX = dims.width; 857 | moveY = dims.height; 858 | break; 859 | case 'center': 860 | moveX = dims.width / 2; 861 | moveY = dims.height / 2; 862 | break; 863 | } 864 | 865 | return new Effect.Parallel( 866 | [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), 867 | new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), 868 | new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) 869 | ], Object.extend({ 870 | beforeStartInternal: function(effect) { 871 | effect.effects[0].element.makePositioned().makeClipping(); 872 | }, 873 | afterFinishInternal: function(effect) { 874 | effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } 875 | }, options) 876 | ); 877 | }; 878 | 879 | Effect.Pulsate = function(element) { 880 | element = $(element); 881 | var options = arguments[1] || { }, 882 | oldOpacity = element.getInlineOpacity(), 883 | transition = options.transition || Effect.Transitions.linear, 884 | reverser = function(pos){ 885 | return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); 886 | }; 887 | 888 | return new Effect.Opacity(element, 889 | Object.extend(Object.extend({ duration: 2.0, from: 0, 890 | afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } 891 | }, options), {transition: reverser})); 892 | }; 893 | 894 | Effect.Fold = function(element) { 895 | element = $(element); 896 | var oldStyle = { 897 | top: element.style.top, 898 | left: element.style.left, 899 | width: element.style.width, 900 | height: element.style.height }; 901 | element.makeClipping(); 902 | return new Effect.Scale(element, 5, Object.extend({ 903 | scaleContent: false, 904 | scaleX: false, 905 | afterFinishInternal: function(effect) { 906 | new Effect.Scale(element, 1, { 907 | scaleContent: false, 908 | scaleY: false, 909 | afterFinishInternal: function(effect) { 910 | effect.element.hide().undoClipping().setStyle(oldStyle); 911 | } }); 912 | }}, arguments[1] || { })); 913 | }; 914 | 915 | Effect.Morph = Class.create(Effect.Base, { 916 | initialize: function(element) { 917 | this.element = $(element); 918 | if (!this.element) throw(Effect._elementDoesNotExistError); 919 | var options = Object.extend({ 920 | style: { } 921 | }, arguments[1] || { }); 922 | 923 | if (!Object.isString(options.style)) this.style = $H(options.style); 924 | else { 925 | if (options.style.include(':')) 926 | this.style = options.style.parseStyle(); 927 | else { 928 | this.element.addClassName(options.style); 929 | this.style = $H(this.element.getStyles()); 930 | this.element.removeClassName(options.style); 931 | var css = this.element.getStyles(); 932 | this.style = this.style.reject(function(style) { 933 | return style.value == css[style.key]; 934 | }); 935 | options.afterFinishInternal = function(effect) { 936 | effect.element.addClassName(effect.options.style); 937 | effect.transforms.each(function(transform) { 938 | effect.element.style[transform.style] = ''; 939 | }); 940 | }; 941 | } 942 | } 943 | this.start(options); 944 | }, 945 | 946 | setup: function(){ 947 | function parseColor(color){ 948 | if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; 949 | color = color.parseColor(); 950 | return $R(0,2).map(function(i){ 951 | return parseInt( color.slice(i*2+1,i*2+3), 16 ); 952 | }); 953 | } 954 | this.transforms = this.style.map(function(pair){ 955 | var property = pair[0], value = pair[1], unit = null; 956 | 957 | if (value.parseColor('#zzzzzz') != '#zzzzzz') { 958 | value = value.parseColor(); 959 | unit = 'color'; 960 | } else if (property == 'opacity') { 961 | value = parseFloat(value); 962 | if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) 963 | this.element.setStyle({zoom: 1}); 964 | } else if (Element.CSS_LENGTH.test(value)) { 965 | var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); 966 | value = parseFloat(components[1]); 967 | unit = (components.length == 3) ? components[2] : null; 968 | } 969 | 970 | var originalValue = this.element.getStyle(property); 971 | return { 972 | style: property.camelize(), 973 | originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), 974 | targetValue: unit=='color' ? parseColor(value) : value, 975 | unit: unit 976 | }; 977 | }.bind(this)).reject(function(transform){ 978 | return ( 979 | (transform.originalValue == transform.targetValue) || 980 | ( 981 | transform.unit != 'color' && 982 | (isNaN(transform.originalValue) || isNaN(transform.targetValue)) 983 | ) 984 | ); 985 | }); 986 | }, 987 | update: function(position) { 988 | var style = { }, transform, i = this.transforms.length; 989 | while(i--) 990 | style[(transform = this.transforms[i]).style] = 991 | transform.unit=='color' ? '#'+ 992 | (Math.round(transform.originalValue[0]+ 993 | (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + 994 | (Math.round(transform.originalValue[1]+ 995 | (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + 996 | (Math.round(transform.originalValue[2]+ 997 | (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : 998 | (transform.originalValue + 999 | (transform.targetValue - transform.originalValue) * position).toFixed(3) + 1000 | (transform.unit === null ? '' : transform.unit); 1001 | this.element.setStyle(style, true); 1002 | } 1003 | }); 1004 | 1005 | Effect.Transform = Class.create({ 1006 | initialize: function(tracks){ 1007 | this.tracks = []; 1008 | this.options = arguments[1] || { }; 1009 | this.addTracks(tracks); 1010 | }, 1011 | addTracks: function(tracks){ 1012 | tracks.each(function(track){ 1013 | track = $H(track); 1014 | var data = track.values().first(); 1015 | this.tracks.push($H({ 1016 | ids: track.keys().first(), 1017 | effect: Effect.Morph, 1018 | options: { style: data } 1019 | })); 1020 | }.bind(this)); 1021 | return this; 1022 | }, 1023 | play: function(){ 1024 | return new Effect.Parallel( 1025 | this.tracks.map(function(track){ 1026 | var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); 1027 | var elements = [$(ids) || $$(ids)].flatten(); 1028 | return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); 1029 | }).flatten(), 1030 | this.options 1031 | ); 1032 | } 1033 | }); 1034 | 1035 | Element.CSS_PROPERTIES = $w( 1036 | 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + 1037 | 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + 1038 | 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + 1039 | 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + 1040 | 'fontSize fontWeight height left letterSpacing lineHeight ' + 1041 | 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ 1042 | 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + 1043 | 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + 1044 | 'right textIndent top width wordSpacing zIndex'); 1045 | 1046 | Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; 1047 | 1048 | String.__parseStyleElement = document.createElement('div'); 1049 | String.prototype.parseStyle = function(){ 1050 | var style, styleRules = $H(); 1051 | if (Prototype.Browser.WebKit) 1052 | style = new Element('div',{style:this}).style; 1053 | else { 1054 | String.__parseStyleElement.innerHTML = '
    '; 1055 | style = String.__parseStyleElement.childNodes[0].style; 1056 | } 1057 | 1058 | Element.CSS_PROPERTIES.each(function(property){ 1059 | if (style[property]) styleRules.set(property, style[property]); 1060 | }); 1061 | 1062 | if (Prototype.Browser.IE && this.include('opacity')) 1063 | styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); 1064 | 1065 | return styleRules; 1066 | }; 1067 | 1068 | if (document.defaultView && document.defaultView.getComputedStyle) { 1069 | Element.getStyles = function(element) { 1070 | var css = document.defaultView.getComputedStyle($(element), null); 1071 | return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { 1072 | styles[property] = css[property]; 1073 | return styles; 1074 | }); 1075 | }; 1076 | } else { 1077 | Element.getStyles = function(element) { 1078 | element = $(element); 1079 | var css = element.currentStyle, styles; 1080 | styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { 1081 | results[property] = css[property]; 1082 | return results; 1083 | }); 1084 | if (!styles.opacity) styles.opacity = element.getOpacity(); 1085 | return styles; 1086 | }; 1087 | } 1088 | 1089 | Effect.Methods = { 1090 | morph: function(element, style) { 1091 | element = $(element); 1092 | new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); 1093 | return element; 1094 | }, 1095 | visualEffect: function(element, effect, options) { 1096 | element = $(element); 1097 | var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); 1098 | new Effect[klass](element, options); 1099 | return element; 1100 | }, 1101 | highlight: function(element, options) { 1102 | element = $(element); 1103 | new Effect.Highlight(element, options); 1104 | return element; 1105 | } 1106 | }; 1107 | 1108 | $w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ 1109 | 'pulsate shake puff squish switchOff dropOut').each( 1110 | function(effect) { 1111 | Effect.Methods[effect] = function(element, options){ 1112 | element = $(element); 1113 | Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); 1114 | return element; 1115 | }; 1116 | } 1117 | ); 1118 | 1119 | $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( 1120 | function(f) { Effect.Methods[f] = Element[f]; } 1121 | ); 1122 | 1123 | Element.addMethods(Effect.Methods); --------------------------------------------------------------------------------