)
+<% end -%>
+
+h3. Ordering
+
+There are times when you want to specify the order of messages sent to a mock.
+It shouldn't be the case very often, but it can be handy at times.
+
+Labeling expectations as being ordered is done by the ordered
call:
+
+<% coderay do -%>
+my_mock.should_receive(:flip).once.ordered
+my_mock.should_receive(:flop).once.ordered
+<% end -%>
+
+If the send of flop
is seen before flip
the specification will fail.
+
+Of course, chains of ordered expectations can be set up:
+
+<% coderay do -%>
+my_mock.should_receive(:one).ordered
+my_mock.should_receive(:two).ordered
+my_mock.should_receive(:three).ordered
+<% end -%>
+
+The expected order is the order in which the expectations are declared.
+
+Order-independent expectations can be set anywhere in the expectation sequence, in any order.
+Only the order of expectations tagged with the ordered
call is significant.
+Likewise, calls to order-independent methods can be made in any order, even interspersed with
+calls to order-dependent methods. For example:
+
+<% coderay do -%>
+my_mock.should_receive(:zero)
+my_mock.should_receive(:one).ordered
+my_mock.should_receive(:two).ordered
+my_mock.should_receive(:one_and_a_half)
+
+# This will pass:
+my_mock.one
+my_mock.one_and_a_half
+my_mock.zero
+my_mock.two
+<% end -%>
+
+h3. Arbitrary Handling of Received Messages
+
+You can supply a block to a message expectation. When the message is received
+by the mock, the block is passed any arguments and evaluated. The result is
+the return value of the block. For example:
+
+<% coderay do -%>
+my_mock.should_receive(:msg) { |a, b|
+ a.should be_true
+ b.should_not include('mice')
+ "Chunky bacon!"
+}
+<% end -%>
+
+This allows arbitrary argument validation and result computation. It's handy and kind of cool to be able to
+do this, but it is advised to not use this form in most situations. Mocks should not be functional.
+They should be completely declarative. That said, it's sometimes useful to give them some minimal behaviour.
diff --git a/website/src/documentation/mocks/other_frameworks.page b/website/src/documentation/mocks/other_frameworks.page
new file mode 100644
index 00000000..d0e01dfa
--- /dev/null
+++ b/website/src/documentation/mocks/other_frameworks.page
@@ -0,0 +1,61 @@
+---
+title: Other Mock/Stub Frameworks
+order: 4
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+If you prefer to use a mocking framework other than RSpec's built in framework, you
+can do this quite simply. RSpec currently supports use of the following frameworks
+out of the box (in alphabetical order):
+
+* "flexmock":http://flexmock.rubyforge.org/
+* "mocha":http://mocha.rubyforge.org
+* "rr":http://rubyforge.org/projects/pivotalrb/
+
+RSpec supports use of a single framework per project, because these frameworks (including)
+RSpec's own, tend to add methods to Object that might or might not work well together. To
+choose a mock framework other than rspec, simply add the following to spec/spec_helper.rb
+(or any file that gets loaded when you run your examples):
+
+<% coderay do -%>
+Spec::Runner.configure do |config|
+ config.mock_with :rr
+end
+<% end -%>
+
+Valid options are :flexmock
, :mocha
, :rspec
(of course),
+and :rr
.
+
+h2. Even more "other" frameworks
+
+If you have a different framework that is not supported directly by RSpec, you can easily
+choose that framework instead by creating an adapter and telling RSpec where to find it.
+Here is RSpec's own adapter as an example:
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/plugins/mock_frameworks/rspec.rb" %><% end -%>
+
+This file must require any libraries or resources that implement the framework, and then define
+a Spec::Plugins::MockFramework
module with the following methods:
+
+* setup_mocks_for_rspec
is called before each example is run.
+* verify_mocks_for_rspec
is called after each example is run. Use this if you want
+RSpec to automatically verify your mocks after each example. You must supply this method either
+way, but you can leave it empty if your framework doesn't support auto-verification.
+* teardown_mocks_for_rspec
is guaranteed to be run after each example even when there
+are errors. Use this to ensure that there is no state shared across example, clearing out changes
+to static resources like class-level mock expectations, etc.
+
+Once you have defined your adapter, you can then tell RSpec to use your adapter like so:
+
+<% coderay do -%>
+Spec::Runner.configure do |config|
+ config.mock_with '/path/to/my/adapater.rb'
+end
+<% end -%>
+
+
+
diff --git a/website/src/documentation/mocks/partial_mocks.page b/website/src/documentation/mocks/partial_mocks.page
new file mode 100644
index 00000000..7c6b3aa5
--- /dev/null
+++ b/website/src/documentation/mocks/partial_mocks.page
@@ -0,0 +1,30 @@
+---
+title: Mock Object behaviour on Real Objects
+order: 3
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec allows you to add mock object behaviour to real objects, so you can set
+"message expectations":message_expectations.html and "method stubs":stubs.html
+on any object in your system.
+
+One common use of this support is isolating examples from ActiveRecord (and
+therefore the database) in Ruby on Rails examples.
+
+<% coderay do -%>
+MyModel.should_receive(:find).with(id).and_return(@mock_model_instance)
+<% end -%>
+
+Controlling the behaviour of the class level methods (for example, having them
+return a mock object instead of a real instance of the model class) allows you
+to describe your controllers and views in isolation from the instance level
+logic of your model classes. This means that you can change the validation
+rules for a model, for example, and drive that in the model examples without
+affecting the controller and view examples.
+
+This also helps to keep the context of your example completely in view (no
+having to look at fixtures/xyz.yml to understand what's going on).
\ No newline at end of file
diff --git a/website/src/documentation/mocks/stubs.page b/website/src/documentation/mocks/stubs.page
new file mode 100644
index 00000000..b9fce97f
--- /dev/null
+++ b/website/src/documentation/mocks/stubs.page
@@ -0,0 +1,66 @@
+---
+title: Method Stubs
+order: 2
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+A method stub is an implementation of a method that returns a canned response.
+
+The main difference between a method stub and a "message
+expectation":message_expectations.html is that message expectations verify
+interactions that you expect, while method stubs just sit there and return
+values.
+
+h3. Setting an explicit return value
+
+This is the simplest (and recommended) approach to stubbing.
+
+<% coderay do -%>
+my_instance.stub!(:msg).and_return(value)
+MyClass.stub!(:msg).and_return(value)
+<% end -%>
+
+h3. Calculating a return value
+
+<% coderay do -%>
+my_instance.stub!(:msg).and_return { ... }
+MyClass.stub!(:msg).and_return { ... }
+<% end -%>
+
+While this is possible, it is generally to be avoided. Calculating a value to
+return defeats the declarative nature of stubs.
+
+h3. Multiple return values
+
+<% coderay do -%>
+my_instance.stub!(:msg).and_return("1",2)
+MyClass.stub!(:msg).and_return("1",2)
+<% end -%>
+
+This will return "1" the first time it is called and 2 every subsequent call. Like
+calculated return values, this is a little un-stubby, but it can be a handy feature
+once in a while.
+
+h3. Mixing method stubs and message expectations
+
+In some cases it can be helpful to stub a default return value, but set a
+message expectation for a specific set of args. For example:
+
+<% coderay do -%>
+A.stub!(:msg).and_return(:default_value)
+A.should_receive(:msg).with(:arg).and_return(:special_value)
+A.msg
+=> :default_value
+A.msg(:any_other_arg)
+=> :default_value
+A.msg(:arg)
+=> :special_value
+A.msg(:any_other_other_arg)
+=> :default_value
+A.msg
+=> :default_value
+<% end -%>
\ No newline at end of file
diff --git a/website/src/documentation/rails/generators.page b/website/src/documentation/rails/generators.page
new file mode 100644
index 00000000..2ee5068c
--- /dev/null
+++ b/website/src/documentation/rails/generators.page
@@ -0,0 +1,31 @@
+---
+title: Generators
+order: 3
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+Spec::Rails contains generators that you can use to generate models and RESTful controllers
+in a similar fashion to Rails' builtin generators. The only difference is that they generate specs instead
+of tests and fixtures will be put under the spec folder. Example:
+
+
+ruby script/generate rspec_scaffold purchase order_id:integer created_at:datetime amount:decimal
+
+
+or
+
+
+ruby script/generate rspec_model person
+
+
+or
+
+
+ruby script/generate rspec_controller person
+
+
+For more information on each generator, just run them without any arguments.
\ No newline at end of file
diff --git a/website/src/documentation/rails/index.page b/website/src/documentation/rails/index.page
new file mode 100644
index 00000000..3b6f0450
--- /dev/null
+++ b/website/src/documentation/rails/index.page
@@ -0,0 +1,100 @@
+---
+title: Spec::Rails
+order: 7
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+A Rails plugin that brings RSpec to Rails.
+
+h3. Features
+
+* Use RSpec to independently specify models, views, controllers and helpers.
+* Integrated fixture loading.
+* Special generators for models and controllers that generate specs instead of tests.
+* Special RSpec matchers for even more readable specs.
+
+h3. Vision
+
+For people for whom TDD is a brand new concept, the testing support built into Rails is a huge
+leap forward. The fact that it's built right in is fantastic, and Rails
+apps are generally much easier to maintain than they would have been without such support.
+
+For those of us coming from a history with TDD, and now BDD, the existing support
+presents some problems related to dependencies across specs. To that end, RSpec
+supports 4 types of specs. This is largely inspired by Test::Rails, which is
+the rails testing framework built into "ZenTest":http://zentest.rubyforge.org.
+We've also built in first class mocking and stubbing support in order to
+break dependencies across these different concerns.
+
+h2. Different Types of Example Groups
+
+Spec::Rails supports different ExampleGroup subclasses for the following types of specs:
+
+h3. Model Examples
+
+These are the equivalent of unit tests in Rails' built in testing. Ironically
+(for the traditional TDD'er) these are the only specs that we feel should actually interact
+with the database. "Learn more ...":writing/models.html.
+
+h3. Controller Examples
+
+These align somewhat with functional tests in rails, except
+that they do not actually render views (though you can force rendering of views
+if you prefer). Instead of setting expectations about what goes on a page, you
+set expectations about what templates get rendered. "Learn more...":writing/controllers.html.
+
+h3. View Examples
+
+This is the other half of Rails' functional testing. View specs allow
+you to set up assigns (thanks to ZenTest). "Learn more ...":writing/views.html.
+
+h3. Helper Examples
+
+... let you specify directly methods that live in your helpers. "Learn more ...":writing/helpers.html.
+
+h3. User Stories for Integration Testing
+
+RSpec 1.1 introduces a Story Runner for running Scenarios related to User Stories.
+This release ships with a new (and somewhat experimental) rails-specific extension
+that supports running scenarios for you rails app. This RailsStory derives from
+ActionController::IntegrationTest, which gives you access to all the services of
+Rails Integration Testing within RSpec Stories/Scenarios. "Learn more ...":writing/stories.html.
+
+h2. Fixtures
+
+You can use fixture in any of your specs, be they model, view, controller, or helper. If you have fixtures that you want to use
+globally, you can set them in spec/spec_helper.rb. See that file for more information.
+
+h2. Naming conventions
+
+For clarity and consistency, RSpec uses a slightly different naming convention
+for directories and Rake tasks than what you get from the Test::Unit testing built into rails.
+
+<% coderay do -%>
+project
+ |
+ +--app
+ |
+ +--components
+ |
+ +--...
+ |
+ +--spec
+ |
+ +-- spec_helper.rb
+ |
+ +-- controllers
+ |
+ +-- helpers
+ |
+ +-- models
+ |
+ +-- views
+<% end -%>
+
+The Rake tasks are named accordingly.
+
diff --git a/website/src/documentation/rails/runners.page b/website/src/documentation/rails/runners.page
new file mode 100644
index 00000000..114fe31c
--- /dev/null
+++ b/website/src/documentation/rails/runners.page
@@ -0,0 +1,74 @@
+---
+title: Runners
+order: 4
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+h3. Standard spec command
+
+
+script/spec path/to/directory/with/specs
+script/spec path/to/individual_spec.rb
+
+
+h3. Run specs fast with −−drb (recommended)
+
+Loading the entire Rails environment every time a spec is executed is quite slow.
+To speed things up, Spec::Rails installs a daemon script that loads the local Rails app
+and listens for incoming connections via DRb.
+
+Open up a separate shell and start the spec server:
+
+
+script/spec_server
+
+
+Now you can run specs with the −−drb flag.
+If you're using Rake you should add −−drb to the spec/spec.opts file.
+
+Note that there are some classes and modules that, by default, won't get reloaded
+by Rails, which means they won't be reloaded by spec_server. There are a number
+of strategies available for coercing Rails to reload files every time in a given
+environment. See the Rails documentation for more information.
+
+h3. Running specs with Rake
+
+Note that the rake tasks don't use the fast rails_spec command - it uses the
+standard spec. Also note that it is pre-configured to run any file that ends
+in "_spec", so you'll have to follow that convention.
+
+Note that you can configure the options passed to RSpec by editing the spec/spec.opts file.
+
+All specs (excluding plugins) can be run with...
+
+
+rake spec
+rake spec:app
+
+
+All specs including plugins can be run with...
+
+
+rake spec:all
+
+
+You can also isolate Model, Controller, View, Helper or Plugin specs with...
+
+
+rake spec:models
+rake spec:controllers
+rake spec:views
+rake spec:helpers
+rake spec:plugins
+
+
+To see all the RSpec related tasks, run
+
+
+rake --tasks spec
+
+
diff --git a/website/src/documentation/rails/writing/controllers.page b/website/src/documentation/rails/writing/controllers.page
new file mode 100644
index 00000000..0afc0905
--- /dev/null
+++ b/website/src/documentation/rails/writing/controllers.page
@@ -0,0 +1,162 @@
+---
+title: Controllers
+order: 3
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+Controller Examples live in $RAILS_ROOT/spec/controllers/.
+
+In contrast to Test::Unit, where the controller is initialized in the setup
method,
+with Spec::Raiks you don't. Instead, pass the controller class to the describe method.
+Spec::Rails automatically instantiates a controller for you, which you can access
+from the examples (it blocks) using the controller
method.
+
+h2. Isolation from views
+
+Spec::Rails has the ability to specify your controllers in complete
+isolation from their related views. This allows you to spec your controllers
+before the views even exist, and will keep the specs from failing when there
+are errors in your views.
+
+h2. Optional integration with views
+
+If you prefer to integrate views (a la rails functional testing) you can by including
+the keyword/commmand "integrate_views".
+
+<% coderay do -%>
+describe ArticlesController do
+ integrate_views
+ ...
+end
+<% end -%>
+
+When you integrate views in the controller specs, you can use any of the
+expectations that are specific to views as well. Read about "View Examples":views.html
+to learn more.
+
+h2. Isolation from the database
+
+We strongly recommend that you use RSpec's "mocking/stubbing framework":../../mocks/index.html
+to intercept class level calls like :find
, :create
and
+even :new
to introduce mock instances instead of real active_record instances.
+
+This allows you to focus your specs on the things that the controller does and not
+worry about complex validations and relationships that should be described in
+detail in the "Model Examples":models.html
+
+<% coderay do -%>
+account = mock_model(Account)
+Account.should_receive(:find).with("37").and_return(account)
+<% end -%>
+
+or
+
+<% coderay do -%>
+account = mock_model(Account)
+Account.stub!(:find).and_return(account)
+<% end -%>
+
+See "Mocks and Stubs":../../mocks/index.html for more information about the
+built in mocking/stubbing framework.
+
+h2. Response Expectations
+
+These expectations will work whether in isolation or integration mode. See "Spec::Rails::Expectations":../../../rdoc-rails/index.html for details.
+
+h3. response.should be_success
+
+Passes if a status of 200 was returned. NOTE that in isolation mode, this will
+always return true, so it's not that useful - but at least your specs won't break.
+
+<% coderay do -%>
+response.should be_success
+<% end -%>
+
+h3. response.should be_redirect
+
+Passes if a status of 300-399 was returned.
+
+<% coderay do -%>
+response.should be_redirect
+<% end -%>
+
+h3. response.should render_template
+
+<% coderay do -%>
+get 'some_action'
+response.should render_template("path/to/template/for/action")
+<% end -%>
+
+h3. response.should have_text
+
+<% coderay do -%>
+get 'some_action'
+response.should have_text("expected text")
+<% end -%>
+
+h3. response.should redirect_to
+
+<% coderay do -%>
+get 'some_action'
+response.should redirect_to(:action => 'other_action')
+<% end -%>
+
+The following forms are supported:
+
+<% coderay do -%>
+response.should redirect_to(:action => 'other_action')
+response.should redirect_to('path/to/local/redirect')
+response.should redirect_to('http://test.host/some_controller/some_action')
+response.should redirect_to('http://some.other.domain.com')
+<% end -%>
+
+h2. Rendering Expectations
+
+In addition to response.should render_template, described above, you can state more explicit rendering
+expectations using controller.expect_render and controller.stub_render. If you have an action that
+renders a partial, for example:
+
+<% coderay do -%>
+controller.expect_render(:partial => 'person', :object => @person) #auto-verified
+controller.stub_render(:partial => 'person', :object => @person) #not verified
+<% end -%>
+
+%{color:red;font-weight:bold}WARNING%: expect_render and stub_render, while very useful, act
+differently from standard Message Expectations (a.k.a. mock expectations), which would never pass calls
+through to the real object. This can be very confusing when there are failures if you're not aware of this
+fact, because some calls will be passed through while others will not. This is especially confusing when
+you use stub_render
because, as with all Method Stubs, you will get very little feedback
+about what is going on.
+
+h3. assigns, flash and session
+
+Use these to access assigns, flash and session.
+
+<% coderay do -%>
+assigns[:key]
+flash[:key]
+session[:key]
+<% end -%>
+
+h2. Routing Expectations
+
+Specify the paths generated by custom routes.
+
+<% coderay do -%>
+route_for(:controller => "hello", :action => "world").should == "/hello/world"
+<% end -%>
+
+Specify the parameters generated from routes.
+
+<% coderay do -%>
+params_from(:get, "/hello/world").should == {:controller => "hello", :action => "world"}
+<% end -%>
+
+
+h2. Sample Controller Example
+
+<% coderay do -%><%= IO.read "../example_rails_app/spec/controllers/people_controller_spec.rb" %><% end -%>
diff --git a/website/src/documentation/rails/writing/helpers.page b/website/src/documentation/rails/writing/helpers.page
new file mode 100644
index 00000000..214a424d
--- /dev/null
+++ b/website/src/documentation/rails/writing/helpers.page
@@ -0,0 +1,33 @@
+---
+title: Helpers
+order: 4
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+Helper Examples live in $RAILS_ROOT/spec/helpers/.
+
+Writing specs for your helpers is a snap. Just tell the Example Group the name of the
+helper ...
+
+<% coderay do -%>
+describe RegistrationHelper do
+ ...
+end
+<% end -%>
+
+... and the Example Group will expose an object that includes the helper.
+
+h2. Conveniences
+
+h3. assigns
+
+Use assigns[:key] to set instance variables to be used in the view that
+includes the helper. See example below.
+
+h2. Example Helper Spec
+
+<% coderay do -%><%= IO.read "../example_rails_app/spec/helpers/people_helper_spec.rb" %><% end -%>
diff --git a/website/src/documentation/rails/writing/index.page b/website/src/documentation/rails/writing/index.page
new file mode 100644
index 00000000..0b52a017
--- /dev/null
+++ b/website/src/documentation/rails/writing/index.page
@@ -0,0 +1,42 @@
+---
+title: Writing
+order: 2
+filter:
+ - erb
+ - textile
+---
+
+h2. Introduction to Writing Examples using Spec::Rails
+
+Spec::Rails supports 4 different types of Executable Examples:
+
+* "Model Examples":models.html
+* "View Examples":views.html
+* "Controller Examples":controllers.html
+* "Helper Examples":helpers.html
+
+Each of these use specialized ExampleGroup subclasses to give you access to
+the appropriate services and methods. The specific type of ExampleGroup is
+determined implicitly by placing the examples in an appropriately named file
+(*_spec.rb) within an appropriately named directory:
+
+
+spec/controllers/**/*_spec.rb
+spec/helpers/**/*_spec.rb
+spec/models/**/*_spec.rb
+spec/views/**/*_spec.rb
+
+
+%{color:red;font-weight:bold}WARNING:% If you do not follow these conventions,
+you will not get the correct ExampleGroup subclass. If you prefer to disregard
+this convention, you can get the correct ExampleGroup subclass by passing a
+hash to the describe method:
+
+<% coderay do -%>
+describe "some model", :type => :model do
+ ...
+end
+<% end -%>
+
+This works with :model, :view, :controller and :helper
.
+
diff --git a/website/src/documentation/rails/writing/models.page b/website/src/documentation/rails/writing/models.page
new file mode 100644
index 00000000..05b5d043
--- /dev/null
+++ b/website/src/documentation/rails/writing/models.page
@@ -0,0 +1,38 @@
+---
+title: Models
+order: 1
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+Model Examples live in $RAILS_ROOT/spec/models/ and provide access to fixtures.
+
+h2. Expectations
+
+Model Examples support the following custom expectations.
+See "Spec::Rails::Expectations":../../../rdoc-rails/index.html for more detail.
+
+h3. Records
+
+<% coderay do -%>
+Model.should have(:no).records
+Model.should have(1).record
+Model.should have(3).records
+<% end -%>
+
+This is a shortcut for Model.find(:all).length.should == 3
.
+
+h3. Errors
+
+<% coderay do -%>
+model.should have(:no).errors_on(:attribute)
+model.should have(1).error_on(:attribute)
+model.should have(3).errors_on(:attribute)
+<% end -%>
+
+h2. Sample Model Examples
+
+<% coderay do -%><%= IO.read "../example_rails_app/spec/models/person_spec.rb" %><% end -%>
diff --git a/website/src/documentation/rails/writing/notes.txt b/website/src/documentation/rails/writing/notes.txt
new file mode 100644
index 00000000..435f4999
--- /dev/null
+++ b/website/src/documentation/rails/writing/notes.txt
@@ -0,0 +1,21 @@
+
+There
+are two opposing forces that guide decisions about how to structure specs in relation
+to the code being specified:
+
+* Rapid Fault Isolation. When we introduce a bug in the system, we want (ideally) only ONE
+spec to fail. We certainly learn something when several fail, but it is often a sign
+of coupling not only in the specs, but in the design as well.
+
+* Freedom to Refactor. When we want to change implementation without changing
+behavior, we want to do so without changing specs. This is a core tenet of TDD
+and an even more obvious part of our approach to BDD. Behaviour is its first name.
+
+The tension created between these two forces is that Rapid Fault Isolation
+guides us to write specs that are as close to the implementation as possible, while
+Freedom to Refactor guides us to write them as far from the implementation as
+possible.
+
+BDD is all about balancing these two forces. There is no right answer as to how
+to do this. The best advice we can give is to be aware of these two forces and
+be aware of the effects of one pulling harder than the other.
\ No newline at end of file
diff --git a/website/src/documentation/rails/writing/stories.page b/website/src/documentation/rails/writing/stories.page
new file mode 100644
index 00000000..71fe61eb
--- /dev/null
+++ b/website/src/documentation/rails/writing/stories.page
@@ -0,0 +1,9 @@
+---
+title: User Stories and Scenarios
+order: 5
+filter:
+ - erb
+ - textile
+---
+
+Docs coming soon ...
diff --git a/website/src/documentation/rails/writing/views.page b/website/src/documentation/rails/writing/views.page
new file mode 100644
index 00000000..b050d29b
--- /dev/null
+++ b/website/src/documentation/rails/writing/views.page
@@ -0,0 +1,135 @@
+---
+title: Views
+order: 2
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+View Examples live in $RAILS_ROOT/spec/views/.
+
+Spec::Rails supports the specification of views in complete isolation from their controllers.
+This allows you to spec and write your views even before their controllers exist,
+or before the related actions have been developed. It also means that bugs introduced
+into controllers will not cause view specs to fail.
+
+As noted in the "introduction":index.html, these are great benefits
+but they could hide bugs that exist in the interaction between a controller and its
+view. We strongly recommend combining these isolated view specs with some sort of
+high level integration testing, ideally using "RSpec stories":stories.html
+
+Here are some of the methods available to you, but see "Spec::Rails::Expectations":../../../rdoc-rails/index.html for more detail.
+
+h2. Conveniences
+
+h3. assigns
+
+Use assigns[:key] to set instance variables to be used in the view. We highly recommend that
+you exploit the mock framework here rather than providing real model objects in order to
+keep the view specs isolated from changes to your models.
+
+<% coderay do -%>
+# example
+article = mock_model(Article)
+article.should_receive(:author).and_return("Joe")
+article.should_receive(:text).and_return("this is the text of the article")
+assigns[:article] = article
+assigns[:articles] = [article]
+
+# template
+<%% for article in @articles -%>
+
+<% end -%>
+
+h3. flash, params and session
+
+Use flash[:key], params[:key] and session[:key] to set values in the example that can
+be accessed by the view.
+
+<% coderay do -%>
+# example
+flash[:notice] = "Message in flash"
+params[:account_id] = "1234"
+session[:user_id] = "5678"
+
+# template
+<%%= flash[:notice] %>
+<%%= params[:account_id] %>
+<%%= session[:user_id] %>
+<% end -%>
+
+h2. Expectations
+
+Spec::Rails' View Examples support the following custom expectations.
+
+h3. template.expect_render/stub_render
+
+This is a custom mock-like expectation that allows you to set expectations about partials and included
+files that will be rendered, intercepting those calls to the #render method, while ignoring other calls
+and passing them on to ActionView::Base. expect_render
is verified at the end of the
+example, while stub_render
is not.
+
+%{color:red;font-weight:bold}WARNING%: expect_render and stub_render, while very useful, act
+differently from standard Message Expectations (a.k.a. mock expectations), which would never pass calls
+through to the real object. This can be very confusing when there are failures if you're not aware of this
+fact, because some calls will be passed through while others will not. This is especially confusing when
+you use stub_render
because, as with all Method Stubs, you will get very little feedback
+about what is going on.
+
+<% coderay do -%>
+template.expect_render(:partial => 'person', :object => @person) #auto-verified
+template.stub_render(:partial => 'person', :object => @person) #not verified
+<% end -%>
+
+
+h3. response.should have_tag
+
+This wraps assert_select and is available in both View Examples and Controller Examples run in integration mode.
+
+<% coderay do -%>
+response.should have_tag('div') #passes if any div tags appear
+response.should have_tag('div#interesting_div')
+response.should have_tag('div', 'expected content')
+response.should have_tag('div', /regexp matching expected content/)
+response.should have_tag('form[action=?]', things_path)
+response.should have_tag("input[type=?][checked=?]", 'checkbox', 'checked')
+response.should have_tag('ul') do
+ with_tag('li', 'list item 1')
+ with_tag('li', 'list item 2')
+ with_tag('li', 'list item 3')
+end
+<% end -%>
+
+Note that any of the hash values can be either Strings or Regexps and will be
+evaluated accordingly.
+
+h3. response[:capture].should have_tag
+
+This way you can access content that has been captured with content_for.
+
+
+# example
+response[:sidebar].should have_tag('div')
+
+# template
+<% content_for :sidebar do %>
+
+<% end %>
+
+
+h2. Mocking and stubbing helpers
+
+If you wish to mock or stub helper methods, this must be done on the template object:
+
+<% coderay do -%>
+template.should_receive(:current_user).and_return(mock("user"))
+<% end -%>
+
+%{color:red;font-weight:bold}WARNING:% Do NOT use mocks on the template object for expecting partials.
+Instead, use expect_render or stub_render, described above.
+
+h2. Sample View Examples
+
+<% coderay do -%><%= IO.read "../example_rails_app/spec/views/people/list_view_spec.rb" %><% end -%>
diff --git a/website/src/documentation/specs.page b/website/src/documentation/specs.page
new file mode 100644
index 00000000..f60163be
--- /dev/null
+++ b/website/src/documentation/specs.page
@@ -0,0 +1,9 @@
+---
+title: Specs
+order: 3
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
\ No newline at end of file
diff --git a/website/src/documentation/stories.page b/website/src/documentation/stories.page
new file mode 100644
index 00000000..620d3e7e
--- /dev/null
+++ b/website/src/documentation/stories.page
@@ -0,0 +1,18 @@
+---
+title: Stories
+order: 5
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+Sorry folks but we just haven't had a chance to get any docs up on this. Here are some resources:
+
+"http://dannorth.net/whats-in-a-story":http://dannorth.net/whats-in-a-story
+"http://evang.eli.st/blog/2007/10/8/story-runner-top-to-bottom-screencast":http://evang.eli.st/blog/2007/10/8/story-runner-top-to-bottom-screencast
+"https://peepcode.com/products/rspec-user-stories":https://peepcode.com/products/rspec-user-stories
+"http://blog.davidchelimsky.net/articles/tag/stories":http://blog.davidchelimsky.net/articles/tag/stories
+
+We'll have more soon. Stay tuned ...
\ No newline at end of file
diff --git a/website/src/documentation/test_unit.page b/website/src/documentation/test_unit.page
new file mode 100644
index 00000000..6246f11a
--- /dev/null
+++ b/website/src/documentation/test_unit.page
@@ -0,0 +1,81 @@
+---
+title: Test::Unit
+order: 4
+filter:
+ - erb
+ - textile
+---
+
+h2. Coming from Test::Unit to RSpec
+
+RSpec's expectation API is a superset of Test::Unit's assertion API. Use this table to
+find the RSpec equivalent of Test::Unit's asserts.
+
+| Test::Unit | RSpec | Comment |
+| assert(object) | N/A | |
+| assert_block {...} | lambda {...}.call.should be_true| |
+| assert_equal(expected, actual) | actual.should == expected | Uses ==
|
+| '' | actual.should eql(expected) | Uses Object.eql?
|
+| assert_in_delta(expected_float, actual_float, delta) | actual_float.should be_close(expected_float, delta) | |
+| assert_instance_of(klass, object) | actual.should be_an_instance_of(klass) | |
+| assert_match(pattern, string) | string.should match(regexp) | |
+| '' | string.should =~ regexp | |
+| assert_nil(object) | actual.should be_nil | |
+| assert_no_match(regexp, string) | string.should_not match(regexp) | |
+| assert_not_equal(expected, actual) | actual.should_not eql(expected) | Uses !Object.eql?
|
+| assert_not_nil(object) | actual.should_not be_nil | |
+| assert_not_same(expected, actual) | actual.should_not equal(nil) | Uses Object.equal?
|
+| assert_nothing_raised(*args) {...} | lambda {...}.should_not raise_error(Exception=nil, message=nil) |
+| assert_nothing_thrown {...} | lambda {...}.should_not throw_symbol(symbol=nil) |
+| assert_operator(object1, operator, object2) | N/A | |
+| assert_raise(*args) {...} | lambda {...}.should raise_error(Exception=nil, message=nil) |
+| assert_raises(*args) {...} | lambda {...}.should raise_error(Exception=nil, message=nil) |
+| assert_respond_to(object, method) | actual.should respond_to(method) | |
+| assert_same(expected, actual) | actual.should equal(expected) | Uses !Object.equal?
|
+| assert_send(send_array) | N/A | |
+| assert_throws(expected_symbol, &proc) | lambda {...}.should throw_symbol(symbol=nil) | |
+| flunk(message="Flunked") | violated(message=nil) | |
+| N/A | actual.should_be_something | Passes if object.something? is true |
+
+h2. Regarding Equality
+
+RSpec lets you express equality the same way Ruby does. This is different from
+Test::Unit and other xUnit frameworks, and this may cause some confusion for those
+of you who are making a switch.
+
+Consider this example:
+
+<% coderay do -%>words = "the words"
+assert_equals("the words", words) #passes
+assert_same("the words", words) #fails
+<% end -%>
+
+xUnit frameworks traditionally use method names like #assert_equals
to
+imply object equivalence (objects with the same values) and method names like #assert_same
to
+imply object identity (actually the same object). For programmers who are used to this syntax,
+it makes perfect sense to see this in a unit testing framework for Ruby.
+
+The problem that we found is that Ruby handles equality differently from other languages. In
+java, for example, we override Object#equals(other)
to describe object equivalence,
+whereas we use ==
to describe object identity. In Ruby, we do just the opposite.
+In fact, Ruby provides us with four ways to express equality:
+
+<% coderay do -%>a == b
+a === b
+a.equal?(b)
+a.eql?(b)
+<% end -%>
+
+Each of these has its own semantics, which can (and often do) vary from class to class. In
+order to minimize the translation required to understand what an example might be expressing,
+we chose to express these directly in RSpec. If you understand Ruby equality, then you
+can understand what RSpec examples are describing:
+
+<% coderay do -%>a.should == b #passes if a == b
+a.should === b #passes if a === b
+a.should equal(b) #passes if a.equal?(b)
+a.should eql(b) #passes if a.eql?(b)
+<% end -%>
+
+See "http://www.ruby-doc.org/core/classes/Object.html#M001057":http://www.ruby-doc.org/core/classes/Object.html#M001057 for
+more information about equality in Ruby.
\ No newline at end of file
diff --git a/website/src/documentation/tools/extensions/editors/index.page b/website/src/documentation/tools/extensions/editors/index.page
new file mode 100644
index 00000000..02041188
--- /dev/null
+++ b/website/src/documentation/tools/extensions/editors/index.page
@@ -0,0 +1,56 @@
+---
+title: Editors
+order: 1
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec's commandline API and pluggable formatters makes it very easy to integrate it with external tools
+such as text editors (including IDEs). Below we describe the general steps you need to perform to write
+an RSpec plugin for your editor.
+
+The RSpec team maintains a TextMate bundle which does exactly this. You may want to look at its source if
+you're about to write a plugin for a new editor.
+
+We'd be happy to accept more editor plugins into RSpec. Please see "Contribute":../../../../community/contribute.html for
+details. Or, if you're a developer of an editor, perhaps you want to bundle it with the editor.
+
+h3. Launch RSpec from the IDE
+
+Most advanced editors provide a mechanism to launch external processes by hitting a keystroke.
+Depending on your editor, you may want to launch RSpec directly via the spec
spec command
+(which must be on your $PATH). Alternatively, if your editor makes it possible to run Ruby scripts
+directly, you may execute RSpec via Spec::Runner::CommandLine.run
(which has a similar API to
+the spec
command, except you can call it straight from Ruby without forking a new Ruby process).
+
+h3. Tell RSpec what spec(s) to run
+
+You may want to assign different keystrokes to run your specs. You could run All specs in your project,
+the ones in the currently open file or just the one you have focussed in the editor.
+RSpec's commandline API has a --line
option which will make RSpec run the spec at the current
+line (it also works if the line is inside the spec).
+Most editors provide a way to query the current file and the line of the cursor when launching an external
+process, and you can use this to feed the right argument to the --line option.
+
+h3. Make the backtrace work with your editor
+
+When a spec fails, the backtrace is displayed in the error message. The backtrace contains lines with
+file paths and line numbers. Most editors have a mechanism that will open the file and put the
+cursor on the right line when such a line is clicked - provided the line is on a format that the editor
+understands.
+
+It is possible to customise the backtrace lines of RSpec's output to achieve this. All you need to do is
+to implement your own Formatter class, typically by subclassing
+"ProgressBarFormatter":../rdoc/classes/Spec/Runner/Formatter/ProgressBarFormatter.html or "HtmlFormatter":../rdoc/classes/Spec/Runner/Formatter/HtmlFormatter.html
+(depending on what your editor understands). In your formatter class you
+can override the backtrace_line
method to make the output be something that works with your editor.
+Example:
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/examples/pure/custom_formatter.rb" %><% end -%>
+
+Then, when the editor plugin launches RSpec, just make sure it uses the --formatter
+option to specify *your* custom formatter. Note that you will probably have to use --require
+too, so that the code for your custom formatter is loaded. See spec --help
for details.
diff --git a/website/src/documentation/tools/extensions/editors/textmate.page b/website/src/documentation/tools/extensions/editors/textmate.page
new file mode 100644
index 00000000..13da6027
--- /dev/null
+++ b/website/src/documentation/tools/extensions/editors/textmate.page
@@ -0,0 +1,62 @@
+---
+title: TextMate
+order: 1
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+TextMate is a popular text editor for OS X. RSpec's TextMate bundle has a big number of snippets for
+all of RSpec's expectations, contexts and specify's. You can even run specs from TextMate using
+CMD-R - and you'll see a nice progress bar while the specs are running (except
+usually they run so fast you won't be able to see it move).
+
+If you select one or more directories or files you can run those too with CMD-R (but you have to have
+an arbitrary Ruby file open to make this work).
+
+h2. Installation
+
+There are several ways to install the RSpec bundle for TextMate. We'll start by the easiest.
+
+h3. Installing from RubyForge
+
+Just download the tgz file from RubyForge, unpack it and double-click the RSpec.tmbundle icon.
+It is very important that your installed Ruby gem is a compatible version.
+
+h3. Installing from GitHub straight into your TextMate
+
+
+cd ~/Library/Application\ Support/TextMate/Bundles/
+git clone git://github.com/dchelimsky/rspec-tmbundle.git RSpec.tmbundle
+
+
+h3. Symlinking an existing RSpec.tmbundle GitHuv clone to TextMate
+
+If you already have the RSpec.tmbundle checked out somewhere, you can make a symbolic link
+from TextMate's bundle directory to your working copy of the bundle:
+
+
+ln -s path/to/checked/out/RSpec.tmbundle ~/Library/Application\ Support/TextMate/Bundles/RSpec.tmbundle
+
+
+h2. Pointing to the right Ruby and RSpec.
+
+You may need to adjust the PATH in your ~/.MacOSX/environment.plist file to point to the directory
+where your ruby and spec executables live. For example:
+
+
+PATH
+/usr/local/bin:/usr/local/sbin:/usr/local/mysql/bin:/opt/local/bin:/usr/local/mysql/bin
+
+
+You may also have to set your TM_RUBY environment variable in TextMate's preferences to point to your ruby executable.
+
+You can also tell RSpec.tmbundle to use a particular RSpec (the library) at a particular location on your filesystem.
+Just define the TM_RSPEC_HOME environment variable in TextMate's preferences. This should
+point to the your working copy's rspec directory.
+
+h2. Setting RSpec command line options
+
+You can specify these in the TM_RSPEC_OPTS environment variable in TextMate's preferences.
\ No newline at end of file
diff --git a/website/src/documentation/tools/extensions/index.page b/website/src/documentation/tools/extensions/index.page
new file mode 100644
index 00000000..204f17a0
--- /dev/null
+++ b/website/src/documentation/tools/extensions/index.page
@@ -0,0 +1,19 @@
+---
+title: Extensions
+order: 5
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec extensions typically come in two different kinds: API extensions and runner extensions.
+
+h3. API Extensions
+
+API extensions are extensions to RSpec's core expectation API.
+
+h3. Runner Extensions
+
+Runner extensions can consist of plugins to other tools (such as editors and IDEs) as well as custom RSpec formatters.
diff --git a/website/src/documentation/tools/heckle.page b/website/src/documentation/tools/heckle.page
new file mode 100644
index 00000000..91815d40
--- /dev/null
+++ b/website/src/documentation/tools/heckle.page
@@ -0,0 +1,23 @@
+---
+title: Heckle
+order: 4
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec has tight integration with "Heckle":https://rubyforge.org/projects/seattlerb/. Heckle is a tool inspired from
+"Jester":http://jester.sourceforge.net/.
+
+RSpec's Heckle support will run your specs many times, each time with a tiny mutation to your code (the
+application code, not the spec code).
+
+The idea is that if your specs still pass after the code has been mutated, then your specs are incomplete,
+and RSpec will tell you.
+
+h3. Running specs with Heckle
+
+Just use the --heckle option and give it an argument, which should be the full name of a module
+or a method. Run spec --help for further details.
\ No newline at end of file
diff --git a/website/src/documentation/tools/index.page b/website/src/documentation/tools/index.page
new file mode 100644
index 00000000..a7d6cc56
--- /dev/null
+++ b/website/src/documentation/tools/index.page
@@ -0,0 +1,56 @@
+---
+title: Tools
+order: 8
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec combines several tools in one package
+
+h3. 1) A language for expressing behavior
+
+RSpec provides programmers with a domain specific language for defining expected behavior of Ruby code.
+
+h3. 2) A runner for verifying behavior
+
+RSpec provides a command line utility for executing behavior specifications.
+
+h3. 3) Mock objects
+
+Mock objects frameworks usually come as separate add-ons to existing XUnit frameworks. In RSpec it's an integrated
+part of the framework.
+
+h3. 4) Testdox-like reports
+
+Expressing behavior in real sentences improves communication between programmer,
+analysts and testers. It also helps programmers keep a more critical mind about the code being developed.
+
+RSpec's built in support for generating "testdox-like":http://agiledox.sourceforge.net/ reports makes the code's behavior more transparent
+to everybody (assuming you publish it to a webpage where everybody who needs to can see it).
+
+h3. 5) RCov Coverage tool integration
+
+While coverage tools cannot prove that you have verified everything there is to verify about your code,
+they can prove when you have not verified everything there is to verify about your code. RCov is such a
+coverage tool, and RSpec supports it out-of the box via its Rake task.
+
+h3. 6) Coverage threshold verification
+
+Except for the times when programmers agree to keep a close eye on coverage and keep it at, say 90%, it
+will inevitably drop when they get other things to think about and start to write code in the wild.
+
+RSpec ships with a special Rake task that can verify that the coverage remains on a level defined by the
+development team. Should the coverage drop, the build will fail, and developers are encouraged to address
+it right away.
+
+h3. 7) Integrated diffing
+
+When you run specs with the --diff option, you'll see unified diffs when Strings don't match.
+Very useful when your specs are comparing big strings!
+
+h3. 8) Mutation with Heckle
+
+The --heckle option will run your specs with mutations.
\ No newline at end of file
diff --git a/website/src/documentation/tools/rake.page b/website/src/documentation/tools/rake.page
new file mode 100644
index 00000000..1ac77dab
--- /dev/null
+++ b/website/src/documentation/tools/rake.page
@@ -0,0 +1,32 @@
+---
+title: Rake Tasks
+order: 2
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec comes with a "Rake":http://rake.rubyforge.org/ task for executing specs.
+See "Spec::Rake::SpecTask":../../rdoc/classes/Spec/Rake/SpecTask.html API for details.
+
+h3. Rake task example
+
+This is a snippet from RSpec's own Rakefile. It creates a task to run the examples.
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/rake_tasks/examples.rake" %><% end -%>
+
+It can be invoked from the command line with:
+
+
+rake examples
+
+
+Also see the "RCov":rcov.html page for info about how to generate a coverage report.
+
+h3. Generate HTML report
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/rake_tasks/failing_examples_with_html.rake" %><% end -%>
+
+This will write a HTML file that looks like "this":failing_examples.html.
diff --git a/website/src/documentation/tools/rcov.page b/website/src/documentation/tools/rcov.page
new file mode 100644
index 00000000..5fa7bb05
--- /dev/null
+++ b/website/src/documentation/tools/rcov.page
@@ -0,0 +1,51 @@
+---
+title: RCov
+order: 3
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec has tight integration with "RCov":http://eigenclass.org/hiki.rb?rcov, the excellent code coverage tool for Ruby.
+
+h3. Running specs with RCov
+
+The easiest way to use RCov with RSpec is from a rake script (Rakefile), using RSpec's
+"Rake task":rake.html. The snippet below is from RSpec's own Rakefile.
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/rake_tasks/examples_with_rcov.rake" %><% end -%>
+
+By adding rcov=true
to the rake task, specs will be run with RCov
+instead of the standard ruby interpreter, and a coverage report like
+"this":../../coverage/index.html will be generated with the following command line:
+
+
+rake examples_with_rcov
+
+
+Also, because you don't want your example files to be included in the coverage report (you just
+want them to run), don't forget to add t.rcov_opts = ['--exclude', 'examples']
. You
+can use this to exclude any other files that must be required but should not be measured (just
+use a comma separated list).
+
+h3. Coverage threshold
+
+You can guard your codebase's coverage from dropping below a certain threshold
+by using RSpec's built-in task for verification of the total RCov coverage.
+(This is not the same as RCov's --threshold option, which is a filtering mechanism).
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/rake_tasks/verify_rcov.rake" %><% end -%>
+
+This will give you a :verify_rcov task that will fail your build if the
+coverage drops below the threshold you define.
+If it's too low you're encouraged to write more specs rather than "fix"
+the threshold value (which should usually only be adjusted upwards).
+
+In fact, the task will also fail the build if the coverage raises above the threshold as well.
+This might seem a bit counterintuitive, but this is when you should go and raise the
+threshold in your Rakefile. This way you won't miss temporary coverage increases. Crank it up!
+This is how we got to 100% coverage for RSpec.
+
+See "RCov::VerifyTask":../../rdoc/classes/RCov/VerifyTask.html for details.
diff --git a/website/src/documentation/tools/spec.page b/website/src/documentation/tools/spec.page
new file mode 100644
index 00000000..5043ef38
--- /dev/null
+++ b/website/src/documentation/tools/spec.page
@@ -0,0 +1,60 @@
+---
+title: Command Line
+order: 1
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+h2. Executing examples directly
+
+Any file RSpec examples can be run directly from the command line:
+
+
+$ ruby path/to/my_spec.rb [options]
+
+
+This will print the results to STDOUT. Use the --help option for more details. This is
+practical when you only want to run one file. If you want to run more, use the spec
+command or the Rake task.
+
+h2. The spec command
+
+After you install RSpec, you should have the spec command on your PATH.
+This command can be used to process several files in one go.
+
+Any number of files, directories and shell globs can be provided, all ruby source files
+that are found are loaded. To see this in action, cd to the RSpec install directory
+(normally under /usr/local/lib/ruby/gems/1.8/gems/rspec-x.y.z/) and type spec examples
:
+
+
+$ spec examples
+
+<%= `ruby docspec.rb examples` %>
+
+
+Very simple and to the point. Passing examples are indicated by a '.',
+failing ones by a 'F'. Note that failure indicates a violated expectation as
+well as an unexpected exception being raised. Here are examples of both:
+
+
+$ spec failing_examples/team_spec.rb
+
+<%= `ruby docspec.rb failing_examples/predicate_example.rb` %>
+
+
+h2. Command line options
+
+When you run spec with the --help option it prints a help message:
+
+
+$ spec --help
+
+<%= `ruby docspec.rb --help` %>
+
+
+The command line options can be passed to customize the output and behaviour of RSpec.
+The options apply whether specs are run in standalone mode (by executing the .rb files directly with ruby),
+or using the spec command.
diff --git a/website/src/download.page b/website/src/download.page
new file mode 100644
index 00000000..70055dc5
--- /dev/null
+++ b/website/src/download.page
@@ -0,0 +1,26 @@
+---
+title: Download
+order: 4
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+h3. Rubygems
+
+
+gem install rspec
+
+
+or if you want an older version:
+
+
+gem install rspec -v 1.0.8
+
+
+h3. From source (at "github":http://github.com)
+
+"http://github.com/dchelimsky/rspec/wikis/home":http://github.com/dchelimsky/rspec/wikis/home
+"http://github.com/dchelimsky/rspec-rails/wikis/home":http://github.com/dchelimsky/rspec-rails/wikis/home
diff --git a/website/src/examples.page b/website/src/examples.page
new file mode 100644
index 00000000..5697f1de
--- /dev/null
+++ b/website/src/examples.page
@@ -0,0 +1,24 @@
+---
+title: Examples
+order: 2
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+Here is an example of a spec for a Stack. We start with a group of shared examples.
+These won't get run directly, but can be included by, and run in the context of
+other example groups.
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/examples/pure/shared_stack_examples.rb" %><% end -%>
+
+And here is the group that runs them. Note the nesting, which is a new feature in
+RSpec 1.1.
+
+<% coderay do -%><%= IO.read "../example_rails_app/vendor/plugins/rspec/examples/pure/stack_spec_with_nested_example_groups.rb" %><% end -%>
+
+And here is what you see when you run these examples in TextMate:
+
+
\ No newline at end of file
diff --git a/website/src/images/David_and_Aslak.jpg b/website/src/images/David_and_Aslak.jpg
new file mode 100644
index 00000000..c037c840
Binary files /dev/null and b/website/src/images/David_and_Aslak.jpg differ
diff --git a/website/src/images/Whats_That_Dude.jpg b/website/src/images/Whats_That_Dude.jpg
new file mode 100644
index 00000000..45faf258
Binary files /dev/null and b/website/src/images/Whats_That_Dude.jpg differ
diff --git a/website/src/images/ali_westside.jpg b/website/src/images/ali_westside.jpg
new file mode 100644
index 00000000..ae2d1bb1
Binary files /dev/null and b/website/src/images/ali_westside.jpg differ
diff --git a/website/src/images/arrow.gif b/website/src/images/arrow.gif
new file mode 100644
index 00000000..e355f3b9
Binary files /dev/null and b/website/src/images/arrow.gif differ
diff --git a/website/src/images/ducks1.png b/website/src/images/ducks1.png
new file mode 100644
index 00000000..4c4a76a8
Binary files /dev/null and b/website/src/images/ducks1.png differ
diff --git a/website/src/images/eylogo.gif b/website/src/images/eylogo.gif
new file mode 100644
index 00000000..e453f1c9
Binary files /dev/null and b/website/src/images/eylogo.gif differ
diff --git a/website/src/images/pat_maddox.jpg b/website/src/images/pat_maddox.jpg
new file mode 100644
index 00000000..4c50ea55
Binary files /dev/null and b/website/src/images/pat_maddox.jpg differ
diff --git a/website/src/images/stack_example_tm_report.jpg b/website/src/images/stack_example_tm_report.jpg
new file mode 100644
index 00000000..3938c5d2
Binary files /dev/null and b/website/src/images/stack_example_tm_report.jpg differ
diff --git a/website/src/images/test_unit.graffle b/website/src/images/test_unit.graffle
new file mode 100644
index 00000000..63a5721f
--- /dev/null
+++ b/website/src/images/test_unit.graffle
@@ -0,0 +1,767 @@
+
+
+
+
+ ActiveLayerIndex
+ 0
+ AutoAdjust
+
+ CanvasColor
+
+ w
+ 1
+
+ CanvasOrigin
+ {0, 0}
+ CanvasScale
+ 1
+ ColumnAlign
+ 1
+ ColumnSpacing
+ 36
+ CreationDate
+ 2007-09-17 13:15:30 +0200
+ Creator
+ Aslak Hellesoy
+ DisplayScale
+ 1 in = 1 in
+ GraphDocumentVersion
+ 5
+ GraphicsList
+
+
+ Bounds
+ {{572.625, 41}, {133.875, 196}}
+ Class
+ ShapedGraphic
+ FitText
+ Vertical
+ Flow
+ Resize
+ ID
+ 76
+ Shape
+ Rectangle
+ Style
+
+ fill
+
+ Draws
+ NO
+
+ shadow
+
+ Draws
+ NO
+
+ stroke
+
+ Draws
+ NO
+
+
+ Text
+
+ Align
+ 0
+ Text
+ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural
+
+\f0\fs24 \cf0 If Test::Unit is loaded (we can check if Test::Unit is defined?) the bridge gets loaded and the Test::Unit runner is used. It delegates work to RSpec's independent runner.\
+\
+Examples get TestCase as superclass\
+}
+
+
+
+ Bounds
+ {{10.5, 63.6599}, {133.875, 112}}
+ Class
+ ShapedGraphic
+ FitText
+ Vertical
+ Flow
+ Resize
+ ID
+ 75
+ Shape
+ Rectangle
+ Style
+
+ fill
+
+ Draws
+ NO
+
+ shadow
+
+ Draws
+ NO
+
+ stroke
+
+ Draws
+ NO
+
+
+ Text
+
+ Align
+ 0
+ Text
+ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural
+
+\f0\fs24 \cf0 Classic specs (when Test::Unit is not required/loaded)\
+\
+Only RSpec's runner is used, Example does *not* have TestCase as superclass}
+
+
+
+ Bounds
+ {{169.365, 11.4316}, {386.999, 371.661}}
+ Class
+ ShapedGraphic
+ ID
+ 74
+ Rotation
+ 317
+ Shape
+ Circle
+ Style
+
+ fill
+
+ Color
+
+ b
+ 0.20978
+ g
+ 1
+ r
+ 0.986404
+
+ Draws
+ NO
+
+ stroke
+
+ Color
+
+ b
+ 0.184314
+ g
+ 0.623529
+ r
+ 0.976471
+
+ Width
+ 2
+
+
+
+
+ Bounds
+ {{10.457, 156.076}, {349.632, 199.579}}
+ Class
+ ShapedGraphic
+ ID
+ 2
+ Rotation
+ 317
+ Shape
+ Circle
+ Style
+
+ fill
+
+ Color
+
+ b
+ 0.20978
+ g
+ 1
+ r
+ 0.986404
+
+ Draws
+ NO
+
+ stroke
+
+ Color
+
+ b
+ 1
+ g
+ 0.0321779
+ r
+ 0
+
+ Width
+ 2
+
+
+
+
+ Class
+ LineGraphic
+ Head
+
+ ID
+ 13
+
+ ID
+ 72
+ Points
+
+ {408.47, 314.92}
+ {458, 297}
+ {481.839, 211.482}
+
+ Style
+
+ stroke
+
+ HeadArrow
+ FilledArrow
+ LineType
+ 1
+ TailArrow
+ 0
+
+
+ Tail
+
+ ID
+ 68
+
+
+
+ Class
+ LineGraphic
+ Head
+
+ ID
+ 56
+
+ ID
+ 70
+ Points
+
+ {308.542, 311.093}
+ {265, 292}
+ {245.99, 211.487}
+
+ Style
+
+ stroke
+
+ HeadArrow
+ FilledArrow
+ LineType
+ 1
+ TailArrow
+ 0
+
+
+ Tail
+
+ ID
+ 68
+
+
+
+ Class
+ Group
+ Graphics
+
+
+ Bounds
+ {{309, 306}, {99, 54}}
+ Class
+ ShapedGraphic
+ ID
+ 68
+ Shape
+ Rectangle
+ Text
+
+ Text
+ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs24 \cf0 Intermingled tests and specs}
+
+
+
+ Bounds
+ {{309, 297}, {29.1176, 9}}
+ Class
+ ShapedGraphic
+ ID
+ 69
+ Shape
+ Rectangle
+
+
+ ID
+ 67
+
+
+ Class
+ LineGraphic
+ Head
+
+ ID
+ 56
+
+ ID
+ 66
+ Points
+
+ {169.458, 311.256}
+ {216, 291}
+ {233.463, 211.488}
+
+ Style
+
+ stroke
+
+ HeadArrow
+ FilledArrow
+ LineType
+ 1
+ TailArrow
+ 0
+
+
+ Tail
+
+ ID
+ 64
+
+
+
+ Class
+ Group
+ Graphics
+
+
+ Bounds
+ {{70, 306}, {99, 54}}
+ Class
+ ShapedGraphic
+ ID
+ 64
+ Shape
+ Rectangle
+ Text
+
+ Text
+ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs24 \cf0 Specs not using\
+Test::Unit}
+
+
+
+ Bounds
+ {{70, 297}, {29.1176, 9}}
+ Class
+ ShapedGraphic
+ ID
+ 65
+ Shape
+ Rectangle
+
+
+ ID
+ 63
+
+
+ Class
+ LineGraphic
+ Head
+
+ ID
+ 13
+
+ ID
+ 62
+ Points
+
+ {408.494, 90.6914}
+ {469, 100}
+ {482.792, 156.514}
+
+ Style
+
+ stroke
+
+ HeadArrow
+ FilledArrow
+ LineType
+ 1
+ TailArrow
+ 0
+
+
+ Tail
+
+ ID
+ 53
+
+
+
+ Class
+ LineGraphic
+ Head
+
+ ID
+ 56
+
+ ID
+ 22
+ Points
+
+ {308.509, 92.358}
+ {257, 102}
+ {245.367, 156.511}
+
+ Style
+
+ stroke
+
+ HeadArrow
+ FilledArrow
+ LineType
+ 1
+ TailArrow
+ 0
+
+
+ Tail
+
+ ID
+ 53
+
+
+
+ Class
+ Group
+ Graphics
+
+
+ Bounds
+ {{190, 157}, {99, 54}}
+ Class
+ ShapedGraphic
+ ID
+ 56
+ Shape
+ Rectangle
+ Text
+
+ Text
+ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs24 \cf0 RSpec core}
+
+
+
+ Bounds
+ {{190, 148}, {29.1176, 9}}
+ Class
+ ShapedGraphic
+ ID
+ 57
+ Shape
+ Rectangle
+
+
+ ID
+ 55
+
+
+ Class
+ Group
+ Graphics
+
+
+ Bounds
+ {{309, 56}, {99, 54}}
+ Class
+ ShapedGraphic
+ ID
+ 53
+ Shape
+ Rectangle
+ Text
+
+ Text
+ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs24 \cf0 RSpec\
+Test::Unit\
+bridge}
+
+
+
+ Bounds
+ {{309, 47}, {29.1176, 9}}
+ Class
+ ShapedGraphic
+ ID
+ 54
+ Shape
+ Rectangle
+
+
+ ID
+ 52
+
+
+ Class
+ Group
+ Graphics
+
+
+ Bounds
+ {{440, 157}, {99, 54}}
+ Class
+ ShapedGraphic
+ ID
+ 13
+ Shape
+ Rectangle
+ Text
+
+ Text
+ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs24 \cf0 Test::Unit}
+
+
+
+ Bounds
+ {{440, 148}, {29.1176, 9}}
+ Class
+ ShapedGraphic
+ ID
+ 14
+ Shape
+ Rectangle
+
+
+ ID
+ 12
+
+
+ GridInfo
+
+ GuidesLocked
+ NO
+ GuidesVisible
+ YES
+ HPages
+ 1
+ ImageCounter
+ 1
+ IsPalette
+ NO
+ KeepToScale
+
+ Layers
+
+
+ Lock
+ NO
+ Name
+ Layer 1
+ Print
+ YES
+ View
+ YES
+
+
+ LayoutInfo
+
+ LinksVisible
+ NO
+ MagnetsVisible
+ NO
+ MasterSheet
+ Master 1
+ MasterSheets
+
+
+ ActiveLayerIndex
+ 0
+ AutoAdjust
+
+ CanvasColor
+
+ w
+ 1
+
+ CanvasOrigin
+ {0, 0}
+ CanvasScale
+ 1
+ ColumnAlign
+ 1
+ ColumnSpacing
+ 36
+ DisplayScale
+ 1 in = 1 in
+ GraphicsList
+
+ GridInfo
+
+ HPages
+ 1
+ IsPalette
+ NO
+ KeepToScale
+
+ Layers
+
+
+ Lock
+ NO
+ Name
+ Layer 1
+ Print
+ YES
+ View
+ YES
+
+
+ LayoutInfo
+
+ Orientation
+ 2
+ OutlineStyle
+ Basic
+ RowAlign
+ 1
+ RowSpacing
+ 36
+ SheetTitle
+ Master 1
+ UniqueID
+ 1
+ VPages
+ 1
+
+
+ ModificationDate
+ 2007-09-28 00:25:33 +0200
+ Modifier
+ Aslak Hellesoy
+ NotesVisible
+ NO
+ Orientation
+ 2
+ OriginVisible
+ NO
+ OutlineStyle
+ Basic
+ PageBreaks
+ YES
+ PrintInfo
+
+ NSBottomMargin
+
+ float
+ 0
+
+ NSLeftMargin
+
+ float
+ 0
+
+ NSOrientation
+
+ int
+ 1
+
+ NSPaperSize
+
+ size
+ {792, 612}
+
+ NSRightMargin
+
+ float
+ 0
+
+ NSTopMargin
+
+ float
+ 0
+
+
+ ReadOnly
+ NO
+ RowAlign
+ 1
+ RowSpacing
+ 36
+ SheetTitle
+ Canvas 1
+ SmartAlignmentGuidesActive
+ YES
+ SmartDistanceGuidesActive
+ YES
+ UniqueID
+ 1
+ UseEntirePage
+
+ VPages
+ 1
+ WindowInfo
+
+ CurrentSheet
+ 0
+ DrawerOpen
+
+ DrawerTab
+ Outline
+ DrawerWidth
+ 209
+ FitInWindow
+
+ Frame
+ {{137, 96}, {916, 584}}
+ ShowRuler
+
+ ShowStatusBar
+
+ VisibleRegion
+ {{-72, -57}, {901, 470}}
+ Zoom
+ 1
+
+
+
diff --git a/website/src/images/test_unit.png b/website/src/images/test_unit.png
new file mode 100644
index 00000000..a0330d44
Binary files /dev/null and b/website/src/images/test_unit.png differ
diff --git a/website/src/index.page b/website/src/index.page
new file mode 100644
index 00000000..14952513
--- /dev/null
+++ b/website/src/index.page
@@ -0,0 +1,149 @@
+---
+title: Overview
+filter:
+ - erb
+ - textile
+---
+
+h2. <%= @page.title %>
+
+RSpec is a Behaviour Driven Development framework for Ruby. It provides two
+frameworks for writing and executing examples of how your Ruby application should
+behave:
+
+ * a Story Framework for describing behaviour at the application level
+ * a Spec Framework for describing behaviour at the object level
+
+h2. Story Framework
+
+Start by writing a Story with one or more Scenarios in plain text, using the following
+format.
+
+
+Story: transfer from savings to checking account
+ As a savings account holder
+ I want to transfer money from my savings account to my checking account
+ So that I can get cash easily from an ATM
+
+ Scenario: savings account has sufficient funds
+ Given my savings account balance is $100
+ And my checking account balance is $10
+ When I transfer $20 from savings to checking
+ Then my savings account balance should be $80
+ And my checking account balance should be $30
+
+ Scenario: savings account has insufficient funds
+ Given my savings account balance is $50
+ And my checking account balance is $10
+ When I transfer $60 from savings to checking
+ Then my savings account balance should be $50
+ And my checking account balance should be $10
+
+
+Each Given, When and Then is a Step. The Ands are each the same kind as the previous Step.
+Steps get defined in Ruby like this (detail left out for brevity):
+
+<% coderay(:lang => 'ruby') do -%>
+steps_for(:accounts) do
+ Given("my $account_type account balance is $amount") do |account_type, amount|
+ create_account(account_type, amount)
+ end
+ When("I transfer $amount from $source_account to $target_account") do |amount, source_account, target_account|
+ get_account(source_account).transfer(amount).to(get_account(target_account))
+ end
+ Then("my $account_type account balance should be $amount") do |account_type, amount|
+ get_account(account_type).should have_a_balance_of(amount)
+ end
+end
+<% end -%>
+
+Then run the Story like this:
+
+<% coderay(:lang => 'ruby') do -%>
+with_steps_for :accounts do
+ run 'path/to/file/with/story'
+end
+<% end -%>
+
+h2. Spec Framework
+
+
+
+
+
+ Start with a very simple example that expresses
+ some basic desired behaviour.
+
+
+<% coderay do -%>
+# bowling_spec.rb
+require 'bowling'
+
+describe Bowling do
+ before(:each) do
+ @bowling = Bowling.new
+ end
+
+ it "should score 0 for gutter game" do
+ 20.times { @bowling.hit(0) }
+ @bowling.score.should == 0
+ end
+end
+<% end -%>
+
+
+ Run the example and watch it fail.
+
+$ spec bowling_spec.rb
+./bowling_spec.rb:4:
+ uninitialized constant Bowling
+
+ |
+
+
+ Now write
+ just enough code to make it pass.
+
+
+<% coderay do -%>
+# bowling.rb
+class Bowling
+ def hit(pins)
+ end
+
+ def score
+ 0
+ end
+end
+<% end -%>
+
+Run the example and bask in the joy that is green.
+
+
+$ spec bowling_spec.rb --format specdoc
+
+Bowling
+- should score 0 for gutter game
+
+Finished in 0.007534 seconds
+
+1 example, 0 failures
+
+ |
+
+
+
+h2. Take very small steps
+
+Don't rush ahead with more code. Instead, add another
+example and let it guide you to what you have
+to do next. And don't forget to take time
+to refactor your code before it gets messy. You should keep
+your code clean at every step of the way.
+
+h2. Take the first step now!
+
+$ gem install rspec
+
+(See special installation instructions for "Spec::Rails":http://github.com/dchelimsky/rspec-rails/wikis/home)
+
diff --git a/website/src/javascripts/application.js b/website/src/javascripts/application.js
new file mode 100644
index 00000000..fe457769
--- /dev/null
+++ b/website/src/javascripts/application.js
@@ -0,0 +1,2 @@
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
diff --git a/website/src/javascripts/controls.js b/website/src/javascripts/controls.js
new file mode 100644
index 00000000..fbc4418b
--- /dev/null
+++ b/website/src/javascripts/controls.js
@@ -0,0 +1,963 @@
+// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// 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.
+
+if(typeof Effect == 'undefined')
+ throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = { }
+Autocompleter.Base = Class.create({
+ baseInitialize: function(element, update, options) {
+ element = $(element)
+ this.element = element;
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+ this.oldElementValue = this.element.value;
+
+ 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);
+ // Force carriage returns as token delimiters anyway
+ if (!this.options.tokens.include('\n'))
+ this.options.tokens.push('\n');
+
+ 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, 'keydown', this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (Prototype.Browser.IE) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+ 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();
+ Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (Prototype.Browser.WebKit > 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;
+ this.getEntry(this.index).scrollIntoView(true);
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++
+ else this.index = 0;
+ this.getEntry(this.index).scrollIntoView(false);
+ },
+
+ 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 = $(selectedElement).select('.' + this.options.select) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var bounds = this.getTokenBounds();
+ if (bounds[0] != -1) {
+ var newValue = this.element.value.substr(0, bounds[0]);
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
+ } else {
+ this.element.value = value;
+ }
+ this.oldElementValue = this.element.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.down());
+
+ if(this.update.firstChild && this.update.down().childNodes) {
+ this.entryCount =
+ this.update.down().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;
+
+ if(this.entryCount==1 && this.options.autoSelect) {
+ this.selectEntry();
+ this.hide();
+ } else {
+ 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;
+ this.tokenBounds = null;
+ if(this.getToken().length>=this.options.minChars) {
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ this.oldElementValue = this.element.value;
+ },
+
+ getToken: function() {
+ var bounds = this.getTokenBounds();
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
+ },
+
+ getTokenBounds: function() {
+ if (null != this.tokenBounds) return this.tokenBounds;
+ var value = this.element.value;
+ if (value.strip().empty()) return [-1, 0];
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
+ var prevTokenPos = -1, nextTokenPos = value.length;
+ var tp;
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+ if (tp > prevTokenPos) prevTokenPos = tp;
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
+ }
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
+ }
+});
+
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+ var boundary = Math.min(newS.length, oldS.length);
+ for (var index = 0; index < boundary; ++index)
+ if (newS[index] != oldS[index])
+ return index;
+ return boundary;
+};
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
+ 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() {
+ this.startIndicator();
+
+ var 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.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("" + elem.substr(0, entry.length) + "" +
+ elem.substr(entry.length) + "");
+ 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("" + elem.substr(0, foundPos) + "" +
+ elem.substr(foundPos, entry.length) + "" + elem.substr(
+ foundPos + entry.length) + "");
+ 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 "";
+ }
+ }, options || { });
+ }
+});
+
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve (April 2007).
+
+// 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({
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = element = $(element);
+ this.prepareOptions();
+ this._controls = { };
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+ Object.extend(this.options, options || { });
+ if (!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + '-inplaceeditor';
+ if ($(this.options.formId))
+ this.options.formId = '';
+ }
+ if (this.options.externalControl)
+ this.options.externalControl = $(this.options.externalControl);
+ if (!this.options.externalControl)
+ this.options.externalControlOnly = false;
+ this._originalBackground = this.element.getStyle('background-color') || 'transparent';
+ this.element.title = this.options.clickToEditText;
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+ this._boundWrapperHandler = this.wrapUp.bind(this);
+ this.registerListeners();
+ },
+ checkForEscapeOrReturn: function(e) {
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+ if (Event.KEY_ESC == e.keyCode)
+ this.handleFormCancellation(e);
+ else if (Event.KEY_RETURN == e.keyCode)
+ this.handleFormSubmission(e);
+ },
+ createControl: function(mode, handler, extraClasses) {
+ var control = this.options[mode + 'Control'];
+ var text = this.options[mode + 'Text'];
+ if ('button' == control) {
+ var btn = document.createElement('input');
+ btn.type = 'submit';
+ btn.value = text;
+ btn.className = 'editor_' + mode + '_button';
+ if ('cancel' == mode)
+ btn.onclick = this._boundCancelHandler;
+ this._form.appendChild(btn);
+ this._controls[mode] = btn;
+ } else if ('link' == control) {
+ var link = document.createElement('a');
+ link.href = '#';
+ link.appendChild(document.createTextNode(text));
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+ link.className = 'editor_' + mode + '_link';
+ if (extraClasses)
+ link.className += ' ' + extraClasses;
+ this._form.appendChild(link);
+ this._controls[mode] = link;
+ }
+ },
+ createEditField: function() {
+ var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+ var fld;
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+ fld = document.createElement('input');
+ fld.type = 'text';
+ var size = this.options.size || this.options.cols || 0;
+ if (0 < size) fld.size = size;
+ } else {
+ fld = document.createElement('textarea');
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+ fld.cols = this.options.cols || 40;
+ }
+ fld.name = this.options.paramName;
+ fld.value = text; // No HTML breaks conversion anymore
+ fld.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ fld.onblur = this._boundSubmitHandler;
+ this._controls.editor = fld;
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+ createForm: function() {
+ var ipe = this;
+ function addText(mode, condition) {
+ var text = ipe.options['text' + mode + 'Controls'];
+ if (!text || condition === false) return;
+ ipe._form.appendChild(document.createTextNode(text));
+ };
+ this._form = $(document.createElement('form'));
+ this._form.id = this.options.formId;
+ this._form.addClassName(this.options.formClassName);
+ this._form.onsubmit = this._boundSubmitHandler;
+ this.createEditField();
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
+ this._form.appendChild(document.createElement('br'));
+ if (this.options.onFormCustomization)
+ this.options.onFormCustomization(this, this._form);
+ addText('Before', this.options.okControl || this.options.cancelControl);
+ this.createControl('ok', this._boundSubmitHandler);
+ addText('Between', this.options.okControl && this.options.cancelControl);
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+ addText('After', this.options.okControl || this.options.cancelControl);
+ },
+ destroy: function() {
+ if (this._oldInnerHTML)
+ this.element.innerHTML = this._oldInnerHTML;
+ this.leaveEditMode();
+ this.unregisterListeners();
+ },
+ enterEditMode: function(e) {
+ if (this._saving || this._editing) return;
+ this._editing = true;
+ this.triggerCallback('onEnterEditMode');
+ if (this.options.externalControl)
+ this.options.externalControl.hide();
+ this.element.hide();
+ this.createForm();
+ this.element.parentNode.insertBefore(this._form, this.element);
+ if (!this.options.loadTextURL)
+ this.postProcessEditField();
+ if (e) Event.stop(e);
+ },
+ enterHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.addClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onEnterHover');
+ },
+ getText: function() {
+ return this.element.innerHTML;
+ },
+ handleAJAXFailure: function(transport) {
+ this.triggerCallback('onFailure', transport);
+ if (this._oldInnerHTML) {
+ this.element.innerHTML = this._oldInnerHTML;
+ this._oldInnerHTML = null;
+ }
+ },
+ handleFormCancellation: function(e) {
+ this.wrapUp();
+ if (e) Event.stop(e);
+ },
+ handleFormSubmission: function(e) {
+ var form = this._form;
+ var value = $F(this._controls.editor);
+ this.prepareSubmission();
+ var params = this.options.callback(form, value) || '';
+ if (Object.isString(params))
+ params = params.toQueryParams();
+ params.editorId = this.element.id;
+ if (this.options.htmlResponse) {
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Updater({ success: this.element }, this.url, options);
+ } else {
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.url, options);
+ }
+ if (e) Event.stop(e);
+ },
+ leaveEditMode: function() {
+ this.element.removeClassName(this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ if (this.options.externalControl)
+ this.options.externalControl.show();
+ this._saving = false;
+ this._editing = false;
+ this._oldInnerHTML = null;
+ this.triggerCallback('onLeaveEditMode');
+ },
+ leaveHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.removeClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onLeaveHover');
+ },
+ loadExternalText: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this._controls.editor.disabled = true;
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._form.removeClassName(this.options.loadingClassName);
+ var text = transport.responseText;
+ if (this.options.stripLoadedTextTags)
+ text = text.stripTags();
+ this._controls.editor.value = text;
+ this._controls.editor.disabled = false;
+ this.postProcessEditField();
+ }.bind(this),
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+ postProcessEditField: function() {
+ var fpc = this.options.fieldPostCreation;
+ if (fpc)
+ $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+ },
+ prepareOptions: function() {
+ this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
+ Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
+ [this._extraDefaultOptions].flatten().compact().each(function(defs) {
+ Object.extend(this.options, defs);
+ }.bind(this));
+ },
+ prepareSubmission: function() {
+ this._saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ registerListeners: function() {
+ this._listeners = { };
+ var listener;
+ $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
+ listener = this[pair.value].bind(this);
+ this._listeners[pair.key] = listener;
+ if (!this.options.externalControlOnly)
+ this.element.observe(pair.key, listener);
+ if (this.options.externalControl)
+ this.options.externalControl.observe(pair.key, listener);
+ }.bind(this));
+ },
+ removeForm: function() {
+ if (!this._form) return;
+ this._form.remove();
+ this._form = null;
+ this._controls = { };
+ },
+ showSaving: function() {
+ this._oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ this.element.addClassName(this.options.savingClassName);
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ },
+ triggerCallback: function(cbName, arg) {
+ if ('function' == typeof this.options[cbName]) {
+ this.options[cbName](this, arg);
+ }
+ },
+ unregisterListeners: function() {
+ $H(this._listeners).each(function(pair) {
+ if (!this.options.externalControlOnly)
+ this.element.stopObserving(pair.key, pair.value);
+ if (this.options.externalControl)
+ this.options.externalControl.stopObserving(pair.key, pair.value);
+ }.bind(this));
+ },
+ wrapUp: function(transport) {
+ this.leaveEditMode();
+ // Can't use triggerCallback due to backward compatibility: requires
+ // binding + direct element
+ this._boundComplete(transport, this.element);
+ }
+});
+
+Object.extend(Ajax.InPlaceEditor.prototype, {
+ dispose: Ajax.InPlaceEditor.prototype.destroy
+});
+
+Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
+ initialize: function($super, element, url, options) {
+ this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
+ $super(element, url, options);
+ },
+
+ createEditField: function() {
+ var list = document.createElement('select');
+ list.name = this.options.paramName;
+ list.size = 1;
+ this._controls.editor = list;
+ this._collection = this.options.collection || [];
+ if (this.options.loadCollectionURL)
+ this.loadCollection();
+ else
+ this.checkForExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+
+ loadCollection: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this.showLoadingText(this.options.loadingCollectionText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ var js = transport.responseText.strip();
+ if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+ throw 'Server returned an invalid collection representation.';
+ this._collection = eval(js);
+ this.checkForExternalText();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadCollectionURL, options);
+ },
+
+ showLoadingText: function(text) {
+ this._controls.editor.disabled = true;
+ var tempOption = this._controls.editor.firstChild;
+ if (!tempOption) {
+ tempOption = document.createElement('option');
+ tempOption.value = '';
+ this._controls.editor.appendChild(tempOption);
+ tempOption.selected = true;
+ }
+ tempOption.update((text || '').stripScripts().stripTags());
+ },
+
+ checkForExternalText: function() {
+ this._text = this.getText();
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ else
+ this.buildOptionList();
+ },
+
+ loadExternalText: function() {
+ this.showLoadingText(this.options.loadingText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._text = transport.responseText.strip();
+ this.buildOptionList();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+
+ buildOptionList: function() {
+ this._form.removeClassName(this.options.loadingClassName);
+ this._collection = this._collection.map(function(entry) {
+ return 2 === entry.length ? entry : [entry, entry].flatten();
+ });
+ var marker = ('value' in this.options) ? this.options.value : this._text;
+ var textFound = this._collection.any(function(entry) {
+ return entry[0] == marker;
+ }.bind(this));
+ this._controls.editor.update('');
+ var option;
+ this._collection.each(function(entry, index) {
+ option = document.createElement('option');
+ option.value = entry[0];
+ option.selected = textFound ? entry[0] == marker : 0 == index;
+ option.appendChild(document.createTextNode(entry[1]));
+ this._controls.editor.appendChild(option);
+ }.bind(this));
+ this._controls.editor.disabled = false;
+ Field.scrollFreeActivate(this._controls.editor);
+ }
+});
+
+//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
+//**** This only exists for a while, in order to let ****
+//**** users adapt to the new API. Read up on the new ****
+//**** API and convert your code to it ASAP! ****
+
+Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
+ if (!options) return;
+ function fallback(name, expr) {
+ if (name in options || expr === undefined) return;
+ options[name] = expr;
+ };
+ fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
+ options.cancelLink == options.cancelButton == false ? false : undefined)));
+ fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
+ options.okLink == options.okButton == false ? false : undefined)));
+ fallback('highlightColor', options.highlightcolor);
+ fallback('highlightEndColor', options.highlightendcolor);
+};
+
+Object.extend(Ajax.InPlaceEditor, {
+ DefaultOptions: {
+ ajaxOptions: { },
+ autoRows: 3, // Use when multi-line w/ rows == 1
+ cancelControl: 'link', // 'link'|'button'|false
+ cancelText: 'cancel',
+ clickToEditText: 'Click to edit',
+ externalControl: null, // id|elt
+ externalControlOnly: false,
+ fieldPostCreation: 'activate', // 'activate'|'focus'|false
+ formClassName: 'inplaceeditor-form',
+ formId: null, // id|elt
+ highlightColor: '#ffff99',
+ highlightEndColor: '#ffffff',
+ hoverClassName: '',
+ htmlResponse: true,
+ loadingClassName: 'inplaceeditor-loading',
+ loadingText: 'Loading...',
+ okControl: 'button', // 'link'|'button'|false
+ okText: 'ok',
+ paramName: 'value',
+ rows: 1, // If 1 and multi-line, uses autoRows
+ savingClassName: 'inplaceeditor-saving',
+ savingText: 'Saving...',
+ size: 0,
+ stripLoadedTextTags: false,
+ submitOnBlur: false,
+ textAfterControls: '',
+ textBeforeControls: '',
+ textBetweenControls: ''
+ },
+ DefaultCallbacks: {
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ onComplete: function(transport, element) {
+ // For backward compatibility, this one is bound to the IPE, and passes
+ // the element directly. It was too often customized, so we don't break it.
+ new Effect.Highlight(element, {
+ startcolor: this.options.highlightColor, keepBackgroundImage: true });
+ },
+ onEnterEditMode: null,
+ onEnterHover: function(ipe) {
+ ipe.element.style.backgroundColor = ipe.options.highlightColor;
+ if (ipe._effect)
+ ipe._effect.cancel();
+ },
+ onFailure: function(transport, ipe) {
+ alert('Error communication with the server: ' + transport.responseText.stripTags());
+ },
+ onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
+ onLeaveEditMode: null,
+ onLeaveHover: function(ipe) {
+ ipe._effect = new Effect.Highlight(ipe.element, {
+ startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+ restorecolor: ipe._originalBackground, keepBackgroundImage: true
+ });
+ }
+ },
+ Listeners: {
+ click: 'enterEditMode',
+ keydown: 'checkForEscapeOrReturn',
+ mouseover: 'enterHover',
+ mouseout: 'leaveHover'
+ }
+});
+
+Ajax.InPlaceCollectionEditor.DefaultOptions = {
+ loadingCollectionText: 'Loading options...'
+};
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create({
+ 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));
+ }
+});
diff --git a/website/src/javascripts/dragdrop.js b/website/src/javascripts/dragdrop.js
new file mode 100644
index 00000000..ccf4a1e4
--- /dev/null
+++ b/website/src/javascripts/dragdrop.js
@@ -0,0 +1,972 @@
+// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+if(Object.isUndefined(Effect))
+ throw("dragdrop.js requires including script.aculo.us' effects.js library");
+
+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(Object.isArray(containment)) {
+ 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 drop, affected = [];
+
+ this.drops.each( function(drop) {
+ if(Droppables.isAffected(point, element, drop))
+ affected.push(drop);
+ });
+
+ if(affected.length>0)
+ drop = Droppables.findDeepestChild(affected);
+
+ if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
+ if (drop) {
+ Position.within(drop.element, point[0], point[1]);
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+
+ if (drop != this.last_active) Droppables.activate(drop);
+ }
+ },
+
+ fire: function(event, element) {
+ if(!this.last_active) return;
+ Position.prepare();
+
+ if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+ if (this.last_active.onDrop) {
+ this.last_active.onDrop(element, this.last_active.element, event);
+ return true;
+ }
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+}
+
+var Draggables = {
+ drags: [],
+ observers: [],
+
+ register: function(draggable) {
+ if(this.drags.length == 0) {
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
+
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", this.eventKeypress);
+ }
+ this.drags.push(draggable);
+ },
+
+ unregister: function(draggable) {
+ this.drags = this.drags.reject(function(d) { return d==draggable });
+ if(this.drags.length == 0) {
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", this.eventKeypress);
+ }
+ },
+
+ activate: function(draggable) {
+ if(draggable.options.delay) {
+ this._timeout = setTimeout(function() {
+ Draggables._timeout = null;
+ window.focus();
+ Draggables.activeDraggable = draggable;
+ }.bind(this), draggable.options.delay);
+ } else {
+ window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+ this.activeDraggable = draggable;
+ }
+ },
+
+ deactivate: function() {
+ this.activeDraggable = null;
+ },
+
+ updateDrag: function(event) {
+ if(!this.activeDraggable) return;
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ // Mozilla-based browsers fire successive mousemove events with
+ // the same coordinates, prevent needless redrawing (moz bug?)
+ if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+ this._lastPointer = pointer;
+
+ this.activeDraggable.updateDrag(event, pointer);
+ },
+
+ endDrag: function(event) {
+ if(this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+ if(!this.activeDraggable) return;
+ this._lastPointer = null;
+ this.activeDraggable.endDrag(event);
+ this.activeDraggable = null;
+ },
+
+ keyPress: function(event) {
+ if(this.activeDraggable)
+ this.activeDraggable.keyPress(event);
+ },
+
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ this._cacheObserverCallbacks();
+ },
+
+ removeObserver: function(element) { // element instead of observer fixes mem leaks
+ this.observers = this.observers.reject( function(o) { return o.element==element });
+ this._cacheObserverCallbacks();
+ },
+
+ notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
+ if(this[eventName+'Count'] > 0)
+ this.observers.each( function(o) {
+ if(o[eventName]) o[eventName](eventName, draggable, event);
+ });
+ if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
+ },
+
+ _cacheObserverCallbacks: function() {
+ ['onStart','onEnd','onDrag'].each( function(eventName) {
+ Draggables[eventName+'Count'] = Draggables.observers.select(
+ function(o) { return o[eventName]; }
+ ).length;
+ });
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create({
+ initialize: function(element) {
+ var defaults = {
+ handle: false,
+ reverteffect: function(element, top_offset, left_offset) {
+ var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+ new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
+ queue: {scope:'_draggable', position:'end'}
+ });
+ },
+ endeffect: function(element) {
+ var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
+ queue: {scope:'_draggable', position:'end'},
+ afterFinish: function(){
+ Draggable._dragging[element] = false
+ }
+ });
+ },
+ zindex: 1000,
+ revert: false,
+ quiet: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
+ delay: 0
+ };
+
+ if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
+ Object.extend(defaults, {
+ starteffect: function(element) {
+ element._opacity = Element.getOpacity(element);
+ Draggable._dragging[element] = true;
+ new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
+ }
+ });
+
+ var options = Object.extend(defaults, arguments[1] || { });
+
+ this.element = $(element);
+
+ if(options.handle && Object.isString(options.handle))
+ this.handle = this.element.down('.'+options.handle, 0);
+
+ if(!this.handle) this.handle = $(options.handle);
+ if(!this.handle) this.handle = this.element;
+
+ if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
+ options.scroll = $(options.scroll);
+ this._isScrollChild = Element.childOf(this.element, options.scroll);
+ }
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.options = options;
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+ Draggables.unregister(this);
+ },
+
+ currentDelta: function() {
+ return([
+ parseInt(Element.getStyle(this.element,'left') || '0'),
+ parseInt(Element.getStyle(this.element,'top') || '0')]);
+ },
+
+ initDrag: function(event) {
+ if(!Object.isUndefined(Draggable._dragging[this.element]) &&
+ Draggable._dragging[this.element]) return;
+ if(Event.isLeftClick(event)) {
+ // abort on form elements, fixes a Firefox issue
+ var src = Event.element(event);
+ if((tag_name = src.tagName.toUpperCase()) && (
+ tag_name=='INPUT' ||
+ tag_name=='SELECT' ||
+ tag_name=='OPTION' ||
+ tag_name=='BUTTON' ||
+ tag_name=='TEXTAREA')) return;
+
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var pos = Position.cumulativeOffset(this.element);
+ this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+
+ Draggables.activate(this);
+ Event.stop(event);
+ }
+ },
+
+ startDrag: function(event) {
+ this.dragging = true;
+ if(!this.delta)
+ this.delta = this.currentDelta();
+
+ if(this.options.zindex) {
+ this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+ this.element.style.zIndex = this.options.zindex;
+ }
+
+ if(this.options.ghosting) {
+ this._clone = this.element.cloneNode(true);
+ this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
+ if (!this.element._originallyAbsolute)
+ Position.absolutize(this.element);
+ this.element.parentNode.insertBefore(this._clone, this.element);
+ }
+
+ if(this.options.scroll) {
+ if (this.options.scroll == window) {
+ var where = this._getWindowScroll(this.options.scroll);
+ this.originalScrollLeft = where.left;
+ this.originalScrollTop = where.top;
+ } else {
+ this.originalScrollLeft = this.options.scroll.scrollLeft;
+ this.originalScrollTop = this.options.scroll.scrollTop;
+ }
+ }
+
+ Draggables.notify('onStart', this, event);
+
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ },
+
+ updateDrag: function(event, pointer) {
+ if(!this.dragging) this.startDrag(event);
+
+ if(!this.options.quiet){
+ Position.prepare();
+ Droppables.show(pointer, this.element);
+ }
+
+ Draggables.notify('onDrag', this, event);
+
+ this.draw(pointer);
+ if(this.options.change) this.options.change(this);
+
+ if(this.options.scroll) {
+ this.stopScrolling();
+
+ var p;
+ if (this.options.scroll == window) {
+ with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+ } else {
+ p = Position.page(this.options.scroll);
+ p[0] += this.options.scroll.scrollLeft + Position.deltaX;
+ p[1] += this.options.scroll.scrollTop + Position.deltaY;
+ p.push(p[0]+this.options.scroll.offsetWidth);
+ p.push(p[1]+this.options.scroll.offsetHeight);
+ }
+ var speed = [0,0];
+ if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+ if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+ if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+ if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+ this.startScrolling(speed);
+ }
+
+ // fix AppleWebKit rendering
+ if(Prototype.Browser.WebKit) window.scrollBy(0,0);
+
+ Event.stop(event);
+ },
+
+ finishDrag: function(event, success) {
+ this.dragging = false;
+
+ if(this.options.quiet){
+ Position.prepare();
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ Droppables.show(pointer, this.element);
+ }
+
+ if(this.options.ghosting) {
+ if (!this.element._originallyAbsolute)
+ Position.relativize(this.element);
+ delete this.element._originallyAbsolute;
+ Element.remove(this._clone);
+ this._clone = null;
+ }
+
+ var dropped = false;
+ if(success) {
+ dropped = Droppables.fire(event, this.element);
+ if (!dropped) dropped = false;
+ }
+ if(dropped && this.options.onDropped) this.options.onDropped(this.element);
+ Draggables.notify('onEnd', this, event);
+
+ var revert = this.options.revert;
+ if(revert && Object.isFunction(revert)) revert = revert(this.element);
+
+ var d = this.currentDelta();
+ if(revert && this.options.reverteffect) {
+ if (dropped == 0 || revert != 'failure')
+ this.options.reverteffect(this.element,
+ d[1]-this.delta[1], d[0]-this.delta[0]);
+ } else {
+ this.delta = d;
+ }
+
+ if(this.options.zindex)
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Draggables.deactivate(this);
+ Droppables.reset();
+ },
+
+ keyPress: function(event) {
+ if(event.keyCode!=Event.KEY_ESC) return;
+ this.finishDrag(event, false);
+ Event.stop(event);
+ },
+
+ endDrag: function(event) {
+ if(!this.dragging) return;
+ this.stopScrolling();
+ this.finishDrag(event, true);
+ Event.stop(event);
+ },
+
+ draw: function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ if(this.options.ghosting) {
+ var r = Position.realOffset(this.element);
+ pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+ }
+
+ var d = this.currentDelta();
+ pos[0] -= d[0]; pos[1] -= d[1];
+
+ if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
+ pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+ pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+ }
+
+ var p = [0,1].map(function(i){
+ return (point[i]-pos[i]-this.offset[i])
+ }.bind(this));
+
+ if(this.options.snap) {
+ if(Object.isFunction(this.options.snap)) {
+ p = this.options.snap(p[0],p[1],this);
+ } else {
+ if(Object.isArray(this.options.snap)) {
+ p = p.map( function(v, i) {
+ return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
+ } else {
+ p = p.map( function(v) {
+ return (v/this.options.snap).round()*this.options.snap }.bind(this))
+ }
+ }}
+
+ var style = this.element.style;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ },
+
+ stopScrolling: function() {
+ if(this.scrollInterval) {
+ clearInterval(this.scrollInterval);
+ this.scrollInterval = null;
+ Draggables._lastScrollPointer = null;
+ }
+ },
+
+ startScrolling: function(speed) {
+ if(!(speed[0] || speed[1])) return;
+ this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+ this.lastScrolled = new Date();
+ this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+ },
+
+ scroll: function() {
+ var current = new Date();
+ var delta = current - this.lastScrolled;
+ this.lastScrolled = current;
+ if(this.options.scroll == window) {
+ with (this._getWindowScroll(this.options.scroll)) {
+ if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+ var d = delta / 1000;
+ this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+ }
+ }
+ } else {
+ this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+ this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
+ }
+
+ Position.prepare();
+ Droppables.show(Draggables._lastPointer, this.element);
+ Draggables.notify('onDrag', this);
+ if (this._isScrollChild) {
+ Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+ Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+ Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+ if (Draggables._lastScrollPointer[0] < 0)
+ Draggables._lastScrollPointer[0] = 0;
+ if (Draggables._lastScrollPointer[1] < 0)
+ Draggables._lastScrollPointer[1] = 0;
+ this.draw(Draggables._lastScrollPointer);
+ }
+
+ if(this.options.change) this.options.change(this);
+ },
+
+ _getWindowScroll: function(w) {
+ var T, L, W, H;
+ with (w.document) {
+ if (w.document.documentElement && documentElement.scrollTop) {
+ T = documentElement.scrollTop;
+ L = documentElement.scrollLeft;
+ } else if (w.document.body) {
+ T = body.scrollTop;
+ L = body.scrollLeft;
+ }
+ if (w.innerWidth) {
+ W = w.innerWidth;
+ H = w.innerHeight;
+ } else if (w.document.documentElement && documentElement.clientWidth) {
+ W = documentElement.clientWidth;
+ H = documentElement.clientHeight;
+ } else {
+ W = body.offsetWidth;
+ H = body.offsetHeight
+ }
+ }
+ return { top: T, left: L, width: W, height: H };
+ }
+});
+
+Draggable._dragging = { };
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create({
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onEnd: function() {
+ Sortable.unmark();
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+});
+
+var Sortable = {
+ SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
+
+ sortables: { },
+
+ _findRootElement: function(element) {
+ while (element.tagName.toUpperCase() != "BODY") {
+ if(element.id && Sortable.sortables[element.id]) return element;
+ element = element.parentNode;
+ }
+ },
+
+ options: function(element) {
+ element = Sortable._findRootElement($(element));
+ if(!element) return;
+ return Sortable.sortables[element.id];
+ },
+
+ destroy: function(element){
+ var s = Sortable.options(element);
+
+ if(s) {
+ Draggables.removeObserver(s.element);
+ s.droppables.each(function(d){ Droppables.remove(d) });
+ s.draggables.invoke('destroy');
+
+ delete Sortable.sortables[s.element.id];
+ }
+ },
+
+ create: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ element: element,
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ dropOnEmpty: false,
+ tree: false,
+ treeTag: 'ul',
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ delay: 0,
+ hoverclass: null,
+ ghosting: false,
+ quiet: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ format: this.SERIALIZE_RULE,
+
+ // these take arrays of elements or ids and can be
+ // used for better initialization performance
+ elements: false,
+ handles: false,
+
+ onChange: Prototype.emptyFunction,
+ onUpdate: Prototype.emptyFunction
+ }, arguments[1] || { });
+
+ // clear any old sortable with same element
+ this.destroy(element);
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ quiet: options.quiet,
+ scroll: options.scroll,
+ scrollSpeed: options.scrollSpeed,
+ scrollSensitivity: options.scrollSensitivity,
+ delay: options.delay,
+ ghosting: options.ghosting,
+ constraint: options.constraint,
+ handle: options.handle };
+
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ else
+ if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+ element.style.top = 0;
+ element.style.left = 0;
+ };
+
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ tree: options.tree,
+ hoverclass: options.hoverclass,
+ onHover: Sortable.onHover
+ }
+
+ var options_for_tree = {
+ onHover: Sortable.onEmptyHover,
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass
+ }
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ options.draggables = [];
+ options.droppables = [];
+
+ // drop on empty handling
+ if(options.dropOnEmpty || options.tree) {
+ Droppables.add(element, options_for_tree);
+ options.droppables.push(element);
+ }
+
+ (options.elements || this.findElements(element, options) || []).each( function(e,i) {
+ var handle = options.handles ? $(options.handles[i]) :
+ (options.handle ? $(e).select('.' + options.handle)[0] : e);
+ options.draggables.push(
+ new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+ Droppables.add(e, options_for_droppable);
+ if(options.tree) e.treeNode = element;
+ options.droppables.push(e);
+ });
+
+ if(options.tree) {
+ (Sortable.findTreeElements(element, options) || []).each( function(e) {
+ Droppables.add(e, options_for_tree);
+ e.treeNode = element;
+ options.droppables.push(e);
+ });
+ }
+
+ // keep reference
+ this.sortables[element.id] = options;
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ },
+
+ // return all suitable-for-sortable elements in a guaranteed order
+ findElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.tag);
+ },
+
+ findTreeElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.treeTag);
+ },
+
+ onHover: function(element, dropon, overlap) {
+ if(Element.isParent(dropon, element)) return;
+
+ if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+ return;
+ } else if(overlap>0.5) {
+ Sortable.mark(dropon, 'before');
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ } else {
+ Sortable.mark(dropon, 'after');
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ }
+ },
+
+ onEmptyHover: function(element, dropon, overlap) {
+ var oldParentNode = element.parentNode;
+ var droponOptions = Sortable.options(dropon);
+
+ if(!Element.isParent(dropon, element)) {
+ var index;
+
+ var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
+ var child = null;
+
+ if(children) {
+ var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+
+ for (index = 0; index < children.length; index += 1) {
+ if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+ offset -= Element.offsetSize (children[index], droponOptions.overlap);
+ } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+ child = index + 1 < children.length ? children[index + 1] : null;
+ break;
+ } else {
+ child = children[index];
+ break;
+ }
+ }
+ }
+
+ dropon.insertBefore(element, child);
+
+ Sortable.options(oldParentNode).onChange(element);
+ droponOptions.onChange(element);
+ }
+ },
+
+ unmark: function() {
+ if(Sortable._marker) Sortable._marker.hide();
+ },
+
+ mark: function(dropon, position) {
+ // mark on ghosting only
+ var sortable = Sortable.options(dropon.parentNode);
+ if(sortable && !sortable.ghosting) return;
+
+ if(!Sortable._marker) {
+ Sortable._marker =
+ ($('dropmarker') || Element.extend(document.createElement('DIV'))).
+ hide().addClassName('dropmarker').setStyle({position:'absolute'});
+ document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+ }
+ var offsets = Position.cumulativeOffset(dropon);
+ Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
+
+ if(position=='after')
+ if(sortable.overlap == 'horizontal')
+ Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
+ else
+ Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
+
+ Sortable._marker.show();
+ },
+
+ _tree: function(element, options, parent) {
+ var children = Sortable.findElements(element, options) || [];
+
+ for (var i = 0; i < children.length; ++i) {
+ var match = children[i].id.match(options.format);
+
+ if (!match) continue;
+
+ var child = {
+ id: encodeURIComponent(match ? match[1] : null),
+ element: element,
+ parent: parent,
+ children: [],
+ position: parent.children.length,
+ container: $(children[i]).down(options.treeTag)
+ }
+
+ /* Get the element containing the children and recurse over it */
+ if (child.container)
+ this._tree(child.container, options, child)
+
+ parent.children.push (child);
+ }
+
+ return parent;
+ },
+
+ tree: function(element) {
+ element = $(element);
+ var sortableOptions = this.options(element);
+ var options = Object.extend({
+ tag: sortableOptions.tag,
+ treeTag: sortableOptions.treeTag,
+ only: sortableOptions.only,
+ name: element.id,
+ format: sortableOptions.format
+ }, arguments[1] || { });
+
+ var root = {
+ id: null,
+ parent: null,
+ children: [],
+ container: element,
+ position: 0
+ }
+
+ return Sortable._tree(element, options, root);
+ },
+
+ /* Construct a [i] index for a particular node */
+ _constructIndex: function(node) {
+ var index = '';
+ do {
+ if (node.id) index = '[' + node.position + ']' + index;
+ } while ((node = node.parent) != null);
+ return index;
+ },
+
+ sequence: function(element) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[1] || { });
+
+ return $(this.findElements(element, options) || []).map( function(item) {
+ return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+ });
+ },
+
+ setSequence: function(element, new_sequence) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[2] || { });
+
+ var nodeMap = { };
+ this.findElements(element, options).each( function(n) {
+ if (n.id.match(options.format))
+ nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+ n.parentNode.removeChild(n);
+ });
+
+ new_sequence.each(function(ident) {
+ var n = nodeMap[ident];
+ if (n) {
+ n[1].appendChild(n[0]);
+ delete nodeMap[ident];
+ }
+ });
+ },
+
+ serialize: function(element) {
+ element = $(element);
+ var options = Object.extend(Sortable.options(element), arguments[1] || { });
+ var name = encodeURIComponent(
+ (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
+
+ if (options.tree) {
+ return Sortable.tree(element, arguments[1]).children.map( function (item) {
+ return [name + Sortable._constructIndex(item) + "[id]=" +
+ encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+ }).flatten().join('&');
+ } else {
+ return Sortable.sequence(element, arguments[1]).map( function(item) {
+ return name + "[]=" + encodeURIComponent(item);
+ }).join('&');
+ }
+ }
+}
+
+// Returns true if child is contained within element
+Element.isParent = function(child, element) {
+ if (!child.parentNode || child == element) return false;
+ if (child.parentNode == element) return true;
+ return Element.isParent(child.parentNode, element);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {
+ if(!element.hasChildNodes()) return null;
+ tagName = tagName.toUpperCase();
+ if(only) only = [only].flatten();
+ var elements = [];
+ $A(element.childNodes).each( function(e) {
+ if(e.tagName && e.tagName.toUpperCase()==tagName &&
+ (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+ elements.push(e);
+ if(recursive) {
+ var grandchildren = Element.findChildren(e, only, recursive, tagName);
+ if(grandchildren) elements.push(grandchildren);
+ }
+ });
+
+ return (elements.length>0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+ return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
+}
diff --git a/website/src/javascripts/effects.js b/website/src/javascripts/effects.js
new file mode 100644
index 00000000..65aed239
--- /dev/null
+++ b/website/src/javascripts/effects.js
@@ -0,0 +1,1120 @@
+// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if (this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if (this.slice(0,1) == '#') {
+ if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if (this.length==7) color = this.toLowerCase();
+ }
+ }
+ return (color.length==7 ? color : (arguments[0] || this));
+};
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+ }).flatten().join('');
+};
+
+Element.collectTextNodesIgnoreClass = function(element, className) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+};
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ element.setStyle({fontSize: (percent/100) + 'em'});
+ if (Prototype.Browser.WebKit) window.scrollBy(0,0);
+ return element;
+};
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+};
+
+Element.forceRerendering = function(element) {
+ try {
+ element = $(element);
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ _elementDoesNotExistError: {
+ name: 'ElementDoesNotExistError',
+ message: 'The specified DOM element does not exist, but is required for this effect to operate'
+ },
+ Transitions: {
+ linear: Prototype.K,
+ sinoidal: function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + 0.5;
+ },
+ reverse: function(pos) {
+ return 1-pos;
+ },
+ flicker: function(pos) {
+ var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+ return pos > 1 ? 1 : pos;
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+ },
+ pulse: function(pos, pulses) {
+ pulses = pulses || 5;
+ return (
+ ((pos % (1/pulses)) * pulses).round() == 0 ?
+ ((pos * pulses * 2) - (pos * pulses * 2).floor()) :
+ 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor())
+ );
+ },
+ spring: function(pos) {
+ return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+ },
+ DefaultOptions: {
+ duration: 1.0, // seconds
+ fps: 100, // 100= assume 66fps max.
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+ },
+ tagifyText: function(element) {
+ var tagifyStyle = 'position:relative';
+ if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
+
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if (child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ new Element('span', {style: tagifyStyle}).update(
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if (((typeof element == 'object') ||
+ Object.isFunction(element)) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || { });
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ },
+ PAIRS: {
+ 'slide': ['SlideDown','SlideUp'],
+ 'blind': ['BlindDown','BlindUp'],
+ 'appear': ['Appear','Fade']
+ },
+ toggle: function(element, effect) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+ var options = Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, arguments[2] || { });
+ Effect[element.visible() ?
+ Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+ }
+};
+
+Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create(Enumerable, {
+ initialize: function() {
+ this.effects = [];
+ this.interval = null;
+ },
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ var position = Object.isString(effect.options.queue) ?
+ effect.options.queue : effect.options.queue.position;
+
+ switch(position) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'with-last':
+ timestamp = this.effects.pluck('startOn').max() || timestamp;
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+
+ if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if (!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 15);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if (this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ for(var i=0, len=this.effects.length;i= this.startOn) {
+ if (timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if (this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / this.totalTime,
+ frame = (pos * this.totalFrames).round();
+ if (frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ cancel: function() {
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ 'global' : this.options.queue.scope).remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if (this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ var data = $H();
+ for(property in this)
+ if (!Object.isFunction(this[property])) data.set(property, this[property]);
+ return '#';
+ }
+});
+
+Effect.Parallel = Class.create(Effect.Base, {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if (effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Tween = Class.create(Effect.Base, {
+ initialize: function(object, from, to) {
+ object = Object.isString(object) ? $(object) : object;
+ var args = $A(arguments), method = args.last(),
+ options = args.length == 5 ? args[3] : null;
+ this.method = Object.isFunction(method) ? method.bind(object) :
+ Object.isFunction(object[method]) ? object[method].bind(object) :
+ function(value) { object[method] = value };
+ this.start(Object.extend({ from: from, to: to }, options || { }));
+ },
+ update: function(position) {
+ this.method(position);
+ }
+});
+
+Effect.Event = Class.create(Effect.Base, {
+ initialize: function() {
+ this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
+ },
+ update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ // make this work on IE on elements without 'layout'
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ var options = Object.extend({
+ from: this.element.getOpacity() || 0.0,
+ to: 1.0
+ }, arguments[1] || { });
+ this.start(options);
+ },
+ update: function(position) {
+ this.element.setOpacity(position);
+ }
+});
+
+Effect.Move = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ x: 0,
+ y: 0,
+ mode: 'relative'
+ }, arguments[1] || { });
+ this.start(options);
+ },
+ setup: function() {
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if (this.options.mode == 'absolute') {
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: (this.options.x * position + this.originalLeft).round() + 'px',
+ top: (this.options.y * position + this.originalTop).round() + 'px'
+ });
+ }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+ return new Effect.Move(element,
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
+};
+
+Effect.Scale = Class.create(Effect.Base, {
+ initialize: function(element, percent) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or { } with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || { });
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = this.element.getStyle('position');
+
+ this.originalStyle = { };
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = this.element.getStyle('font-size') || '100%';
+ ['em','px','%','pt'].each( function(fontSizeType) {
+ if (fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if (this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if (/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if (!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if (this.options.scaleContent && this.fontSize)
+ this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = { };
+ if (this.options.scaleX) d.width = width.round() + 'px';
+ if (this.options.scaleY) d.height = height.round() + 'px';
+ if (this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if (this.elementPositioning == 'absolute') {
+ if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if (this.options.scaleY) d.top = -topd + 'px';
+ if (this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ this.element.setStyle(d);
+ }
+});
+
+Effect.Highlight = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if (this.element.getStyle('display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = { };
+ if (!this.options.keepBackgroundImage) {
+ this.oldStyle.backgroundImage = this.element.getStyle('background-image');
+ this.element.setStyle({backgroundImage: 'none'});
+ }
+ if (!this.options.endcolor)
+ this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+ if (!this.options.restorecolor)
+ this.options.restorecolor = this.element.getStyle('background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = function(element) {
+ var options = arguments[1] || { },
+ scrollOffsets = document.viewport.getScrollOffsets(),
+ elementOffsets = $(element).cumulativeOffset(),
+ max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();
+
+ if (options.offset) elementOffsets[1] += options.offset;
+
+ return new Effect.Tween(null,
+ scrollOffsets.top,
+ elementOffsets[1] > max ? max : elementOffsets[1],
+ options,
+ function(p){ scrollTo(scrollOffsets.left, p.round()) }
+ );
+};
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ var options = Object.extend({
+ from: element.getOpacity() || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) {
+ if (effect.options.to!=0) return;
+ effect.element.hide().setStyle({opacity: oldOpacity});
+ }
+ }, arguments[1] || { });
+ return new Effect.Opacity(element,options);
+};
+
+Effect.Appear = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+ to: 1.0,
+ // force Safari to render floated elements properly
+ afterFinishInternal: function(effect) {
+ effect.element.forceRerendering();
+ },
+ beforeSetup: function(effect) {
+ effect.element.setOpacity(effect.options.from).show();
+ }}, arguments[1] || { });
+ return new Effect.Opacity(element,options);
+};
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = {
+ opacity: element.getInlineOpacity(),
+ position: element.getStyle('position'),
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height
+ };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) {
+ Position.absolutize(effect.effects[0].element)
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().setStyle(oldStyle); }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ element.makeClipping();
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ return new Effect.Appear(element, Object.extend({
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+ }
+ })
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left'),
+ opacity: element.getInlineOpacity() };
+ return new Effect.Parallel(
+ [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) {
+ effect.effects[0].element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ distance: 20,
+ duration: 0.5
+ }, arguments[1] || {});
+ var distance = parseFloat(options.distance);
+ var split = parseFloat(options.duration) / 10.0;
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
+ effect.element.undoPositioned().setStyle(oldStyle);
+ }}) }}) }}) }}) }}) }});
+};
+
+Effect.SlideDown = function(element) {
+ element = $(element).cleanWhitespace();
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: window.opera ? 0 : 1,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if (window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.SlideUp = function(element) {
+ element = $(element).cleanWhitespace();
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if (window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
+ }
+ }, arguments[1] || { })
+ );
+};
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0, {
+ restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ });
+};
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || { });
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.Move(element, {
+ x: initialMoveX,
+ y: initialMoveY,
+ duration: 0.01,
+ beforeSetup: function(effect) {
+ effect.element.hide().makeClipping().makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) {
+ effect.effects[0].element.setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
+ }
+ }, options)
+ )
+ }
+ });
+};
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || { });
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) {
+ effect.effects[0].element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+ }, options)
+ );
+};
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || { };
+ var oldOpacity = element.getInlineOpacity();
+ var transition = options.transition || Effect.Transitions.sinoidal;
+ var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
+ reverser.bind(transition);
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 2.0, from: 0,
+ afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+};
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ element.makeClipping();
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().setStyle(oldStyle);
+ } });
+ }}, arguments[1] || { }));
+};
+
+Effect.Morph = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ style: { }
+ }, arguments[1] || { });
+
+ if (!Object.isString(options.style)) this.style = $H(options.style);
+ else {
+ if (options.style.include(':'))
+ this.style = options.style.parseStyle();
+ else {
+ this.element.addClassName(options.style);
+ this.style = $H(this.element.getStyles());
+ this.element.removeClassName(options.style);
+ var css = this.element.getStyles();
+ this.style = this.style.reject(function(style) {
+ return style.value == css[style.key];
+ });
+ options.afterFinishInternal = function(effect) {
+ effect.element.addClassName(effect.options.style);
+ effect.transforms.each(function(transform) {
+ effect.element.style[transform.style] = '';
+ });
+ }
+ }
+ }
+ this.start(options);
+ },
+
+ setup: function(){
+ function parseColor(color){
+ if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+ color = color.parseColor();
+ return $R(0,2).map(function(i){
+ return parseInt( color.slice(i*2+1,i*2+3), 16 )
+ });
+ }
+ this.transforms = this.style.map(function(pair){
+ var property = pair[0], value = pair[1], unit = null;
+
+ if (value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if (property == 'opacity') {
+ value = parseFloat(value);
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ } else if (Element.CSS_LENGTH.test(value)) {
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
+ value = parseFloat(components[1]);
+ unit = (components.length == 3) ? components[2] : null;
+ }
+
+ var originalValue = this.element.getStyle(property);
+ return {
+ style: property.camelize(),
+ originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: unit=='color' ? parseColor(value) : value,
+ unit: unit
+ };
+ }.bind(this)).reject(function(transform){
+ return (
+ (transform.originalValue == transform.targetValue) ||
+ (
+ transform.unit != 'color' &&
+ (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+ )
+ )
+ });
+ },
+ update: function(position) {
+ var style = { }, transform, i = this.transforms.length;
+ while(i--)
+ style[(transform = this.transforms[i]).style] =
+ transform.unit=='color' ? '#'+
+ (Math.round(transform.originalValue[0]+
+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
+ (Math.round(transform.originalValue[1]+
+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
+ (Math.round(transform.originalValue[2]+
+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
+ (transform.originalValue +
+ (transform.targetValue - transform.originalValue) * position).toFixed(3) +
+ (transform.unit === null ? '' : transform.unit);
+ this.element.setStyle(style, true);
+ }
+});
+
+Effect.Transform = Class.create({
+ initialize: function(tracks){
+ this.tracks = [];
+ this.options = arguments[1] || { };
+ this.addTracks(tracks);
+ },
+ addTracks: function(tracks){
+ tracks.each(function(track){
+ track = $H(track);
+ var data = track.values().first();
+ this.tracks.push($H({
+ ids: track.keys().first(),
+ effect: Effect.Morph,
+ options: { style: data }
+ }));
+ }.bind(this));
+ return this;
+ },
+ play: function(){
+ return new Effect.Parallel(
+ this.tracks.map(function(track){
+ var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
+ var elements = [$(ids) || $$(ids)].flatten();
+ return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
+ }).flatten(),
+ this.options
+ );
+ }
+});
+
+Element.CSS_PROPERTIES = $w(
+ 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+ 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+ 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+ 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+ 'fontSize fontWeight height left letterSpacing lineHeight ' +
+ 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+ 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+ 'right textIndent top width wordSpacing zIndex');
+
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.__parseStyleElement = document.createElement('div');
+String.prototype.parseStyle = function(){
+ var style, styleRules = $H();
+ if (Prototype.Browser.WebKit)
+ style = new Element('div',{style:this}).style;
+ else {
+ String.__parseStyleElement.innerHTML = '';
+ style = String.__parseStyleElement.childNodes[0].style;
+ }
+
+ Element.CSS_PROPERTIES.each(function(property){
+ if (style[property]) styleRules.set(property, style[property]);
+ });
+
+ if (Prototype.Browser.IE && this.include('opacity'))
+ styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
+
+ return styleRules;
+};
+
+if (document.defaultView && document.defaultView.getComputedStyle) {
+ Element.getStyles = function(element) {
+ var css = document.defaultView.getComputedStyle($(element), null);
+ return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
+ styles[property] = css[property];
+ return styles;
+ });
+ };
+} else {
+ Element.getStyles = function(element) {
+ element = $(element);
+ var css = element.currentStyle, styles;
+ styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) {
+ hash.set(property, css[property]);
+ return hash;
+ });
+ if (!styles.opacity) styles.set('opacity', element.getOpacity());
+ return styles;
+ };
+};
+
+Effect.Methods = {
+ morph: function(element, style) {
+ element = $(element);
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
+ return element;
+ },
+ visualEffect: function(element, effect, options) {
+ element = $(element)
+ var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[klass](element, options);
+ return element;
+ },
+ highlight: function(element, options) {
+ element = $(element);
+ new Effect.Highlight(element, options);
+ return element;
+ }
+};
+
+$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
+ 'pulsate shake puff squish switchOff dropOut').each(
+ function(effect) {
+ Effect.Methods[effect] = function(element, options){
+ element = $(element);
+ Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
+ return element;
+ }
+ }
+);
+
+$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
+ function(f) { Effect.Methods[f] = Element[f]; }
+);
+
+Element.addMethods(Effect.Methods);
diff --git a/website/src/javascripts/prototype.js b/website/src/javascripts/prototype.js
new file mode 100644
index 00000000..546f9fe4
--- /dev/null
+++ b/website/src/javascripts/prototype.js
@@ -0,0 +1,4225 @@
+/* Prototype JavaScript framework, version 1.6.0.1
+ * (c) 2005-2007 Sam Stephenson
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.6.0.1',
+
+ Browser: {
+ IE: !!(window.attachEvent && !window.opera),
+ Opera: !!window.opera,
+ WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
+ MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+ },
+
+ BrowserFeatures: {
+ XPath: !!document.evaluate,
+ ElementExtensions: !!window.HTMLElement,
+ SpecificElementExtensions:
+ document.createElement('div').__proto__ &&
+ document.createElement('div').__proto__ !==
+ document.createElement('form').__proto__
+ },
+
+ ScriptFragment: '