├── 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"] ||= ""
19 | config["navPostfix"] ||= " "
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"] = "");
27 | config["navPostfix"] || (config["navPostfix"] = " ");
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 | "#{label.charAt(0).toUpperCase()}#{label.substr(1)} "
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 ""
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 ""
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 ""
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 'Name '
6 |
7 | it "assigns humanized default value", ->
8 | tag = labelTag 'supervising_boss_id'
9 | (expect tag).toEqual 'Supervising boss '
10 |
11 | it "allows alternative value", ->
12 | tag = labelTag 'name', 'Your Name'
13 | (expect tag).toEqual 'Your Name '
14 |
15 | it "allows class to be assigned", ->
16 | tag = labelTag 'name', 'class':'small_label'
17 | (expect tag).toEqual 'Name '
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 "" + (label.charAt(0).toUpperCase()) + (label.substr(1)) + " ";
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("");
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("ff " + this.separator + "aa ");
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("");
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('Name ');
7 | });
8 | it("assigns humanized default value", function() {
9 | var tag;
10 | tag = labelTag('supervising_boss_id');
11 | return (expect(tag)).toEqual('Supervising boss ');
12 | });
13 | it("allows alternative value", function() {
14 | var tag;
15 | tag = labelTag('name', 'Your Name');
16 | return (expect(tag)).toEqual('Your Name ');
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('Name ');
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 | Name Email
22 | <% for user in @users %>
23 |
24 | <%= user.name %> <%= mailTo(user.email) %>
25 |
26 | <% end %>
27 |
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 |
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("" + (name) + ">");
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);
--------------------------------------------------------------------------------