Removed superfluous 'tracks' directory at the root of the repository.

Testing commits to github.
This commit is contained in:
bsag 2008-05-20 21:28:26 +01:00
parent 6a42901514
commit 4cbf5a34d3
2269 changed files with 0 additions and 0 deletions

23
vendor/plugins/acts_as_list/README vendored Normal file
View file

@ -0,0 +1,23 @@
ActsAsList
==========
This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table.
Example
=======
class TodoList < ActiveRecord::Base
has_many :todo_items, :order => "position"
end
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
acts_as_list :scope => :todo_list
end
todo_list.first.move_to_bottom
todo_list.last.move_higher
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

3
vendor/plugins/acts_as_list/init.rb vendored Normal file
View file

@ -0,0 +1,3 @@
$:.unshift "#{File.dirname(__FILE__)}/lib"
require 'active_record/acts/list'
ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }

View file

@ -0,0 +1,256 @@
module ActiveRecord
module Acts #:nodoc:
module List #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
# The class that has this specified needs to have a +position+ column defined as an integer on
# the mapped database table.
#
# Todo list example:
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, :order => "position"
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
# acts_as_list :scope => :todo_list
# end
#
# todo_list.first.move_to_bottom
# todo_list.last.move_higher
module ClassMethods
# Configuration options are:
#
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
def acts_as_list(options = {})
configuration = { :column => "position", :scope => "1 = 1" }
configuration.update(options) if options.is_a?(Hash)
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
if configuration[:scope].is_a?(Symbol)
scope_condition_method = %(
def scope_condition
if #{configuration[:scope].to_s}.nil?
"#{configuration[:scope].to_s} IS NULL"
else
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
end
end
)
else
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
end
class_eval <<-EOV
include ActiveRecord::Acts::List::InstanceMethods
def acts_as_list_class
::#{self.name}
end
def position_column
'#{configuration[:column]}'
end
#{scope_condition_method}
before_destroy :remove_from_list
before_create :add_to_list_bottom
EOV
end
end
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
# the first in the list of all chapters.
module InstanceMethods
# Insert the item at the given position (defaults to the top position of 1).
def insert_at(position = 1)
insert_at_position(position)
end
# Swap positions with the next lower item, if one exists.
def move_lower
return unless lower_item
acts_as_list_class.transaction do
lower_item.decrement_position
increment_position
end
end
# Swap positions with the next higher item, if one exists.
def move_higher
return unless higher_item
acts_as_list_class.transaction do
higher_item.increment_position
decrement_position
end
end
# Move to the bottom of the list. If the item is already in the list, the items below it have their
# position adjusted accordingly.
def move_to_bottom
return unless in_list?
acts_as_list_class.transaction do
decrement_positions_on_lower_items
assume_bottom_position
end
end
# Move to the top of the list. If the item is already in the list, the items above it have their
# position adjusted accordingly.
def move_to_top
return unless in_list?
acts_as_list_class.transaction do
increment_positions_on_higher_items
assume_top_position
end
end
# Removes the item from the list.
def remove_from_list
if in_list?
decrement_positions_on_lower_items
update_attribute position_column, nil
end
end
# Increase the position of this item without adjusting the rest of the list.
def increment_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i + 1
end
# Decrease the position of this item without adjusting the rest of the list.
def decrement_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i - 1
end
# Return +true+ if this object is the first in the list.
def first?
return false unless in_list?
self.send(position_column) == 1
end
# Return +true+ if this object is the last in the list.
def last?
return false unless in_list?
self.send(position_column) == bottom_position_in_list
end
# Return the next higher item in the list.
def higher_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
)
end
# Return the next lower item in the list.
def lower_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
)
end
# Test if this record is in a list
def in_list?
!send(position_column).nil?
end
private
def add_to_list_top
increment_positions_on_all_items
end
def add_to_list_bottom
self[position_column] = bottom_position_in_list.to_i + 1
end
# Overwrite this method to define the scope of the list changes
def scope_condition() "1" end
# Returns the bottom position number in the list.
# bottom_position_in_list # => 2
def bottom_position_in_list(except = nil)
item = bottom_item(except)
item ? item.send(position_column) : 0
end
# Returns the bottom item
def bottom_item(except = nil)
conditions = scope_condition
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
end
# Forces item to assume the bottom position in the list.
def assume_bottom_position
update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
end
# Forces item to assume the top position in the list.
def assume_top_position
update_attribute(position_column, 1)
end
# This has the effect of moving all the higher items up one.
def decrement_positions_on_higher_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
)
end
# This has the effect of moving all the lower items up one.
def decrement_positions_on_lower_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
)
end
# This has the effect of moving all the higher items down one.
def increment_positions_on_higher_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
)
end
# This has the effect of moving all the lower items down one.
def increment_positions_on_lower_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
)
end
# Increments position (<tt>position_column</tt>) of all items in the list.
def increment_positions_on_all_items
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
)
end
def insert_at_position(position)
remove_from_list
increment_positions_on_lower_items(position)
self.update_attribute(position_column, position)
end
end
end
end
end

View file

@ -0,0 +1,332 @@
require 'test/unit'
require 'rubygems'
gem 'activerecord', '>= 1.15.4.7794'
require 'active_record'
require "#{File.dirname(__FILE__)}/../init"
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
def setup_db
ActiveRecord::Schema.define(:version => 1) do
create_table :mixins do |t|
t.column :pos, :integer
t.column :parent_id, :integer
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
end
def teardown_db
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table)
end
end
class Mixin < ActiveRecord::Base
end
class ListMixin < Mixin
acts_as_list :column => "pos", :scope => :parent
def self.table_name() "mixins" end
end
class ListMixinSub1 < ListMixin
end
class ListMixinSub2 < ListMixin
end
class ListWithStringScopeMixin < ActiveRecord::Base
acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
def self.table_name() "mixins" end
end
class ListTest < Test::Unit::TestCase
def setup
setup_db
(1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
end
def teardown
teardown_db
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_lower
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_higher
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_top
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(4).move_to_top
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(3).move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
assert_nil ListMixin.find(1).higher_item
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
assert_nil ListMixin.find(4).lower_item
end
def test_injection
item = ListMixin.new(:parent_id => 1)
assert_equal "parent_id = 1", item.scope_condition
assert_equal "pos", item.position_column
end
def test_insert
new = ListMixin.create(:parent_id => 20)
assert_equal 1, new.pos
assert new.first?
assert new.last?
new = ListMixin.create(:parent_id => 20)
assert_equal 2, new.pos
assert !new.first?
assert new.last?
new = ListMixin.create(:parent_id => 20)
assert_equal 3, new.pos
assert !new.first?
assert new.last?
new = ListMixin.create(:parent_id => 0)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_insert_at
new = ListMixin.create(:parent_id => 20)
assert_equal 1, new.pos
new = ListMixin.create(:parent_id => 20)
assert_equal 2, new.pos
new = ListMixin.create(:parent_id => 20)
assert_equal 3, new.pos
new4 = ListMixin.create(:parent_id => 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixin.create(:parent_id => 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
ListMixin.find(1).destroy
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(3).pos
assert_equal 2, ListMixin.find(4).pos
end
def test_with_string_based_scope
new = ListWithStringScopeMixin.create(:parent_id => 500)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_nil_scope
new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
new2.move_higher
assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
end
def test_remove_from_list_should_then_fail_in_list?
assert_equal true, ListMixin.find(1).in_list?
ListMixin.find(1).remove_from_list
assert_equal false, ListMixin.find(1).in_list?
end
def test_remove_from_list_should_set_position_to_nil
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).remove_from_list
assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal nil, ListMixin.find(2).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
end
def test_remove_before_destroy_does_not_shift_lower_items_twice
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).remove_from_list
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
end
end
class ListSubTest < Test::Unit::TestCase
def setup
setup_db
(1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
end
def teardown
teardown_db
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_lower
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_higher
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_top
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(4).move_to_top
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(3).move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
assert_nil ListMixin.find(1).higher_item
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
assert_nil ListMixin.find(4).lower_item
end
def test_injection
item = ListMixin.new("parent_id"=>1)
assert_equal "parent_id = 1", item.scope_condition
assert_equal "pos", item.position_column
end
def test_insert_at
new = ListMixin.create("parent_id" => 20)
assert_equal 1, new.pos
new = ListMixinSub1.create("parent_id" => 20)
assert_equal 2, new.pos
new = ListMixinSub2.create("parent_id" => 20)
assert_equal 3, new.pos
new4 = ListMixin.create("parent_id" => 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixinSub1.create("parent_id" => 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
ListMixin.find(1).destroy
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(3).pos
assert_equal 2, ListMixin.find(4).pos
end
end

View file

@ -0,0 +1,13 @@
* trunk *
break with true value [Kaspar Schiess]
* 2.1 *
After actions [Saimon Moore]
* 2.0 * (2006-01-20 15:26:28 -0500)
Enter / Exit actions
Transition guards
Guards and actions can be a symbol pointing to a method or a Proc
* 1.0 * (2006-01-15 12:16:55 -0500)
Initial Release

View file

@ -0,0 +1,20 @@
Copyright (c) 2006 Scott Barron
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,33 @@
= Acts As State Machine
This act gives an Active Record model the ability to act as a finite state
machine (FSM).
Acquire via subversion at:
http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
If prompted, use the user/pass anonymous/anonymous.
== Example
class Order < ActiveRecord::Base
acts_as_state_machine :initial => :opened
state :opened
state :closed, :enter => Proc.new {|o| Mailer.send_notice(o)}
state :returned
event :close do
transitions :to => :closed, :from => :opened
end
event :return do
transitions :to => :returned, :from => :closed
end
end
o = Order.create
o.close! # notice is sent by mailer
o.return!

View file

@ -0,0 +1,28 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => [:clean_db, :test]
desc 'Remove the stale db file'
task :clean_db do
`rm -f #{File.dirname(__FILE__)}/test/state_machine.sqlite.db`
end
desc 'Test the acts as state machine plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the acts as state machine plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Acts As State Machine'
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('TODO')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,11 @@
* Currently invalid events are ignored, create an option so that they can be
ignored or raise an exception.
* Query for a list of possible next states.
* Make listing states optional since they can be inferred from the events.
Only required to list a state if you want to define a transition block for it.
* Real transition actions
* Default states

View file

@ -0,0 +1,5 @@
require 'acts_as_state_machine'
ActiveRecord::Base.class_eval do
include ScottBarron::Acts::StateMachine
end

View file

@ -0,0 +1,268 @@
module ScottBarron #:nodoc:
module Acts #:nodoc:
module StateMachine #:nodoc:
class InvalidState < Exception #:nodoc:
end
class NoInitialState < Exception #:nodoc:
end
def self.included(base) #:nodoc:
base.extend ActMacro
end
module SupportingClasses
class State
attr_reader :name
def initialize(name, opts)
@name, @opts = name, opts
end
def entering(record)
enteract = @opts[:enter]
record.send(:run_transition_action, enteract) if enteract
end
def entered(record)
afteractions = @opts[:after]
return unless afteractions
Array(afteractions).each do |afteract|
record.send(:run_transition_action, afteract)
end
end
def exited(record)
exitact = @opts[:exit]
record.send(:run_transition_action, exitact) if exitact
end
end
class StateTransition
attr_reader :from, :to, :opts
def initialize(opts)
@from, @to, @guard = opts[:from], opts[:to], opts[:guard]
@opts = opts
end
def guard(obj)
@guard ? obj.send(:run_transition_action, @guard) : true
end
def perform(record)
return false unless guard(record)
loopback = record.current_state == to
states = record.class.read_inheritable_attribute(:states)
next_state = states[to]
old_state = states[record.current_state]
next_state.entering(record) unless loopback
record.update_attribute(record.class.state_column, to.to_s)
next_state.entered(record) unless loopback
old_state.exited(record) unless loopback
true
end
def ==(obj)
@from == obj.from && @to == obj.to
end
end
class Event
attr_reader :name
attr_reader :transitions
attr_reader :opts
def initialize(name, opts, transition_table, &block)
@name = name.to_sym
@transitions = transition_table[@name] = []
instance_eval(&block) if block
@opts = opts
@opts.freeze
@transitions.freeze
freeze
end
def next_states(record)
@transitions.select { |t| t.from == record.current_state }
end
def fire(record)
next_states(record).each do |transition|
break true if transition.perform(record)
end
end
def transitions(trans_opts)
Array(trans_opts[:from]).each do |s|
@transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
end
end
end
end
module ActMacro
# Configuration options are
#
# * +column+ - specifies the column name to use for keeping the state (default: state)
# * +initial+ - specifies an initial state for newly created objects (required)
def acts_as_state_machine(opts)
self.extend(ClassMethods)
raise NoInitialState unless opts[:initial]
write_inheritable_attribute :states, {}
write_inheritable_attribute :initial_state, opts[:initial]
write_inheritable_attribute :transition_table, {}
write_inheritable_attribute :event_table, {}
write_inheritable_attribute :state_column, opts[:column] || 'state'
class_inheritable_reader :initial_state
class_inheritable_reader :state_column
class_inheritable_reader :transition_table
class_inheritable_reader :event_table
self.send(:include, ScottBarron::Acts::StateMachine::InstanceMethods)
before_create :set_initial_state
after_create :run_initial_state_actions
end
end
module InstanceMethods
def set_initial_state #:nodoc:
write_attribute self.class.state_column, self.class.initial_state.to_s
end
def run_initial_state_actions
initial = self.class.read_inheritable_attribute(:states)[self.class.initial_state.to_sym]
initial.entering(self)
initial.entered(self)
end
# Returns the current state the object is in, as a Ruby symbol.
def current_state
self.send(self.class.state_column).to_sym
end
# Returns what the next state for a given event would be, as a Ruby symbol.
def next_state_for_event(event)
ns = next_states_for_event(event)
ns.empty? ? nil : ns.first.to
end
def next_states_for_event(event)
self.class.read_inheritable_attribute(:transition_table)[event.to_sym].select do |s|
s.from == current_state
end
end
def run_transition_action(action)
Symbol === action ? self.method(action).call : action.call(self)
end
private :run_transition_action
end
module ClassMethods
# Returns an array of all known states.
def states
read_inheritable_attribute(:states).keys
end
# Define an event. This takes a block which describes all valid transitions
# for this event.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed
#
# event :close_order do
# transitions :to => :closed, :from => :open
# end
# end
#
# +transitions+ takes a hash where <tt>:to</tt> is the state to transition
# to and <tt>:from</tt> is a state (or Array of states) from which this
# event can be fired.
#
# This creates an instance method used for firing the event. The method
# created is the name of the event followed by an exclamation point (!).
# Example: <tt>order.close_order!</tt>.
def event(event, opts={}, &block)
tt = read_inheritable_attribute(:transition_table)
et = read_inheritable_attribute(:event_table)
e = et[event.to_sym] = SupportingClasses::Event.new(event, opts, tt, &block)
define_method("#{event.to_s}!") { e.fire(self) }
end
# Define a state of the system. +state+ can take an optional Proc object
# which will be executed every time the system transitions into that
# state. The proc will be passed the current object.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed, Proc.new { |o| Mailer.send_notice(o) }
# end
def state(name, opts={})
state = SupportingClasses::State.new(name.to_sym, opts)
read_inheritable_attribute(:states)[name.to_sym] = state
define_method("#{state.name}?") { current_state == state.name }
end
# Wraps ActiveRecord::Base.find to conveniently find all records in
# a given state. Options:
#
# * +number+ - This is just :first or :all from ActiveRecord +find+
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def find_in_state(number, state, *args)
with_state_scope state do
find(number, *args)
end
end
# Wraps ActiveRecord::Base.count to conveniently count all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def count_in_state(state, *args)
with_state_scope state do
count(*args)
end
end
# Wraps ActiveRecord::Base.calculate to conveniently calculate all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +calculate+
def calculate_in_state(state, *args)
with_state_scope state do
calculate(*args)
end
end
protected
def with_state_scope(state)
raise InvalidState unless states.include?(state)
with_scope :find => {:conditions => ["#{table_name}.#{state_column} = ?", state.to_s]} do
yield if block_given?
end
end
end
end
end
end

View file

@ -0,0 +1,224 @@
require File.dirname(__FILE__) + '/test_helper'
include ScottBarron::Acts::StateMachine
class ActsAsStateMachineTest < Test::Unit::TestCase
fixtures :conversations
def test_no_initial_value_raises_exception
assert_raise(NoInitialState) {
Person.acts_as_state_machine({})
}
end
def test_initial_state_value
assert_equal :needs_attention, Conversation.initial_state
end
def test_column_was_set
assert_equal 'state_machine', Conversation.state_column
end
def test_initial_state
c = Conversation.create
assert_equal :needs_attention, c.current_state
assert c.needs_attention?
end
def test_states_were_set
[:needs_attention, :read, :closed, :awaiting_response, :junk].each do |s|
assert Conversation.states.include?(s)
end
end
def test_event_methods_created
c = Conversation.create
%w(new_message! view! reply! close! junk! unjunk!).each do |event|
assert c.respond_to?(event)
end
end
def test_query_methods_created
c = Conversation.create
%w(needs_attention? read? closed? awaiting_response? junk?).each do |event|
assert c.respond_to?(event)
end
end
def test_transition_table
tt = Conversation.transition_table
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :read, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :closed, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :awaiting_response, :to => :needs_attention))
end
def test_next_state_for_event
c = Conversation.create
assert_equal :read, c.next_state_for_event(:view)
end
def test_change_state
c = Conversation.create
c.view!
assert c.read?
end
def test_can_go_from_read_to_closed_because_guard_passes
c = Conversation.create
c.can_close = true
c.view!
c.reply!
c.close!
assert_equal :closed, c.current_state
end
def test_cannot_go_from_read_to_closed_because_of_guard
c = Conversation.create
c.can_close = false
c.view!
c.reply!
c.close!
assert_equal :read, c.current_state
end
def test_ignore_invalid_events
c = Conversation.create
c.view!
c.junk!
# This is the invalid event
c.new_message!
assert_equal :junk, c.current_state
end
def test_entry_action_executed
c = Conversation.create
c.read_enter = false
c.view!
assert c.read_enter
end
def test_after_actions_executed
c = Conversation.create
c.read_after_first = false
c.read_after_second = false
c.closed_after = false
c.view!
assert c.read_after_first
assert c.read_after_second
c.can_close = true
c.close!
assert c.closed_after
assert_equal :closed, c.current_state
end
def test_after_actions_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_after_first = false
c.read_after_second = false
c.view!
assert !c.read_after_first
assert !c.read_after_second
c.can_close = true
c.close!
c.closed_after = false
c.close!
assert !c.closed_after
end
def test_exit_action_executed
c = Conversation.create
c.read_exit = false
c.view!
c.junk!
assert c.read_exit
end
def test_entry_and_exit_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_enter = false
c.read_exit = false
c.view!
assert !c.read_enter
assert !c.read_exit
end
def test_entry_and_after_actions_called_for_initial_state
c = Conversation.create
assert c.needs_attention_enter
assert c.needs_attention_after
end
def test_run_transition_action_is_private
c = Conversation.create
assert_raise(NoMethodError) { c.run_transition_action :foo }
end
def test_find_all_in_state
cs = Conversation.find_in_state(:all, :read)
assert_equal 2, cs.size
end
def test_find_first_in_state
c = Conversation.find_in_state(:first, :read)
assert_equal conversations(:first).id, c.id
end
def test_find_all_in_state_with_conditions
cs = Conversation.find_in_state(:all, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal 1, cs.size
assert_equal conversations(:second).id, cs.first.id
end
def test_find_first_in_state_with_conditions
c = Conversation.find_in_state(:first, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal conversations(:second).id, c.id
end
def test_count_in_state
cnt0 = Conversation.count(['state_machine = ?', 'read'])
cnt = Conversation.count_in_state(:read)
assert_equal cnt0, cnt
end
def test_count_in_state_with_conditions
cnt0 = Conversation.count(['state_machine = ? AND subject = ?', 'read', 'Foo'])
cnt = Conversation.count_in_state(:read, ['subject = ?', 'Foo'])
assert_equal cnt0, cnt
end
def test_find_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.find_in_state(:all, :dead)
}
end
def test_count_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.count_in_state(:dead)
}
end
def test_can_access_events_via_event_table
event = Conversation.event_table[:junk]
assert_equal :junk, event.name
assert_equal "finished", event.opts[:note]
end
end

View file

@ -0,0 +1,18 @@
sqlite:
:adapter: sqlite
:dbfile: state_machine.sqlite.db
sqlite3:
:adapter: sqlite3
:dbfile: state_machine.sqlite3.db
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: state_machine_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: rails
:password:
:database: state_machine_test

View file

@ -0,0 +1,67 @@
class Conversation < ActiveRecord::Base
attr_writer :can_close
attr_accessor :read_enter, :read_exit, :read_after_first, :read_after_second,
:closed_after, :needs_attention_enter, :needs_attention_after
acts_as_state_machine :initial => :needs_attention, :column => 'state_machine'
state :needs_attention, :enter => Proc.new { |o| o.needs_attention_enter = true },
:after => Proc.new { |o| o.needs_attention_after = true }
state :read, :enter => :read_enter_action,
:exit => Proc.new { |o| o.read_exit = true },
:after => [:read_after_first_action, :read_after_second_action]
state :closed, :after => :closed_after_action
state :awaiting_response
state :junk
event :new_message do
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
end
event :view do
transitions :to => :read, :from => [:needs_attention, :read]
end
event :reply do
transitions :to => :awaiting_response, :from => [:read, :closed]
end
event :close do
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => Proc.new {|o| o.can_close?}
transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
end
event :junk, :note => "finished" do
transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
end
event :unjunk do
transitions :to => :closed, :from => :junk
end
def can_close?
@can_close
end
def read_enter_action
self.read_enter = true
end
def always_true
true
end
def read_after_first_action
self.read_after_first = true
end
def read_after_second_action
self.read_after_second = true
end
def closed_after_action
self.closed_after = true
end
end

View file

@ -0,0 +1,11 @@
first:
id: 1
state_machine: read
subject: This is a test
closed: false
second:
id: 2
state_machine: read
subject: Foo
closed: false

View file

@ -0,0 +1,2 @@
class Person < ActiveRecord::Base
end

View file

@ -0,0 +1,11 @@
ActiveRecord::Schema.define(:version => 1) do
create_table :conversations, :force => true do |t|
t.column :state_machine, :string
t.column :subject, :string
t.column :closed, :boolean
end
create_table :people, :force => true do |t|
t.column :name, :string
end
end

View file

@ -0,0 +1,38 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
RAILS_ROOT = File.dirname(__FILE__)
require 'rubygems'
require 'test/unit'
require 'active_record'
require 'active_record/fixtures'
require 'active_support/binding_of_caller'
require 'active_support/breakpoint'
require "#{File.dirname(__FILE__)}/../init"
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb")
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
def create_fixtures(*table_names)
if block_given?
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
else
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
end
end
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end

28
vendor/plugins/arts/README vendored Normal file
View file

@ -0,0 +1,28 @@
ARTS is Another RJS Test System
For a complete tutorial, see http://glu.ttono.us/articles/2006/05/29/guide-test-driven-rjs-with-arts.
Usage:
assert_rjs :alert, 'Hi!'
assert_rjs :assign, 'a', '2'
assert_rjs :call, 'foo', 'bar', 'baz'
assert_rjs :draggable, 'draggable_item'
assert_rjs :drop_receiving, 'receiving_item'
assert_rjs :hide, "post_1", "post_2", "post_3"
assert_rjs :insert_html, :bottom, 'posts'
assert_rjs :redirect_to, :action => 'list'
assert_rjs :remove, "post_1", "post_2", "post_3"
assert_rjs :replace, 'completely_replaced_div'
assert_rjs :replace, 'completely_replaced_div', '<p>This replaced the div</p>'
assert_rjs :replace, 'completely_replaced_div', /replaced the div/
assert_rjs :replace_html, 'replaceable_div', "This goes inside the div"
assert_rjs :show, "post_1", "post_2", "post_3"
assert_rjs :sortable, 'sortable_item'
assert_rjs :toggle, "post_1", "post_2", "post_3"
assert_rjs :visual_effect, :highlight, "posts", :duration => '1.0'
For the square bracket syntax (page['some_id'].toggle) use :page followed by the id and then subsequent method calls. Assignment requires a '=' at the end of the method name followed by the value.
assert_rjs :page, 'some_id', :toggle
assert_rjs :page, 'some_id', :style, :color=, 'red'

7
vendor/plugins/arts/about.yml vendored Normal file
View file

@ -0,0 +1,7 @@
author: Kevin Clark
summary: RJS Assertion Plugin
homepage: http://glu.ttono.us
plugin:
version: 0.6
license: MIT
rails_version: 1.1.2+

3
vendor/plugins/arts/init.rb vendored Normal file
View file

@ -0,0 +1,3 @@
# Give testing some culture
require 'test/unit/testcase'
Test::Unit::TestCase.send :include, Arts

1
vendor/plugins/arts/install.rb vendored Normal file
View file

@ -0,0 +1 @@
puts IO.read(File.join(File.dirname(__FILE__), 'README'))

133
vendor/plugins/arts/lib/arts.rb vendored Normal file
View file

@ -0,0 +1,133 @@
module Arts
include ActionView::Helpers::PrototypeHelper
include ActionView::Helpers::ScriptaculousHelper
include ActionView::Helpers::JavaScriptHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
def assert_rjs(action, *args, &block)
respond_to?("assert_rjs_#{action}") ?
send("assert_rjs_#{action}", *args) :
assert_response_contains(create_generator.send(action, *args, &block),
generic_error(action, args))
end
def assert_no_rjs(action, *args, &block)
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs(action, *args, &block) }
end
def assert_rjs_insert_html(*args)
position = args.shift
item_id = args.shift
content = extract_matchable_content(args)
unless content.blank?
case content
when Regexp
assert_match Regexp.new("new Insertion\.#{position.to_s.camelize}(.*#{item_id}.*,.*#{content.source}.*);"),
@response.body
when String
assert_response_contains("new Insertion.#{position.to_s.camelize}(\"#{item_id}\", #{content});",
"No insert_html call found for \n" +
" position: '#{position}' id: '#{item_id}' \ncontent: \n" +
"#{content}\n" +
"in response:\n#{@response.body}")
else
raise "Invalid content type"
end
else
assert_match Regexp.new("new Insertion\.#{position.to_s.camelize}(.*#{item_id}.*,.*?);"),
@response.body
end
end
def assert_rjs_replace_html(*args)
div = args.shift
content = extract_matchable_content(args)
unless content.blank?
case content
when Regexp
assert_match Regexp.new("Element.update(.*#{div}.*,.*#{content.source}.*);"),
@response.body
when String
assert_response_contains("Element.update(\"#{div}\", #{content});",
"No replace_html call found on div: '#{div}' and content: \n#{content}\n" +
"in response:\n#{@response.body}")
else
raise "Invalid content type"
end
else
assert_match Regexp.new("Element.update(.*#{div}.*,.*?);"), @response.body
end
end
def assert_rjs_replace(*args)
div = args.shift
content = extract_matchable_content(args)
unless content.blank?
case content
when Regexp
assert_match Regexp.new("Element.replace(.*#{div}.*,.*#{content.source}.*);"),
@response.body
when String
assert_response_contains("Element.replace(\"#{div}\", #{content});",
"No replace call found on div: '#{div}' and content: \n#{content}\n" +
"in response:\n#{@response.body}")
else
raise "Invalid content type"
end
else
assert_match Regexp.new("Element.replace(.*#{div}.*,.*?);"), @response.body
end
end
# To deal with [] syntax. I hate JavaScriptProxy so.. SO very much
def assert_rjs_page(*args)
content = build_method_chain!(args)
assert_match Regexp.new(Regexp.escape(content)), @response.body,
"Content did not include:\n #{content.to_s}"
end
protected
def assert_response_contains(str, message)
assert @response.body.to_s.index(str), message
end
def build_method_chain!(args)
content = create_generator.send(:[], args.shift) # start $('some_id')....
while !args.empty?
if (method = args.shift.to_s) =~ /(.*)=$/
content = content.__send__(method, args.shift)
break
else
content = content.__send__(method)
content = content.__send__(:function_chain).first if args.empty?
end
end
content
end
def create_generator
block = Proc.new { |*args| yield *args if block_given? }
JavaScriptGenerator.new self, &block
end
def generic_error(action, args)
"#{action} with args [#{args.join(" ")}] does not show up in response:\n#{@response.body}"
end
def extract_matchable_content(args)
if args.size == 1 and args.first.is_a? Regexp
return args.first
else
return create_generator.send(:arguments_for_call, args)
end
end
end

402
vendor/plugins/arts/test/arts_test.rb vendored Normal file
View file

@ -0,0 +1,402 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'breakpoint'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
class ArtsController < ActionController::Base
def alert
render :update do |page|
page.alert 'This is an alert'
end
end
def assign
render :update do |page|
page.assign 'a', '2'
end
end
def call
render :update do |page|
page.call 'foo', 'bar', 'baz'
end
end
def draggable
render :update do |page|
page.draggable 'my_image', :revert => true
end
end
def drop_receiving
render :update do |page|
page.drop_receiving "my_cart", :url => { :controller => "cart", :action => "add" }
end
end
def hide
render :update do |page|
page.hide 'some_div'
end
end
def insert_html
render :update do |page|
page.insert_html :bottom, 'content', 'Stuff in the content div'
end
end
def redirect
render :update do |page|
page.redirect_to :controller => 'sample', :action => 'index'
end
end
def remove
render :update do |page|
page.remove 'offending_div'
end
end
def replace
render :update do |page|
page.replace 'person_45', '<div>This replaces person_45</div>'
end
end
def replace_html
render :update do |page|
page.replace_html 'person_45', 'This goes inside person_45'
end
end
def show
render :update do |page|
page.show 'post_1', 'post_2', 'post_3'
end
end
def sortable
render :update do |page|
page.sortable 'sortable_item'
end
end
def toggle
render :update do |page|
page.toggle "post_1", "post_2", "post_3"
end
end
def visual_effect
render :update do |page|
page.visual_effect :highlight, "posts", :duration => '1.0'
end
end
def page_with_one_chained_method
render :update do |page|
page['some_id'].toggle
end
end
def page_with_assignment
render :update do |page|
page['some_id'].style.color = 'red'
end
end
def rescue_errors(e) raise e end
end
class ArtsTest < Test::Unit::TestCase
def setup
@controller = ArtsController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_alert
get :alert
assert_nothing_raised { assert_rjs :alert, 'This is an alert' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :alert, 'This is not an alert'
end
assert_nothing_raised { assert_no_rjs :alert, 'This is not an alert' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :alert, 'This is an alert'
end
end
def test_assign
get :assign
assert_nothing_raised { assert_rjs :assign, 'a', '2' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :assign, 'a', '3'
end
assert_nothing_raised { assert_no_rjs :assign, 'a', '3' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :assign, 'a', '2'
end
end
def test_call
get :call
assert_nothing_raised { assert_rjs :call, 'foo', 'bar', 'baz' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :call, 'foo', 'bar'
end
assert_nothing_raised { assert_no_rjs :call, 'foo', 'bar' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :call, 'foo', 'bar', 'baz'
end
end
def test_draggable
get :draggable
assert_nothing_raised { assert_rjs :draggable, 'my_image', :revert => true }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :draggable, 'not_my_image'
end
assert_nothing_raised { assert_no_rjs :draggable, 'not_my_image' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :draggable, 'my_image', :revert => true
end
end
def test_drop_receiving
get :drop_receiving
assert_nothing_raised { assert_rjs :drop_receiving, "my_cart", :url => { :controller => "cart", :action => "add" } }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :drop_receiving, "my_cart"
end
assert_nothing_raised { assert_no_rjs :drop_receiving, "my_cart" }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :drop_receiving, "my_cart", :url => { :controller => "cart", :action => "add" }
end
end
def test_hide
get :hide
assert_nothing_raised { assert_rjs :hide, 'some_div' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :hide, 'some_other_div'
end
assert_nothing_raised { assert_no_rjs :hide, 'not_some_div' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :hide, 'some_div'
end
end
def test_insert_html
get :insert_html
assert_nothing_raised do
# No content matching
assert_rjs :insert_html, :bottom, 'content'
# Exact content matching
assert_rjs :insert_html, :bottom, 'content', 'Stuff in the content div'
# Regex matching
assert_rjs :insert_html, :bottom, 'content', /in.*content/
assert_no_rjs :insert_html, :bottom, 'not_our_div'
assert_no_rjs :insert_html, :bottom, 'content', /in.*no content/
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :insert_html, :bottom, 'content'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :insert_html, :bottom, 'no_content'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :insert_html, :bottom, 'content', /in the/
end
end
def test_redirect_to
get :redirect
assert_nothing_raised do
assert_rjs :redirect_to, :controller => 'sample', :action => 'index'
assert_no_rjs :redirect_to, :controller => 'sample', :action => 'show'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :redirect_to, :controller => 'doesnt', :action => 'exist'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :redirect_to, :controller => 'sample', :action => 'index'
end
end
def test_remove
get :remove
assert_nothing_raised do
assert_rjs :remove, 'offending_div'
assert_no_rjs :remove, 'dancing_happy_div'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :remove, 'dancing_happy_div'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :remove, 'offending_div'
end
end
def test_replace
get :replace
assert_nothing_raised do
# No content matching
assert_rjs :replace, 'person_45'
# String content matching
assert_rjs :replace, 'person_45', '<div>This replaces person_45</div>'
# regexp content matching
assert_rjs :replace, 'person_45', /<div>.*person_45.*<\/div>/
assert_no_rjs :replace, 'person_45', '<div>This replaces person_46</div>'
assert_no_rjs :replace, 'person_45', /person_46/
end
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace, 'person_45' }
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace, 'person_45', /person_45/ }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace, 'person_46' }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace, 'person_45', 'bad stuff' }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace, 'person_45', /not there/}
end
def test_replace_html
get :replace_html
assert_nothing_raised do
# No content matching
assert_rjs :replace_html, 'person_45'
# String content matching
assert_rjs :replace_html, 'person_45', 'This goes inside person_45'
# Regexp content matching
assert_rjs :replace_html, 'person_45', /goes inside/
assert_no_rjs :replace_html, 'person_46'
assert_no_rjs :replace_html, 'person_45', /doesn't go inside/
end
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace_html, 'person_45' }
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace_html, 'person_45', /goes/ }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace_html, 'person_46' }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace_html, 'person_45', /gos inside/ }
end
def test_show
get :show
assert_nothing_raised do
assert_rjs :show, "post_1", "post_2", "post_3"
assert_no_rjs :show, 'post_4'
end
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :show, 'post_4' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :show, "post_1", "post_2", "post_3"
end
end
def test_sortable
get :sortable
assert_nothing_raised do
assert_rjs :sortable, 'sortable_item'
assert_no_rjs :sortable, 'non-sortable-item'
end
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :sortable, 'non-sortable-item' }
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :sortable, 'sortable_item' }
end
def test_toggle
get :toggle
assert_nothing_raised do
assert_rjs :toggle, "post_1", "post_2", "post_3"
assert_no_rjs :toggle, 'post_4'
end
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :toggle, 'post_4' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :toggle, "post_1", "post_2", "post_3"
end
end
def test_visual_effect
get :visual_effect
assert_nothing_raised do
assert_rjs :visual_effect, :highlight, "posts", :duration => '1.0'
assert_no_rjs :visual_effect, :highlight, "lists"
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :visual_effect, :highlight, "lists"
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :visual_effect, :highlight, "posts", :duration => '1.0'
end
end
# [] support
def test_page_with_one_chained_method
get :page_with_one_chained_method
assert_nothing_raised do
assert_rjs :page, 'some_id', :toggle
assert_no_rjs :page, 'some_other_id', :toggle
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :page, 'some_other_id', :toggle
assert_no_rjs :page, 'some_id', :toggle
end
end
def test_page_with_assignment
get :page_with_assignment
assert_nothing_raised do
assert_rjs :page, 'some_id', :style, :color=, 'red'
assert_no_rjs :page, 'some_id', :color=, 'red'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :page, 'some_id', :style, :color=, 'red'
assert_rjs :page, 'some_other_id', :style, :color=, 'red'
end
end
end

122
vendor/plugins/asset_packager/CHANGELOG vendored Normal file
View file

@ -0,0 +1,122 @@
------------------------------------------------------------------------
r52 | sbecker | 2007-11-04 01:38:21 -0400 (Sun, 04 Nov 2007) | 3 lines
* Allow configuration of which environments the helpers should merge scripts with the Synthesis::AssetPackage.merge_environments variable.
* Refactored tests so they can all run together, and not depend on what the RAILS_ENV constant is.
* Only add file extension if it was explicitly passed in, fixes other helpers in rails.
------------------------------------------------------------------------
r51 | sbecker | 2007-10-26 16:24:48 -0400 (Fri, 26 Oct 2007) | 1 line
* Updated jsmin.rb to latest version from 2007-07-20
------------------------------------------------------------------------
r50 | sbecker | 2007-10-23 23:16:07 -0400 (Tue, 23 Oct 2007) | 1 line
Updated CHANGELOG
------------------------------------------------------------------------
r49 | sbecker | 2007-10-23 23:13:27 -0400 (Tue, 23 Oct 2007) | 1 line
* Finally committed the subdirectory patch. (Thanks James Coglan!)
------------------------------------------------------------------------
r48 | sbecker | 2007-10-15 15:10:43 -0400 (Mon, 15 Oct 2007) | 1 line
* Speed up rake tasks and remove rails environment dependencies
------------------------------------------------------------------------
r43 | sbecker | 2007-07-02 15:30:29 -0400 (Mon, 02 Jul 2007) | 1 line
* Updated the docs regarding testing.
------------------------------------------------------------------------
r42 | sbecker | 2007-07-02 15:27:00 -0400 (Mon, 02 Jul 2007) | 1 line
* For production helper test, build packages once - on first setup.
------------------------------------------------------------------------
r41 | sbecker | 2007-07-02 15:14:13 -0400 (Mon, 02 Jul 2007) | 1 line
* Put build_all in test setup and delete_all in test teardown so all tests will pass the on first run of test suite.
------------------------------------------------------------------------
r40 | sbecker | 2007-07-02 14:55:28 -0400 (Mon, 02 Jul 2007) | 1 line
* Fix quotes, add contact info
------------------------------------------------------------------------
r39 | sbecker | 2007-07-02 14:53:52 -0400 (Mon, 02 Jul 2007) | 1 line
* Add note on how to run the tests for asset packager.
------------------------------------------------------------------------
r38 | sbecker | 2007-01-25 15:36:42 -0500 (Thu, 25 Jan 2007) | 1 line
added CHANGELOG w/ subversion log entries
------------------------------------------------------------------------
r37 | sbecker | 2007-01-25 15:34:39 -0500 (Thu, 25 Jan 2007) | 1 line
updated jsmin with new version from 2007-01-23
------------------------------------------------------------------------
r35 | sbecker | 2007-01-15 19:22:16 -0500 (Mon, 15 Jan 2007) | 1 line
require synthesis/asset_package in rake tasks, as Rails 1.2 seems to necessitate
------------------------------------------------------------------------
r34 | sbecker | 2007-01-05 12:22:09 -0500 (Fri, 05 Jan 2007) | 1 line
do a require before including in action view, because when running migrations, the plugin lib files don't automatically get required, causing the include to error out
------------------------------------------------------------------------
r33 | sbecker | 2006-12-23 02:03:41 -0500 (Sat, 23 Dec 2006) | 1 line
updating readme with various tweaks
------------------------------------------------------------------------
r32 | sbecker | 2006-12-23 02:03:12 -0500 (Sat, 23 Dec 2006) | 1 line
updating readme with various tweaks
------------------------------------------------------------------------
r31 | sbecker | 2006-12-23 01:52:25 -0500 (Sat, 23 Dec 2006) | 1 line
updated readme to show how to use different media for stylesheets
------------------------------------------------------------------------
r28 | sbecker | 2006-11-27 21:02:14 -0500 (Mon, 27 Nov 2006) | 1 line
updated compute_public_path, added check for images
------------------------------------------------------------------------
r27 | sbecker | 2006-11-10 18:28:29 -0500 (Fri, 10 Nov 2006) | 1 line
tolerate extra periods in source asset names. fixed subversion revision checking to be file specific, instead of repository specific.
------------------------------------------------------------------------
r26 | sbecker | 2006-06-24 17:04:27 -0400 (Sat, 24 Jun 2006) | 1 line
convert asset_packages_yml var to a class var
------------------------------------------------------------------------
r25 | sbecker | 2006-06-24 12:37:47 -0400 (Sat, 24 Jun 2006) | 1 line
Added ability to include assets by package name. In development, include all uncompressed asset files. In production, include the single compressed asset.
------------------------------------------------------------------------
r24 | sbecker | 2006-06-19 21:57:23 -0400 (Mon, 19 Jun 2006) | 1 line
Updates to README and about.yml
------------------------------------------------------------------------
r23 | sbecker | 2006-06-19 14:55:39 -0400 (Mon, 19 Jun 2006) | 2 lines
Modifying about.yml and README
------------------------------------------------------------------------
r21 | sbecker | 2006-06-19 12:18:32 -0400 (Mon, 19 Jun 2006) | 2 lines
added "formerly known as MergeJS"
------------------------------------------------------------------------
r20 | sbecker | 2006-06-19 12:14:46 -0400 (Mon, 19 Jun 2006) | 2 lines
Updating docs
------------------------------------------------------------------------
r19 | sbecker | 2006-06-19 11:26:08 -0400 (Mon, 19 Jun 2006) | 2 lines
removing compiled test assets from subversion
------------------------------------------------------------------------
r18 | sbecker | 2006-06-19 11:19:59 -0400 (Mon, 19 Jun 2006) | 2 lines
Initial import.
------------------------------------------------------------------------
r17 | sbecker | 2006-06-19 11:18:56 -0400 (Mon, 19 Jun 2006) | 2 lines
Creating directory.
------------------------------------------------------------------------

173
vendor/plugins/asset_packager/README vendored Normal file
View file

@ -0,0 +1,173 @@
= AssetPackager
JavaScript and CSS Asset Compression for Production Rails Apps
== Description
When it comes time to deploy your new web application, instead of
sending down a dozen JavaScript and CSS files full of formatting
and comments, this Rails plugin makes it simple to merge and
compress JavaScript and CSS down into one or more files, increasing
speed and saving bandwidth.
When in development, it allows you to use your original versions
and retain formatting and comments for readability and debugging.
Because not all browsers will dependably cache JavaScript and CSS
files with query string parameters, AssetPackager writes a timestamp
or subversion revision stamp (if available) into the merged file names.
Therefore files are correctly cached by the browser AND your users
always get the latest version when you re-deploy.
This code is released under the MIT license (like Ruby). Youre free
to rip it up, enhance it, etc. And if you make any enhancements,
Id like to know so I can add them back in. Thanks!
* Formerly known as MergeJS.
== Credit
This Rails Plugin was inspired by Cal Henderson's article
"Serving JavaScript Fast" on Vitamin:
http://www.thinkvitamin.com/features/webapps/serving-javascript-fast
It also uses the Ruby JavaScript Minifier created by
Douglas Crockford.
http://www.crockford.com/javascript/jsmin.html
== Key Features
* Merges and compresses JavaScript and CSS when running in production.
* Uses uncompressed originals when running in development.
* Handles caching correctly. (No querystring parameters - filename timestamps)
* Versions each package individually. Updates to files in one won't re-trigger downloading the others.
* Uses subversion revision numbers instead of timestamps if within a subversion controlled directory.
* Guarantees new version will get downloaded the next time you deploy.
== Components
* Rake Task for merging and compressing JavaScript and CSS files.
* Helper functions for including these JavaScript and CSS files in your views.
* YAML configuration file for mapping JavaScript and CSS files to merged versions.
* Rake Task for auto-generating the YAML file from your existing JavaScript files.
== How to Use:
1. Download and install the plugin:
./script/plugin install http://sbecker.net/shared/plugins/asset_packager
2. Run the rake task "asset:packager:create_yml" to generate the /config/asset_packages.yml
file the first time. You will need to reorder files under 'base' so dependencies are loaded
in correct order. Feel free to rename or create new file packages.
IMPORTANT: JavaScript files can break once compressed if each statement doesn't end with a semi-colon.
The minifier puts multiple statements on one line, so if the semi-colon is missing, the statement may no
longer makes sense and cause a syntax error.
Example from a fresh rails app after running the rake task. (Stylesheets is blank because a
default rails app has no stylesheets yet.):
---
javascripts:
- base:
- prototype
- effects
- dragdrop
- controls
- application
stylesheets:
- base: []
Example with multiple merged files:
---
javascripts:
- base:
- prototype
- effects
- controls
- dragdrop
- application
- secondary:
- foo
- bar
stylesheets:
- base:
- screen
- header
- secondary:
- foo
- bar
3. Run the rake task "asset:packager:build_all" to generate the compressed, merged versions
for each package. Whenever you rearrange the yaml file, you'll need to run this task again.
Merging and compressing is expensive, so this is something we want to do once, not every time
your app starts. Thats why its a rake task.
4. Use the helper functions whenever including these files in your application. See below for examples.
5. Potential warning: css compressor function currently removes CSS comments. This might blow
away some CSS hackery. To disable comment removal, comment out /lib/synthesis/asset_package.rb line 176.
== JavaScript Examples
Example call:
<%= javascript_include_merged 'prototype', 'effects', 'controls', 'dragdrop', 'application', 'foo', 'bar' %>
In development, this generates:
<script type="text/javascript" src="/javascripts/prototype.js"></script>
<script type="text/javascript" src="/javascripts/effects.js"></script>
<script type="text/javascript" src="/javascripts/controls.js"></script>
<script type="text/javascript" src="/javascripts/dragdrop.js"></script>
<script type="text/javascript" src="/javascripts/application.js"></script>
<script type="text/javascript" src="/javascripts/foo.js"></script>
<script type="text/javascript" src="/javascripts/bar.js"></script>
In production, this generates:
<script type="text/javascript" src="/javascripts/base_1150571523.js"></script>
<script type="text/javascript" src="/javascripts/secondary_1150729166.js"></script>
Now supports symbols and :defaults as well:
<%= javascript_include_merged :defaults %>
<%= javascript_include_merged :foo, :bar %>
== Stylesheet Examples
Example call:
<%= stylesheet_link_merged 'screen', 'header' %>
In development, this generates:
<link href="/stylesheets/screen.css" media="screen" rel="Stylesheet" type="text/css" />
<link href="/stylesheets/header.css" media="screen" rel="Stylesheet" type="text/css" />
In production this generates:
<link href="/stylesheets/base_1150729166.css" media="screen" rel="Stylesheet" type="text/css" />
== Different CSS Media
All options for stylesheet_link_tag still work, so if you want to specify a different media type:
<%= stylesheet_link_merged :secondary, 'media' => 'print' %>
== Running the tests
So you want to run the tests eh? Ok, then listen:
This plugin has a full suite of tests. But since they
depend on rails, it has to be run in the context of a
rails app, in the vendor/plugins directory. Observe:
> rails newtestapp
> cd newtestapp
> ./script/plugin install http://sbecker.net/shared/plugins/asset_packager
> cd vendor/plugins/asset_packager/
> rake # all tests pass
== License
Copyright (c) 2006 Scott Becker - http://synthesis.sbecker.net
Contact Email: becker.scott@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

22
vendor/plugins/asset_packager/Rakefile vendored Normal file
View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the asset_packager plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the asset_packager plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'AssetPackager'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,8 @@
author: Scott Becker
name: AssetPackager
summary: JavaScript and CSS Asset Compression for Production Rails Apps
homepage: http://synthesis.sbecker.net/pages/asset_packager
plugin: http://sbecker.net/shared/plugins/asset_packager
license: MIT
version: 0.2
rails_version: 1.1.2+

2
vendor/plugins/asset_packager/init.rb vendored Normal file
View file

@ -0,0 +1,2 @@
require 'synthesis/asset_package_helper'
ActionView::Base.send :include, Synthesis::AssetPackageHelper

View file

@ -0,0 +1 @@
# Install hook code here

View file

@ -0,0 +1,205 @@
#!/usr/bin/ruby
# jsmin.rb 2007-07-20
# Author: Uladzislau Latynski
# This work is a translation from C to Ruby of jsmin.c published by
# Douglas Crockford. Permission is hereby granted to use the Ruby
# version under the same conditions as the jsmin.c on which it is
# based.
#
# /* jsmin.c
# 2003-04-21
#
# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software shall be used for Good, not Evil.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
EOF = -1
$theA = ""
$theB = ""
# isAlphanum -- return true if the character is a letter, digit, underscore,
# dollar sign, or non-ASCII character
def isAlphanum(c)
return false if !c || c == EOF
return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') || c == '_' || c == '$' ||
c == '\\' || c[0] > 126)
end
# get -- return the next character from stdin. Watch out for lookahead. If
# the character is a control character, translate it to a space or linefeed.
def get()
c = $stdin.getc
return EOF if(!c)
c = c.chr
return c if (c >= " " || c == "\n" || c.unpack("c") == EOF)
return "\n" if (c == "\r")
return " "
end
# Get the next character without getting it.
def peek()
lookaheadChar = $stdin.getc
$stdin.ungetc(lookaheadChar)
return lookaheadChar.chr
end
# mynext -- get the next character, excluding comments.
# peek() is used to see if a '/' is followed by a '/' or '*'.
def mynext()
c = get
if (c == "/")
if(peek == "/")
while(true)
c = get
if (c <= "\n")
return c
end
end
end
if(peek == "*")
get
while(true)
case get
when "*"
if (peek == "/")
get
return " "
end
when EOF
raise "Unterminated comment"
end
end
end
end
return c
end
# action -- do something! What you do is determined by the argument: 1
# Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B.
# (Delete A). 3 Get the next B. (Delete B). action treats a string as a
# single character. Wow! action recognizes a regular expression if it is
# preceded by ( or , or =.
def action(a)
if(a==1)
$stdout.write $theA
end
if(a==1 || a==2)
$theA = $theB
if ($theA == "\'" || $theA == "\"")
while (true)
$stdout.write $theA
$theA = get
break if ($theA == $theB)
raise "Unterminated string literal" if ($theA <= "\n")
if ($theA == "\\")
$stdout.write $theA
$theA = get
end
end
end
end
if(a==1 || a==2 || a==3)
$theB = mynext
if ($theB == "/" && ($theA == "(" || $theA == "," || $theA == "=" ||
$theA == ":" || $theA == "[" || $theA == "!" ||
$theA == "&" || $theA == "|" || $theA == "?" ||
$theA == "{" || $theA == "}" || $theA == ";" ||
$theA == "\n"))
$stdout.write $theA
$stdout.write $theB
while (true)
$theA = get
if ($theA == "/")
break
elsif ($theA == "\\")
$stdout.write $theA
$theA = get
elsif ($theA <= "\n")
raise "Unterminated RegExp Literal"
end
$stdout.write $theA
end
$theB = mynext
end
end
end
# jsmin -- Copy the input to the output, deleting the characters which are
# insignificant to JavaScript. Comments will be removed. Tabs will be
# replaced with spaces. Carriage returns will be replaced with linefeeds.
# Most spaces and linefeeds will be removed.
def jsmin
$theA = "\n"
action(3)
while ($theA != EOF)
case $theA
when " "
if (isAlphanum($theB))
action(1)
else
action(2)
end
when "\n"
case ($theB)
when "{","[","(","+","-"
action(1)
when " "
action(3)
else
if (isAlphanum($theB))
action(1)
else
action(2)
end
end
else
case ($theB)
when " "
if (isAlphanum($theA))
action(1)
else
action(3)
end
when "\n"
case ($theA)
when "}","]",")","+","-","\"","\\", "'", '"'
action(1)
else
if (isAlphanum($theA))
action(1)
else
action(3)
end
end
else
action(1)
end
end
end
end
ARGV.each do |anArg|
$stdout.write "// #{anArg}\n"
end
jsmin

View file

@ -0,0 +1,233 @@
require 'yaml'
module Synthesis
class AssetPackage
# class variables
@@asset_packages_yml = $asset_packages_yml ||
(File.exists?("#{RAILS_ROOT}/config/asset_packages.yml") ? YAML.load_file("#{RAILS_ROOT}/config/asset_packages.yml") : nil)
# singleton methods
class << self
def merge_environments=(environments)
@@merge_environments = environments
end
def merge_environments
@@merge_environments ||= ["production"]
end
def parse_path(path)
/^(?:(.*)\/)?([^\/]+)$/.match(path).to_a
end
def find_by_type(asset_type)
@@asset_packages_yml[asset_type].map { |p| self.new(asset_type, p) }
end
def find_by_target(asset_type, target)
package_hash = @@asset_packages_yml[asset_type].find {|p| p.keys.first == target }
package_hash ? self.new(asset_type, package_hash) : nil
end
def find_by_source(asset_type, source)
path_parts = parse_path(source)
package_hash = @@asset_packages_yml[asset_type].find do |p|
key = p.keys.first
p[key].include?(path_parts[2]) && (parse_path(key)[1] == path_parts[1])
end
package_hash ? self.new(asset_type, package_hash) : nil
end
def targets_from_sources(asset_type, sources)
package_names = Array.new
sources.each do |source|
package = find_by_target(asset_type, source) || find_by_source(asset_type, source)
package_names << (package ? package.current_file : source)
end
package_names.uniq
end
def sources_from_targets(asset_type, targets)
source_names = Array.new
targets.each do |target|
package = find_by_target(asset_type, target)
source_names += (package ? package.sources.collect do |src|
package.target_dir.gsub(/^(.+)$/, '\1/') + src
end : target.to_a)
end
source_names.uniq
end
def build_all
@@asset_packages_yml.keys.each do |asset_type|
@@asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).build }
end
end
def delete_all
@@asset_packages_yml.keys.each do |asset_type|
@@asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).delete_all_builds }
end
end
def create_yml
unless File.exists?("#{RAILS_ROOT}/config/asset_packages.yml")
asset_yml = Hash.new
asset_yml['javascripts'] = [{"base" => build_file_list("#{RAILS_ROOT}/public/javascripts", "js")}]
asset_yml['stylesheets'] = [{"base" => build_file_list("#{RAILS_ROOT}/public/stylesheets", "css")}]
File.open("#{RAILS_ROOT}/config/asset_packages.yml", "w") do |out|
YAML.dump(asset_yml, out)
end
log "config/asset_packages.yml example file created!"
log "Please reorder files under 'base' so dependencies are loaded in correct order."
else
log "config/asset_packages.yml already exists. Aborting task..."
end
end
end
# instance methods
attr_accessor :asset_type, :target, :target_dir, :sources
def initialize(asset_type, package_hash)
target_parts = self.class.parse_path(package_hash.keys.first)
@target_dir = target_parts[1].to_s
@target = target_parts[2].to_s
@sources = package_hash[package_hash.keys.first]
@asset_type = asset_type
@asset_path = ($asset_base_path ? "#{$asset_base_path}/" : "#{RAILS_ROOT}/public/") +
"#{@asset_type}#{@target_dir.gsub(/^(.+)$/, '/\1')}"
@extension = get_extension
@match_regex = Regexp.new("\\A#{@target}_\\d+.#{@extension}\\z")
end
def current_file
@target_dir.gsub(/^(.+)$/, '\1/') +
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.sort.reverse[0].chomp(".#{@extension}")
end
def build
delete_old_builds
create_new_build
end
def delete_old_builds
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.each do |x|
File.delete("#{@asset_path}/#{x}") unless x.index(revision.to_s)
end
end
def delete_all_builds
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.each do |x|
File.delete("#{@asset_path}/#{x}")
end
end
private
def revision
unless @revision
revisions = [1]
@sources.each do |source|
revisions << get_file_revision("#{@asset_path}/#{source}.#{@extension}")
end
@revision = revisions.max
end
@revision
end
def get_file_revision(path)
if File.exists?(path)
begin
`svn info #{path}`[/Last Changed Rev: (.*?)\n/][/(\d+)/].to_i
rescue # use filename timestamp if not in subversion
File.mtime(path).to_i
end
else
0
end
end
def create_new_build
if File.exists?("#{@asset_path}/#{@target}_#{revision}.#{@extension}")
log "Latest version already exists: #{@asset_path}/#{@target}_#{revision}.#{@extension}"
else
File.open("#{@asset_path}/#{@target}_#{revision}.#{@extension}", "w") {|f| f.write(compressed_file) }
log "Created #{@asset_path}/#{@target}_#{revision}.#{@extension}"
end
end
def merged_file
merged_file = ""
@sources.each {|s|
File.open("#{@asset_path}/#{s}.#{@extension}", "r") { |f|
merged_file += f.read + "\n"
}
}
merged_file
end
def compressed_file
case @asset_type
when "javascripts" then compress_js(merged_file)
when "stylesheets" then compress_css(merged_file)
end
end
def compress_js(source)
jsmin_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/lib"
tmp_path = "#{RAILS_ROOT}/tmp/#{@target}_#{revision}"
# write out to a temp file
File.open("#{tmp_path}_uncompressed.js", "w") {|f| f.write(source) }
# compress file with JSMin library
`ruby #{jsmin_path}/jsmin.rb <#{tmp_path}_uncompressed.js >#{tmp_path}_compressed.js \n`
# read it back in and trim it
result = ""
File.open("#{tmp_path}_compressed.js", "r") { |f| result += f.read.strip }
# delete temp files if they exist
File.delete("#{tmp_path}_uncompressed.js") if File.exists?("#{tmp_path}_uncompressed.js")
File.delete("#{tmp_path}_compressed.js") if File.exists?("#{tmp_path}_compressed.js")
result
end
def compress_css(source)
source.gsub!(/\s+/, " ") # collapse space
source.gsub!(/\/\*(.*?)\*\/ /, "") # remove comments - caution, might want to remove this if using css hacks
source.gsub!(/\} /, "}\n") # add line breaks
source.gsub!(/\n$/, "") # remove last break
source.gsub!(/ \{ /, " {") # trim inside brackets
source.gsub!(/; \}/, "}") # trim inside brackets
source
end
def get_extension
case @asset_type
when "javascripts" then "js"
when "stylesheets" then "css"
end
end
def log(message)
puts message
end
def self.build_file_list(path, extension)
re = Regexp.new(".#{extension}\\z")
file_list = Dir.new(path).entries.delete_if { |x| ! (x =~ re) }.map {|x| x.chomp(".#{extension}")}
# reverse javascript entries so prototype comes first on a base rails app
file_list.reverse! if extension == "js"
file_list
end
end
end

View file

@ -0,0 +1,67 @@
module Synthesis
module AssetPackageHelper
def should_merge?
AssetPackage.merge_environments.include?(RAILS_ENV)
end
def javascript_include_merged(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
if sources.include?(:defaults)
sources = sources[0..(sources.index(:defaults))] +
['prototype', 'effects', 'dragdrop', 'controls'] +
(File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") ? ['application'] : []) +
sources[(sources.index(:defaults) + 1)..sources.length]
sources.delete(:defaults)
end
sources.collect!{|s| s.to_s}
sources = (should_merge? ?
AssetPackage.targets_from_sources("javascripts", sources) :
AssetPackage.sources_from_targets("javascripts", sources))
sources.collect {|source| javascript_include_tag(source, options) }.join("\n")
end
def stylesheet_link_merged(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
sources.collect!{|s| s.to_s}
sources = (should_merge? ?
AssetPackage.targets_from_sources("stylesheets", sources) :
AssetPackage.sources_from_targets("stylesheets", sources))
sources.collect { |source|
source = stylesheet_path(source)
tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options))
}.join("\n")
end
private
# rewrite compute_public_path to allow us to not include the query string timestamp
# used by ActionView::Helpers::AssetTagHelper
def compute_public_path(source, dir, ext=nil, add_asset_id=true)
source = source.dup
source << ".#{ext}" if File.extname(source).blank? && ext
unless source =~ %r{^[-a-z]+://}
source = "/#{dir}/#{source}" unless source[0] == ?/
asset_id = rails_asset_id(source)
source << '?' + asset_id if defined?(RAILS_ROOT) and add_asset_id and not asset_id.blank?
source = "#{ActionController::Base.asset_host}#{@controller.request.relative_url_root}#{source}"
end
source
end
# rewrite javascript path function to not include query string timestamp
def javascript_path(source)
compute_public_path(source, 'javascripts', 'js', false)
end
# rewrite stylesheet path function to not include query string timestamp
def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css', false)
end
end
end

View file

@ -0,0 +1,22 @@
require File.dirname(__FILE__) + '/../lib/synthesis/asset_package'
namespace :asset do
namespace :packager do
desc "Merge and compress assets"
task :build_all do
Synthesis::AssetPackage.build_all
end
desc "Delete all asset builds"
task :delete_all do
Synthesis::AssetPackage.delete_all
end
desc "Generate asset_packages.yml from existing assets"
task :create_yml do
Synthesis::AssetPackage.create_yml
end
end
end

View file

@ -0,0 +1,107 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
ENV['RAILS_ENV'] = "development"
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
$asset_base_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/test/assets"
class AssetPackageHelperProductionTest < Test::Unit::TestCase
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include Synthesis::AssetPackageHelper
def setup
Synthesis::AssetPackage.any_instance.stubs(:log)
@controller = Class.new do
attr_reader :request
def initialize
@request = Class.new do
def relative_url_root
""
end
end.new
end
end.new
end
def build_js_expected_string(*sources)
sources.map {|s| %(<script src="/javascripts/#{s}.js" type="text/javascript"></script>) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| %(<link href="/stylesheets/#{s}.css" rel="Stylesheet" type="text/css" media="screen" />) }.join("\n")
end
def test_js_basic
assert_dom_equal build_js_expected_string("prototype"),
javascript_include_merged("prototype")
end
def test_js_multiple_packages
assert_dom_equal build_js_expected_string("prototype", "foo"),
javascript_include_merged("prototype", "foo")
end
def test_js_unpackaged_file
assert_dom_equal build_js_expected_string("prototype", "foo", "not_part_of_a_package"),
javascript_include_merged("prototype", "foo", "not_part_of_a_package")
end
def test_js_multiple_from_same_package
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "not_part_of_a_package", "foo"),
javascript_include_merged("prototype", "effects", "controls", "not_part_of_a_package", "foo")
end
def test_js_by_package_name
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "dragdrop"),
javascript_include_merged(:base)
end
def test_js_multiple_package_names
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "dragdrop", "foo", "bar", "application"),
javascript_include_merged(:base, :secondary)
end
def test_css_basic
assert_dom_equal build_css_expected_string("screen"),
stylesheet_link_merged("screen")
end
def test_css_multiple_packages
assert_dom_equal build_css_expected_string("screen", "foo", "subdir/bar"),
stylesheet_link_merged("screen", "foo", "subdir/bar")
end
def test_css_unpackaged_file
assert_dom_equal build_css_expected_string("screen", "foo", "not_part_of_a_package", "subdir/bar"),
stylesheet_link_merged("screen", "foo", "not_part_of_a_package", "subdir/bar")
end
def test_css_multiple_from_same_package
assert_dom_equal build_css_expected_string("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar"),
stylesheet_link_merged("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar")
end
def test_css_by_package_name
assert_dom_equal build_css_expected_string("screen", "header"),
stylesheet_link_merged(:base)
end
def test_css_multiple_package_names
assert_dom_equal build_css_expected_string("screen", "header", "foo", "bar", "subdir/foo", "subdir/bar"),
stylesheet_link_merged(:base, :secondary, "subdir/styles")
end
end

View file

@ -0,0 +1,153 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
$asset_base_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/test/assets"
class AssetPackageHelperProductionTest < Test::Unit::TestCase
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include Synthesis::AssetPackageHelper
cattr_accessor :packages_built
def setup
Synthesis::AssetPackage.any_instance.stubs(:log)
self.stubs(:should_merge?).returns(true)
@controller = Class.new do
attr_reader :request
def initialize
@request = Class.new do
def relative_url_root
""
end
end.new
end
end.new
build_packages_once
end
def build_packages_once
unless @@packages_built
Synthesis::AssetPackage.build_all
@@packages_built = true
end
end
def build_js_expected_string(*sources)
sources.map {|s| %(<script src="/javascripts/#{s}.js" type="text/javascript"></script>) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| %(<link href="/stylesheets/#{s}.css" rel="Stylesheet" type="text/css" media="screen" />) }.join("\n")
end
def test_js_basic
current_file = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
assert_dom_equal build_js_expected_string(current_file),
javascript_include_merged("prototype")
end
def test_js_multiple_packages
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, current_file2),
javascript_include_merged("prototype", "foo")
end
def test_js_unpackaged_file
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, current_file2, "not_part_of_a_package"),
javascript_include_merged("prototype", "foo", "not_part_of_a_package")
end
def test_js_multiple_from_same_package
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, "not_part_of_a_package", current_file2),
javascript_include_merged("prototype", "effects", "controls", "not_part_of_a_package", "foo")
end
def test_js_by_package_name
package_name = Synthesis::AssetPackage.find_by_target("javascripts", "base").current_file
assert_dom_equal build_js_expected_string(package_name),
javascript_include_merged(:base)
end
def test_js_multiple_package_names
package_name1 = Synthesis::AssetPackage.find_by_target("javascripts", "base").current_file
package_name2 = Synthesis::AssetPackage.find_by_target("javascripts", "secondary").current_file
assert_dom_equal build_js_expected_string(package_name1, package_name2),
javascript_include_merged(:base, :secondary)
end
def test_css_basic
current_file = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
assert_dom_equal build_css_expected_string(current_file),
stylesheet_link_merged("screen")
end
def test_css_multiple_packages
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
current_file3 = Synthesis::AssetPackage.find_by_source("stylesheets", "subdir/bar").current_file
assert_dom_equal build_css_expected_string(current_file1, current_file2, current_file3),
stylesheet_link_merged("screen", "foo", "subdir/bar")
end
def test_css_unpackaged_file
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
assert_dom_equal build_css_expected_string(current_file1, current_file2, "not_part_of_a_package"),
stylesheet_link_merged("screen", "foo", "not_part_of_a_package")
end
def test_css_multiple_from_same_package
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
current_file3 = Synthesis::AssetPackage.find_by_source("stylesheets", "subdir/bar").current_file
assert_dom_equal build_css_expected_string(current_file1, "not_part_of_a_package", current_file2, current_file3),
stylesheet_link_merged("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar")
end
def test_css_by_package_name
package_name = Synthesis::AssetPackage.find_by_target("stylesheets", "base").current_file
assert_dom_equal build_css_expected_string(package_name),
stylesheet_link_merged(:base)
end
def test_css_multiple_package_names
package_name1 = Synthesis::AssetPackage.find_by_target("stylesheets", "base").current_file
package_name2 = Synthesis::AssetPackage.find_by_target("stylesheets", "secondary").current_file
package_name3 = Synthesis::AssetPackage.find_by_target("stylesheets", "subdir/styles").current_file
assert_dom_equal build_css_expected_string(package_name1, package_name2, package_name3),
stylesheet_link_merged(:base, :secondary, "subdir/styles")
end
def test_image_tag
timestamp = rails_asset_id("images/rails.png")
assert_dom_equal %(<img alt="Rails" src="/images/rails.png?#{timestamp}" />), image_tag("rails")
end
end

View file

@ -0,0 +1,92 @@
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'mocha'
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
$asset_base_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/test/assets"
class AssetPackagerTest < Test::Unit::TestCase
include Synthesis
def setup
Synthesis::AssetPackage.any_instance.stubs(:log)
Synthesis::AssetPackage.build_all
end
def teardown
Synthesis::AssetPackage.delete_all
end
def test_find_by_type
js_asset_packages = Synthesis::AssetPackage.find_by_type("javascripts")
assert_equal 2, js_asset_packages.length
assert_equal "base", js_asset_packages[0].target
assert_equal ["prototype", "effects", "controls", "dragdrop"], js_asset_packages[0].sources
end
def test_find_by_target
package = Synthesis::AssetPackage.find_by_target("javascripts", "base")
assert_equal "base", package.target
assert_equal ["prototype", "effects", "controls", "dragdrop"], package.sources
end
def test_find_by_source
package = Synthesis::AssetPackage.find_by_source("javascripts", "controls")
assert_equal "base", package.target
assert_equal ["prototype", "effects", "controls", "dragdrop"], package.sources
end
def test_delete_and_build
Synthesis::AssetPackage.delete_all
js_package_names = Dir.new("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.js/) }
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }
css_subdir_package_names = Dir.new("#{$asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }
assert_equal 0, js_package_names.length
assert_equal 0, css_package_names.length
assert_equal 0, css_subdir_package_names.length
Synthesis::AssetPackage.build_all
js_package_names = Dir.new("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.js/) }.sort
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }.sort
css_subdir_package_names = Dir.new("#{$asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }.sort
assert_equal 2, js_package_names.length
assert_equal 2, css_package_names.length
assert_equal 1, css_subdir_package_names.length
assert js_package_names[0].match(/\Abase_\d+.js\z/)
assert js_package_names[1].match(/\Asecondary_\d+.js\z/)
assert css_package_names[0].match(/\Abase_\d+.css\z/)
assert css_package_names[1].match(/\Asecondary_\d+.css\z/)
assert css_subdir_package_names[0].match(/\Astyles_\d+.css\z/)
end
def test_js_names_from_sources
package_names = Synthesis::AssetPackage.targets_from_sources("javascripts", ["prototype", "effects", "noexist1", "controls", "foo", "noexist2"])
assert_equal 4, package_names.length
assert package_names[0].match(/\Abase_\d+\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_\d+\z/)
assert_equal package_names[3], "noexist2"
end
def test_css_names_from_sources
package_names = Synthesis::AssetPackage.targets_from_sources("stylesheets", ["header", "screen", "noexist1", "foo", "noexist2"])
assert_equal 4, package_names.length
assert package_names[0].match(/\Abase_\d+\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_\d+\z/)
assert_equal package_names[3], "noexist2"
end
def test_should_return_merge_environments_when_set
Synthesis::AssetPackage.merge_environments = ["staging", "production"]
assert_equal ["staging", "production"], Synthesis::AssetPackage.merge_environments
end
def test_should_only_return_production_merge_environment_when_not_set
assert_equal ["production"], Synthesis::AssetPackage.merge_environments
end
end

View file

@ -0,0 +1,20 @@
javascripts:
- base:
- prototype
- effects
- controls
- dragdrop
- secondary:
- foo
- bar
- application
stylesheets:
- base:
- screen
- header
- secondary:
- foo
- bar
- subdir/styles:
- foo
- bar

View file

@ -0,0 +1,2 @@
// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults

View file

@ -0,0 +1,4 @@
bar bar bar
bar bar bar
bar bar bar

View file

@ -0,0 +1,815 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// See scriptaculous.js for full license.
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
baseInitialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
this.setOptions(options);
else
this.options = options || {};
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
(navigator.appVersion.indexOf('MSIE')>0) &&
(navigator.userAgent.indexOf('Opera')<0) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix);
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
activate: function() {
this.changed = false;
this.hasFocus = true;
this.getUpdatedChoices();
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = '';
if (this.options.select) {
var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
this.element.value = value;
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
initialize: function(element, url, options) {
this.url = url;
this.element = $(element);
this.options = Object.extend({
okButton: true,
okText: "ok",
cancelLink: true,
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
okText: "ok",
rows: 1,
onComplete: function(transport, element) {
new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
},
onFailure: function(transport) {
alert("Error communicating with the server: " + transport.responseText.stripTags());
},
callback: function(form) {
return Form.serialize(form);
},
handleLineBreaks: true,
loadingText: 'Loading...',
savingClassName: 'inplaceeditor-saving',
loadingClassName: 'inplaceeditor-loading',
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
submitOnBlur: false,
ajaxOptions: {},
evalScripts: false
}, options || {});
if(!this.options.formId && this.element.id) {
this.options.formId = this.element.id + "-inplaceeditor";
if ($(this.options.formId)) {
// there's already a form with that name, don't specify an id
this.options.formId = null;
}
}
if (this.options.externalControl) {
this.options.externalControl = $(this.options.externalControl);
}
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground) {
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.onclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.observe(this.options.externalControl, 'click', this.onclickListener);
Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function(evt) {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
this.onEnterEditMode();
if (this.options.externalControl) {
Element.hide(this.options.externalControl);
}
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.scrollFreeActivate(this.editField);
// stop the event to avoid a page refresh in Safari
if (evt) {
Event.stop(evt);
}
return false;
},
createForm: function() {
this.form = document.createElement("form");
this.form.id = this.options.formId;
Element.addClassName(this.form, this.options.formClassName)
this.form.onsubmit = this.onSubmit.bind(this);
this.createEditField();
if (this.options.textarea) {
var br = document.createElement("br");
this.form.appendChild(br);
}
if (this.options.okButton) {
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
okButton.className = 'editor_ok_button';
this.form.appendChild(okButton);
}
if (this.options.cancelLink) {
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
cancelLink.className = 'editor_cancel';
this.form.appendChild(cancelLink);
}
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
return string.match(/<br/i) || string.match(/<p>/i);
},
convertHTMLLineBreaks: function(string) {
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
},
createEditField: function() {
var text;
if(this.options.loadTextURL) {
text = this.options.loadingText;
} else {
text = this.getText();
}
var obj = this;
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.obj = this;
textField.type = "text";
textField.name = "value";
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
textField.className = 'editor_field';
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
if (this.options.submitOnBlur)
textField.onblur = this.onSubmit.bind(this);
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.obj = this;
textArea.name = "value";
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
textArea.className = 'editor_field';
if (this.options.submitOnBlur)
textArea.onblur = this.onSubmit.bind(this);
this.editField = textArea;
}
if(this.options.loadTextURL) {
this.loadExternalText();
}
this.form.appendChild(this.editField);
},
getText: function() {
return this.element.innerHTML;
},
loadExternalText: function() {
Element.addClassName(this.form, this.options.loadingClassName);
this.editField.disabled = true;
new Ajax.Request(
this.options.loadTextURL,
Object.extend({
asynchronous: true,
onComplete: this.onLoadedExternalText.bind(this)
}, this.options.ajaxOptions)
);
},
onLoadedExternalText: function(transport) {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
},
onclickCancel: function() {
this.onComplete();
this.leaveEditMode();
return false;
},
onFailure: function(transport) {
this.options.onFailure(transport);
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
onSubmit: function() {
// onLoading resets these so we need to save them away for the Ajax call
var form = this.form;
var value = this.editField.value;
// do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
// which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
// to be displayed indefinitely
this.onLoading();
if (this.options.evalScripts) {
new Ajax.Request(
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this),
asynchronous:true,
evalScripts:true
}, this.options.ajaxOptions));
} else {
new Ajax.Updater(
{ success: this.element,
// don't update on failure (this could be an option)
failure: null },
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions));
}
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
return false;
},
onLoading: function() {
this.saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
showSaving: function() {
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
Element.addClassName(this.element, this.options.savingClassName);
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
},
removeForm: function() {
if(this.form) {
if (this.form.parentNode) Element.remove(this.form);
this.form = null;
}
},
enterHover: function() {
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) {
this.effect.cancel();
}
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHover: function() {
if (this.options.backgroundColor) {
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(this.element, {
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
});
},
leaveEditMode: function() {
Element.removeClassName(this.element, this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
if (this.options.externalControl) {
Element.show(this.options.externalControl);
}
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
this.onLeaveEditMode();
},
onComplete: function(transport) {
this.leaveEditMode();
this.options.onComplete.bind(this)(transport, this.element);
},
onEnterEditMode: function() {},
onLeaveEditMode: function() {},
dispose: function() {
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
}
this.leaveEditMode();
Event.stopObserving(this.element, 'click', this.onclickListener);
Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};
Ajax.InPlaceCollectionEditor = Class.create();
Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
createEditField: function() {
if (!this.cached_selectTag) {
var selectTag = document.createElement("select");
var collection = this.options.collection || [];
var optionTag;
collection.each(function(e,i) {
optionTag = document.createElement("option");
optionTag.value = (e instanceof Array) ? e[0] : e;
if(this.options.value==optionTag.value) optionTag.selected = true;
optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
selectTag.appendChild(optionTag);
}.bind(this));
this.cached_selectTag = selectTag;
}
this.editField = this.cached_selectTag;
if(this.options.loadTextURL) this.loadExternalText();
this.form.appendChild(this.editField);
this.options.callback = function(form, value) {
return "value=" + encodeURIComponent(value);
}
}
});
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
};

View file

@ -0,0 +1,913 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// See scriptaculous.js for full license.
/*--------------------------------------------------------------------------*/
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==$(element) });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null,
tree: false
}, arguments[1] || {});
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if((typeof containment == 'object') &&
(containment.constructor == Array)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
findDeepestChild: function(drops) {
deepest = drops[0];
for (i = 1; i < drops.length; ++i)
if (Element.isParent(drops[i].element, deepest.element))
deepest = drops[i];
return deepest;
},
isContained: function(element, drop) {
var containmentNode;
if(drop.tree) {
containmentNode = element.treeNode;
} else {
containmentNode = element.parentNode;
}
return drop._containers.detect(function(c) { return containmentNode == c });
},
isAffected: function(point, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
Position.within(drop.element, point[0], point[1]) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.removeClassName(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(drop.hoverclass)
Element.addClassName(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(point, element) {
if(!this.drops.length) return;
var affected = [];
if(this.last_active) this.deactivate(this.last_active);
this.drops.each( function(drop) {
if(Droppables.isAffected(point, element, drop))
affected.push(drop);
});
if(affected.length>0) {
drop = Droppables.findDeepestChild(affected);
Position.within(drop.element, point[0], point[1]);
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
Droppables.activate(drop);
}
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
if (this.last_active.onDrop)
this.last_active.onDrop(element, this.last_active.element, event);
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
drags: [],
observers: [],
register: function(draggable) {
if(this.drags.length == 0) {
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
}
this.drags.push(draggable);
},
unregister: function(draggable) {
this.drags = this.drags.reject(function(d) { return d==draggable });
if(this.drags.length == 0) {
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
}
},
activate: function(draggable) {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
},
deactivate: function() {
this.activeDraggable = null;
},
updateDrag: function(event) {
if(!this.activeDraggable) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
// Mozilla-based browsers fire successive mousemove events with
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
this.activeDraggable = null;
},
keyPress: function(event) {
if(this.activeDraggable)
this.activeDraggable.keyPress(event);
},
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
function(o) { return o[eventName]; }
).length;
});
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
},
endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
},
zindex: 1000,
revert: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
}, arguments[1] || {});
this.element = $(element);
if(options.handle && (typeof options.handle == 'string')) {
var h = Element.childrenWithClassName(this.element, options.handle, true);
if(h.length>0) this.handle = h[0];
}
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML)
options.scroll = $(options.scroll);
Element.makePositioned(this.element); // fix IE
this.delta = this.currentDelta();
this.options = options;
this.dragging = false;
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Draggables.register(this);
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Draggables.unregister(this);
},
currentDelta: function() {
return([
parseInt(Element.getStyle(this.element,'left') || '0'),
parseInt(Element.getStyle(this.element,'top') || '0')]);
},
initDrag: function(event) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if(src.tagName && (
src.tagName=='INPUT' ||
src.tagName=='SELECT' ||
src.tagName=='OPTION' ||
src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return;
if(this.element._revert) {
this.element._revert.cancel();
this.element._revert = null;
}
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
Event.stop(event);
}
},
startDrag: function(event) {
this.dragging = true;
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
this.element.style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
if(this.options.scroll) {
if (this.options.scroll == window) {
var where = this._getWindowScroll(this.options.scroll);
this.originalScrollLeft = where.left;
this.originalScrollTop = where.top;
} else {
this.originalScrollLeft = this.options.scroll.scrollLeft;
this.originalScrollTop = this.options.scroll.scrollTop;
}
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
Position.prepare();
Droppables.show(pointer, this.element);
Draggables.notify('onDrag', this, event);
this.draw(pointer);
if(this.options.change) this.options.change(this);
if(this.options.scroll) {
this.stopScrolling();
var p;
if (this.options.scroll == window) {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
} else {
p = Position.page(this.options.scroll);
p[0] += this.options.scroll.scrollLeft;
p[1] += this.options.scroll.scrollTop;
p.push(p[0]+this.options.scroll.offsetWidth);
p.push(p[1]+this.options.scroll.offsetHeight);
}
var speed = [0,0];
if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
this.startScrolling(speed);
}
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
if(this.options.ghosting) {
Position.relativize(this.element);
Element.remove(this._clone);
this._clone = null;
}
if(success) Droppables.fire(event, this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);
var d = this.currentDelta();
if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element,
d[1]-this.delta[1], d[0]-this.delta[0]);
} else {
this.delta = d;
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
keyPress: function(event) {
if(event.keyCode!=Event.KEY_ESC) return;
this.finishDrag(event, false);
Event.stop(event);
},
endDrag: function(event) {
if(!this.dragging) return;
this.stopScrolling();
this.finishDrag(event, true);
Event.stop(event);
},
draw: function(point) {
var pos = Position.cumulativeOffset(this.element);
var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1];
if(this.options.scroll && (this.options.scroll != window)) {
pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
}
var p = [0,1].map(function(i){
return (point[i]-pos[i]-this.offset[i])
}.bind(this));
if(this.options.snap) {
if(typeof this.options.snap == 'function') {
p = this.options.snap(p[0],p[1]);
} else {
if(this.options.snap instanceof Array) {
p = p.map( function(v, i) {
return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
} else {
p = p.map( function(v) {
return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
}
}}
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
stopScrolling: function() {
if(this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
Draggables._lastScrollPointer = null;
}
},
startScrolling: function(speed) {
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10);
},
scroll: function() {
var current = new Date();
var delta = current - this.lastScrolled;
this.lastScrolled = current;
if(this.options.scroll == window) {
with (this._getWindowScroll(this.options.scroll)) {
if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
var d = delta / 1000;
this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
}
}
} else {
this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
}
Position.prepare();
Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this);
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0)
Draggables._lastScrollPointer[0] = 0;
if (Draggables._lastScrollPointer[1] < 0)
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
if(this.options.change) this.options.change(this);
},
_getWindowScroll: function(w) {
var T, L, W, H;
with (w.document) {
if (w.document.documentElement && documentElement.scrollTop) {
T = documentElement.scrollTop;
L = documentElement.scrollLeft;
} else if (w.document.body) {
T = body.scrollTop;
L = body.scrollLeft;
}
if (w.innerWidth) {
W = w.innerWidth;
H = w.innerHeight;
} else if (w.document.documentElement && documentElement.clientWidth) {
W = documentElement.clientWidth;
H = documentElement.clientHeight;
} else {
W = body.offsetWidth;
H = body.offsetHeight
}
}
return { top: T, left: L, width: W, height: H };
}
}
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create();
SortableObserver.prototype = {
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
}
var Sortable = {
sortables: {},
_findRootElement: function(element) {
while (element.tagName != "BODY") {
if(element.id && Sortable.sortables[element.id]) return element;
element = element.parentNode;
}
},
options: function(element) {
element = Sortable._findRootElement($(element));
if(!element) return;
return Sortable.sortables[element.id];
},
destroy: function(element){
var s = Sortable.options(element);
if(s) {
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
delete Sortable.sortables[s.element.id];
}
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false,
treeTag: 'ul',
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
hoverclass: null,
ghosting: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: /^[^_]*_(.*)$/,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || {});
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
scroll: options.scroll,
scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
tree: options.tree,
hoverclass: options.hoverclass,
onHover: Sortable.onHover
//greedy: !options.dropOnEmpty
}
var options_for_tree = {
onHover: Sortable.onEmptyHover,
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// drop on empty handling
if(options.dropOnEmpty || options.tree) {
Droppables.add(element, options_for_tree);
options.droppables.push(element);
}
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.childrenWithClassName(e, options.handle)[0] : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
if(options.tree) e.treeNode = element;
options.droppables.push(e);
});
if(options.tree) {
(Sortable.findTreeElements(element, options) || []).each( function(e) {
Droppables.add(e, options_for_tree);
e.treeNode = element;
options.droppables.push(e);
});
}
// keep reference
this.sortables[element.id] = options;
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.tag);
},
findTreeElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.treeTag);
},
onHover: function(element, dropon, overlap) {
if(Element.isParent(dropon, element)) return;
if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
return;
} else if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon, overlap) {
var oldParentNode = element.parentNode;
var droponOptions = Sortable.options(dropon);
if(!Element.isParent(dropon, element)) {
var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag});
var child = null;
if(children) {
var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
for (index = 0; index < children.length; index += 1) {
if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
offset -= Element.offsetSize (children[index], droponOptions.overlap);
} else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
child = index + 1 < children.length ? children[index + 1] : null;
break;
} else {
child = children[index];
break;
}
}
}
dropon.insertBefore(element, child);
Sortable.options(oldParentNode).onChange(element);
droponOptions.onChange(element);
}
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.addClassName(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.left = offsets[0] + 'px';
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
else
Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
Element.show(Sortable._marker);
},
_tree: function(element, options, parent) {
var children = Sortable.findElements(element, options) || [];
for (var i = 0; i < children.length; ++i) {
var match = children[i].id.match(options.format);
if (!match) continue;
var child = {
id: encodeURIComponent(match ? match[1] : null),
element: element,
parent: parent,
children: new Array,
position: parent.children.length,
container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
}
/* Get the element containing the children and recurse over it */
if (child.container)
this._tree(child.container, options, child)
parent.children.push (child);
}
return parent;
},
/* Finds the first element of the given tag type within a parent element.
Used for finding the first LI[ST] within a L[IST]I[TEM].*/
_findChildrenElement: function (element, containerTag) {
if (element && element.hasChildNodes)
for (var i = 0; i < element.childNodes.length; ++i)
if (element.childNodes[i].tagName == containerTag)
return element.childNodes[i];
return null;
},
tree: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
treeTag: sortableOptions.treeTag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format
}, arguments[1] || {});
var root = {
id: null,
parent: null,
children: new Array,
container: element,
position: 0
}
return Sortable._tree (element, options, root);
},
/* Construct a [i] index for a particular node */
_constructIndex: function(node) {
var index = '';
do {
if (node.id) index = '[' + node.position + ']' + index;
} while ((node = node.parent) != null);
return index;
},
sequence: function(element) {
element = $(element);
var options = Object.extend(this.options(element), arguments[1] || {});
return $(this.findElements(element, options) || []).map( function(item) {
return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
});
},
setSequence: function(element, new_sequence) {
element = $(element);
var options = Object.extend(this.options(element), arguments[2] || {});
var nodeMap = {};
this.findElements(element, options).each( function(n) {
if (n.id.match(options.format))
nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
n.parentNode.removeChild(n);
});
new_sequence.each(function(ident) {
var n = nodeMap[ident];
if (n) {
n[1].appendChild(n[0]);
delete nodeMap[ident];
}
});
},
serialize: function(element) {
element = $(element);
var options = Object.extend(Sortable.options(element), arguments[1] || {});
var name = encodeURIComponent(
(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
if (options.tree) {
return Sortable.tree(element, arguments[1]).children.map( function (item) {
return [name + Sortable._constructIndex(item) + "=" +
encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
}).flatten().join('&');
} else {
return Sortable.sequence(element, arguments[1]).map( function(item) {
return name + "[]=" + encodeURIComponent(item);
}).join('&');
}
}
}
/* Returns true if child is contained within element */
Element.isParent = function(child, element) {
if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element);
}
Element.findChildren = function(element, only, recursive, tagName) {
if(!element.hasChildNodes()) return null;
tagName = tagName.toUpperCase();
if(only) only = [only].flatten();
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName.toUpperCase()==tagName &&
(!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
elements.push(e);
if(recursive) {
var grandchildren = Element.findChildren(e, only, recursive, tagName);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : []);
}
Element.offsetSize = function (element, type) {
if (type == 'vertical' || type == 'height')
return element.offsetHeight;
else
return element.offsetWidth;
}

View file

@ -0,0 +1,958 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
// Martin Bialasinki
//
// See scriptaculous.js for full license.
// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
var color = '#';
if(this.slice(0,4) == 'rgb(') {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
} else {
if(this.slice(0,1) == '#') {
if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
if(this.length==7) color = this.toLowerCase();
}
}
return(color.length==7 ? color : (arguments[0] || this));
}
/*--------------------------------------------------------------------------*/
Element.collectTextNodes = function(element) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
(node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
}).flatten().join('');
}
Element.collectTextNodesIgnoreClass = function(element, className) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
Element.collectTextNodesIgnoreClass(node, className) : ''));
}).flatten().join('');
}
Element.setContentZoom = function(element, percent) {
element = $(element);
Element.setStyle(element, {fontSize: (percent/100) + 'em'});
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}
Element.getOpacity = function(element){
var opacity;
if (opacity = Element.getStyle(element, 'opacity'))
return parseFloat(opacity);
if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))
if(opacity[1]) return parseFloat(opacity[1]) / 100;
return 1.0;
}
Element.setOpacity = function(element, value){
element= $(element);
if (value == 1){
Element.setStyle(element, { opacity:
(/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
0.999999 : null });
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
} else {
if(value < 0.00001) value = 0;
Element.setStyle(element, {opacity: value});
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element,
{ filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
'alpha(opacity='+value*100+')' });
}
}
Element.getInlineOpacity = function(element){
return $(element).style.opacity || '';
}
Element.childrenWithClassName = function(element, className, findFirst) {
var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) {
return (c.className && c.className.match(classNameRegExp));
});
if(!results) results = [];
return results;
}
Element.forceRerendering = function(element) {
try {
element = $(element);
var n = document.createTextNode(' ');
element.appendChild(n);
element.removeChild(n);
} catch(e) { }
};
/*--------------------------------------------------------------------------*/
Array.prototype.call = function() {
var args = arguments;
this.each(function(f){ f.apply(this, args) });
}
/*--------------------------------------------------------------------------*/
var Effect = {
tagifyText: function(element) {
var tagifyStyle = 'position:relative';
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
element = $(element);
$A(element.childNodes).each( function(child) {
if(child.nodeType==3) {
child.nodeValue.toArray().each( function(character) {
element.insertBefore(
Builder.node('span',{style: tagifyStyle},
character == ' ' ? String.fromCharCode(160) : character),
child);
});
Element.remove(child);
}
});
},
multiple: function(element, effect) {
var elements;
if(((typeof element == 'object') ||
(typeof element == 'function')) &&
(element.length))
elements = element;
else
elements = $(element).childNodes;
var options = Object.extend({
speed: 0.1,
delay: 0.0
}, arguments[2] || {});
var masterDelay = options.delay;
$A(elements).each( function(element, index) {
new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
});
},
PAIRS: {
'slide': ['SlideDown','SlideUp'],
'blind': ['BlindDown','BlindUp'],
'appear': ['Appear','Fade']
},
toggle: function(element, effect) {
element = $(element);
effect = (effect || 'appear').toLowerCase();
var options = Object.extend({
queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
}, arguments[2] || {});
Effect[element.visible() ?
Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
}
};
var Effect2 = Effect; // deprecated
/* ------------- transitions ------------- */
Effect.Transitions = {}
Effect.Transitions.linear = function(pos) {
return pos;
}
Effect.Transitions.sinoidal = function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse = function(pos) {
return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
return (Math.floor(pos*10) % 2 == 0 ?
(pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
return 0;
}
Effect.Transitions.full = function(pos) {
return 1;
}
/* ------------- core effects ------------- */
Effect.ScopedQueue = Class.create();
Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
initialize: function() {
this.effects = [];
this.interval = null;
},
_each: function(iterator) {
this.effects._each(iterator);
},
add: function(effect) {
var timestamp = new Date().getTime();
var position = (typeof effect.options.queue == 'string') ?
effect.options.queue : effect.options.queue.position;
switch(position) {
case 'front':
// move unstarted effects after this effect
this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
e.startOn += effect.finishOn;
e.finishOn += effect.finishOn;
});
break;
case 'end':
// start effect after last queued effect has finished
timestamp = this.effects.pluck('finishOn').max() || timestamp;
break;
}
effect.startOn += timestamp;
effect.finishOn += timestamp;
if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
this.effects.push(effect);
if(!this.interval)
this.interval = setInterval(this.loop.bind(this), 40);
},
remove: function(effect) {
this.effects = this.effects.reject(function(e) { return e==effect });
if(this.effects.length == 0) {
clearInterval(this.interval);
this.interval = null;
}
},
loop: function() {
var timePos = new Date().getTime();
this.effects.invoke('loop', timePos);
}
});
Effect.Queues = {
instances: $H(),
get: function(queueName) {
if(typeof queueName != 'string') return queueName;
if(!this.instances[queueName])
this.instances[queueName] = new Effect.ScopedQueue();
return this.instances[queueName];
}
}
Effect.Queue = Effect.Queues.get('global');
Effect.DefaultOptions = {
transition: Effect.Transitions.sinoidal,
duration: 1.0, // seconds
fps: 25.0, // max. 25fps due to Effect.Queue implementation
sync: false, // true for combining
from: 0.0,
to: 1.0,
delay: 0.0,
queue: 'parallel'
}
Effect.Base = function() {};
Effect.Base.prototype = {
position: null,
start: function(options) {
this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
this.currentFrame = 0;
this.state = 'idle';
this.startOn = this.options.delay*1000;
this.finishOn = this.startOn + (this.options.duration*1000);
this.event('beforeStart');
if(!this.options.sync)
Effect.Queues.get(typeof this.options.queue == 'string' ?
'global' : this.options.queue.scope).add(this);
},
loop: function(timePos) {
if(timePos >= this.startOn) {
if(timePos >= this.finishOn) {
this.render(1.0);
this.cancel();
this.event('beforeFinish');
if(this.finish) this.finish();
this.event('afterFinish');
return;
}
var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
var frame = Math.round(pos * this.options.fps * this.options.duration);
if(frame > this.currentFrame) {
this.render(pos);
this.currentFrame = frame;
}
}
},
render: function(pos) {
if(this.state == 'idle') {
this.state = 'running';
this.event('beforeSetup');
if(this.setup) this.setup();
this.event('afterSetup');
}
if(this.state == 'running') {
if(this.options.transition) pos = this.options.transition(pos);
pos *= (this.options.to-this.options.from);
pos += this.options.from;
this.position = pos;
this.event('beforeUpdate');
if(this.update) this.update(pos);
this.event('afterUpdate');
}
},
cancel: function() {
if(!this.options.sync)
Effect.Queues.get(typeof this.options.queue == 'string' ?
'global' : this.options.queue.scope).remove(this);
this.state = 'finished';
},
event: function(eventName) {
if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
if(this.options[eventName]) this.options[eventName](this);
},
inspect: function() {
return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
}
}
Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
initialize: function(effects) {
this.effects = effects || [];
this.start(arguments[1]);
},
update: function(position) {
this.effects.invoke('render', position);
},
finish: function(position) {
this.effects.each( function(effect) {
effect.render(1.0);
effect.cancel();
effect.event('beforeFinish');
if(effect.finish) effect.finish(position);
effect.event('afterFinish');
});
}
});
Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
// make this work on IE on elements without 'layout'
if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
this.element.setStyle({zoom: 1});
var options = Object.extend({
from: this.element.getOpacity() || 0.0,
to: 1.0
}, arguments[1] || {});
this.start(options);
},
update: function(position) {
this.element.setOpacity(position);
}
});
Effect.Move = Class.create();
Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({
x: 0,
y: 0,
mode: 'relative'
}, arguments[1] || {});
this.start(options);
},
setup: function() {
// Bug in Opera: Opera returns the "real" position of a static element or
// relative element that does not have top/left explicitly set.
// ==> Always set top and left for position relative elements in your stylesheets
// (to 0 if you do not need them)
this.element.makePositioned();
this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
this.originalTop = parseFloat(this.element.getStyle('top') || '0');
if(this.options.mode == 'absolute') {
// absolute movement, so we need to calc deltaX and deltaY
this.options.x = this.options.x - this.originalLeft;
this.options.y = this.options.y - this.originalTop;
}
},
update: function(position) {
this.element.setStyle({
left: this.options.x * position + this.originalLeft + 'px',
top: this.options.y * position + this.originalTop + 'px'
});
}
});
// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
return new Effect.Move(element,
Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
};
Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
initialize: function(element, percent) {
this.element = $(element)
var options = Object.extend({
scaleX: true,
scaleY: true,
scaleContent: true,
scaleFromCenter: false,
scaleMode: 'box', // 'box' or 'contents' or {} with provided values
scaleFrom: 100.0,
scaleTo: percent
}, arguments[2] || {});
this.start(options);
},
setup: function() {
this.restoreAfterFinish = this.options.restoreAfterFinish || false;
this.elementPositioning = this.element.getStyle('position');
this.originalStyle = {};
['top','left','width','height','fontSize'].each( function(k) {
this.originalStyle[k] = this.element.style[k];
}.bind(this));
this.originalTop = this.element.offsetTop;
this.originalLeft = this.element.offsetLeft;
var fontSize = this.element.getStyle('font-size') || '100%';
['em','px','%'].each( function(fontSizeType) {
if(fontSize.indexOf(fontSizeType)>0) {
this.fontSize = parseFloat(fontSize);
this.fontSizeType = fontSizeType;
}
}.bind(this));
this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
this.dims = null;
if(this.options.scaleMode=='box')
this.dims = [this.element.offsetHeight, this.element.offsetWidth];
if(/^content/.test(this.options.scaleMode))
this.dims = [this.element.scrollHeight, this.element.scrollWidth];
if(!this.dims)
this.dims = [this.options.scaleMode.originalHeight,
this.options.scaleMode.originalWidth];
},
update: function(position) {
var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
if(this.options.scaleContent && this.fontSize)
this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
},
finish: function(position) {
if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
},
setDimensions: function(height, width) {
var d = {};
if(this.options.scaleX) d.width = width + 'px';
if(this.options.scaleY) d.height = height + 'px';
if(this.options.scaleFromCenter) {
var topd = (height - this.dims[0])/2;
var leftd = (width - this.dims[1])/2;
if(this.elementPositioning == 'absolute') {
if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
} else {
if(this.options.scaleY) d.top = -topd + 'px';
if(this.options.scaleX) d.left = -leftd + 'px';
}
}
this.element.setStyle(d);
}
});
Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
this.start(options);
},
setup: function() {
// Prevent executing on elements not in the layout flow
if(this.element.getStyle('display')=='none') { this.cancel(); return; }
// Disable background image during the effect
this.oldStyle = {
backgroundImage: this.element.getStyle('background-image') };
this.element.setStyle({backgroundImage: 'none'});
if(!this.options.endcolor)
this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
if(!this.options.restorecolor)
this.options.restorecolor = this.element.getStyle('background-color');
// init color calculations
this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
},
update: function(position) {
this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
},
finish: function() {
this.element.setStyle(Object.extend(this.oldStyle, {
backgroundColor: this.options.restorecolor
}));
}
});
Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
this.start(arguments[1] || {});
},
setup: function() {
Position.prepare();
var offsets = Position.cumulativeOffset(this.element);
if(this.options.offset) offsets[1] += this.options.offset;
var max = window.innerHeight ?
window.height - window.innerHeight :
document.body.scrollHeight -
(document.documentElement.clientHeight ?
document.documentElement.clientHeight : document.body.clientHeight);
this.scrollStart = Position.deltaY;
this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
},
update: function(position) {
Position.prepare();
window.scrollTo(Position.deltaX,
this.scrollStart + (position*this.delta));
}
});
/* ------------- combination effects ------------- */
Effect.Fade = function(element) {
element = $(element);
var oldOpacity = element.getInlineOpacity();
var options = Object.extend({
from: element.getOpacity() || 1.0,
to: 0.0,
afterFinishInternal: function(effect) {
if(effect.options.to!=0) return;
effect.element.hide();
effect.element.setStyle({opacity: oldOpacity});
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Appear = function(element) {
element = $(element);
var options = Object.extend({
from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
to: 1.0,
// force Safari to render floated elements properly
afterFinishInternal: function(effect) {
effect.element.forceRerendering();
},
beforeSetup: function(effect) {
effect.element.setOpacity(effect.options.from);
effect.element.show();
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Puff = function(element) {
element = $(element);
var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') };
return new Effect.Parallel(
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect) {
effect.effects[0].element.setStyle({position: 'absolute'}); },
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.setStyle(oldStyle); }
}, arguments[1] || {})
);
}
Effect.BlindUp = function(element) {
element = $(element);
element.makeClipping();
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
restoreAfterFinish: true,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
}
}, arguments[1] || {})
);
}
Effect.BlindDown = function(element) {
element = $(element);
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100,
Object.extend({ scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show();
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
}
}, arguments[1] || {})
);
}
Effect.SwitchOff = function(element) {
element = $(element);
var oldOpacity = element.getInlineOpacity();
return new Effect.Appear(element, {
duration: 0.4,
from: 0,
transition: Effect.Transitions.flicker,
afterFinishInternal: function(effect) {
new Effect.Scale(effect.element, 1, {
duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makePositioned();
effect.element.makeClipping();
},
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.undoPositioned();
effect.element.setStyle({opacity: oldOpacity});
}
})
}
});
}
Effect.DropOut = function(element) {
element = $(element);
var oldStyle = {
top: element.getStyle('top'),
left: element.getStyle('left'),
opacity: element.getInlineOpacity() };
return new Effect.Parallel(
[ new Effect.Move(element, {x: 0, y: 100, sync: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
Object.extend(
{ duration: 0.5,
beforeSetup: function(effect) {
effect.effects[0].element.makePositioned();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
}
}, arguments[1] || {}));
}
Effect.Shake = function(element) {
element = $(element);
var oldStyle = {
top: element.getStyle('top'),
left: element.getStyle('left') };
return new Effect.Move(element,
{ x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
effect.element.undoPositioned();
effect.element.setStyle(oldStyle);
}}) }}) }}) }}) }}) }});
}
Effect.SlideDown = function(element) {
element = $(element);
element.cleanWhitespace();
// SlideDown need to have the content of the element wrapped in a container element with fixed height!
var oldInnerBottom = $(element.firstChild).getStyle('bottom');
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show(); },
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
// IE will crash if child is undoPositioned first
if(/MSIE/.test(navigator.userAgent)){
effect.element.undoPositioned();
effect.element.firstChild.undoPositioned();
}else{
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
}
effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {})
);
}
Effect.SlideUp = function(element) {
element = $(element);
element.cleanWhitespace();
var oldInnerBottom = $(element.firstChild).getStyle('bottom');
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
scaleFrom: 100,
restoreAfterFinish: true,
beforeStartInternal: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.show(); },
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); },
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
effect.element.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {})
);
}
// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
return new Effect.Scale(element, window.opera ? 1 : 0,
{ restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makeClipping(effect.element); },
afterFinishInternal: function(effect) {
effect.element.hide(effect.element);
effect.element.undoClipping(effect.element); }
});
}
Effect.Grow = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.full
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: element.getInlineOpacity() };
var dims = element.getDimensions();
var initialMoveX, initialMoveY;
var moveX, moveY;
switch (options.direction) {
case 'top-left':
initialMoveX = initialMoveY = moveX = moveY = 0;
break;
case 'top-right':
initialMoveX = dims.width;
initialMoveY = moveY = 0;
moveX = -dims.width;
break;
case 'bottom-left':
initialMoveX = moveX = 0;
initialMoveY = dims.height;
moveY = -dims.height;
break;
case 'bottom-right':
initialMoveX = dims.width;
initialMoveY = dims.height;
moveX = -dims.width;
moveY = -dims.height;
break;
case 'center':
initialMoveX = dims.width / 2;
initialMoveY = dims.height / 2;
moveX = -dims.width / 2;
moveY = -dims.height / 2;
break;
}
return new Effect.Move(element, {
x: initialMoveX,
y: initialMoveY,
duration: 0.01,
beforeSetup: function(effect) {
effect.element.hide();
effect.element.makeClipping();
effect.element.makePositioned();
},
afterFinishInternal: function(effect) {
new Effect.Parallel(
[ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
new Effect.Scale(effect.element, 100, {
scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
], Object.extend({
beforeSetup: function(effect) {
effect.effects[0].element.setStyle({height: '0px'});
effect.effects[0].element.show();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
}
}, options)
)
}
});
}
Effect.Shrink = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.none
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: element.getInlineOpacity() };
var dims = element.getDimensions();
var moveX, moveY;
switch (options.direction) {
case 'top-left':
moveX = moveY = 0;
break;
case 'top-right':
moveX = dims.width;
moveY = 0;
break;
case 'bottom-left':
moveX = 0;
moveY = dims.height;
break;
case 'bottom-right':
moveX = dims.width;
moveY = dims.height;
break;
case 'center':
moveX = dims.width / 2;
moveY = dims.height / 2;
break;
}
return new Effect.Parallel(
[ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
], Object.extend({
beforeStartInternal: function(effect) {
effect.effects[0].element.makePositioned();
effect.effects[0].element.makeClipping(); },
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle); }
}, options)
);
}
Effect.Pulsate = function(element) {
element = $(element);
var options = arguments[1] || {};
var oldOpacity = element.getInlineOpacity();
var transition = options.transition || Effect.Transitions.sinoidal;
var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
reverser.bind(transition);
return new Effect.Opacity(element,
Object.extend(Object.extend({ duration: 3.0, from: 0,
afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
}, options), {transition: reverser}));
}
Effect.Fold = function(element) {
element = $(element);
var oldStyle = {
top: element.style.top,
left: element.style.left,
width: element.style.width,
height: element.style.height };
Element.makeClipping(element);
return new Effect.Scale(element, 5, Object.extend({
scaleContent: false,
scaleX: false,
afterFinishInternal: function(effect) {
new Effect.Scale(element, 1, {
scaleContent: false,
scaleY: false,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.setStyle(oldStyle);
} });
}}, arguments[1] || {}));
};
['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each(
function(f) { Element.Methods[f] = Element[f]; }
);
Element.Methods.visualEffect = function(element, effect, options) {
s = effect.gsub(/_/, '-').camelize();
effect_class = s.charAt(0).toUpperCase() + s.substring(1);
new Effect[effect_class](element, options);
return $(element);
};
Element.addMethods();

View file

@ -0,0 +1,4 @@
foo foo foo
foo foo foo
foo foo foo

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
#bar1 {
background: #fff;
color: #000;
text-align: center;
}
#bar2 {
background: #fff;
color: #000;
text-align: center;
}
#bar3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#foo1 {
background: #fff;
color: #000;
text-align: center;
}
#foo2 {
background: #fff;
color: #000;
text-align: center;
}
#foo3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#header1 {
background: #fff;
color: #000;
text-align: center;
}
#header2 {
background: #fff;
color: #000;
text-align: center;
}
#header3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#screen1 {
background: #fff;
color: #000;
text-align: center;
}
#screen2 {
background: #fff;
color: #000;
text-align: center;
}
#screen3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#bar1 {
background: #fff;
color: #000;
text-align: center;
}
#bar2 {
background: #fff;
color: #000;
text-align: center;
}
#bar3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#foo1 {
background: #fff;
color: #000;
text-align: center;
}
#foo2 {
background: #fff;
color: #000;
text-align: center;
}
#foo3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,152 @@
* Exported the changelog of Pagination code for historical reference.
* Imported some patches from Rails Trac (others closed as "wontfix"):
#8176, #7325, #7028, #4113. Documentation is much cleaner now and there
are some new unobtrusive features!
* Extracted Pagination from Rails trunk (r6795)
#
# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb
#
# Generated by Trac 0.10.3
# 05/20/07 23:48:02
#
09/03/06 23:28:54 david [4953]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Docs and deprecation
08/07/06 12:40:14 bitsweat [4715]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Deprecate direct usage of @params. Update ActionView::Base for
instance var deprecation.
06/21/06 02:16:11 rick [4476]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Fix indent in pagination documentation. Closes #4990. [Kevin Clark]
04/25/06 17:42:48 marcel [4268]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Remove all remaining references to @params in the documentation.
03/16/06 06:38:08 rick [3899]
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
trivial documentation patch for #pagination_links [Francois
Beausoleil] closes #4258
02/20/06 03:15:22 david [3620]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/test/activerecord/pagination_test.rb (modified)
* trunk/activerecord/CHANGELOG (modified)
* trunk/activerecord/lib/active_record/base.rb (modified)
* trunk/activerecord/test/base_test.rb (modified)
Added :count option to pagination that'll make it possible for the
ActiveRecord::Base.count call to using something else than * for the
count. Especially important for count queries using DISTINCT #3839
[skaes]. Added :select option to Base.count that'll allow you to
select something else than * to be counted on. Especially important
for count queries using DISTINCT (closes #3839) [skaes].
02/09/06 09:17:40 nzkoz [3553]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/test/active_record_unit.rb (added)
* trunk/actionpack/test/activerecord (added)
* trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added)
* trunk/actionpack/test/activerecord/pagination_test.rb (added)
* trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted)
* trunk/actionpack/test/fixtures/companies.yml (added)
* trunk/actionpack/test/fixtures/company.rb (added)
* trunk/actionpack/test/fixtures/db_definitions (added)
* trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added)
* trunk/actionpack/test/fixtures/developer.rb (added)
* trunk/actionpack/test/fixtures/developers_projects.yml (added)
* trunk/actionpack/test/fixtures/developers.yml (added)
* trunk/actionpack/test/fixtures/project.rb (added)
* trunk/actionpack/test/fixtures/projects.yml (added)
* trunk/actionpack/test/fixtures/replies.yml (added)
* trunk/actionpack/test/fixtures/reply.rb (added)
* trunk/actionpack/test/fixtures/topic.rb (added)
* trunk/actionpack/test/fixtures/topics.yml (added)
* Fix pagination problems when using include
* Introduce Unit Tests for pagination
* Allow count to work with :include by using count distinct.
[Kevin Clark &amp; Jeremy Hopple]
11/05/05 02:10:29 bitsweat [2878]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Update paginator docs. Closes #2744.
10/16/05 15:42:03 minam [2649]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Update/clean up AP documentation (rdoc)
08/31/05 00:13:10 ulysses [2078]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Add option to specify the singular name used by pagination. Closes
#1960
08/23/05 14:24:15 minam [2041]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Add support for :include with pagination (subject to existing
constraints for :include with :limit and :offset) #1478
[michael@schubert.cx]
07/15/05 20:27:38 david [1839]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
More pagination speed #1334 [Stefan Kaes]
07/14/05 08:02:01 david [1832]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
* trunk/actionpack/test/controller/addresses_render_test.rb (modified)
Made pagination faster #1334 [Stefan Kaes]
04/13/05 05:40:22 david [1159]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/activerecord/lib/active_record/base.rb (modified)
Fixed pagination to work with joins #1034 [scott@sigkill.org]
04/02/05 09:11:17 david [1067]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_controller/scaffolding.rb (modified)
* trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified)
* trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified)
* trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified)
Added pagination for scaffolding (10 items per page) #964
[mortonda@dgrmm.net]
03/31/05 14:46:11 david [1048]
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Improved the message display on the exception handler pages #963
[Johan Sorensen]
03/27/05 00:04:07 david [1017]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Fixed that pagination_helper would ignore :params #947 [Sebastian
Kanthak]
03/22/05 13:09:44 david [976]
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Fixed documentation and prepared for 0.11.0 release
03/21/05 14:35:36 david [967]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Tweaked the documentation
03/20/05 23:12:05 david [949]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller.rb (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (added)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added)
* trunk/activesupport/lib/active_support/core_ext/kernel.rb (added)
Added pagination support through both a controller and helper add-on
#817 [Sam Stephenson]

View file

@ -0,0 +1,18 @@
Pagination
==========
To install:
script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination
This code was extracted from Rails trunk after the release 1.2.3.
WARNING: this code is dead. It is unmaintained, untested and full of cruft.
There is a much better pagination plugin called will_paginate.
Install it like this and glance through the README:
script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
It doesn't have the same API, but is in fact much nicer. You can
have both plugins installed until you change your controller/view code that
handles pagination. Then, simply uninstall classic_pagination.

View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the classic_pagination plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the classic_pagination plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Pagination'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,33 @@
#--
# Copyright (c) 2004-2006 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require 'pagination'
require 'pagination_helper'
ActionController::Base.class_eval do
include ActionController::Pagination
end
ActionView::Base.class_eval do
include ActionView::Helpers::PaginationHelper
end

View file

@ -0,0 +1 @@
puts "\n\n" + File.read(File.dirname(__FILE__) + '/README')

View file

@ -0,0 +1,405 @@
module ActionController
# === Action Pack pagination for Active Record collections
#
# The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if
# the magic isn't flexible enough for your needs, you can create your own
# paginators with a minimal amount of code.
#
# The Pagination module can handle as much or as little as you wish. In the
# controller, have it automatically query your model for pagination; or,
# if you prefer, create Paginator objects yourself.
#
# Pagination is included automatically for all controllers.
#
# For help rendering pagination links, see
# ActionView::Helpers::PaginationHelper.
#
# ==== Automatic pagination for every action in a controller
#
# class PersonController < ApplicationController
# model :person
#
# paginate :people, :order => 'last_name, first_name',
# :per_page => 20
#
# # ...
# end
#
# Each action in this controller now has access to a <tt>@people</tt>
# instance variable, which is an ordered collection of model objects for the
# current page (at most 20, sorted by last name and first name), and a
# <tt>@person_pages</tt> Paginator instance. The current page is determined
# by the <tt>params[:page]</tt> variable.
#
# ==== Pagination for a single action
#
# def list
# @person_pages, @people =
# paginate :people, :order => 'last_name, first_name'
# end
#
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
# and <tt>@people</tt> for a single action, and uses the default of 10 items
# per page.
#
# ==== Custom/"classic" pagination
#
# def list
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
# @people = Person.find :all, :order => 'last_name, first_name',
# :limit => @person_pages.items_per_page,
# :offset => @person_pages.current.offset
# end
#
# Explicitly creates the paginator from the previous example and uses
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
#
module Pagination
unless const_defined?(:OPTIONS)
# A hash holding options for controllers using macro-style pagination
OPTIONS = Hash.new
# The default options for pagination
DEFAULT_OPTIONS = {
:class_name => nil,
:singular_name => nil,
:per_page => 10,
:conditions => nil,
:order_by => nil,
:order => nil,
:join => nil,
:joins => nil,
:count => nil,
:include => nil,
:select => nil,
:group => nil,
:parameter => 'page'
}
else
DEFAULT_OPTIONS[:group] = nil
end
def self.included(base) #:nodoc:
super
base.extend(ClassMethods)
end
def self.validate_options!(collection_id, options, in_action) #:nodoc:
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
valid_options = DEFAULT_OPTIONS.keys
valid_options << :actions unless in_action
unknown_option_keys = options.keys - valid_options
raise ActionController::ActionControllerError,
"Unknown options: #{unknown_option_keys.join(', ')}" unless
unknown_option_keys.empty?
options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
options[:class_name] ||= Inflector.camelize(options[:singular_name])
end
# Returns a paginator and a collection of Active Record model instances
# for the paginator's current page. This is designed to be used in a
# single action; to automatically paginate multiple actions, consider
# ClassMethods#paginate.
#
# +options+ are:
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# camelizing the singular name
# <tt>:per_page</tt>:: the maximum number of items to include in a
# single page. Defaults to 10
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
# Model.count
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
#
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
#
# <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
#
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, true)
paginator_and_collection_for(collection_id, options)
end
# These methods become class methods on any controller
module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active
# Record model for all actions in a controller (or certain actions if
# specified with the <tt>:actions</tt> option).
#
# +options+ are the same as PaginationHelper#paginate, with the addition
# of:
# <tt>:actions</tt>:: an array of actions for which the pagination is
# active. Defaults to +nil+ (i.e., every action)
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, false)
module_eval do
before_filter :create_paginators_and_retrieve_collections
OPTIONS[self] ||= Hash.new
OPTIONS[self][collection_id] = options
end
end
end
def create_paginators_and_retrieve_collections #:nodoc:
Pagination::OPTIONS[self.class].each do |collection_id, options|
next unless options[:actions].include? action_name if
options[:actions]
paginator, collection =
paginator_and_collection_for(collection_id, options)
paginator_name = "@#{options[:singular_name]}_pages"
self.instance_variable_set(paginator_name, paginator)
collection_name = "@#{collection_id.to_s}"
self.instance_variable_set(collection_name, collection)
end
end
# Returns the total number of items in the collection to be paginated for
# the +model+ and given +conditions+. Override this method to implement a
# custom counter.
def count_collection_for_pagination(model, options)
model.count(:conditions => options[:conditions],
:joins => options[:join] || options[:joins],
:include => options[:include],
:select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
end
# Returns a collection of items for the given +model+ and +options[conditions]+,
# ordered by +options[order]+, for the current page in the given +paginator+.
# Override this method to implement a custom finder.
def find_collection_for_pagination(model, options, paginator)
model.find(:all, :conditions => options[:conditions],
:order => options[:order_by] || options[:order],
:joins => options[:join] || options[:joins], :include => options[:include],
:select => options[:select], :limit => options[:per_page],
:group => options[:group], :offset => paginator.current.offset)
end
protected :create_paginators_and_retrieve_collections,
:count_collection_for_pagination,
:find_collection_for_pagination
def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = options[:class_name].constantize
page = params[options[:parameter]]
count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator)
return paginator, collection
end
private :paginator_and_collection_for
# A class representing a paginator for an Active Record collection.
class Paginator
include Enumerable
# Creates a new Paginator on the given +controller+ for a set of items
# of size +item_count+ and having +items_per_page+ items per page.
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
# than or equal to zero). The page CGI parameter for links defaults to
# "page" and can be overridden with +page_parameter+.
def initialize(controller, item_count, items_per_page, current_page=1)
raise ArgumentError, 'must have at least one item per page' if
items_per_page <= 0
@controller = controller
@item_count = item_count || 0
@items_per_page = items_per_page
@pages = {}
self.current_page = current_page
end
attr_reader :controller, :item_count, :items_per_page
# Sets the current page number of this paginator. If +page+ is a Page
# object, its +number+ attribute is used as the value; if the page does
# not belong to this Paginator, an ArgumentError is raised.
def current_page=(page)
if page.is_a? Page
raise ArgumentError, 'Page/Paginator mismatch' unless
page.paginator == self
end
page = page.to_i
@current_page_number = has_page_number?(page) ? page : 1
end
# Returns a Page object representing this paginator's current page.
def current_page
@current_page ||= self[@current_page_number]
end
alias current :current_page
# Returns a new Page representing the first page in this paginator.
def first_page
@first_page ||= self[1]
end
alias first :first_page
# Returns a new Page representing the last page in this paginator.
def last_page
@last_page ||= self[page_count]
end
alias last :last_page
# Returns the number of pages in this paginator.
def page_count
@page_count ||= @item_count.zero? ? 1 :
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
end
alias length :page_count
# Returns true if this paginator contains the page of index +number+.
def has_page_number?(number)
number >= 1 and number <= page_count
end
# Returns a new Page representing the page with the given index
# +number+.
def [](number)
@pages[number] ||= Page.new(self, number)
end
# Successively yields all the paginator's pages to the given block.
def each(&block)
page_count.times do |n|
yield self[n+1]
end
end
# A class representing a single page in a paginator.
class Page
include Comparable
# Creates a new Page for the given +paginator+ with the index
# +number+. If +number+ is not in the range of valid page numbers or
# is not a number at all, it defaults to 1.
def initialize(paginator, number)
@paginator = paginator
@number = number.to_i
@number = 1 unless @paginator.has_page_number? @number
end
attr_reader :paginator, :number
alias to_i :number
# Compares two Page objects and returns true when they represent the
# same page (i.e., their paginators are the same and they have the
# same page number).
def ==(page)
return false if page.nil?
@paginator == page.paginator and
@number == page.number
end
# Compares two Page objects and returns -1 if the left-hand page comes
# before the right-hand page, 0 if the pages are equal, and 1 if the
# left-hand page comes after the right-hand page. Raises ArgumentError
# if the pages do not belong to the same Paginator object.
def <=>(page)
raise ArgumentError unless @paginator == page.paginator
@number <=> page.number
end
# Returns the item offset for the first item in this page.
def offset
@paginator.items_per_page * (@number - 1)
end
# Returns the number of the first item displayed.
def first_item
offset + 1
end
# Returns the number of the last item displayed.
def last_item
[@paginator.items_per_page * @number, @paginator.item_count].min
end
# Returns true if this page is the first page in the paginator.
def first?
self == @paginator.first
end
# Returns true if this page is the last page in the paginator.
def last?
self == @paginator.last
end
# Returns a new Page object representing the page just before this
# page, or nil if this is the first page.
def previous
if first? then nil else @paginator[@number - 1] end
end
# Returns a new Page object representing the page just after this
# page, or nil if this is the last page.
def next
if last? then nil else @paginator[@number + 1] end
end
# Returns a new Window object for this page with the specified
# +padding+.
def window(padding=2)
Window.new(self, padding)
end
# Returns the limit/offset array for this page.
def to_sql
[@paginator.items_per_page, offset]
end
def to_param #:nodoc:
@number.to_s
end
end
# A class for representing ranges around a given page.
class Window
# Creates a new Window object for the given +page+ with the specified
# +padding+.
def initialize(page, padding=2)
@paginator = page.paginator
@page = page
self.padding = padding
end
attr_reader :paginator, :page
# Sets the window's padding (the number of pages on either side of the
# window page).
def padding=(padding)
@padding = padding < 0 ? 0 : padding
# Find the beginning and end pages of the window
@first = @paginator.has_page_number?(@page.number - @padding) ?
@paginator[@page.number - @padding] : @paginator.first
@last = @paginator.has_page_number?(@page.number + @padding) ?
@paginator[@page.number + @padding] : @paginator.last
end
attr_reader :padding, :first, :last
# Returns an array of Page objects in the current window.
def pages
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
end
alias to_a :pages
end
end
end
end

View file

@ -0,0 +1,135 @@
module ActionView
module Helpers
# Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally
# also build your links manually using ActionView::Helpers::AssetHelper#link_to like so:
#
# <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
# <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
module PaginationHelper
unless const_defined?(:DEFAULT_OPTIONS)
DEFAULT_OPTIONS = {
:name => :page,
:window_size => 2,
:always_show_anchors => true,
:link_to_current_page => false,
:params => {}
}
end
# Creates a basic HTML link bar for the given +paginator+. Links will be created
# for the next and/or previous page and for a number of other pages around the current
# pages position. The +html_options+ hash is passed to +link_to+ when the links are created.
#
# ==== Options
# <tt>:name</tt>:: the routing name for this paginator
# (defaults to +page+)
# <tt>:prefix</tt>:: prefix for pagination links
# (i.e. Older Pages: 1 2 3 4)
# <tt>:suffix</tt>:: suffix for pagination links
# (i.e. 1 2 3 4 <- Older Pages)
# <tt>:window_size</tt>:: the number of pages to show around
# the current page (defaults to <tt>2</tt>)
# <tt>:always_show_anchors</tt>:: whether or not the first and last
# pages should always be shown
# (defaults to +true+)
# <tt>:link_to_current_page</tt>:: whether or not the current page
# should be linked to (defaults to
# +false+)
# <tt>:params</tt>:: any additional routing parameters
# for page URLs
#
# ==== Examples
# # We'll assume we have a paginator setup in @person_pages...
#
# pagination_links(@person_pages)
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :link_to_current_page => true)
# # => <a href="/?page=1/">1</a> <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :always_show_anchors => false)
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>
#
# pagination_links(@person_pages, :window_size => 1)
# # => 1 <a href="/?page=2/">2</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :params => { :viewer => "flash" })
# # => 1 <a href="/?page=2&amp;viewer=flash/">2</a> <a href="/?page=3&amp;viewer=flash/">3</a> ...
# # <a href="/?page=10&amp;viewer=flash/">10</a>
def pagination_links(paginator, options={}, html_options={})
name = options[:name] || DEFAULT_OPTIONS[:name]
params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
prefix = options[:prefix] || ''
suffix = options[:suffix] || ''
pagination_links_each(paginator, options, prefix, suffix) do |n|
params[name] = n
link_to(n.to_s, params, html_options)
end
end
# Iterate through the pages of a given +paginator+, invoking a
# block for each page number that needs to be rendered as a link.
#
# ==== Options
# <tt>:window_size</tt>:: the number of pages to show around
# the current page (defaults to +2+)
# <tt>:always_show_anchors</tt>:: whether or not the first and last
# pages should always be shown
# (defaults to +true+)
# <tt>:link_to_current_page</tt>:: whether or not the current page
# should be linked to (defaults to
# +false+)
#
# ==== Example
# # Turn paginated links into an Ajax call
# pagination_links_each(paginator, page_options) do |link|
# options = { :url => {:action => 'list'}, :update => 'results' }
# html_options = { :href => url_for(:action => 'list') }
#
# link_to_remote(link.to_s, options, html_options)
# end
def pagination_links_each(paginator, options, prefix = nil, suffix = nil)
options = DEFAULT_OPTIONS.merge(options)
link_to_current_page = options[:link_to_current_page]
always_show_anchors = options[:always_show_anchors]
current_page = paginator.current_page
window_pages = current_page.window(options[:window_size]).pages
return if window_pages.length <= 1 unless link_to_current_page
first, last = paginator.first, paginator.last
html = ''
html << prefix if prefix
if always_show_anchors and not (wp_first = window_pages[0]).first?
html << yield(first.number)
html << ' ... ' if wp_first.number - first.number > 1
html << ' '
end
window_pages.each do |page|
if current_page == page && !link_to_current_page
html << page.number.to_s
else
html << yield(page.number)
end
html << ' '
end
if always_show_anchors and not (wp_last = window_pages[-1]).last?
html << ' ... ' if last.number - wp_last.number > 1
html << yield(last.number)
end
html << suffix if suffix
html
end
end # PaginationHelper
end # Helpers
end # ActionView

View file

@ -0,0 +1,24 @@
thirty_seven_signals:
id: 1
name: 37Signals
rating: 4
TextDrive:
id: 2
name: TextDrive
rating: 4
PlanetArgon:
id: 3
name: Planet Argon
rating: 4
Google:
id: 4
name: Google
rating: 4
Ionist:
id: 5
name: Ioni.st
rating: 4

View file

@ -0,0 +1,9 @@
class Company < ActiveRecord::Base
attr_protected :rating
set_sequence_name :companies_nonstd_seq
validates_presence_of :name
def validate
errors.add('rating', 'rating should not be 2') if rating == 2
end
end

View file

@ -0,0 +1,7 @@
class Developer < ActiveRecord::Base
has_and_belongs_to_many :projects
end
class DeVeLoPeR < ActiveRecord::Base
set_table_name "developers"
end

View file

@ -0,0 +1,21 @@
david:
id: 1
name: David
salary: 80000
jamis:
id: 2
name: Jamis
salary: 150000
<% for digit in 3..10 %>
dev_<%= digit %>:
id: <%= digit %>
name: fixture_<%= digit %>
salary: 100000
<% end %>
poor_jamis:
id: 11
name: Jamis
salary: 9000

View file

@ -0,0 +1,13 @@
david_action_controller:
developer_id: 1
project_id: 2
joined_on: 2004-10-10
david_active_record:
developer_id: 1
project_id: 1
joined_on: 2004-10-10
jamis_active_record:
developer_id: 2
project_id: 1

View file

@ -0,0 +1,3 @@
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, :uniq => true
end

View file

@ -0,0 +1,7 @@
action_controller:
id: 2
name: Active Controller
active_record:
id: 1
name: Active Record

View file

@ -0,0 +1,13 @@
witty_retort:
id: 1
topic_id: 1
content: Birdman is better!
created_at: <%= 6.hours.ago.to_s(:db) %>
updated_at: nil
another:
id: 2
topic_id: 2
content: Nuh uh!
created_at: <%= 1.hour.ago.to_s(:db) %>
updated_at: nil

View file

@ -0,0 +1,5 @@
class Reply < ActiveRecord::Base
belongs_to :topic, :include => [:replies]
validates_presence_of :content
end

View file

@ -0,0 +1,42 @@
CREATE TABLE 'companies' (
'id' INTEGER PRIMARY KEY NOT NULL,
'name' TEXT DEFAULT NULL,
'rating' INTEGER DEFAULT 1
);
CREATE TABLE 'replies' (
'id' INTEGER PRIMARY KEY NOT NULL,
'content' text,
'created_at' datetime,
'updated_at' datetime,
'topic_id' integer
);
CREATE TABLE 'topics' (
'id' INTEGER PRIMARY KEY NOT NULL,
'title' varchar(255),
'subtitle' varchar(255),
'content' text,
'created_at' datetime,
'updated_at' datetime
);
CREATE TABLE 'developers' (
'id' INTEGER PRIMARY KEY NOT NULL,
'name' TEXT DEFAULT NULL,
'salary' INTEGER DEFAULT 70000,
'created_at' DATETIME DEFAULT NULL,
'updated_at' DATETIME DEFAULT NULL
);
CREATE TABLE 'projects' (
'id' INTEGER PRIMARY KEY NOT NULL,
'name' TEXT DEFAULT NULL
);
CREATE TABLE 'developers_projects' (
'developer_id' INTEGER NOT NULL,
'project_id' INTEGER NOT NULL,
'joined_on' DATE DEFAULT NULL,
'access_level' INTEGER DEFAULT 1
);

View file

@ -0,0 +1,3 @@
class Topic < ActiveRecord::Base
has_many :replies, :include => [:user], :dependent => :destroy
end

View file

@ -0,0 +1,22 @@
futurama:
id: 1
title: Isnt futurama awesome?
subtitle: It really is, isnt it.
content: I like futurama
created_at: <%= 1.day.ago.to_s(:db) %>
updated_at:
harvey_birdman:
id: 2
title: Harvey Birdman is the king of all men
subtitle: yup
content: It really is
created_at: <%= 2.hours.ago.to_s(:db) %>
updated_at:
rails:
id: 3
title: Rails is nice
subtitle: It makes me happy
content: except when I have to hack internals to fix pagination. even then really.
created_at: <%= 20.minutes.ago.to_s(:db) %>

View file

@ -0,0 +1,117 @@
require 'test/unit'
unless defined?(ActiveRecord)
plugin_root = File.join(File.dirname(__FILE__), '..')
# first look for a symlink to a copy of the framework
if framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p }
puts "found framework root: #{framework_root}"
# this allows for a plugin to be tested outside an app
$:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib"
else
# is the plugin installed in an application?
app_root = plugin_root + '/../../..'
if File.directory? app_root + '/config'
puts 'using config/boot.rb'
ENV['RAILS_ENV'] = 'test'
require File.expand_path(app_root + '/config/boot')
else
# simply use installed gems if available
puts 'using rubygems'
require 'rubygems'
gem 'actionpack'; gem 'activerecord'
end
end
%w(action_pack active_record action_controller active_record/fixtures action_controller/test_process).each {|f| require f}
Dependencies.load_paths.unshift "#{plugin_root}/lib"
end
# Define the connector
class ActiveRecordTestConnector
cattr_accessor :able_to_connect
cattr_accessor :connected
# Set our defaults
self.connected = false
self.able_to_connect = true
class << self
def setup
unless self.connected || !self.able_to_connect
setup_connection
load_schema
require_fixture_models
self.connected = true
end
rescue Exception => e # errors from ActiveRecord setup
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
#$stderr.puts " #{e.backtrace.join("\n ")}\n"
self.able_to_connect = false
end
private
def setup_connection
if Object.const_defined?(:ActiveRecord)
defaults = { :database => ':memory:' }
begin
options = defaults.merge :adapter => 'sqlite3', :timeout => 500
ActiveRecord::Base.establish_connection(options)
ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
ActiveRecord::Base.connection
rescue Exception # errors from establishing a connection
$stderr.puts 'SQLite 3 unavailable; trying SQLite 2.'
options = defaults.merge :adapter => 'sqlite'
ActiveRecord::Base.establish_connection(options)
ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options }
ActiveRecord::Base.connection
end
Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
else
raise "Can't setup connection since ActiveRecord isn't loaded."
end
end
# Load actionpack sqlite tables
def load_schema
File.read(File.dirname(__FILE__) + "/fixtures/schema.sql").split(';').each do |sql|
ActiveRecord::Base.connection.execute(sql) unless sql.blank?
end
end
def require_fixture_models
Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
end
end
end
# Test case for inheritance
class ActiveRecordTestCase < Test::Unit::TestCase
# Set our fixture path
if ActiveRecordTestConnector.able_to_connect
self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/"
self.use_transactional_fixtures = false
end
def self.fixtures(*args)
super if ActiveRecordTestConnector.connected
end
def run(*args)
super if ActiveRecordTestConnector.connected
end
# Default so Test::Unit::TestCase doesn't complain
def test_truth
end
end
ActiveRecordTestConnector.setup
ActionController::Routing::Routes.reload rescue nil
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action/:id'
end

View file

@ -0,0 +1,38 @@
require File.dirname(__FILE__) + '/helper'
require File.dirname(__FILE__) + '/../init'
class PaginationHelperTest < Test::Unit::TestCase
include ActionController::Pagination
include ActionView::Helpers::PaginationHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
def setup
@controller = Class.new do
attr_accessor :url, :request
def url_for(options, *parameters_for_method_reference)
url
end
end
@controller = @controller.new
@controller.url = "http://www.example.com"
end
def test_pagination_links
total, per_page, page = 30, 10, 1
output = pagination_links Paginator.new(@controller, total, per_page, page)
assert_equal "1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> ", output
end
def test_pagination_links_with_prefix
total, per_page, page = 30, 10, 1
output = pagination_links Paginator.new(@controller, total, per_page, page), :prefix => 'Newer '
assert_equal "Newer 1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> ", output
end
def test_pagination_links_with_suffix
total, per_page, page = 30, 10, 1
output = pagination_links Paginator.new(@controller, total, per_page, page), :suffix => 'Older'
assert_equal "1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> Older", output
end
end

View file

@ -0,0 +1,177 @@
require File.dirname(__FILE__) + '/helper'
require File.dirname(__FILE__) + '/../init'
class PaginationTest < ActiveRecordTestCase
fixtures :topics, :replies, :developers, :projects, :developers_projects
class PaginationController < ActionController::Base
if respond_to? :view_paths=
self.view_paths = [ "#{File.dirname(__FILE__)}/../fixtures/" ]
else
self.template_root = [ "#{File.dirname(__FILE__)}/../fixtures/" ]
end
def simple_paginate
@topic_pages, @topics = paginate(:topics)
render :nothing => true
end
def paginate_with_per_page
@topic_pages, @topics = paginate(:topics, :per_page => 1)
render :nothing => true
end
def paginate_with_order
@topic_pages, @topics = paginate(:topics, :order => 'created_at asc')
render :nothing => true
end
def paginate_with_order_by
@topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc')
render :nothing => true
end
def paginate_with_include_and_order
@topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc')
render :nothing => true
end
def paginate_with_conditions
@topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago])
render :nothing => true
end
def paginate_with_class_name
@developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR")
render :nothing => true
end
def paginate_with_singular_name
@developer_pages, @developers = paginate()
render :nothing => true
end
def paginate_with_joins
@developer_pages, @developers = paginate(:developers,
:joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
:conditions => 'project_id=1')
render :nothing => true
end
def paginate_with_join
@developer_pages, @developers = paginate(:developers,
:join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
:conditions => 'project_id=1')
render :nothing => true
end
def paginate_with_join_and_count
@developer_pages, @developers = paginate(:developers,
:join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id',
:conditions => 'project_id=1',
:count => "d.id")
render :nothing => true
end
def paginate_with_join_and_group
@developer_pages, @developers = paginate(:developers,
:join => 'INNER JOIN developers_projects ON developers.id = developers_projects.developer_id',
:group => 'developers.id')
render :nothing => true
end
def rescue_errors(e) raise e end
def rescue_action(e) raise end
end
def setup
@controller = PaginationController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
super
end
# Single Action Pagination Tests
def test_simple_paginate
get :simple_paginate
assert_equal 1, assigns(:topic_pages).page_count
assert_equal 3, assigns(:topics).size
end
def test_paginate_with_per_page
get :paginate_with_per_page
assert_equal 1, assigns(:topics).size
assert_equal 3, assigns(:topic_pages).page_count
end
def test_paginate_with_order
get :paginate_with_order
expected = [topics(:futurama),
topics(:harvey_birdman),
topics(:rails)]
assert_equal expected, assigns(:topics)
assert_equal 1, assigns(:topic_pages).page_count
end
def test_paginate_with_order_by
get :paginate_with_order
expected = assigns(:topics)
get :paginate_with_order_by
assert_equal expected, assigns(:topics)
assert_equal 1, assigns(:topic_pages).page_count
end
def test_paginate_with_conditions
get :paginate_with_conditions
expected = [topics(:rails)]
assert_equal expected, assigns(:topics)
assert_equal 1, assigns(:topic_pages).page_count
end
def test_paginate_with_class_name
get :paginate_with_class_name
assert assigns(:developers).size > 0
assert_equal DeVeLoPeR, assigns(:developers).first.class
end
def test_paginate_with_joins
get :paginate_with_joins
assert_equal 2, assigns(:developers).size
developer_names = assigns(:developers).map { |d| d.name }
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
def test_paginate_with_join_and_conditions
get :paginate_with_joins
expected = assigns(:developers)
get :paginate_with_join
assert_equal expected, assigns(:developers)
end
def test_paginate_with_join_and_count
get :paginate_with_joins
expected = assigns(:developers)
get :paginate_with_join_and_count
assert_equal expected, assigns(:developers)
end
def test_paginate_with_include_and_order
get :paginate_with_include_and_order
expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10)
assert_equal expected, assigns(:topics)
end
def test_paginate_with_join_and_group
get :paginate_with_join_and_group
assert_equal 2, assigns(:developers).size
assert_equal 2, assigns(:developer_pages).item_count
developer_names = assigns(:developers).map { |d| d.name }
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
end

View file

@ -0,0 +1,2 @@
require 'extra_validations'
ActiveRecord::Base.extend ExtraValidations

View file

@ -0,0 +1,29 @@
module ExtraValidations
# Validates the value of the specified attribute by checking for a forbidden string
#
# class Person < ActiveRecord::Base
# validates_does_not_contain :first_name, :string => ','
# end
#
# A string must be provided or else an exception will be raised.
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "is invalid")
# * <tt>string</tt> - The string to verify is not included (note: must be supplied!)
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_does_not_contain(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :string => nil }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
raise(ArgumentError, "A string must be supplied as the :string option of the configuration hash") unless configuration[:string].is_a?(String)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) if value.to_s =~ Regexp.new(Regexp.escape(configuration[:string]))
end
end
end

View file

@ -0,0 +1,35 @@
FlashObject v.1.0
====================
This FlashObject Helper is based on the flash view helper of the plugin http://laszlo-plugin.rubyforge.org/
It's only copy the FlashObject.js (version 1.5) on your public/javascript directory and register it in the AssetTagHelper of rails,
so for include the javascript in your page you only can add this (if you havn't yet):
<%= javascript_include_tag :defaults %>
or if you only need the flashobject add this:
<%= javascript_include_tag "flashobject" %>
The is very simple for add a Flash in your page add this:
<%= flashobject_tag "/source/of/your/flash.swf" %>
It's simple.
You can add some options if you want:
- div_id: the HTML +id+ of the +div+ element that is used to contain the Flash object; default "flashcontent"
- flash_id: the +id+ of the Flash object itself.
- background_color: the background color of the Flash object; default white
- flash_version: the version of the Flash player that is required; default "7"
- size: the size of the Flash object, in the form "100x100". Defaults to "100%x100%"
- variables: a Hash of initialization variables that are passed to the object; default <tt>{:lzproxied => false}</tt>
- parameters: a Hash of parameters that configure the display of the object; default <tt>{:scale => 'noscale'}</tt>
- fallback_html: HTML text that is displayed when the Flash player is not available.
Example:
<%= flashobject_tag "/source/of/your/flash.swf", :size => "350x320" %>
Vist my website: http://blog.lipsiasoft.com

View file

@ -0,0 +1,4 @@
# Author:: Davide D'Agostino Aka DAddYE
# WebSite:: http://www.lipsiasoft.com
require 'flashobject_view_helper'

View file

@ -0,0 +1,5 @@
require 'fileutils'
flashobject = File.dirname(__FILE__) + '/../../../public/javascripts/flashobject.js'
FileUtils.cp File.dirname(__FILE__) + '/javascripts/flashobject.js', flashobject unless File.exist?(flashobject)
puts IO.read(File.join(File.dirname(__FILE__), 'README'))

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,85 @@
# Author:: Davide D'Agostino aka DAddYE
# WebSite:: http://www.lipsiasoft.com
require 'action_view'
module ActionView #:nodoc:
module Helpers # :nodoc:
module FlashObjectHelper # :nodoc:
def self.included(base)
base.class_eval do
include InstanceMethods
end
end
module InstanceMethods
# Returns a set of tags that display a Flash object within an
# HTML page.
#
# Options:
# * <tt>:div_id</tt> - the HTML +id+ of the +div+ element that is used to contain the Flash object; default "flashcontent"
# * <tt>:flash_id</tt> - the +id+ of the Flash object itself.
# * <tt>:background_color</tt> - the background color of the Flash object; default white
# * <tt>:flash_version</tt> - the version of the Flash player that is required; default "7"
# * <tt>:size</tt> - the size of the Flash object, in the form "100x100". Defaults to "100%x100%"
# * <tt>:variables</tt> - a Hash of initialization variables that are passed to the object; default <tt>{:lzproxied => false}</tt>
# * <tt>:parameters</tt> - a Hash of parameters that configure the display of the object; default <tt>{:scale => 'noscale'}</tt>
# * <tt>:fallback_html</tt> - HTML text that is displayed when the Flash player is not available.
#
# The following options are for developers. They default to true in
# development mode, and false otherwise.
# * <tt>:check_for_javascript_include</tt> - if true, the return value will cause the browser to display a diagnostic message if the FlashObject JavaScript was not included.
# * <tt>:verify_file_exists</tt> - if true, the return value will cause the browser to display a diagnostic message if the Flash object does not exist.
#
# (This method is called flashobject_tag instead of flashobject_tag
# because it returns a *sequence* of HTML tags: a +div+, followed by
# a +script+.)
def flashobject_tag source, options={}
source = flash_path(source)
query_params = '?' + options[:query_params].map{ |key, value| "#{key}=#{value}" }.join('&') if options[:query_params]
div_id = options[:div_id] || "flashcontent_#{rand(1_100)}"
flash_id = options[:flash_id] || File.basename(source, '.swf') + "_#{rand(1_100)}"
width, height = (options[:size]||'100%x100%').scan(/^(\d*%?)x(\d*%?)$/).first
background_color = options[:background_color] || '#ffffff'
flash_version = options[:flash_version] || 7
class_name = options[:class_name] || 'flash'
variables = options.fetch(:variables, {})
parameters = options.fetch(:parameters, {:scale => 'noscale'})
fallback_html = options[:fallback_html] || %q{<p>Requires the Flash plugin. If the plugin is already installed, click <a href="?detectflash=false">here</a>.</p>}
if options.fetch(:check_for_javascript_include, ENV['RAILS_ENV'] == 'development')
check_for_javascript ="if (typeof FlashObject == 'undefined') document.getElementById('#{div_id}').innerHTML = '<strong>Warning:</strong> FlashObject is undefined. Did you forget to execute <tt>rake update_javascripts</tt>, or to include <tt>&lt;%= javascript_include_tag :defaults %></tt> in your view file?';"
end
return <<-"EOF"
<div id="#{div_id}" class="#{class_name}" style="height: #{height}">
#{fallback_html}
</div>
<script type="text/javascript">//<![CDATA[
#{check_for_javascript}
var fo = new FlashObject("#{source}#{query_params}", "#{flash_id}", "#{width}", "#{height}", "#{flash_version}", "#{background_color}");
#{parameters.map{|k,v|%Q[fo.addParam("#{k}", "#{v}");]}.join("\n")}
#{variables.map{|k,v|%Q[fo.addVariable("#{k}", "#{v}");]}.join("\n")}
fo.write("#{div_id}");
//]]>
</script>
EOF
end
# Computes the path to a flash asset in the public swf directory.
# If the +source+ filename has no extension, .swf will be appended.
# Full paths from the document root will be passed through.
#
# flash_path "movie" # => /swf/movie.swf
# flash_path "dir/movie.swf" # => /swf/dir/movie.swf
# flash_path "/dir/movie" # => /dir/movie.swf
def flash_path(source)
compute_public_path(source, 'swf', 'swf', false)
end
end
end
end
end
ActionView::Base.class_eval do
include ActionView::Helpers::FlashObjectHelper
end
ActionView::Helpers::AssetTagHelper.register_javascript_include_default 'flashobject'

View file

@ -0,0 +1,81 @@
v2.11. Rails 1.2.6 tagging generator compatibility; change test suite to use included integration app; include commenting generator (not well tested) [Josh Stephenson].
v2.10. Add :parent_conditions option; bugfix for nullified conditions; bugfix for self-referential tagging generator; allow setting of has_many_polymorphs_options hash in Configuration's after_initialize if you need to adjust the autoload behavior; clear error message on missing or improperly namespaced models; fix .build on double-sided relationships; add :namespace key for easier set up of Camping apps or other unusual class structures.
v2.9. Gem version renumbering; my apologies if this messes anyone up.
v2.8. RDoc documentation; repository relocation; Rakefile cleanup; remove deprecated plugin-specific class caching.
v2.7.5. Various bugfixes; Postgres problems may remain on edge.
v2.7.3. Use new :source and :source_type options in 1.2.3 (David Lemstra); fix pluralization bug; add some tests; experimental tagging generator.
v2.7.2. Deprecate has_many_polymorphs_cache_classes= option because it doesn't really work. Use config.cache_classes= instead to cache all reloadable items.
v2.7.1. Dispatcher.to_prepare didn't fire in the console; now using a config.after_initialize wrapper instead.
v2.7. Dependency injection framework elimates having to care about load order.
v2.6. Make the logger act sane for the gem version.
v2.5.2. Allow :skip_duplicates on double relationships.
v2.5.1. Renamed :ignore_duplicates to :skip_duplicates to better express its non-passive behavior; made sure not to load target set on push unless necessary.
v2.5. Activerecord compatibility branch becomes trunk: extra options now supported for double polymorphism; conditions nulled-out and propogated to child relationships; more tests; new :ignore_duplicates option on macro can be set to false if you want << to push duplicate associations.
v2.4.1. Code split into multiple files; tests added for pluralization check; Rails 1.1.6 no longer supported.
v2.4. Unlimited mixed class association extensions for both single and double targets and joins.
v2.3. Gem version
v2.2. API change; prefix on methods is now singular when using :rename_individual_collections.
v2.1. Add configuration option to cache polymorphic classes in development mode.
v2.0. Collection methods (push, delete, clear) now on individual collections.
v1.9.2. Disjoint collection sides bugfix, don't raise on new records.
v1.9.1. Double classify bugfix.
v1.9. Large changes to properly support double polymorphism.
v1.8.2. Bugfix to make sure the type gets checked on doubly polymorphic parents.
v1.8.1. Bugfix for sqlite3 child attribute retrieval.
v1.8. Bugfix for instantiating attributes of namespaced models.
v1.7.1. Bugfix for double polymorphic relationships.
v1.7. Double polymorphic relationships (includes new API method).
v1.6. Namespaced model support.
v1.5. Bugfix for Postgres and Mysql under 1.1.6; refactored tests (hildofur); properly handles legacy table names set with set_table_name().
v1.4. STI support added (use the child class names, not the base class).
v1.3. Bug regarding table names with underscores in SQL query fixed.
v1.2. License change, again.
v1.1. File_column bug fixed.
v1.0. Tests written; after_find and after_initialize now correctly called.
v0.5. SQL performance enhancements added.
v0.4. Rewrote singletons as full-fledged proxy class so that marshalling works (e.g. in the session).
v0.3. Caching added.
v0.2. Fixed dependency reloading problem in development mode.
v0.1. License change.
v0. Added :dependent support on the join table; no changelog before this version.

View file

@ -0,0 +1,184 @@
Academic Free License (AFL) v. 3.0
This Academic Free License (the "License") applies to any original work
of authorship (the "Original Work") whose owner (the "Licensor") has
placed the following licensing notice adjacent to the copyright notice
for the Original Work:
Licensed under the Academic Free License version 3.0
1) Grant of Copyright License. Licensor grants You a worldwide,
royalty-free, non-exclusive, sublicensable license, for the duration of
the copyright, to do the following:
a) to reproduce the Original Work in copies, either alone or as part of
a collective work;
b) to translate, adapt, alter, transform, modify, or arrange the
Original Work, thereby creating derivative works ("Derivative Works")
based upon the Original Work;
c) to distribute or communicate copies of the Original Work and
Derivative Works to the public, under any license of your choice that
does not contradict the terms and conditions, including Licensor's
reserved rights and remedies, in this Academic Free License;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor grants You a worldwide,
royalty-free, non-exclusive, sublicensable license, under patent claims
owned or controlled by the Licensor that are embodied in the Original
Work as furnished by the Licensor, for the duration of the patents, to
make, use, sell, offer for sale, have made, and import the Original Work
and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the
preferred form of the Original Work for making modifications to it and
all available documentation describing how to modify the Original Work.
Licensor agrees to provide a machine-readable copy of the Source Code of
the Original Work along with each copy of the Original Work that
Licensor distributes. Licensor reserves the right to satisfy this
obligation by placing a machine-readable copy of the Source Code in an
information repository reasonably calculated to permit inexpensive and
convenient access by You for as long as Licensor continues to distribute
the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor the
names of any contributors to the Original Work, nor any of their
trademarks or service marks, may be used to endorse or promote products
derived from this Original Work without express prior permission of the
Licensor. Except as expressly stated herein, nothing in this License
grants any license to Licensor's trademarks, copyrights, patents, trade
secrets or any other intellectual property. No patent license is granted
to make, use, sell, offer for sale, have made, or import embodiments of
any patent claims other than the licensed claims defined in Section 2.
No license is granted to the trademarks of Licensor even if such marks
are included in the Original Work. Nothing in this License shall be
interpreted to prohibit Licensor from licensing under terms different
from this License any Original Work that Licensor otherwise would have a
right to license.
5) External Deployment. The term "External Deployment" means the use,
distribution, or communication of the Original Work or Derivative Works
in any way such that the Original Work or Derivative Works may be used
by anyone other than You, whether those works are distributed or
communicated to those persons or made available as an application
intended for use over a network. As an express condition for the grants
of license hereunder, You must treat any External Deployment by You of
the Original Work or a Derivative Work as a distribution under section
1(c).
6) Attribution Rights. You must retain, in the Source Code of any
Derivative Works that You create, all copyright, patent, or trademark
notices from the Source Code of the Original Work, as well as any
notices of licensing and any descriptive text identified therein as an
"Attribution Notice." You must cause the Source Code for any Derivative
Works that You create to carry a prominent Attribution Notice reasonably
calculated to inform recipients that You have modified the Original
Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants
that the copyright in and to the Original Work and the patent rights
granted herein by Licensor are owned by the Licensor or are sublicensed
to You under the terms of this License with the permission of the
contributor(s) of those copyrights and patent rights. Except as
expressly stated in the immediately preceding sentence, the Original
Work is provided under this License on an "AS IS" BASIS and WITHOUT
WARRANTY, either express or implied, including, without limitation, the
warranties of non-infringement, merchantability or fitness for a
particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL
WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential
part of this License. No license to the Original Work is granted by this
License except under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal
theory, whether in tort (including negligence), contract, or otherwise,
shall the Licensor be liable to anyone for any indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or the use of the Original Work including,
without limitation, damages for loss of goodwill, work stoppage,
computer failure or malfunction, or any and all other commercial damages
or losses. This limitation of liability shall not apply to the extent
applicable law prohibits such limitation.
9) Acceptance and Termination. If, at any time, You expressly assented
to this License, that assent indicates your clear and irrevocable
acceptance of this License and all of its terms and conditions. If You
distribute or communicate copies of the Original Work or a Derivative
Work, You must make a reasonable effort under the circumstances to
obtain the express assent of recipients to the terms of this License.
This License conditions your rights to undertake the activities listed
in Section 1, including your right to create Derivative Works based upon
the Original Work, and doing so without honoring these terms and
conditions is prohibited by copyright law and international treaty.
Nothing in this License is intended to affect copyright exceptions and
limitations (including "fair use" or "fair dealing"). This License shall
terminate immediately and You may no longer exercise any of the rights
granted to You by this License upon your failure to honor the conditions
in Section 1(c).
10) Termination for Patent Action. This License shall terminate
automatically and You may no longer exercise any of the rights granted
to You by this License as of the date You commence an action, including
a cross-claim or counterclaim, against Licensor or any licensee alleging
that the Original Work infringes a patent. This termination provision
shall not apply for an action alleging patent infringement by
combinations of the Original Work with other software or hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating
to this License may be brought only in the courts of a jurisdiction
wherein the Licensor resides or in which Licensor conducts its primary
business, and under the laws of that jurisdiction excluding its
conflict-of-law provisions. The application of the United Nations
Convention on Contracts for the International Sale of Goods is expressly
excluded. Any use of the Original Work outside the scope of this License
or after its termination shall be subject to the requirements and
penalties of copyright or patent law in the appropriate jurisdiction.
This section shall survive the termination of this License.
12) Attorneys' Fees. In any action to enforce the terms of this License
or seeking damages relating thereto, the prevailing party shall be
entitled to recover its costs and expenses, including, without
limitation, reasonable attorneys' fees and costs incurred in connection
with such action, including any appeal of such action. This section
shall survive the termination of this License.
13) Miscellaneous. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable.
14) Definition of "You" in This License. "You" throughout this License,
whether in upper or lower case, means an individual or a legal entity
exercising rights under, and complying with all of the terms of, this
License. For legal entities, "You" includes any entity that controls, is
controlled by, or is under common control with you. For purposes of this
definition, "control" means (i) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
15) Right to Use. You may use the Original Work in all ways not
otherwise restricted or conditioned by this License or by law, and
Licensor promises not to interfere with or be responsible for such uses
by You.
16) Modification of This License. This License is Copyright (c) 2005
Lawrence Rosen. Permission is granted to copy, distribute, or
communicate this License without modification. Nothing in this License
permits You to modify this License as applied to the Original Work or to
Derivative Works. However, You may modify the text of this License and
copy, distribute or communicate your modified version (the "Modified
License") and apply it to other original works of authorship subject to
the following conditions: (i) You may not indicate in any way that your
Modified License is the "Academic Free License" or "AFL" and you may not
use those names in the name of your Modified License; (ii) You must
replace the notice specified in the first paragraph above with the
notice "Licensed under <insert your license name here>" or with a notice
of your own that is not confusingly similar to the notice in this
License; and (iii) You may not claim that your original works are open
source software unless your Modified License has been approved by Open
Source Initiative (OSI) and You comply with its license review and
certification process.

View file

@ -0,0 +1,192 @@
CHANGELOG
examples/hmph.rb
generators/commenting/commenting_generator.rb
generators/commenting/templates/comment.rb
generators/commenting/templates/comment_test.rb
generators/commenting/templates/commenting.rb
generators/commenting/templates/commenting_extensions.rb
generators/commenting/templates/commenting_test.rb
generators/commenting/templates/commentings.yml
generators/commenting/templates/comments.yml
generators/commenting/templates/migration.rb
generators/tagging/tagging_generator.rb
generators/tagging/templates/migration.rb
generators/tagging/templates/tag.rb
generators/tagging/templates/tag_test.rb
generators/tagging/templates/tagging.rb
generators/tagging/templates/tagging_extensions.rb
generators/tagging/templates/tagging_test.rb
generators/tagging/templates/taggings.yml
generators/tagging/templates/tags.yml
init.rb
lib/has_many_polymorphs/association.rb
lib/has_many_polymorphs/autoload.rb
lib/has_many_polymorphs/base.rb
lib/has_many_polymorphs/class_methods.rb
lib/has_many_polymorphs/configuration.rb
lib/has_many_polymorphs/debugging_tools.rb
lib/has_many_polymorphs/dependencies.rb
lib/has_many_polymorphs/rake_task_redefine_task.rb
lib/has_many_polymorphs/reflection.rb
lib/has_many_polymorphs/support_methods.rb
lib/has_many_polymorphs.rb
LICENSE
Manifest
README
test/fixtures/bow_wows.yml
test/fixtures/cats.yml
test/fixtures/eaters_foodstuffs.yml
test/fixtures/fish.yml
test/fixtures/frogs.yml
test/fixtures/keep_your_enemies_close.yml
test/fixtures/little_whale_pupils.yml
test/fixtures/people.yml
test/fixtures/petfoods.yml
test/fixtures/whales.yml
test/fixtures/wild_boars.yml
test/integration/app/app/controllers/addresses_controller.rb
test/integration/app/app/controllers/application.rb
test/integration/app/app/controllers/sellers_controller.rb
test/integration/app/app/controllers/states_controller.rb
test/integration/app/app/controllers/users_controller.rb
test/integration/app/app/helpers/addresses_helper.rb
test/integration/app/app/helpers/application_helper.rb
test/integration/app/app/helpers/sellers_helper.rb
test/integration/app/app/helpers/states_helper.rb
test/integration/app/app/helpers/users_helper.rb
test/integration/app/app/models/address.rb
test/integration/app/app/models/citation.rb
test/integration/app/app/models/citations_item.rb
test/integration/app/app/models/seller.rb
test/integration/app/app/models/state.rb
test/integration/app/app/models/user.rb
test/integration/app/app/views/addresses/edit.html.erb
test/integration/app/app/views/addresses/index.html.erb
test/integration/app/app/views/addresses/new.html.erb
test/integration/app/app/views/addresses/show.html.erb
test/integration/app/app/views/layouts/addresses.html.erb
test/integration/app/app/views/layouts/sellers.html.erb
test/integration/app/app/views/layouts/states.html.erb
test/integration/app/app/views/layouts/users.html.erb
test/integration/app/app/views/sellers/edit.html.erb
test/integration/app/app/views/sellers/index.html.erb
test/integration/app/app/views/sellers/new.html.erb
test/integration/app/app/views/sellers/show.html.erb
test/integration/app/app/views/states/edit.html.erb
test/integration/app/app/views/states/index.html.erb
test/integration/app/app/views/states/new.html.erb
test/integration/app/app/views/states/show.html.erb
test/integration/app/app/views/users/edit.html.erb
test/integration/app/app/views/users/index.html.erb
test/integration/app/app/views/users/new.html.erb
test/integration/app/app/views/users/show.html.erb
test/integration/app/config/boot.rb
test/integration/app/config/database.yml
test/integration/app/config/environment.rb
test/integration/app/config/environments/development.rb
test/integration/app/config/environments/production.rb
test/integration/app/config/environments/test.rb
test/integration/app/config/locomotive.yml
test/integration/app/config/routes.rb
test/integration/app/config/ultrasphinx/default.base
test/integration/app/config/ultrasphinx/development.conf.canonical
test/integration/app/db/migrate/001_create_users.rb
test/integration/app/db/migrate/002_create_sellers.rb
test/integration/app/db/migrate/003_create_addresses.rb
test/integration/app/db/migrate/004_create_states.rb
test/integration/app/db/migrate/005_add_capitalization_to_seller.rb
test/integration/app/db/migrate/006_add_deleted_to_user.rb
test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb
test/integration/app/db/migrate/008_create_citations.rb
test/integration/app/db/migrate/009_create_citations_items.rb
test/integration/app/db/schema.rb
test/integration/app/doc/README_FOR_APP
test/integration/app/generated_models/aquatic_fish.rb
test/integration/app/generated_models/aquatic_pupils_whale.rb
test/integration/app/generated_models/aquatic_whale.rb
test/integration/app/generated_models/beautiful_fight_relationship.rb
test/integration/app/generated_models/citation.rb
test/integration/app/generated_models/citations_item.rb
test/integration/app/generated_models/dog.rb
test/integration/app/generated_models/eaters_foodstuff.rb
test/integration/app/generated_models/frog.rb
test/integration/app/generated_models/kitten.rb
test/integration/app/generated_models/parentship.rb
test/integration/app/generated_models/person.rb
test/integration/app/generated_models/petfood.rb
test/integration/app/generated_models/polymorph_test_some_model.rb
test/integration/app/generated_models/seller.rb
test/integration/app/generated_models/tabby.rb
test/integration/app/generated_models/user.rb
test/integration/app/generated_models/wild_boar.rb
test/integration/app/generators/commenting_generator_test.rb
test/integration/app/public/404.html
test/integration/app/public/500.html
test/integration/app/public/dispatch.cgi
test/integration/app/public/dispatch.fcgi
test/integration/app/public/dispatch.rb
test/integration/app/public/favicon.ico
test/integration/app/public/images/rails.png
test/integration/app/public/index.html
test/integration/app/public/javascripts/application.js
test/integration/app/public/javascripts/controls.js
test/integration/app/public/javascripts/dragdrop.js
test/integration/app/public/javascripts/effects.js
test/integration/app/public/javascripts/prototype.js
test/integration/app/public/robots.txt
test/integration/app/public/stylesheets/scaffold.css
test/integration/app/Rakefile
test/integration/app/README
test/integration/app/script/about
test/integration/app/script/breakpointer
test/integration/app/script/console
test/integration/app/script/destroy
test/integration/app/script/generate
test/integration/app/script/performance/benchmarker
test/integration/app/script/performance/profiler
test/integration/app/script/plugin
test/integration/app/script/process/inspector
test/integration/app/script/process/reaper
test/integration/app/script/process/spawner
test/integration/app/script/runner
test/integration/app/script/server
test/integration/app/test/fixtures/addresses.yml
test/integration/app/test/fixtures/citations.yml
test/integration/app/test/fixtures/citations_items.yml
test/integration/app/test/fixtures/sellers.yml
test/integration/app/test/fixtures/states.yml
test/integration/app/test/fixtures/users.yml
test/integration/app/test/functional/addresses_controller_test.rb
test/integration/app/test/functional/sellers_controller_test.rb
test/integration/app/test/functional/states_controller_test.rb
test/integration/app/test/functional/users_controller_test.rb
test/integration/app/test/test_helper.rb
test/integration/app/test/unit/address_test.rb
test/integration/app/test/unit/citation_test.rb
test/integration/app/test/unit/citations_item_test.rb
test/integration/app/test/unit/seller_test.rb
test/integration/app/test/unit/state_test.rb
test/integration/app/test/unit/user_test.rb
test/models/aquatic/fish.rb
test/models/aquatic/pupils_whale.rb
test/models/aquatic/whale.rb
test/models/beautiful_fight_relationship.rb
test/models/canine.rb
test/models/cat.rb
test/models/dog.rb
test/models/eaters_foodstuff.rb
test/models/frog.rb
test/models/kitten.rb
test/models/parentship.rb
test/models/person.rb
test/models/petfood.rb
test/models/tabby.rb
test/models/wild_boar.rb
test/modules/extension_module.rb
test/modules/other_extension_module.rb
test/schema.rb
test/setup.rb
test/test_all.rb
test/test_helper.rb
test/unit/polymorph_test.rb
TODO

View file

@ -0,0 +1,174 @@
Has_many_polymorphs
An ActiveRecord plugin for self-referential and double-sided polymorphic associations.
== License
Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file.
The public certificate for this gem is at http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem.
== Description
This plugin lets you define self-referential and double-sided polymorphic associations in your models. It is an extension of <tt>has_many :through</tt>.
“Polymorphic” means an association can freely point to any of several unrelated model classes, instead of being tied to one particular class.
== Features
* self-references
* double-sided polymorphism
* efficient database usage
* STI support
* namespace support
* automatic individual and reverse associations
The plugin also includes a generator for a tagging system, a common use case (see below).
== Requirements
* Rails 1.2.3 or greater
= Usage
== Installation
To install the Rails plugin, run:
script/plugin install svn://rubyforge.org/var/svn/fauna/has_many_polymorphs/trunk
There's also a gem version. To install it instead, run:
sudo gem install has_many_polymorphs
If you are using the gem, make sure to add <tt>require 'has_many_polymorphs'</tt> to <tt>environment.rb</tt>, before Rails::Initializer block.
== Configuration
Setup the parent model as so:
class Kennel < ActiveRecord::Base
has_many_polymorphs :guests, :from => [:dogs, :cats, :birds]
end
The join model:
class GuestsKennel < ActiveRecord::Base
belongs_to :kennel
belongs_to :guest, :polymorphic => true
end
One of the child models:
class Dog < ActiveRecord::Base
# nothing
end
See ActiveRecord::Associations::PolymorphicClassMethods for more configuration options.
== Helper methods example
>> k = Kennel.find(1)
#<Kennel id: 1, name: "Happy Paws">
>> k.guests.map(&:class)
[Dog, Cat, Cat, Bird]
>> k.guests.push(Cat.create); k.cats.size
3
>> k.guests << Cat.create; k.cats.size
4
>> k.guests.size
6
>> d = k.dogs.first
#<Dog id: 3, name: "Rover">
>> d.kennels
[#<Kennel id: 1, name: "Happy Paws">]
>> k.guests.delete(d); k.dogs.size
0
>> k.guests.size
5
Note that the parent method is always plural, even if there is only one parent (<tt>Dog#kennels</tt>, not <tt>Dog#kennel</tt>).
See ActiveRecord::Associations::PolymorphicAssociation for more helper method details.
= Extras
== Double-sided polymorphism
Double-sided relationships are defined on the join model:
class Devouring < ActiveRecord::Base
belongs_to :guest, :polymorphic => true
belongs_to :eaten, :polymorphic => true
acts_as_double_polymorphic_join(
:guests =>[:dogs, :cats],
:eatens => [:cats, :birds]
)
end
Now, dogs and cats can eat birds and cats. Birds can't eat anything (they aren't <tt>guests</tt>) and dogs can't be eaten by anything (since they aren't <tt>eatens</tt>). The keys stand for what the models are, not what they do.
In this case, each guest/eaten relationship is called a Devouring.
See ActiveRecord::Associations::PolymorphicClassMethods for more.
== Tagging generator
Has_many_polymorphs includes a tagging system generator. Run:
script/generate tagging Dog Cat [...MoreModels...]
This adds a migration and new Tag and Tagging models in <tt>app/models</tt>. It configures Tag with an appropriate <tt>has_many_polymorphs</tt> call against the models you list at the command line. It also adds the file <tt>lib/tagging_extensions.rb</tt> and <tt>requires</tt> it in <tt>environment.rb</tt>.
Tests will also be generated.
Once you've run the generator, you can tag records as follows:
>> d = Dog.create(:name => "Rover")
#<Dog id: 3, name: "Rover">
>> d.tag_list
""
>> d.tag_with "fierce loud"
#<Dog id: 3, name: "Rover">
>> d.tag_list
"fierce loud"
>> c = Cat.create(:name => "Chloe")
#<Cat id: 1, name: "Chloe">
>> c.tag_with "fierce cute"
#<Cat id: 1, name: "Chloe">
>> c.tag_list
"cute fierce"
>> Tag.find_by_name("fierce").taggables
[#<Cat id: 1, name: "Chloe">, #<Dog id: 3, name: "Rover">]
The generator accepts the optional flag <tt>--skip-migration</tt> to skip generating a migration (for example, if you are converting from <tt>acts_as_taggable</tt>). It also accepts the flag <tt>--self-referential</tt> if you want to be able to tag tags.
See ActiveRecord::Base::TaggingExtensions, Tag, and Tagging for more.
== Troubleshooting
Some debugging tools are available in <tt>lib/has_many_polymorphs/debugging_tools.rb</tt>.
If you are having trouble, think very carefully about how your model classes, key columns, and table names relate. You may have to explicitly specify options on your join model such as <tt>:class_name</tt>, <tt>:foreign_key</tt>, or <tt>:as</tt>. The included tests are a good place to look for examples.
Note that because of the way Rails reloads model classes, the plugin can sometimes bog down your development server. Set <tt>config.cache_classes = true</tt> in <tt>config/environments/development.rb</tt> to avoid this.
== Reporting problems
* http://rubyforge.org/forum/forum.php?forum_id=16450
Patches and contributions are very welcome. Please note that contributors are required to assign copyright for their additions to Cloudburst, LLC.
== Further resources
* http://blog.evanweaver.com/articles/2007/08/15/polymorphs-tutorial
* http://blog.evanweaver.com/articles/2007/02/22/polymorphs-25-total-insanity-branch
* http://blog.evanweaver.com/articles/2007/02/09/how-to-find-the-most-popular-tags
* http://blog.evanweaver.com/articles/2007/01/13/growing-up-your-acts_as_taggable
* http://blog.evanweaver.com/articles/2006/12/02/polymorphs-19
* http://blog.evanweaver.com/articles/2006/11/05/directed-double-polymorphic-associations
* http://blog.evanweaver.com/articles/2006/11/04/namespaced-model-support-in-has_many_polymorphs
* http://blog.evanweaver.com/articles/2006/09/26/sti-support-in-has_many_polymorphs
* http://blog.evanweaver.com/articles/2006/09/11/make-polymorphic-children-belong-to-only-one-parent

View file

@ -0,0 +1,21 @@
require 'rubygems'
gem 'echoe', '>=2.2'
require 'echoe'
require 'lib/has_many_polymorphs/rake_task_redefine_task'
Echoe.new("has_many_polymorphs") do |p|
p.project = "fauna"
p.summary = "An ActiveRecord plugin for self-referential and double-sided polymorphic associations."
p.url = "http://blog.evanweaver.com/files/doc/fauna/has_many_polymorphs/"
p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
p.dependencies = ["activerecord"]
p.rdoc_pattern = /polymorphs\/association|polymorphs\/class_methods|polymorphs\/reflection|polymorphs\/autoload|polymorphs\/configuration|README|CHANGELOG|TODO|LICENSE|templates\/migration\.rb|templates\/tag\.rb|templates\/tagging\.rb|templates\/tagging_extensions\.rb/
p.require_signed = true
end
desc 'Run the test suite.'
Rake::Task.redefine_task("test") do
puts "Warning! Tests must be run with the plugin installed in a functioning Rails\nenvironment."
system "ruby -Ibin:lib:test test/unit/polymorph_test.rb #{ENV['METHOD'] ? "--name=#{ENV['METHOD']}" : ""}"
end

View file

@ -0,0 +1,5 @@
* Does :namespace key definitely work with doubles?
* Migration examples in docs
* Controller for tagging generator
* Tag cloud method

View file

@ -0,0 +1,69 @@
require 'camping'
require 'has_many_polymorphs'
Camping.goes :Hmph
module Hmph::Models
class GuestsKennel < Base
belongs_to :kennel
belongs_to :guest, :polymorphic => true
end
class Dog < Base
end
class Cat < Base
end
class Bird < Base
end
class Kennel < Base
has_many_polymorphs :guests,
:from => [:dogs, :cats, :birds],
:through => :guests_kennels,
:namespace => :"hmph/models/"
end
class InitialSchema < V 1.0
def self.up
create_table :hmph_kennels do |t|
t.column :created_at, :datetime
t.column :modified_at, :datetime
t.column :name, :string, :default => 'Anonymous Kennel'
end
create_table :hmph_guests_kennels do |t|
t.column :guest_id, :integer
t.column :guest_type, :string
t.column :kennel_id, :integer
end
create_table :hmph_dogs do |t|
t.column :name, :string, :default => 'Fido'
end
create_table :hmph_cats do |t|
t.column :name, :string, :default => 'Morris'
end
create_table :hmph_birds do |t|
t.column :name, :string, :default => 'Polly'
end
end
def self.down
drop_table :hmph_kennels
drop_table :hmph_guests_kennels
drop_table :hmph_dogs
drop_table :hmph_cats
drop_table :hmph_birds
end
end
end
module Hmph::Controllers
end
module Hmph::Views
end

View file

@ -0,0 +1,94 @@
class CommentingGenerator < Rails::Generator::NamedBase
default_options :skip_migration => false
default_options :self_referential => false
attr_reader :parent_association_name
attr_reader :commentable_models
def initialize(runtime_args, runtime_options = {})
@parent_association_name = (runtime_args.include?("--self-referential") ? "commenter" : "comment")
@commentable_models = runtime_args.reject{|opt| opt =~ /^--/}.map do |commentable|
":" + commentable.underscore.pluralize
end
@commentable_models += [":comments"] if runtime_args.include?("--self-referential")
@commentable_models.uniq!
verify @commentable_models
hacks
runtime_args.unshift("placeholder")
super
end
def verify models
puts "** Warning: only one commentable model specified; tests may not run properly." if models.size < 2
models.each do |model|
model = model[1..-1].classify
next if model == "Comment" # don't load ourselves when --self-referential is used
self.class.const_get(model) rescue puts "** Error: model #{model[1..-1].classify} could not be loaded." or exit
end
end
def hacks
# add the extension require in environment.rb
phrase = "require 'commenting_extensions'"
filename = "#{RAILS_ROOT}/config/environment.rb"
unless (open(filename) do |file|
file.grep(/#{Regexp.escape phrase}/).any?
end)
open(filename, 'a+') do |file|
file.puts "\n" + phrase + "\n"
end
end
end
def manifest
record do |m|
m.class_collisions class_path, class_name, "#{class_name}Test"
m.directory File.join('app/models', class_path)
m.directory File.join('test/unit', class_path)
m.directory File.join('test/fixtures', class_path)
m.directory File.join('test/fixtures', class_path)
m.directory File.join('lib')
m.template 'comment.rb', File.join('app/models', class_path, "comment.rb")
m.template 'comment_test.rb', File.join('test/unit', class_path, "comment_test.rb")
m.template 'comments.yml', File.join('test/fixtures', class_path, "comments.yml")
m.template 'commenting.rb', File.join('app/models', class_path, "commenting.rb")
m.template 'commenting_test.rb', File.join('test/unit', class_path, "commenting_test.rb")
m.template 'commentings.yml', File.join('test/fixtures', class_path, "commentings.yml")
m.template 'commenting_extensions.rb', File.join('lib', 'commenting_extensions.rb')
unless options[:skip_migration]
m.migration_template 'migration.rb', 'db/migrate',
:migration_file_name => "create_comments_and_commentings"
end
end
end
protected
def banner
"Usage: #{$0} generate commenting [CommentableModelA CommentableModelB ...]"
end
def add_options!(opt)
opt.separator ''
opt.separator 'Options:'
opt.on("--skip-migration",
"Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
opt.on("--self-referential",
"Allow comments to comment themselves.") { |v| options[:self_referential] = v }
end
# Useful for generating tests/fixtures
def model_one
commentable_models[0][1..-1].classify
end
def model_two
commentable_models[1][1..-1].classify rescue model_one
end
end

View file

@ -0,0 +1,33 @@
# The Comment model. This model is automatically generated and added to your app if you run the commenting generator.
class Comment < ActiveRecord::Base
# If database speed becomes an issue, you could remove these validations and rescue the ActiveRecord database constraint errors instead.
validates_presence_of :name, :email, :body
validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
after_validation :prepend_url
# Set up the polymorphic relationship.
has_many_polymorphs :commentables,
:from => [<%= commentable_models.join(", ") %>],
:through => :commentings,
:dependent => :destroy,
<% if options[:self_referential] -%> :as => :<%= parent_association_name -%>,
<% end -%>
:parent_extend => proc {
}
# Tag::Error class. Raised by ActiveRecord::Base::TaggingExtensions if something goes wrong.
class Error < StandardError
end
protected
def prepend_url
return if self[:url].blank?
if self[:url] !~ /^http(s):\/\//i
self.url = 'http://' + self[:url]
end
end
end

View file

@ -0,0 +1,12 @@
require File.dirname(__FILE__) + '/../test_helper'
class CommentTest < Test::Unit::TestCase
fixtures :comments, :commentings, <%= commentable_models[0..1].join(", ") -%>
def test_to_s
assert_equal "no1@nowhere.com", <%= model_two -%>.find(2).comments.first.email
assert_equal "http://letrails.cn", <%= model_two -%>.find(2).comments.last.url
assert_equal "http://fr.ivolo.us", <%= model_two -%>.find(2).comments.first.url
end
end

View file

@ -0,0 +1,13 @@
# The Commenting join model. This model is automatically generated and added to your app if you run the commenting generator.
class Commenting < ActiveRecord::Base
belongs_to :<%= parent_association_name -%><%= ", :foreign_key => \"#{parent_association_name}_id\", :class_name => \"Comment\"" if options[:self_referential] %>
belongs_to :commentable, :polymorphic => true
# This callback makes sure that an orphaned <tt>Comment</tt> is deleted if it no longer tags anything.
def before_destroy
<%= parent_association_name -%>.destroy_without_callbacks if <%= parent_association_name -%> and <%= parent_association_name -%>.commentings.count == 1
end
end

View file

@ -0,0 +1,30 @@
class ActiveRecord::Base
module CommentingExtensions
def comment_count
commentable?
self.comments.size
end
def comment_with(attributes)
commentable?(true)
begin
comment = Comment.create(attributes)
raise Comment::Error, "Comment could not be saved with" if comment.new_record?
comment.commentables << self
rescue
end
end
private
def commentable?(should_raise = false) #:nodoc:
unless flag = respond_to?(:<%= parent_association_name -%>s)
raise "#{self.class} is not a commentable model" if should_raise
end
flag
end
end
include CommentingExtensions
end

View file

@ -0,0 +1,30 @@
require File.dirname(__FILE__) + '/../test_helper'
class CommentingTest < Test::Unit::TestCase
fixtures :commentings, :comments, <%= commentable_models[0..1].join(", ") -%>
def setup
@obj1 = <%= model_two %>.find(1)
@obj2 = <%= model_two %>.find(2)
<% if commentable_models.size > 1 -%>
@obj3 = <%= model_one -%>.find(1)
<% end -%>
@comment1 = Comment.find(1)
@comment2 = Comment.find(2)
@commenting1 = Commenting.find(1)
end
def test_commentable
assert_raises(RuntimeError) do
@commenting1.send(:commentable?, true)
end
assert !@commenting1.send(:commentable?)
<% if commentable_models.size > 1 -%>
assert @obj3.send(:commentable?)
<% end -%>
<% if options[:self_referential] -%>
assert @comment1.send(:commentable?)
<% end -%>
end
end

View file

@ -0,0 +1,23 @@
---
<% if commentable_models.size > 1 -%>
commentings_003:
<%= parent_association_name -%>_id: "2"
id: "3"
commentable_type: <%= model_one %>
commentable_id: "1"
<% end -%>
commentings_004:
<%= parent_association_name -%>_id: "2"
id: "4"
commentable_type: <%= model_two %>
commentable_id: "2"
commentings_001:
<%= parent_association_name -%>_id: "1"
id: "1"
commentable_type: <%= model_two %>
commentable_id: "1"
commentings_002:
<%= parent_association_name -%>_id: "1"
id: "2"
commentable_type: <%= model_two %>
commentable_id: "2"

View file

@ -0,0 +1,13 @@
---
comments_001:
id: "1"
name: frivolous
email: no1@nowhere.com
url: http://fr.ivolo.us
body: this plugin rocks!
tags_002:
id: "2"
name: yuanyiz
email: no1@nowhere.com
url: http://letrails.cn
body: this plugin has saved my life

View file

@ -0,0 +1,28 @@
# A migration to add tables for Comment and Commenting. This file is automatically generated and added to your app if you run the commenting generator.
class CreateCommentsAndCommentings < ActiveRecord::Migration
# Add the new tables.
def self.up
create_table :comments do |t|
t.column :name, :string, :null => false
t.column :url, :string
t.column :email, :string
t.column :body, :text
end
create_table :commentings do |t|
t.column :<%= parent_association_name -%>_id, :integer, :null => false
t.column :commentable_id, :integer, :null => false
t.column :commentable_type, :string, :null => false
end
end
# Remove the tables.
def self.down
drop_table :comments
drop_table :commentings
end
end

View file

@ -0,0 +1,95 @@
require 'ruby-debug' and Debugger.start if ENV['USER'] == 'eweaver'
class TaggingGenerator < Rails::Generator::NamedBase
default_options :skip_migration => false
default_options :self_referential => false
attr_reader :parent_association_name
attr_reader :taggable_models
def initialize(runtime_args, runtime_options = {})
@parent_association_name = (runtime_args.include?("--self-referential") ? "tagger" : "tag")
@taggable_models = runtime_args.reject{|opt| opt =~ /^--/}.map do |taggable|
":" + taggable.underscore.pluralize
end
@taggable_models += [":tags"] if runtime_args.include?("--self-referential")
@taggable_models.uniq!
verify @taggable_models
hacks
runtime_args.unshift("placeholder")
super
end
def verify models
puts "** Warning: only one taggable model specified; tests may not run properly." if models.size < 2
models.each do |model|
model = model[1..-1].classify
next if model == "Tag" # don't load ourselves when --self-referential is used
self.class.const_get(model) rescue puts "** Error: model #{model[1..-1].classify} could not be loaded." or exit
end
end
def hacks
# add the extension require in environment.rb
phrase = "require 'tagging_extensions'"
filename = "#{RAILS_ROOT}/config/environment.rb"
unless (open(filename) do |file|
file.grep(/#{Regexp.escape phrase}/).any?
end)
open(filename, 'a+') do |file|
file.puts "\n" + phrase + "\n"
end
end
end
def manifest
record do |m|
m.class_collisions class_path, class_name, "#{class_name}Test"
m.directory File.join('app/models', class_path)
m.directory File.join('test/unit', class_path)
m.directory File.join('test/fixtures', class_path)
m.directory File.join('test/fixtures', class_path)
m.directory File.join('lib')
m.template 'tag.rb', File.join('app/models', class_path, "tag.rb")
m.template 'tag_test.rb', File.join('test/unit', class_path, "tag_test.rb")
m.template 'tags.yml', File.join('test/fixtures', class_path, "tags.yml")
m.template 'tagging.rb', File.join('app/models', class_path, "tagging.rb")
m.template 'tagging_test.rb', File.join('test/unit', class_path, "tagging_test.rb")
m.template 'taggings.yml', File.join('test/fixtures', class_path, "taggings.yml")
m.template 'tagging_extensions.rb', File.join('lib', 'tagging_extensions.rb')
unless options[:skip_migration]
m.migration_template 'migration.rb', 'db/migrate',
:migration_file_name => "create_tags_and_taggings"
end
end
end
protected
def banner
"Usage: #{$0} generate tagging [TaggableModelA TaggableModelB ...]"
end
def add_options!(opt)
opt.separator ''
opt.separator 'Options:'
opt.on("--skip-migration",
"Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
opt.on("--self-referential",
"Allow tags to tag themselves.") { |v| options[:self_referential] = v }
end
# Useful for generating tests/fixtures
def model_one
taggable_models[0][1..-1].classify
end
def model_two
taggable_models[1][1..-1].classify rescue model_one
end
end

View file

@ -0,0 +1,28 @@
# A migration to add tables for Tag and Tagging. This file is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs.
class CreateTagsAndTaggings < ActiveRecord::Migration
# Add the new tables.
def self.up
create_table :tags do |t|
t.column :name, :string, :null => false
end
add_index :tags, :name, :unique => true
create_table :taggings do |t|
t.column :<%= parent_association_name -%>_id, :integer, :null => false
t.column :taggable_id, :integer, :null => false
t.column :taggable_type, :string, :null => false
# t.column :position, :integer # Uncomment this if you need to use <tt>acts_as_list</tt>.
end
add_index :taggings, [:<%= parent_association_name -%>_id, :taggable_id, :taggable_type], :unique => true
end
# Remove the tables.
def self.down
drop_table :tags
drop_table :taggings
end
end

View file

@ -0,0 +1,39 @@
# The Tag model. This model is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs.
class Tag < ActiveRecord::Base
DELIMITER = " " # Controls how to split and join tagnames from strings. You may need to change the <tt>validates_format_of parameters</tt> if you change this.
# If database speed becomes an issue, you could remove these validations and rescue the ActiveRecord database constraint errors instead.
validates_presence_of :name
validates_uniqueness_of :name, :case_sensitive => false
# Change this validation if you need more complex tag names.
validates_format_of :name, :with => /^[a-zA-Z0-9\_\-]+$/, :message => "can not contain special characters"
# Set up the polymorphic relationship.
has_many_polymorphs :taggables,
:from => [<%= taggable_models.join(", ") %>],
:through => :taggings,
:dependent => :destroy,
<% if options[:self_referential] -%> :as => :<%= parent_association_name -%>,
<% end -%>
:skip_duplicates => false,
:parent_extend => proc {
# Defined on the taggable models, not on Tag itself. Return the tagnames associated with this record as a string.
def to_s
self.map(&:name).sort.join(Tag::DELIMITER)
end
}
# Callback to strip extra spaces from the tagname before saving it. If you allow tags to be renamed later, you might want to use the <tt>before_save</tt> callback instead.
def before_create
self.name = name.downcase.strip.squeeze(" ")
end
# Tag::Error class. Raised by ActiveRecord::Base::TaggingExtensions if something goes wrong.
class Error < StandardError
end
end

View file

@ -0,0 +1,10 @@
require File.dirname(__FILE__) + '/../test_helper'
class TagTest < Test::Unit::TestCase
fixtures :tags, :taggings, <%= taggable_models[0..1].join(", ") -%>
def test_to_s
assert_equal "delicious sexy", <%= model_two -%>.find(2).tags.to_s
end
end

Some files were not shown because too many files have changed in this diff Show more