aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/rails-2.0.2/actionpack/lib/action_view
diff options
context:
space:
mode:
authorfrancis <francis>2008-01-23 01:48:14 +0000
committerfrancis <francis>2008-01-23 01:48:14 +0000
commit60eaae4f7df1f1dae91defb87d3707451c359cf4 (patch)
treee74835c37779a2f094e810960cda07b99a75330e /vendor/rails-2.0.2/actionpack/lib/action_view
parent71d22c740302e1f83bbbd89b229734ea9c67493c (diff)
Freeze in rails 2.0.2 (Am I going to regret having this beast in CVS?)
Diffstat (limited to 'vendor/rails-2.0.2/actionpack/lib/action_view')
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/base.rb642
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/compiled_templates.rb69
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/active_record_helper.rb255
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/asset_tag_helper.rb548
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/atom_feed_helper.rb111
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/benchmark_helper.rb31
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/cache_helper.rb39
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/capture_helper.rb162
-rwxr-xr-xvendor/rails-2.0.2/actionpack/lib/action_view/helpers/date_helper.rb689
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/debug_helper.rb31
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_helper.rb689
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_options_helper.rb425
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_tag_helper.rb432
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascript_helper.rb217
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/controls.js963
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/dragdrop.js972
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/effects.js1120
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/prototype.js4225
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/number_helper.rb179
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/prototype_helper.rb1268
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_identification_helper.rb20
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_tag_helper.rb59
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/sanitize_helper.rb223
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/scriptaculous_helper.rb199
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/tag_helper.rb133
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/text_helper.rb479
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/helpers/url_helper.rb524
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/partials.rb200
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/template_error.rb102
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/template_handler.rb17
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/builder.rb19
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/erb.rb21
-rw-r--r--vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/rjs.rb14
33 files changed, 15077 insertions, 0 deletions
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/base.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/base.rb
new file mode 100644
index 000000000..0f966addb
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/base.rb
@@ -0,0 +1,642 @@
+module ActionView #:nodoc:
+ class ActionViewError < StandardError #:nodoc:
+ end
+
+ # Action View templates can be written in three ways. If the template file has a +.erb+ (or +.rhtml+) extension then it uses a mixture of ERb
+ # (included in Ruby) and HTML. If the template file has a +.builder+ (or +.rxml+) extension then Jim Weirich's Builder::XmlMarkup library is used.
+ # If the template file has a +.rjs+ extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.
+ #
+ # = ERb
+ #
+ # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the
+ # following loop for names:
+ #
+ # <b>Names of all the people</b>
+ # <% for person in @people %>
+ # Name: <%= person.name %><br/>
+ # <% end %>
+ #
+ # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this
+ # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong:
+ #
+ # Hi, Mr. <% puts "Frodo" %>
+ #
+ # If you absolutely must write from within a function, you can use the TextHelper#concat
+ #
+ # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>.
+ #
+ # == Using sub templates
+ #
+ # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
+ # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
+ #
+ # <%= render "shared/header" %>
+ # Something really specific and terrific
+ # <%= render "shared/footer" %>
+ #
+ # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
+ # result of the rendering. The output embedding writes it to the current template.
+ #
+ # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
+ # variables defined using the regular embedding tags. Like this:
+ #
+ # <% @page_title = "A Wonderful Hello" %>
+ # <%= render "shared/header" %>
+ #
+ # Now the header can pick up on the @page_title variable and use it for outputting a title tag:
+ #
+ # <title><%= @page_title %></title>
+ #
+ # == Passing local variables to sub templates
+ #
+ # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
+ #
+ # <%= render "shared/header", { :headline => "Welcome", :person => person } %>
+ #
+ # These can now be accessed in shared/header with:
+ #
+ # Headline: <%= headline %>
+ # First name: <%= person.first_name %>
+ #
+ # If you need to find out whether a certain local variable has been assigned a value in a particular render call,
+ # you need to use the following pattern:
+ #
+ # <% if local_assigns.has_key? :headline %>
+ # Headline: <%= headline %>
+ # <% end %>
+ #
+ # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction.
+ #
+ # == Template caching
+ #
+ # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will
+ # check the file's modification time and recompile it.
+ #
+ # == Builder
+ #
+ # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An +XmlMarkup+ object
+ # named +xml+ is automatically made available to templates with a +.builder+ extension.
+ #
+ # Here are some basic examples:
+ #
+ # xml.em("emphasized") # => <em>emphasized</em>
+ # xml.em { xml.b("emph & bold") } # => <em><b>emph &amp; bold</b></em>
+ # xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
+ # xml.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\>
+ # # NOTE: order of attributes is not specified.
+ #
+ # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
+ #
+ # xml.div {
+ # xml.h1(@person.name)
+ # xml.p(@person.bio)
+ # }
+ #
+ # would produce something like:
+ #
+ # <div>
+ # <h1>David Heinemeier Hansson</h1>
+ # <p>A product of Danish Design during the Winter of '79...</p>
+ # </div>
+ #
+ # A full-length RSS example actually used on Basecamp:
+ #
+ # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
+ # xml.channel do
+ # xml.title(@feed_title)
+ # xml.link(@url)
+ # xml.description "Basecamp: Recent items"
+ # xml.language "en-us"
+ # xml.ttl "40"
+ #
+ # for item in @recent_items
+ # xml.item do
+ # xml.title(item_title(item))
+ # xml.description(item_description(item)) if item_description(item)
+ # xml.pubDate(item_pubDate(item))
+ # xml.guid(@person.firm.account.url + @recent_items.url(item))
+ # xml.link(@person.firm.account.url + @recent_items.url(item))
+ #
+ # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
+ # end
+ # end
+ # end
+ # end
+ #
+ # More builder documentation can be found at http://builder.rubyforge.org.
+ #
+ # == JavaScriptGenerator
+ #
+ # JavaScriptGenerator templates end in +.rjs+. Unlike conventional templates which are used to
+ # render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to
+ # modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax
+ # and make updates to the page where the request originated from.
+ #
+ # An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block.
+ #
+ # When an .rjs action is called with +link_to_remote+, the generated JavaScript is automatically evaluated. Example:
+ #
+ # link_to_remote :url => {:action => 'delete'}
+ #
+ # The subsequently rendered +delete.rjs+ might look like:
+ #
+ # page.replace_html 'sidebar', :partial => 'sidebar'
+ # page.remove "person-#{@person.id}"
+ # page.visual_effect :highlight, 'user-list'
+ #
+ # This refreshes the sidebar, removes a person element and highlights the user list.
+ #
+ # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details.
+ class Base
+ include ERB::Util
+
+ attr_reader :first_render
+ attr_accessor :base_path, :assigns, :template_extension
+ attr_accessor :controller, :view_paths
+
+ attr_reader :logger, :response, :headers
+ attr_internal :cookies, :flash, :headers, :params, :request, :response, :session
+
+ attr_writer :template_format
+
+ # Specify trim mode for the ERB compiler. Defaults to '-'.
+ # See ERb documentation for suitable values.
+ @@erb_trim_mode = '-'
+ cattr_accessor :erb_trim_mode
+
+ # Specify whether file modification times should be checked to see if a template needs recompilation
+ @@cache_template_loading = false
+ cattr_accessor :cache_template_loading
+
+ # Specify whether file extension lookup should be cached, and whether template base path lookup should be cached.
+ # Should be +false+ for development environments. Defaults to +true+.
+ @@cache_template_extensions = true
+ cattr_accessor :cache_template_extensions
+
+ # Specify whether local_assigns should be able to use string keys.
+ # Defaults to +true+. String keys are deprecated and will be removed
+ # shortly.
+ @@local_assigns_support_string_keys = true
+ cattr_accessor :local_assigns_support_string_keys
+
+ # Specify whether RJS responses should be wrapped in a try/catch block
+ # that alert()s the caught exception (and then re-raises it).
+ @@debug_rjs = false
+ cattr_accessor :debug_rjs
+
+ @@erb_variable = '_erbout'
+ cattr_accessor :erb_variable
+
+ delegate :request_forgery_protection_token, :to => :controller
+
+ @@template_handlers = HashWithIndifferentAccess.new
+
+ module CompiledTemplates #:nodoc:
+ # holds compiled template code
+ end
+ include CompiledTemplates
+
+ # Maps inline templates to their method names
+ @@method_names = {}
+ # Map method names to their compile time
+ @@compile_time = {}
+ # Map method names to the names passed in local assigns so far
+ @@template_args = {}
+ # Count the number of inline templates
+ @@inline_template_count = 0
+ # Maps template paths without extension to their file extension returned by pick_template_extension.
+ # If for a given path, path.ext1 and path.ext2 exist on the file system, the order of extensions
+ # used by pick_template_extension determines whether ext1 or ext2 will be stored.
+ @@cached_template_extension = {}
+ # Maps template paths / extensions to
+ @@cached_base_paths = {}
+
+ # Cache public asset paths
+ cattr_reader :computed_public_paths
+ @@computed_public_paths = {}
+
+ @@template_handlers = {}
+ @@default_template_handlers = nil
+
+ class ObjectWrapper < Struct.new(:value) #:nodoc:
+ end
+
+ def self.load_helpers #:nodoc:
+ Dir.entries("#{File.dirname(__FILE__)}/helpers").sort.each do |file|
+ next unless file =~ /^([a-z][a-z_]*_helper).rb$/
+ require "action_view/helpers/#{$1}"
+ helper_module_name = $1.camelize
+ if Helpers.const_defined?(helper_module_name)
+ include Helpers.const_get(helper_module_name)
+ end
+ end
+ end
+
+ # Register a class that knows how to handle template files with the given
+ # extension. This can be used to implement new template types.
+ # The constructor for the class must take the ActiveView::Base instance
+ # as a parameter, and the class must implement a #render method that
+ # takes the contents of the template to render as well as the Hash of
+ # local assigns available to the template. The #render method ought to
+ # return the rendered template as a string.
+ def self.register_template_handler(extension, klass)
+ @@template_handlers[extension.to_sym] = klass
+ end
+
+ def self.template_handler_extensions
+ @@template_handler_extensions ||= @@template_handlers.keys.map(&:to_s).sort
+ end
+
+ def self.register_default_template_handler(extension, klass)
+ register_template_handler(extension, klass)
+ @@default_template_handlers = klass
+ end
+
+ def self.handler_for_extension(extension)
+ (extension && @@template_handlers[extension.to_sym]) || @@default_template_handlers
+ end
+
+ register_default_template_handler :erb, TemplateHandlers::ERB
+ register_template_handler :rjs, TemplateHandlers::RJS
+ register_template_handler :builder, TemplateHandlers::Builder
+
+ # TODO: Depreciate old template extensions
+ register_template_handler :rhtml, TemplateHandlers::ERB
+ register_template_handler :rxml, TemplateHandlers::Builder
+
+ def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
+ @view_paths = view_paths.respond_to?(:find) ? view_paths.dup : [*view_paths].compact
+ @assigns = assigns_for_first_render
+ @assigns_added = nil
+ @controller = controller
+ @logger = controller && controller.logger
+ end
+
+ # Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true,
+ # it's relative to the view_paths array, otherwise it's absolute. The hash in <tt>local_assigns</tt>
+ # is made available as local variables.
+ def render_file(template_path, use_full_path = true, local_assigns = {}) #:nodoc:
+ if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/")
+ raise ActionViewError, <<-END_ERROR
+Due to changes in ActionMailer, you need to provide the mailer_name along with the template name.
+
+ render "user_mailer/signup"
+ render :file => "user_mailer/signup"
+
+If you are rendering a subtemplate, you must now use controller-like partial syntax:
+
+ render :partial => 'signup' # no mailer_name necessary
+ END_ERROR
+ end
+
+ @first_render ||= template_path
+ template_path_without_extension, template_extension = path_and_extension(template_path)
+ if use_full_path
+ if template_extension
+ template_file_name = full_template_path(template_path_without_extension, template_extension)
+ else
+ template_extension = pick_template_extension(template_path).to_s
+ unless template_extension
+ raise ActionViewError, "No template found for #{template_path} in #{view_paths.inspect}"
+ end
+ template_file_name = full_template_path(template_path, template_extension)
+ template_extension = template_extension.gsub(/^.+\./, '') # strip off any formats
+ end
+ else
+ template_file_name = template_path
+ end
+
+ template_source = nil # Don't read the source until we know that it is required
+
+ if template_file_name.blank?
+ raise ActionViewError, "Couldn't find template file for #{template_path} in #{view_paths.inspect}"
+ end
+
+ begin
+ render_template(template_extension, template_source, template_file_name, local_assigns)
+ rescue Exception => e
+ if TemplateError === e
+ e.sub_template_of(template_file_name)
+ raise e
+ else
+ raise TemplateError.new(find_base_path_for("#{template_path_without_extension}.#{template_extension}") || view_paths.first, template_file_name, @assigns, template_source, e)
+ end
+ end
+ end
+
+ # Renders the template present at <tt>template_path</tt> (relative to the view_paths array).
+ # The hash in <tt>local_assigns</tt> is made available as local variables.
+ def render(options = {}, old_local_assigns = {}, &block) #:nodoc:
+ if options.is_a?(String)
+ render_file(options, true, old_local_assigns)
+ elsif options == :update
+ update_page(&block)
+ elsif options.is_a?(Hash)
+ options = options.reverse_merge(:locals => {}, :use_full_path => true)
+
+ if options[:layout]
+ path, partial_name = partial_pieces(options.delete(:layout))
+
+ if block_given?
+ @content_for_layout = capture(&block)
+ concat(render(options.merge(:partial => "#{path}/#{partial_name}")), block.binding)
+ else
+ @content_for_layout = render(options)
+ render(options.merge(:partial => "#{path}/#{partial_name}"))
+ end
+ elsif options[:file]
+ render_file(options[:file], options[:use_full_path], options[:locals])
+ elsif options[:partial] && options[:collection]
+ render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals])
+ elsif options[:partial]
+ render_partial(options[:partial], ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals])
+ elsif options[:inline]
+ render_template(options[:type], options[:inline], nil, options[:locals])
+ end
+ end
+ end
+
+ # Renders the +template+ which is given as a string as either erb or builder depending on <tt>template_extension</tt>.
+ # The hash in <tt>local_assigns</tt> is made available as local variables.
+ def render_template(template_extension, template, file_path = nil, local_assigns = {}) #:nodoc:
+ handler = self.class.handler_for_extension(template_extension)
+
+ if template_handler_is_compilable?(handler)
+ compile_and_render_template(handler, template, file_path, local_assigns)
+ else
+ template ||= read_template_file(file_path, template_extension) # Make sure that a lazyily-read template is loaded.
+ delegate_render(handler, template, local_assigns)
+ end
+ end
+
+ # Gets the full template path with base path for the given template_path and extension.
+ #
+ # full_template_path('users/show', 'html.erb')
+ # # => '~/rails/app/views/users/show.html.erb
+ #
+ def full_template_path(template_path, extension)
+ if @@cache_template_extensions
+ (@@cached_base_paths[template_path] ||= {})[extension.to_s] ||= find_full_template_path(template_path, extension)
+ else
+ find_full_template_path(template_path, extension)
+ end
+ end
+
+ # Gets the extension for an existing template with the given template_path.
+ # Returns the format with the extension if that template exists.
+ #
+ # pick_template_extension('users/show')
+ # # => 'html.erb'
+ #
+ # pick_template_extension('users/legacy')
+ # # => "rhtml"
+ #
+ def pick_template_extension(template_path)#:nodoc:
+ if @@cache_template_extensions
+ (@@cached_template_extension[template_path] ||= {})[template_format] ||= find_template_extension_for(template_path)
+ else
+ find_template_extension_for(template_path)
+ end
+ end
+
+ def file_exists?(template_path)#:nodoc:
+ template_file_name, template_file_extension = path_and_extension(template_path)
+ if template_file_extension
+ template_exists?(template_file_name, template_file_extension)
+ else
+ template_exists?(template_file_name, pick_template_extension(template_path))
+ end
+ end
+
+ # Returns true is the file may be rendered implicitly.
+ def file_public?(template_path)#:nodoc:
+ template_path.split('/').last[0,1] != '_'
+ end
+
+ # symbolized version of the :format parameter of the request, or :html by default.
+ def template_format
+ return @template_format if @template_format
+ format = controller && controller.respond_to?(:request) && controller.request.parameters[:format]
+ @template_format = format.blank? ? :html : format.to_sym
+ end
+
+ # Adds a view_path to the front of the view_paths array.
+ # This change affects the current request only.
+ #
+ # @template.prepend_view_path("views/default")
+ # @template.prepend_view_path(["views/default", "views/custom"])
+ #
+ def prepend_view_path(path)
+ @view_paths.unshift(*path)
+ end
+
+ # Adds a view_path to the end of the view_paths array.
+ # This change affects the current request only.
+ #
+ # @template.append_view_path("views/default")
+ # @template.append_view_path(["views/default", "views/custom"])
+ #
+ def append_view_path(path)
+ @view_paths.push(*path)
+ end
+
+ private
+ def find_full_template_path(template_path, extension)
+ file_name = "#{template_path}.#{extension}"
+ base_path = find_base_path_for(file_name)
+ base_path.blank? ? "" : "#{base_path}/#{file_name}"
+ end
+
+ # Asserts the existence of a template.
+ def template_exists?(template_path, extension)
+ file_path = full_template_path(template_path, extension)
+ !file_path.blank? && @@method_names.has_key?(file_path) || File.exist?(file_path)
+ end
+
+ # Splits the path and extension from the given template_path and returns as an array.
+ def path_and_extension(template_path)
+ template_path_without_extension = template_path.sub(/\.(\w+)$/, '')
+ [ template_path_without_extension, $1 ]
+ end
+
+ # Returns the view path that contains the given relative template path.
+ def find_base_path_for(template_file_name)
+ view_paths.find { |p| File.file?(File.join(p, template_file_name)) }
+ end
+
+ # Returns the view path that the full path resides in.
+ def extract_base_path_from(full_path)
+ view_paths.find { |p| full_path[0..p.size - 1] == p }
+ end
+
+ # Determines the template's file extension, such as rhtml, rxml, or rjs.
+ def find_template_extension_for(template_path)
+ find_template_extension_from_handler(template_path, true) ||
+ find_template_extension_from_handler(template_path) ||
+ find_template_extension_from_first_render()
+ end
+
+ def find_template_extension_from_handler(template_path, formatted = nil)
+ checked_template_path = formatted ? "#{template_path}.#{template_format}" : template_path
+
+ self.class.template_handler_extensions.each do |extension|
+ if template_exists?(checked_template_path, extension)
+ return formatted ? "#{template_format}.#{extension}" : extension.to_s
+ end
+ end
+ nil
+ end
+
+ # Determine the template extension from the <tt>@first_render</tt> filename
+ def find_template_extension_from_first_render
+ File.basename(@first_render.to_s)[/^[^.]+\.(.+)$/, 1]
+ end
+
+ # This method reads a template file.
+ def read_template_file(template_path, extension)
+ File.read(template_path)
+ end
+
+ # Evaluate the local assigns and pushes them to the view.
+ def evaluate_assigns
+ unless @assigns_added
+ assign_variables_from_controller
+ @assigns_added = true
+ end
+ end
+
+ def delegate_render(handler, template, local_assigns)
+ handler.new(self).render(template, local_assigns)
+ end
+
+ def delegate_compile(handler, template)
+ handler.new(self).compile(template)
+ end
+
+ def template_handler_is_compilable?(handler)
+ handler.new(self).respond_to?(:compile)
+ end
+
+ # Assigns instance variables from the controller to the view.
+ def assign_variables_from_controller
+ @assigns.each { |key, value| instance_variable_set("@#{key}", value) }
+ end
+
+
+ # Return true if the given template was compiled for a superset of the keys in local_assigns
+ def supports_local_assigns?(render_symbol, local_assigns)
+ local_assigns.empty? ||
+ ((args = @@template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) })
+ end
+
+ # Method to check whether template compilation is necessary.
+ # The template will be compiled if the inline template or file has not been compiled yet,
+ # if local_assigns has a new key, which isn't supported by the compiled code yet,
+ # or if the file has changed on disk and checking file mods hasn't been disabled.
+ def compile_template?(template, file_name, local_assigns)
+ method_key = file_name || template
+ render_symbol = @@method_names[method_key]
+
+ compile_time = @@compile_time[render_symbol]
+ if compile_time && supports_local_assigns?(render_symbol, local_assigns)
+ if file_name && !@@cache_template_loading
+ template_changed_since?(file_name, compile_time)
+ end
+ else
+ true
+ end
+ end
+
+ # Method to handle checking a whether a template has changed since last compile; isolated so that templates
+ # not stored on the file system can hook and extend appropriately.
+ def template_changed_since?(file_name, compile_time)
+ lstat = File.lstat(file_name)
+ compile_time < lstat.mtime ||
+ (lstat.symlink? && compile_time < File.stat(file_name).mtime)
+ end
+
+ # Method to create the source code for a given template.
+ def create_template_source(handler, template, render_symbol, locals)
+ body = delegate_compile(handler, template)
+
+ @@template_args[render_symbol] ||= {}
+ locals_keys = @@template_args[render_symbol].keys | locals
+ @@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h }
+
+ locals_code = ""
+ locals_keys.each do |key|
+ locals_code << "#{key} = local_assigns[:#{key}]\n"
+ end
+
+ "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
+ end
+
+ def assign_method_name(handler, template, file_name)
+ method_key = file_name || template
+ @@method_names[method_key] ||= compiled_method_name(handler, template, file_name)
+ end
+
+ def compiled_method_name(handler, template, file_name)
+ ['_run', handler.to_s.demodulize.underscore, compiled_method_name_file_path_segment(file_name)].compact.join('_').to_sym
+ end
+
+ def compiled_method_name_file_path_segment(file_name)
+ if file_name
+ s = File.expand_path(file_name)
+ s.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}/, '') if defined?(RAILS_ROOT)
+ s.gsub!(/([^a-zA-Z0-9_])/) { $1.ord }
+ s
+ else
+ (@@inline_template_count += 1).to_s
+ end
+ end
+
+ # Compile and evaluate the template's code
+ def compile_template(handler, template, file_name, local_assigns)
+ render_symbol = assign_method_name(handler, template, file_name)
+ render_source = create_template_source(handler, template, render_symbol, local_assigns.keys)
+ line_offset = @@template_args[render_symbol].size + handler.line_offset
+
+ begin
+ file_name = 'compiled-template' if file_name.blank?
+ CompiledTemplates.module_eval(render_source, file_name, -line_offset)
+ rescue Exception => e # errors from template code
+ if logger
+ logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
+ logger.debug "Function body: #{render_source}"
+ logger.debug "Backtrace: #{e.backtrace.join("\n")}"
+ end
+
+ raise TemplateError.new(extract_base_path_from(file_name) || view_paths.first, file_name || template, @assigns, template, e)
+ end
+
+ @@compile_time[render_symbol] = Time.now
+ # logger.debug "Compiled template #{file_name || template}\n ==> #{render_symbol}" if logger
+ end
+
+ # Render the provided template with the given local assigns. If the template has not been rendered with the provided
+ # local assigns yet, or if the template has been updated on disk, then the template will be compiled to a method.
+ #
+ # Either, but not both, of template and file_path may be nil. If file_path is given, the template
+ # will only be read if it has to be compiled.
+ #
+ def compile_and_render_template(handler, template = nil, file_path = nil, local_assigns = {}) #:nodoc:
+ # convert string keys to symbols if requested
+ local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys
+
+ # compile the given template, if necessary
+ if compile_template?(template, file_path, local_assigns)
+ template ||= read_template_file(file_path, nil)
+ compile_template(handler, template, file_path, local_assigns)
+ end
+
+ # Get the method name for this template and run it
+ method_name = @@method_names[file_path || template]
+ evaluate_assigns
+
+ send(method_name, local_assigns) do |*name|
+ instance_variable_get "@content_for_#{name.first || 'layout'}"
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/compiled_templates.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/compiled_templates.rb
new file mode 100644
index 000000000..5a286432e
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/compiled_templates.rb
@@ -0,0 +1,69 @@
+module ActionView
+
+ # CompiledTemplates modules hold methods that have been compiled.
+ # Templates are compiled into these methods so that they do not need to be
+ # read and parsed for each request.
+ #
+ # Each template may be compiled into one or more methods. Each method accepts a given
+ # set of parameters which is used to implement local assigns passing.
+ #
+ # To use a compiled template module, create a new instance and include it into the class
+ # in which you want the template to be rendered.
+ class CompiledTemplates < Module
+ attr_reader :method_names
+
+ def initialize
+ @method_names = Hash.new do |hash, key|
+ hash[key] = "__compiled_method_#{(hash.length + 1)}"
+ end
+ @mtimes = {}
+ end
+
+ # Return the full key for the given identifier and argument names
+ def full_key(identifier, arg_names)
+ [identifier, arg_names]
+ end
+
+ # Return the selector for this method or nil if it has not been compiled
+ def selector(identifier, arg_names)
+ key = full_key(identifier, arg_names)
+ method_names.key?(key) ? method_names[key] : nil
+ end
+ alias :compiled? :selector
+
+ # Return the time at which the method for the given identifier and argument names was compiled.
+ def mtime(identifier, arg_names)
+ @mtimes[full_key(identifier, arg_names)]
+ end
+
+ # Compile the provided source code for the given argument names and with the given initial line number.
+ # The identifier should be unique to this source.
+ #
+ # The file_name, if provided will appear in backtraces. If not provided, the file_name defaults
+ # to the identifier.
+ #
+ # This method will return the selector for the compiled version of this method.
+ def compile_source(identifier, arg_names, source, initial_line_number = 0, file_name = nil)
+ file_name ||= identifier
+ name = method_names[full_key(identifier, arg_names)]
+ arg_desc = arg_names.empty? ? '' : "(#{arg_names * ', '})"
+ fake_file_name = "#{file_name}#{arg_desc}" # Include the arguments for this version (for now)
+
+ method_def = wrap_source(name, arg_names, source)
+
+ begin
+ module_eval(method_def, fake_file_name, initial_line_number)
+ @mtimes[full_key(identifier, arg_names)] = Time.now
+ rescue Exception => e # errors from compiled source
+ e.blame_file! identifier
+ raise
+ end
+ name
+ end
+
+ # Wrap the provided source in a def ... end block.
+ def wrap_source(name, arg_names, source)
+ "def #{name}(#{arg_names * ', '})\n#{source}\nend"
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/active_record_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/active_record_helper.rb
new file mode 100644
index 000000000..9b3292a9b
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/active_record_helper.rb
@@ -0,0 +1,255 @@
+require 'cgi'
+require 'action_view/helpers/form_helper'
+
+module ActionView
+ class Base
+ @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" }
+ cattr_accessor :field_error_proc
+ end
+
+ module Helpers
+ # The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form
+ # method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This
+ # is a great way of making the record quickly available for editing, but likely to prove lackluster for a complicated real-world form.
+ # In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html
+ module ActiveRecordHelper
+ # Returns a default input tag for the type of object returned by the method. For example, let's say you have a model
+ # that has an attribute +title+ of type VARCHAR column, and this instance holds "Hello World":
+ # input("post", "title") =>
+ # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
+ def input(record_name, method, options = {})
+ InstanceTag.new(record_name, method, self).to_tag(options)
+ end
+
+ # Returns an entire form with all needed input tags for a specified Active Record object. For example, let's say you
+ # have a table model <tt>Post</tt> with attributes named <tt>title</tt> of type <tt>VARCHAR</tt> and <tt>body</tt> of type <tt>TEXT</tt>:
+ # form("post")
+ # That line would yield a form like the following:
+ # <form action='/post/create' method='post'>
+ # <p>
+ # <label for="post_title">Title</label><br />
+ # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
+ # </p>
+ # <p>
+ # <label for="post_body">Body</label><br />
+ # <textarea cols="40" id="post_body" name="post[body]" rows="20">
+ # </textarea>
+ # </p>
+ # <input type='submit' value='Create' />
+ # </form>
+ #
+ # It's possible to specialize the form builder by using a different action name and by supplying another
+ # block renderer. For example, let's say you have a model <tt>Entry</tt> with an attribute <tt>message</tt> of type <tt>VARCHAR</tt>:
+ #
+ # form("entry", :action => "sign", :input_block =>
+ # Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) =>
+ #
+ # <form action='/post/sign' method='post'>
+ # Message:
+ # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /><br />
+ # <input type='submit' value='Sign' />
+ # </form>
+ #
+ # It's also possible to add additional content to the form by giving it a block, such as:
+ #
+ # form("entry", :action => "sign") do |form|
+ # form << content_tag("b", "Department")
+ # form << collection_select("department", "id", @departments, "id", "name")
+ # end
+ def form(record_name, options = {})
+ record = instance_variable_get("@#{record_name}")
+
+ options = options.symbolize_keys
+ options[:action] ||= record.new_record? ? "create" : "update"
+ action = url_for(:action => options[:action], :id => record)
+
+ submit_value = options[:submit_value] || options[:action].gsub(/[^\w]/, '').capitalize
+
+ contents = ''
+ contents << hidden_field(record_name, :id) unless record.new_record?
+ contents << all_input_tags(record, record_name, options)
+ yield contents if block_given?
+ contents << submit_tag(submit_value)
+
+ content_tag('form', contents, :action => action, :method => 'post', :enctype => options[:multipart] ? 'multipart/form-data': nil)
+ end
+
+ # Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
+ # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a +prepend_text+ and/or +append_text+
+ # (to properly explain the error), and a +css_class+ to style it accordingly. +object+ should either be the name of an instance variable or
+ # the actual object. As an example, let's say you have a model
+ # +post+ that has an error message on the +title+ attribute:
+ #
+ # <%= error_message_on "post", "title" %> =>
+ # <div class="formError">can't be empty</div>
+ #
+ # <%= error_message_on @post, "title" %> =>
+ # <div class="formError">can't be empty</div>
+ #
+ # <%= error_message_on "post", "title", "Title simply ", " (or it won't work).", "inputError" %> =>
+ # <div class="inputError">Title simply can't be empty (or it won't work).</div>
+ def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
+ if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
+ (errors = obj.errors.on(method))
+ content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class)
+ else
+ ''
+ end
+ end
+
+ # Returns a string with a <tt>DIV</tt> containing all of the error messages for the objects located as instance variables by the names
+ # given. If more than one object is specified, the errors for the objects are displayed in the order that the object names are
+ # provided.
+ #
+ # This <tt>DIV</tt> can be tailored by the following options:
+ #
+ # * <tt>header_tag</tt> - Used for the header of the error div (default: h2)
+ # * <tt>id</tt> - The id of the error div (default: errorExplanation)
+ # * <tt>class</tt> - The class of the error div (default: errorExplanation)
+ # * <tt>object</tt> - The object (or array of objects) for which to display errors, if you need to escape the instance variable convention
+ # * <tt>object_name</tt> - The object name to use in the header, or any text that you prefer. If <tt>object_name</tt> is not set, the name of the first object will be used.
+ # * <tt>header_message</tt> - The message in the header of the error div. Pass +nil+ or an empty string to avoid the header message altogether. (default: X errors prohibited this object from being saved)
+ # * <tt>message</tt> - The explanation message after the header message and before the error list. Pass +nil+ or an empty string to avoid the explanation message altogether. (default: There were problems with the following fields:)
+ #
+ # To specify the display for one object, you simply provide its name as a parameter. For example, for the +User+ model:
+ #
+ # error_messages_for 'user'
+ #
+ # To specify more than one object, you simply list them; optionally, you can add an extra +object_name+ parameter, which
+ # will be the name used in the header message.
+ #
+ # error_messages_for 'user_common', 'user', :object_name => 'user'
+ #
+ # If the objects cannot be located as instance variables, you can add an extra +object+ paremeter which gives the actual
+ # object (or array of objects to use)
+ #
+ # error_messages_for 'user', :object => @question.user
+ #
+ # NOTE: This is a pre-packaged presentation of the errors with embedded strings and a certain HTML structure. If what
+ # you need is significantly different from the default presentation, it makes plenty of sense to access the object.errors
+ # instance yourself and set it up. View the source of this method to see how easy it is.
+ def error_messages_for(*params)
+ options = params.extract_options!.symbolize_keys
+ if object = options.delete(:object)
+ objects = [object].flatten
+ else
+ objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
+ end
+ count = objects.inject(0) {|sum, object| sum + object.errors.count }
+ unless count.zero?
+ html = {}
+ [:id, :class].each do |key|
+ if options.include?(key)
+ value = options[key]
+ html[key] = value unless value.blank?
+ else
+ html[key] = 'errorExplanation'
+ end
+ end
+ options[:object_name] ||= params.first
+ options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message)
+ options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message)
+ error_messages = objects.map {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }
+
+ contents = ''
+ contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank?
+ contents << content_tag(:p, options[:message]) unless options[:message].blank?
+ contents << content_tag(:ul, error_messages)
+
+ content_tag(:div, contents, html)
+ else
+ ''
+ end
+ end
+
+ private
+ def all_input_tags(record, record_name, options)
+ input_block = options[:input_block] || default_input_block
+ record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n")
+ end
+
+ def default_input_block
+ Proc.new { |record, column| %(<p><label for="#{record}_#{column.name}">#{column.human_name}</label><br />#{input(record, column.name)}</p>) }
+ end
+ end
+
+ class InstanceTag #:nodoc:
+ def to_tag(options = {})
+ case column_type
+ when :string
+ field_type = @method_name.include?("password") ? "password" : "text"
+ to_input_field_tag(field_type, options)
+ when :text
+ to_text_area_tag(options)
+ when :integer, :float, :decimal
+ to_input_field_tag("text", options)
+ when :date
+ to_date_select_tag(options)
+ when :datetime, :timestamp
+ to_datetime_select_tag(options)
+ when :time
+ to_time_select_tag(options)
+ when :boolean
+ to_boolean_select_tag(options)
+ end
+ end
+
+ alias_method :tag_without_error_wrapping, :tag
+ def tag(name, options)
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name))
+ else
+ tag_without_error_wrapping(name, options)
+ end
+ end
+
+ alias_method :content_tag_without_error_wrapping, :content_tag
+ def content_tag(name, value, options)
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name))
+ else
+ content_tag_without_error_wrapping(name, value, options)
+ end
+ end
+
+ alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag
+ def to_date_select_tag(options = {})
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
+ else
+ to_date_select_tag_without_error_wrapping(options)
+ end
+ end
+
+ alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag
+ def to_datetime_select_tag(options = {})
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
+ else
+ to_datetime_select_tag_without_error_wrapping(options)
+ end
+ end
+
+ alias_method :to_time_select_tag_without_error_wrapping, :to_time_select_tag
+ def to_time_select_tag(options = {})
+ if object.respond_to?("errors") && object.errors.respond_to?("on")
+ error_wrapping(to_time_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
+ else
+ to_time_select_tag_without_error_wrapping(options)
+ end
+ end
+
+ def error_wrapping(html_tag, has_error)
+ has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
+ end
+
+ def error_message
+ object.errors.on(@method_name)
+ end
+
+ def column_type
+ object.send("column_for_attribute", @method_name).type
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/asset_tag_helper.rb
new file mode 100644
index 000000000..ba474a8ff
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/asset_tag_helper.rb
@@ -0,0 +1,548 @@
+require 'cgi'
+require 'action_view/helpers/url_helper'
+require 'action_view/helpers/tag_helper'
+
+module ActionView
+ module Helpers #:nodoc:
+ # This module provides methods for generating HTML that links views to assets such
+ # as images, javascripts, stylesheets, and feeds. These methods do not verify
+ # the assets exist before linking to them.
+ #
+ # === Using asset hosts
+ # By default, Rails links to these assets on the current host in the public
+ # folder, but you can direct Rails to link to assets from a dedicated assets server by
+ # setting ActionController::Base.asset_host in your environment.rb. For example,
+ # let's say your asset host is assets.example.com.
+ #
+ # ActionController::Base.asset_host = "assets.example.com"
+ # image_tag("rails.png")
+ # => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
+ # stylesheet_include_tag("application")
+ # => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # This is useful since browsers typically open at most two connections to a single host,
+ # which means your assets often wait in single file for their turn to load. You can
+ # alleviate this by using a %d wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com")
+ # to automatically distribute asset requests among four hosts (e.g., assets0.example.com through assets3.example.com)
+ # so browsers will open eight connections rather than two.
+ #
+ # image_tag("rails.png")
+ # => <img src="http://assets0.example.com/images/rails.png" alt="Rails" />
+ # stylesheet_include_tag("application")
+ # => <link href="http://assets3.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # To do this, you can either setup 4 actual hosts, or you can use wildcard DNS to CNAME
+ # the wildcard to a single asset host. You can read more about setting up your DNS CNAME records from
+ # your ISP.
+ #
+ # Note: This is purely a browser performance optimization and is not meant
+ # for server load balancing. See http://www.die.net/musings/page_load_time/
+ # for background.
+ #
+ # Alternatively, you can exert more control over the asset host by setting <tt>asset_host</tt> to a proc
+ # that takes a single source argument. This is useful if you are unable to setup 4 actual hosts or have
+ # fewer/more than 4 hosts. The example proc below generates http://assets1.example.com and
+ # http://assets2.example.com randomly.
+ #
+ # ActionController::Base.asset_host = Proc.new { |source| "http://assets#{rand(2) + 1}.example.com" }
+ # image_tag("rails.png")
+ # => <img src="http://assets2.example.com/images/rails.png" alt="Rails" />
+ # stylesheet_include_tag("application")
+ # => <link href="http://assets1.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # The proc takes a single <tt>source</tt> parameter which is the path of the source asset. This can be used to
+ # generate a particular asset host depending on the asset path.
+ #
+ # ActionController::Base.asset_host = Proc.new { |source|
+ # if source.starts_with?('/images')
+ # "http://images.example.com"
+ # else
+ # "http://assets.example.com"
+ # end
+ # }
+ # image_tag("rails.png")
+ # => <img src="http://images.example.com/images/rails.png" alt="Rails" />
+ # stylesheet_include_tag("application")
+ # => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # === Using asset timestamps
+ #
+ # By default, Rails will append all asset paths with that asset's timestamp. This allows you to set a cache-expiration date for the
+ # asset far into the future, but still be able to instantly invalidate it by simply updating the file (and hence updating the timestamp,
+ # which then updates the URL as the timestamp is part of that, which in turn busts the cache).
+ #
+ # It's the responsibility of the web server you use to set the far-future expiration date on cache assets that you need to take
+ # advantage of this feature. Here's an example for Apache:
+ #
+ # # Asset Expiration
+ # ExpiresActive On
+ # <FilesMatch "\.(ico|gif|jpe?g|png|js|css)$">
+ # ExpiresDefault "access plus 1 year"
+ # </FilesMatch>
+ #
+ # Also note that in order for this to work, all your application servers must return the same timestamps. This means that they must
+ # have their clocks synchronized. If one of them drift out of sync, you'll see different timestamps at random and the cache won't
+ # work. Which means that the browser will request the same assets over and over again even thought they didn't change. You can use
+ # something like Live HTTP Headers for Firefox to verify that the cache is indeed working (and that the assets are not being
+ # requested over and over).
+ module AssetTagHelper
+ ASSETS_DIR = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : "public"
+ JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts"
+ STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets"
+
+ # Returns a link tag that browsers and news readers can use to auto-detect
+ # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or
+ # <tt>:atom</tt>. Control the link options in url_for format using the
+ # +url_options+. You can modify the LINK tag itself in +tag_options+.
+ #
+ # ==== Options:
+ # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
+ # * <tt>:type</tt> - Override the auto-generated mime type
+ # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
+ #
+ # ==== Examples
+ # auto_discovery_link_tag # =>
+ # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
+ # auto_discovery_link_tag(:atom) # =>
+ # <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
+ # auto_discovery_link_tag(:rss, {:action => "feed"}) # =>
+ # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
+ # auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) # =>
+ # <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" />
+ # auto_discovery_link_tag(:rss, {:controller => "news", :action => "feed"}) # =>
+ # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
+ # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {:title => "Example RSS"}) # =>
+ # <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" />
+ def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
+ tag(
+ "link",
+ "rel" => tag_options[:rel] || "alternate",
+ "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
+ "title" => tag_options[:title] || type.to_s.upcase,
+ "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
+ )
+ end
+
+ # Computes the path to a javascript asset in the public javascripts directory.
+ # If the +source+ filename has no extension, .js will be appended.
+ # Full paths from the document root will be passed through.
+ # Used internally by javascript_include_tag to build the script path.
+ #
+ # ==== Examples
+ # javascript_path "xmlhr" # => /javascripts/xmlhr.js
+ # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
+ # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
+ # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js
+ # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js
+ def javascript_path(source)
+ compute_public_path(source, 'javascripts', 'js')
+ end
+ alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
+
+ JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
+ @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
+
+ # Returns an html script tag for each of the +sources+ provided. You
+ # can pass in the filename (.js extension is optional) of javascript files
+ # that exist in your public/javascripts directory for inclusion into the
+ # current page or you can pass the full path relative to your document
+ # root. To include the Prototype and Scriptaculous javascript libraries in
+ # your application, pass <tt>:defaults</tt> as the source. When using
+ # :defaults, if an <tt>application.js</tt> file exists in your public
+ # javascripts directory, it will be included as well. You can modify the
+ # html attributes of the script tag by passing a hash as the last argument.
+ #
+ # ==== Examples
+ # javascript_include_tag "xmlhr" # =>
+ # <script type="text/javascript" src="/javascripts/xmlhr.js"></script>
+ #
+ # javascript_include_tag "xmlhr.js" # =>
+ # <script type="text/javascript" src="/javascripts/xmlhr.js"></script>
+ #
+ # javascript_include_tag "common.javascript", "/elsewhere/cools" # =>
+ # <script type="text/javascript" src="/javascripts/common.javascript"></script>
+ # <script type="text/javascript" src="/elsewhere/cools.js"></script>
+ #
+ # javascript_include_tag "http://www.railsapplication.com/xmlhr" # =>
+ # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script>
+ #
+ # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # =>
+ # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script>
+ #
+ # javascript_include_tag :defaults # =>
+ # <script type="text/javascript" src="/javascripts/prototype.js"></script>
+ # <script type="text/javascript" src="/javascripts/effects.js"></script>
+ # ...
+ # <script type="text/javascript" src="/javascripts/application.js"></script>
+ #
+ # * = The application.js file is only referenced if it exists
+ #
+ # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason
+ # (e.g., you're going to be using a certain .js file in every action), then take a look at the register_javascript_include_default method.
+ #
+ # You can also include all javascripts in the javascripts directory using <tt>:all</tt> as the source:
+ #
+ # javascript_include_tag :all # =>
+ # <script type="text/javascript" src="/javascripts/prototype.js"></script>
+ # <script type="text/javascript" src="/javascripts/effects.js"></script>
+ # ...
+ # <script type="text/javascript" src="/javascripts/application.js"></script>
+ # <script type="text/javascript" src="/javascripts/shop.js"></script>
+ # <script type="text/javascript" src="/javascripts/checkout.js"></script>
+ #
+ # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to
+ # all subsequently included files.
+ #
+ # == Caching multiple javascripts into one
+ #
+ # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be
+ # compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching
+ # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development
+ # environment).
+ #
+ # ==== Examples
+ # javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is false =>
+ # <script type="text/javascript" src="/javascripts/prototype.js"></script>
+ # <script type="text/javascript" src="/javascripts/effects.js"></script>
+ # ...
+ # <script type="text/javascript" src="/javascripts/application.js"></script>
+ # <script type="text/javascript" src="/javascripts/shop.js"></script>
+ # <script type="text/javascript" src="/javascripts/checkout.js"></script>
+ #
+ # javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is true =>
+ # <script type="text/javascript" src="/javascripts/all.js"></script>
+ #
+ # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is false =>
+ # <script type="text/javascript" src="/javascripts/prototype.js"></script>
+ # <script type="text/javascript" src="/javascripts/cart.js"></script>
+ # <script type="text/javascript" src="/javascripts/checkout.js"></script>
+ #
+ # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is false =>
+ # <script type="text/javascript" src="/javascripts/shop.js"></script>
+ def javascript_include_tag(*sources)
+ options = sources.extract_options!.stringify_keys
+ cache = options.delete("cache")
+
+ if ActionController::Base.perform_caching && cache
+ joined_javascript_name = (cache == true ? "all" : cache) + ".js"
+ joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name)
+
+ write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources))
+ javascript_src_tag(joined_javascript_name, options)
+ else
+ expand_javascript_sources(sources).collect { |source| javascript_src_tag(source, options) }.join("\n")
+ end
+ end
+
+ # Register one or more additional JavaScript files to be included when
+ # <tt>javascript_include_tag :defaults</tt> is called. This method is
+ # typically intended to be called from plugin initialization to register additional
+ # .js files that the plugin installed in <tt>public/javascripts</tt>.
+ def self.register_javascript_include_default(*sources)
+ @@javascript_default_sources.concat(sources)
+ end
+
+ def self.reset_javascript_include_default #:nodoc:
+ @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
+ end
+
+ # Computes the path to a stylesheet asset in the public stylesheets directory.
+ # If the +source+ filename has no extension, .css will be appended.
+ # Full paths from the document root will be passed through.
+ # Used internally by stylesheet_link_tag to build the stylesheet path.
+ #
+ # ==== Examples
+ # stylesheet_path "style" # => /stylesheets/style.css
+ # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css
+ # stylesheet_path "/dir/style.css" # => /dir/style.css
+ # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css
+ # stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css
+ def stylesheet_path(source)
+ compute_public_path(source, 'stylesheets', 'css')
+ end
+ alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
+
+ # Returns a stylesheet link tag for the sources specified as arguments. If
+ # you don't specify an extension, .css will be appended automatically.
+ # You can modify the link attributes by passing a hash as the last argument.
+ #
+ # ==== Examples
+ # stylesheet_link_tag "style" # =>
+ # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "style.css" # =>
+ # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "http://www.railsapplication.com/style.css" # =>
+ # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "style", :media => "all" # =>
+ # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "style", :media => "print" # =>
+ # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "random.styles", "/css/stylish" # =>
+ # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # You can also include all styles in the stylesheet directory using :all as the source:
+ #
+ # stylesheet_link_tag :all # =>
+ # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # == Caching multiple stylesheets into one
+ #
+ # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be
+ # compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching
+ # is set to true (which is the case by default for the Rails production environment, but not for the development
+ # environment). Examples:
+ #
+ # ==== Examples
+ # stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is false =>
+ # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is true =>
+ # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is false =>
+ # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is true =>
+ # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" />
+ def stylesheet_link_tag(*sources)
+ options = sources.extract_options!.stringify_keys
+ cache = options.delete("cache")
+
+ if ActionController::Base.perform_caching && cache
+ joined_stylesheet_name = (cache == true ? "all" : cache) + ".css"
+ joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name)
+
+ write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources))
+ stylesheet_tag(joined_stylesheet_name, options)
+ else
+ expand_stylesheet_sources(sources).collect { |source| stylesheet_tag(source, options) }.join("\n")
+ end
+ end
+
+ # Computes the path to an image asset in the public images directory.
+ # Full paths from the document root will be passed through.
+ # Used internally by image_tag to build the image path.
+ #
+ # ==== Examples
+ # image_path("edit") # => /images/edit
+ # image_path("edit.png") # => /images/edit.png
+ # image_path("icons/edit.png") # => /images/icons/edit.png
+ # image_path("/icons/edit.png") # => /icons/edit.png
+ # image_path("http://www.railsapplication.com/img/edit.png") # => http://www.railsapplication.com/img/edit.png
+ def image_path(source)
+ compute_public_path(source, 'images')
+ end
+ alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
+
+ # Returns an html image tag for the +source+. The +source+ can be a full
+ # path or a file that exists in your public images directory.
+ #
+ # ==== Options
+ # You can add HTML attributes using the +options+. The +options+ supports
+ # three additional keys for convenience and conformance:
+ #
+ # * <tt>:alt</tt> - If no alt text is given, the file name part of the
+ # +source+ is used (capitalized and without the extension)
+ # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes
+ # width="30" and height="45". <tt>:size</tt> will be ignored if the
+ # value is not in the correct format.
+ # * <tt>:mouseover</tt> - Set an alternate image to be used when the onmouseover
+ # event is fired, and sets the original image to be replaced onmouseout.
+ # This can be used to implement an easy image toggle that fires on onmouseover.
+ #
+ # ==== Examples
+ # image_tag("icon") # =>
+ # <img src="/images/icon" alt="Icon" />
+ # image_tag("icon.png") # =>
+ # <img src="/images/icon.png" alt="Icon" />
+ # image_tag("icon.png", :size => "16x10", :alt => "Edit Entry") # =>
+ # <img src="/images/icon.png" width="16" height="10" alt="Edit Entry" />
+ # image_tag("/icons/icon.gif", :size => "16x16") # =>
+ # <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
+ # image_tag("/icons/icon.gif", :height => '32', :width => '32') # =>
+ # <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
+ # image_tag("/icons/icon.gif", :class => "menu_icon") # =>
+ # <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
+ # image_tag("mouse.png", :mouseover => "/images/mouse_over.png") # =>
+ # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" />
+ # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # =>
+ # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" />
+ def image_tag(source, options = {})
+ options.symbolize_keys!
+
+ options[:src] = path_to_image(source)
+ options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize
+
+ if size = options.delete(:size)
+ options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$}
+ end
+
+ if mouseover = options.delete(:mouseover)
+ options[:onmouseover] = "this.src='#{image_path(mouseover)}'"
+ options[:onmouseout] = "this.src='#{image_path(options[:src])}'"
+ end
+
+ tag("img", options)
+ end
+
+ private
+ def file_exist?(path)
+ @@file_exist_cache ||= {}
+ if !(@@file_exist_cache[path] ||= File.exist?(path))
+ @@file_exist_cache[path] = true
+ false
+ else
+ true
+ end
+ end
+
+ # Add the .ext if not present. Return full URLs otherwise untouched.
+ # Prefix with /dir/ if lacking a leading /. Account for relative URL
+ # roots. Rewrite the asset path for cache-busting asset ids. Include
+ # asset host, if configured, with the correct request protocol.
+ def compute_public_path(source, dir, ext = nil, include_host = true)
+ has_request = @controller.respond_to?(:request)
+
+ cache_key =
+ if has_request
+ [ @controller.request.protocol,
+ ActionController::Base.asset_host.to_s,
+ @controller.request.relative_url_root,
+ dir, source, ext, include_host ].join
+ else
+ [ ActionController::Base.asset_host.to_s,
+ dir, source, ext, include_host ].join
+ end
+
+ ActionView::Base.computed_public_paths[cache_key] ||=
+ begin
+ source += ".#{ext}" if File.extname(source).blank? && ext
+
+ if source =~ %r{^[-a-z]+://}
+ source
+ else
+ source = "/#{dir}/#{source}" unless source[0] == ?/
+ if has_request
+ source = "#{@controller.request.relative_url_root}#{source}"
+ end
+ rewrite_asset_path!(source)
+
+ if include_host
+ host = compute_asset_host(source)
+
+ if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
+ host = "#{@controller.request.protocol}#{host}"
+ end
+
+ "#{host}#{source}"
+ else
+ source
+ end
+ end
+ end
+ end
+
+ # Pick an asset host for this source. Returns nil if no host is set,
+ # the host if no wildcard is set, the host interpolated with the
+ # numbers 0-3 if it contains %d (the number is the source hash mod 4),
+ # or the value returned from invoking the proc if it's a proc.
+ def compute_asset_host(source)
+ if host = ActionController::Base.asset_host
+ if host.is_a?(Proc)
+ host.call(source)
+ else
+ host % (source.hash % 4)
+ end
+ end
+ end
+
+ # Use the RAILS_ASSET_ID environment variable or the source's
+ # modification time as its cache-busting asset id.
+ def rails_asset_id(source)
+ if asset_id = ENV["RAILS_ASSET_ID"]
+ asset_id
+ else
+ path = File.join(ASSETS_DIR, source)
+
+ if File.exist?(path)
+ File.mtime(path).to_i.to_s
+ else
+ ''
+ end
+ end
+ end
+
+ # Break out the asset path rewrite so you wish to put the asset id
+ # someplace other than the query string.
+ def rewrite_asset_path!(source)
+ asset_id = rails_asset_id(source)
+ source << "?#{asset_id}" if !asset_id.blank?
+ end
+
+ def javascript_src_tag(source, options)
+ content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options))
+ end
+
+ def stylesheet_tag(source, options)
+ tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false)
+ end
+
+ def compute_javascript_paths(sources)
+ expand_javascript_sources(sources).collect { |source| compute_public_path(source, 'javascripts', 'js', false) }
+ end
+
+ def compute_stylesheet_paths(sources)
+ expand_stylesheet_sources(sources).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) }
+ end
+
+ def expand_javascript_sources(sources)
+ case
+ when sources.include?(:all)
+ all_javascript_files = Dir[File.join(JAVASCRIPTS_DIR, '*.js')].collect { |file| File.basename(file).split(".", 0).first }.sort
+ sources = ((@@javascript_default_sources.dup & all_javascript_files) + all_javascript_files).uniq
+
+ when sources.include?(:defaults)
+ sources = sources[0..(sources.index(:defaults))] +
+ @@javascript_default_sources.dup +
+ sources[(sources.index(:defaults) + 1)..sources.length]
+
+ sources.delete(:defaults)
+ sources << "application" if file_exist?(File.join(JAVASCRIPTS_DIR, "application.js"))
+ end
+
+ sources
+ end
+
+ def expand_stylesheet_sources(sources)
+ if sources.first == :all
+ @@all_stylesheet_sources ||= Dir[File.join(STYLESHEETS_DIR, '*.css')].collect { |file| File.basename(file).split(".", 1).first }.sort
+ else
+ sources
+ end
+ end
+
+ def join_asset_file_contents(paths)
+ paths.collect { |path| File.read(File.join(ASSETS_DIR, path.split("?").first)) }.join("\n\n")
+ end
+
+ def write_asset_file_contents(joined_asset_path, asset_paths)
+ unless file_exist?(joined_asset_path)
+ FileUtils.mkdir_p(File.dirname(joined_asset_path))
+ File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) }
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/atom_feed_helper.rb
new file mode 100644
index 000000000..c3ceecbf5
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/atom_feed_helper.rb
@@ -0,0 +1,111 @@
+# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other
+# template languages).
+module ActionView
+ module Helpers #:nodoc:
+ module AtomFeedHelper
+ # Full usage example:
+ #
+ # config/routes.rb:
+ # ActionController::Routing::Routes.draw do |map|
+ # map.resources :posts
+ # map.root :controller => "posts"
+ # end
+ #
+ # app/controllers/posts_controller.rb:
+ # class PostsController < ApplicationController::Base
+ # # GET /posts.html
+ # # GET /posts.atom
+ # def index
+ # @posts = Post.find(:all)
+ #
+ # respond_to do |format|
+ # format.html
+ # format.atom
+ # end
+ # end
+ # end
+ #
+ # app/views/posts/index.atom.builder:
+ # atom_feed do |feed|
+ # feed.title("My great blog!")
+ # feed.updated((@posts.first.created_at))
+ #
+ # for post in @posts
+ # feed.entry(post) do |entry|
+ # entry.title(post.title)
+ # entry.content(post.body, :type => 'html')
+ #
+ # entry.author do |author|
+ # author.name("DHH")
+ # end
+ # end
+ # end
+ # end
+ #
+ # The options are for atom_feed are:
+ #
+ # * <tt>:language</tt>: Defaults to "en-US".
+ # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
+ # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
+ #
+ # atom_feed yields a AtomFeedBuilder instance.
+ def atom_feed(options = {}, &block)
+ xml = options[:xml] || eval("xml", block.binding)
+ xml.instruct!
+
+ xml.feed "xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom' do
+ xml.id("tag:#{request.host}:#{request.request_uri.split(".")[0].gsub("/", "")}")
+ xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port))
+
+ if options[:url]
+ xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url)
+ end
+
+ yield AtomFeedBuilder.new(xml, self)
+ end
+ end
+
+
+ class AtomFeedBuilder
+ def initialize(xml, view)
+ @xml, @view = xml, view
+ end
+
+ # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used.
+ def updated(date_or_time = nil)
+ @xml.updated((date_or_time || Time.now.utc).xmlschema)
+ end
+
+ # Creates an entry tag for a specific record and prefills the id using class and id.
+ #
+ # Options:
+ #
+ # * <tt>:updated</tt>: Time of update. Defaults to the created_at attribute on the record if one such exists.
+ # * <tt>:published</tt>: Time first published. Defaults to the updated_at attribute on the record if one such exists.
+ # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record.
+ def entry(record, options = {})
+ @xml.entry do
+ @xml.id("tag:#{@view.request.host_with_port}:#{record.class}#{record.id}")
+
+ if options[:published] || (record.respond_to?(:created_at) && record.created_at)
+ @xml.published((options[:published] || record.created_at).xmlschema)
+ end
+
+ if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at)
+ @xml.updated((options[:updated] || record.updated_at).xmlschema)
+ end
+
+ @xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:url] || @view.polymorphic_url(record))
+
+ yield @xml
+ end
+ end
+
+ private
+ def method_missing(method, *arguments)
+ @xml.__send__(method, *arguments)
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/benchmark_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/benchmark_helper.rb
new file mode 100644
index 000000000..fefa28f4d
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/benchmark_helper.rb
@@ -0,0 +1,31 @@
+require 'benchmark'
+
+module ActionView
+ module Helpers
+ # This helper offers a method to measure the execution time of a block
+ # in a template.
+ module BenchmarkHelper
+ # Allows you to measure the execution time of a block
+ # in a template and records the result to the log. Wrap this block around
+ # expensive operations or possible bottlenecks to get a time reading
+ # for the operation. For example, let's say you thought your file
+ # processing method was taking too long; you could wrap it in a benchmark block.
+ #
+ # <% benchmark "Process data files" do %>
+ # <%= expensive_files_operation %>
+ # <% end %>
+ #
+ # That would add something like "Process data files (0.34523)" to the log,
+ # which you can then use to compare timings when optimizing your code.
+ #
+ # You may give an optional logger level as the second argument
+ # (:debug, :info, :warn, :error); the default value is :info.
+ def benchmark(message = "Benchmarking", level = :info)
+ if @logger
+ real = Benchmark.realtime { yield }
+ @logger.send level, "#{message} (#{'%.5f' % real})"
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/cache_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/cache_helper.rb
new file mode 100644
index 000000000..cf5420a35
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/cache_helper.rb
@@ -0,0 +1,39 @@
+module ActionView
+ module Helpers
+ # This helper to exposes a method for caching of view fragments.
+ # See ActionController::Caching::Fragments for usage instructions.
+ module CacheHelper
+ # A method for caching fragments of a view rather than an entire
+ # action or page. This technique is useful caching pieces like
+ # menus, lists of news topics, static HTML fragments, and so on.
+ # This method takes a block that contains the content you wish
+ # to cache. See ActionController::Caching::Fragments for more
+ # information.
+ #
+ # ==== Examples
+ # If you wanted to cache a navigation menu, you could do the
+ # following.
+ #
+ # <% cache do %>
+ # <%= render :partial => "menu" %>
+ # <% end %>
+ #
+ # You can also cache static content...
+ #
+ # <% cache do %>
+ # <p>Hello users! Welcome to our website!</p>
+ # <% end %>
+ #
+ # ...and static content mixed with RHTML content.
+ #
+ # <% cache do %>
+ # Topics:
+ # <%= render :partial => "topics", :collection => @topic_list %>
+ # <i>Topics listed alphabetically</i>
+ # <% end %>
+ def cache(name = {}, &block)
+ @controller.cache_erb_fragment(block, name)
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/capture_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/capture_helper.rb
new file mode 100644
index 000000000..fc8cd66d7
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/capture_helper.rb
@@ -0,0 +1,162 @@
+module ActionView
+ module Helpers
+ # CaptureHelper exposes methods to let you extract generated markup which
+ # can be used in other parts of a template or layout file.
+ # It provides a method to capture blocks into variables through capture and
+ # a way to capture a block of markup for use in a layout through content_for.
+ module CaptureHelper
+ # The capture method allows you to extract part of a template into a
+ # variable. You can then use this variable anywhere in your templates or layout.
+ #
+ # ==== Examples
+ # The capture method can be used in ERb templates...
+ #
+ # <% @greeting = capture do %>
+ # Welcome to my shiny new web page! The date and time is
+ # <%= Time.now %>
+ # <% end %>
+ #
+ # ...and Builder (RXML) templates.
+ #
+ # @timestamp = capture do
+ # "The current timestamp is #{Time.now}."
+ # end
+ #
+ # You can then use that variable anywhere else. For example:
+ #
+ # <html>
+ # <head><title><%= @greeting %></title></head>
+ # <body>
+ # <b><%= @greeting %></b>
+ # </body></html>
+ #
+ def capture(*args, &block)
+ # execute the block
+ begin
+ buffer = eval(ActionView::Base.erb_variable, block.binding)
+ rescue
+ buffer = nil
+ end
+
+ if buffer.nil?
+ capture_block(*args, &block).to_s
+ else
+ capture_erb_with_buffer(buffer, *args, &block).to_s
+ end
+ end
+
+ # Calling content_for stores a block of markup in an identifier for later use.
+ # You can make subsequent calls to the stored content in other templates or the layout
+ # by passing the identifier as an argument to <tt>yield</tt>.
+ #
+ # ==== Examples
+ #
+ # <% content_for :not_authorized do %>
+ # alert('You are not authorized to do that!')
+ # <% end %>
+ #
+ # You can then use <tt>yield :not_authorized</tt> anywhere in your templates.
+ #
+ # <%= yield :not_authorized if current_user.nil? %>
+ #
+ # You can also use this syntax alongside an existing call to <tt>yield</tt> in a layout. For example:
+ #
+ # <%# This is the layout %>
+ # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ # <head>
+ # <title>My Website</title>
+ # <%= yield :script %>
+ # </head>
+ # <body>
+ # <%= yield %>
+ # </body>
+ # </html>
+ #
+ # And now, we'll create a view that has a content_for call that
+ # creates the <tt>script</tt> identifier.
+ #
+ # <%# This is our view %>
+ # Please login!
+ #
+ # <% content_for :script do %>
+ # <script type="text/javascript">alert('You are not authorized to view this page!')</script>
+ # <% end %>
+ #
+ # Then, in another view, you could to do something like this:
+ #
+ # <%= link_to_remote 'Logout', :action => 'logout' %>
+ #
+ # <% content_for :script do %>
+ # <%= javascript_include_tag :defaults %>
+ # <% end %>
+ #
+ # That will place <script> tags for Prototype, Scriptaculous, and application.js (if it exists)
+ # on the page; this technique is useful if you'll only be using these scripts in a few views.
+ #
+ # Note that content_for concatenates the blocks it is given for a particular
+ # identifier in order. For example:
+ #
+ # <% content_for :navigation do %>
+ # <li><%= link_to 'Home', :action => 'index' %></li>
+ # <% end %>
+ #
+ # <%# Add some other content, or use a different template: %>
+ #
+ # <% content_for :navigation do %>
+ # <li><%= link_to 'Login', :action => 'login' %></li>
+ # <% end %>
+ #
+ # Then, in another template or layout, this code would render both links in order:
+ #
+ # <ul><%= yield :navigation %></ul>
+ #
+ # Lastly, simple content can be passed as a parameter:
+ #
+ # <% content_for :script, javascript_include_tag(:defaults) %>
+ #
+ # WARNING: content_for is ignored in caches. So you shouldn't use it
+ # for elements that will be fragment cached.
+ #
+ # The deprecated way of accessing a content_for block is to use an instance variable
+ # named <tt>@content_for_#{name_of_the_content_block}</tt>. So <tt><%= content_for :footer %></tt>
+ # would be available as <tt><%= @content_for_footer %></tt>. The preferred usage is now
+ # <tt><%= yield :footer %></tt>.
+ def content_for(name, content = nil, &block)
+ existing_content_for = instance_variable_get("@content_for_#{name}").to_s
+ new_content_for = existing_content_for + (block_given? ? capture(&block) : content)
+ instance_variable_set("@content_for_#{name}", new_content_for)
+ end
+
+ private
+ def capture_block(*args, &block)
+ block.call(*args)
+ end
+
+ def capture_erb(*args, &block)
+ buffer = eval(ActionView::Base.erb_variable, block.binding)
+ capture_erb_with_buffer(buffer, *args, &block)
+ end
+
+ def capture_erb_with_buffer(buffer, *args, &block)
+ pos = buffer.length
+ block.call(*args)
+
+ # extract the block
+ data = buffer[pos..-1]
+
+ # replace it in the original with empty string
+ buffer[pos..-1] = ''
+
+ data
+ end
+
+ def erb_content_for(name, &block)
+ eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)"
+ end
+
+ def block_content_for(name, &block)
+ eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)"
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/date_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/date_helper.rb
new file mode 100755
index 000000000..c2091dfd4
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/date_helper.rb
@@ -0,0 +1,689 @@
+require "date"
+
+module ActionView
+ module Helpers
+ # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods
+ # share a number of common options that are as follows:
+ #
+ # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give
+ # birthday[month] instead of date[month] if passed to the select_month method.
+ # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
+ # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, the select_month
+ # method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]".
+ module DateHelper
+ DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX')
+
+ # Reports the approximate distance in time between two Time or Date objects or integers as seconds.
+ # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
+ # Distances are reported base on the following table:
+ #
+ # 0 <-> 29 secs # => less than a minute
+ # 30 secs <-> 1 min, 29 secs # => 1 minute
+ # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
+ # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
+ # 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
+ # 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day
+ # 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
+ # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month
+ # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
+ # 1 yr <-> 2 yrs minus 1 secs # => about 1 year
+ # 2 yrs <-> max time or date # => over [2..X] years
+ #
+ # With include_seconds = true and the difference < 1 minute 29 seconds
+ # 0-4 secs # => less than 5 seconds
+ # 5-9 secs # => less than 10 seconds
+ # 10-19 secs # => less than 20 seconds
+ # 20-39 secs # => half a minute
+ # 40-59 secs # => less than a minute
+ # 60-89 secs # => 1 minute
+ #
+ # ==== Examples
+ # from_time = Time.now
+ # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
+ # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
+ # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds
+ # distance_of_time_in_words(from_time, 3.years.from_now) # => over 3 years
+ # distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days
+ # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
+ # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
+ # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
+ # distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years
+ #
+ # to_time = Time.now + 6.years + 19.days
+ # distance_of_time_in_words(from_time, to_time, true) # => over 6 years
+ # distance_of_time_in_words(to_time, from_time, true) # => over 6 years
+ # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
+ #
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
+ distance_in_seconds = ((to_time - from_time).abs).round
+
+ case distance_in_minutes
+ when 0..1
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
+ case distance_in_seconds
+ when 0..4 then 'less than 5 seconds'
+ when 5..9 then 'less than 10 seconds'
+ when 10..19 then 'less than 20 seconds'
+ when 20..39 then 'half a minute'
+ when 40..59 then 'less than a minute'
+ else '1 minute'
+ end
+
+ when 2..44 then "#{distance_in_minutes} minutes"
+ when 45..89 then 'about 1 hour'
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
+ when 1440..2879 then '1 day'
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
+ when 43200..86399 then 'about 1 month'
+ when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
+ when 525600..1051199 then 'about 1 year'
+ else "over #{(distance_in_minutes / 525600).round} years"
+ end
+ end
+
+ # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
+ #
+ # ==== Examples
+ # time_ago_in_words(3.minutes.from_now) # => 3 minutes
+ # time_ago_in_words(Time.now - 15.hours) # => 15 hours
+ # time_ago_in_words(Time.now) # => less than a minute
+ #
+ # from_time = Time.now - 3.days - 14.minutes - 25.seconds # => 3 days
+ def time_ago_in_words(from_time, include_seconds = false)
+ distance_of_time_in_words(from_time, Time.now, include_seconds)
+ end
+
+ alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
+
+ # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by
+ # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash,
+ # which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of
+ # discard options. The discard options are <tt>:discard_year</tt>, <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll
+ # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly
+ # set the order of the tags using the <tt>:order</tt> option with an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in
+ # the desired order. Symbols may be omitted and the respective select is not included.
+ #
+ # Pass the <tt>:default</tt> option to set the default date. Use a Time object or a Hash of :year, :month, :day, :hour, :minute, and :second.
+ #
+ # Passing :disabled => true as part of the +options+ will make elements inaccessible for change.
+ #
+ # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed.
+ #
+ # ==== Examples
+ # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute
+ # date_select("post", "written_on")
+ #
+ # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute,
+ # # with the year in the year drop down box starting at 1995.
+ # date_select("post", "written_on", :start_year => 1995)
+ #
+ # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute,
+ # # with the year in the year drop down box starting at 1995, numbers used for months instead of words,
+ # # and without a day select box.
+ # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true,
+ # :discard_day => true, :include_blank => true)
+ #
+ # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute
+ # # with the fields ordered as day, month, year rather than month, day, year.
+ # date_select("post", "written_on", :order => [:day, :month, :year])
+ #
+ # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
+ # # lacking a year field.
+ # date_select("user", "birthday", :order => [:month, :day])
+ #
+ # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
+ # # which is initially set to the date 3 days from the current date
+ # date_select("post", "written_on", :default => 3.days.from_now)
+ #
+ # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute
+ # # that will have a default day of 20.
+ # date_select("credit_card", "bill_due", :default => { :day => 20 })
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ #
+ # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month
+ # choices are valid.
+ def date_select(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options)
+ end
+
+ # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified
+ # time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+).
+ # You can include the seconds with <tt>:include_seconds</tt>.
+ #
+ # ==== Examples
+ # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute
+ # time_select("post", "sunrise")
+ #
+ # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute
+ # time_select("order", "submitted")
+ #
+ # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute
+ # time_select("mail", "sent_at")
+ #
+ # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in
+ # # the sunrise attribute.
+ # time_select("post", "start_time", :include_seconds => true)
+ #
+ # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in
+ # # the submission_time attribute.
+ # time_select("entry", "submission_time", :include_seconds => true)
+ #
+ # # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45.
+ # time_select 'game', 'game_time', {:minute_step => 15}
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ #
+ # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month
+ # choices are valid.
+ def time_select(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_time_select_tag(options)
+ end
+
+ # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based
+ # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples:
+ #
+ # ==== Examples
+ # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute
+ # datetime_select("post", "written_on")
+ #
+ # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
+ # # post variable in the written_on attribute.
+ # datetime_select("post", "written_on", :start_year => 1995)
+ #
+ # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will be stored in the
+ # # trip variable in the departing attribute.
+ # datetime_select("trip", "departing", :default => 3.days.from_now)
+ #
+ # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable as the written_on
+ # # attribute.
+ # datetime_select("post", "written_on", :discard_type => true)
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ def datetime_select(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options)
+ end
+
+ # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+.
+ # It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
+ # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it
+ # will be appended onto the <tt>:order</tt> passed in. You can also add <tt>:date_separator</tt> and <tt>:time_separator</tt>
+ # keys to the +options+ to control visual display of the elements.
+ #
+ # ==== Examples
+ # my_date_time = Time.now + 4.days
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # select_datetime(my_date_time)
+ #
+ # # Generates a datetime select that defaults to today (no specified datetime)
+ # select_datetime()
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # with the fields ordered year, month, day rather than month, day, year.
+ # select_datetime(my_date_time, :order => [:year, :month, :day])
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # with a '/' between each date field.
+ # select_datetime(my_date_time, :date_separator => '/')
+ #
+ # # Generates a datetime select that discards the type of the field and defaults to the datetime in
+ # # my_date_time (four days after today)
+ # select_datetime(my_date_time, :discard_type => true)
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # prefixed with 'payday' rather than 'date'
+ # select_datetime(my_date_time, :prefix => 'payday')
+ #
+ def select_datetime(datetime = Time.now, options = {})
+ separator = options[:datetime_separator] || ''
+ select_date(datetime, options) + separator + select_time(datetime, options)
+ end
+
+ # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
+ # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
+ # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it
+ # will be appended onto the <tt>:order</tt> passed in.
+ #
+ # ==== Examples
+ # my_date = Time.today + 6.days
+ #
+ # # Generates a date select that defaults to the date in my_date (six days after today)
+ # select_date(my_date)
+ #
+ # # Generates a date select that defaults to today (no specified date)
+ # select_date()
+ #
+ # # Generates a date select that defaults to the date in my_date (six days after today)
+ # # with the fields ordered year, month, day rather than month, day, year.
+ # select_date(my_date, :order => [:year, :month, :day])
+ #
+ # # Generates a date select that discards the type of the field and defaults to the date in
+ # # my_date (six days after today)
+ # select_datetime(my_date_time, :discard_type => true)
+ #
+ # # Generates a date select that defaults to the datetime in my_date (six days after today)
+ # # prefixed with 'payday' rather than 'date'
+ # select_datetime(my_date_time, :prefix => 'payday')
+ #
+ def select_date(date = Date.today, options = {})
+ options[:order] ||= []
+ [:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) }
+
+ select_date = ''
+ options[:order].each do |o|
+ select_date << self.send("select_#{o}", date, options)
+ end
+ select_date
+ end
+
+ # Returns a set of html select-tags (one for hour and minute)
+ # You can set <tt>:time_separator</tt> key to format the output, and
+ # the <tt>:include_seconds</tt> option to include an input for seconds.
+ #
+ # ==== Examples
+ # my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds
+ #
+ # # Generates a time select that defaults to the time in my_time
+ # select_time(my_time)
+ #
+ # # Generates a time select that defaults to the current time (no specified time)
+ # select_time()
+ #
+ # # Generates a time select that defaults to the time in my_time,
+ # # which has fields separated by ':'
+ # select_time(my_time, :time_separator => ':')
+ #
+ # # Generates a time select that defaults to the time in my_time,
+ # # that also includes an input for seconds
+ # select_time(my_time, :include_seconds => true)
+ #
+ # # Generates a time select that defaults to the time in my_time, that has fields
+ # # separated by ':' and includes an input for seconds
+ # select_time(my_time, :time_separator => ':', :include_seconds => true)
+ #
+ def select_time(datetime = Time.now, options = {})
+ separator = options[:time_separator] || ''
+ select_hour(datetime, options) + separator + select_minute(datetime, options) + (options[:include_seconds] ? separator + select_second(datetime, options) : '')
+ end
+
+ # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
+ # The <tt>second</tt> can also be substituted for a second number.
+ # Override the field name using the <tt>:field_name</tt> option, 'second' by default.
+ #
+ # ==== Examples
+ # my_time = Time.now + 16.minutes
+ #
+ # # Generates a select field for seconds that defaults to the seconds for the time in my_time
+ # select_second(my_time)
+ #
+ # # Generates a select field for seconds that defaults to the number given
+ # select_second(33)
+ #
+ # # Generates a select field for seconds that defaults to the seconds for the time in my_time
+ # # that is named 'interval' rather than 'second'
+ # select_second(my_time, :field_name => 'interval')
+ #
+ def select_second(datetime, options = {})
+ val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : ''
+ if options[:use_hidden]
+ options[:include_seconds] ? hidden_html(options[:field_name] || 'second', val, options) : ''
+ else
+ second_options = []
+ 0.upto(59) do |second|
+ second_options << ((val == second) ?
+ %(<option value="#{leading_zero_on_single_digits(second)}" selected="selected">#{leading_zero_on_single_digits(second)}</option>\n) :
+ %(<option value="#{leading_zero_on_single_digits(second)}">#{leading_zero_on_single_digits(second)}</option>\n)
+ )
+ end
+ select_html(options[:field_name] || 'second', second_options, options)
+ end
+ end
+
+ # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
+ # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute selected
+ # The <tt>minute</tt> can also be substituted for a minute number.
+ # Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
+ #
+ # ==== Examples
+ # my_time = Time.now + 6.hours
+ #
+ # # Generates a select field for minutes that defaults to the minutes for the time in my_time
+ # select_minute(my_time)
+ #
+ # # Generates a select field for minutes that defaults to the number given
+ # select_minute(14)
+ #
+ # # Generates a select field for minutes that defaults to the minutes for the time in my_time
+ # # that is named 'stride' rather than 'second'
+ # select_minute(my_time, :field_name => 'stride')
+ #
+ def select_minute(datetime, options = {})
+ val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : ''
+ if options[:use_hidden]
+ hidden_html(options[:field_name] || 'minute', val, options)
+ else
+ minute_options = []
+ 0.step(59, options[:minute_step] || 1) do |minute|
+ minute_options << ((val == minute) ?
+ %(<option value="#{leading_zero_on_single_digits(minute)}" selected="selected">#{leading_zero_on_single_digits(minute)}</option>\n) :
+ %(<option value="#{leading_zero_on_single_digits(minute)}">#{leading_zero_on_single_digits(minute)}</option>\n)
+ )
+ end
+ select_html(options[:field_name] || 'minute', minute_options, options)
+ end
+ end
+
+ # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
+ # The <tt>hour</tt> can also be substituted for a hour number.
+ # Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
+ #
+ # ==== Examples
+ # my_time = Time.now + 6.hours
+ #
+ # # Generates a select field for minutes that defaults to the minutes for the time in my_time
+ # select_minute(my_time)
+ #
+ # # Generates a select field for minutes that defaults to the number given
+ # select_minute(14)
+ #
+ # # Generates a select field for minutes that defaults to the minutes for the time in my_time
+ # # that is named 'stride' rather than 'second'
+ # select_minute(my_time, :field_name => 'stride')
+ #
+ def select_hour(datetime, options = {})
+ val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : ''
+ if options[:use_hidden]
+ hidden_html(options[:field_name] || 'hour', val, options)
+ else
+ hour_options = []
+ 0.upto(23) do |hour|
+ hour_options << ((val == hour) ?
+ %(<option value="#{leading_zero_on_single_digits(hour)}" selected="selected">#{leading_zero_on_single_digits(hour)}</option>\n) :
+ %(<option value="#{leading_zero_on_single_digits(hour)}">#{leading_zero_on_single_digits(hour)}</option>\n)
+ )
+ end
+ select_html(options[:field_name] || 'hour', hour_options, options)
+ end
+ end
+
+ # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
+ # The <tt>date</tt> can also be substituted for a hour number.
+ # Override the field name using the <tt>:field_name</tt> option, 'day' by default.
+ #
+ # ==== Examples
+ # my_date = Time.today + 2.days
+ #
+ # # Generates a select field for days that defaults to the day for the date in my_date
+ # select_day(my_time)
+ #
+ # # Generates a select field for days that defaults to the number given
+ # select_day(5)
+ #
+ # # Generates a select field for days that defaults to the day for the date in my_date
+ # # that is named 'due' rather than 'day'
+ # select_day(my_time, :field_name => 'due')
+ #
+ def select_day(date, options = {})
+ val = date ? (date.kind_of?(Fixnum) ? date : date.day) : ''
+ if options[:use_hidden]
+ hidden_html(options[:field_name] || 'day', val, options)
+ else
+ day_options = []
+ 1.upto(31) do |day|
+ day_options << ((val == day) ?
+ %(<option value="#{day}" selected="selected">#{day}</option>\n) :
+ %(<option value="#{day}">#{day}</option>\n)
+ )
+ end
+ select_html(options[:field_name] || 'day', day_options, options)
+ end
+ end
+
+ # Returns a select tag with options for each of the months January through December with the current month selected.
+ # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values
+ # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names --
+ # set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names,
+ # set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer to show month names as abbreviations,
+ # set the <tt>:use_short_month</tt> key in +options+ to true. If you want to use your own month names, set the
+ # <tt>:use_month_names</tt> key in +options+ to an array of 12 month names. Override the field name using the
+ # <tt>:field_name</tt> option, 'month' by default.
+ #
+ # ==== Examples
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "January", "March".
+ # select_month(Date.today)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # is named "start" rather than "month"
+ # select_month(Date.today, :field_name => 'start')
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "1", "3".
+ # select_month(Date.today, :use_month_numbers => true)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "1 - January", "3 - March".
+ # select_month(Date.today, :add_month_numbers => true)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "Jan", "Mar".
+ # select_month(Date.today, :use_short_month => true)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "Januar", "Marts."
+ # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...))
+ #
+ def select_month(date, options = {})
+ val = date ? (date.kind_of?(Fixnum) ? date : date.month) : ''
+ if options[:use_hidden]
+ hidden_html(options[:field_name] || 'month', val, options)
+ else
+ month_options = []
+ month_names = options[:use_month_names] || (options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES)
+ month_names.unshift(nil) if month_names.size < 13
+ 1.upto(12) do |month_number|
+ month_name = if options[:use_month_numbers]
+ month_number
+ elsif options[:add_month_numbers]
+ month_number.to_s + ' - ' + month_names[month_number]
+ else
+ month_names[month_number]
+ end
+
+ month_options << ((val == month_number) ?
+ %(<option value="#{month_number}" selected="selected">#{month_name}</option>\n) :
+ %(<option value="#{month_number}">#{month_name}</option>\n)
+ )
+ end
+ select_html(options[:field_name] || 'month', month_options, options)
+ end
+ end
+
+ # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius
+ # can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. Both ascending and descending year
+ # lists are supported by making <tt>:start_year</tt> less than or greater than <tt>:end_year</tt>. The <tt>date</tt> can also be
+ # substituted for a year given as a number. Override the field name using the <tt>:field_name</tt> option, 'year' by default.
+ #
+ # ==== Examples
+ # # Generates a select field for years that defaults to the current year that
+ # # has ascending year values
+ # select_year(Date.today, :start_year => 1992, :end_year => 2007)
+ #
+ # # Generates a select field for years that defaults to the current year that
+ # # is named 'birth' rather than 'year'
+ # select_year(Date.today, :field_name => 'birth')
+ #
+ # # Generates a select field for years that defaults to the current year that
+ # # has descending year values
+ # select_year(Date.today, :start_year => 2005, :end_year => 1900)
+ #
+ # # Generates a select field for years that defaults to the year 2006 that
+ # # has ascending year values
+ # select_year(2006, :start_year => 2000, :end_year => 2010)
+ #
+ def select_year(date, options = {})
+ val = date ? (date.kind_of?(Fixnum) ? date : date.year) : ''
+ if options[:use_hidden]
+ hidden_html(options[:field_name] || 'year', val, options)
+ else
+ year_options = []
+ y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year
+
+ start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5)
+ step_val = start_year < end_year ? 1 : -1
+ start_year.step(end_year, step_val) do |year|
+ year_options << ((val == year) ?
+ %(<option value="#{year}" selected="selected">#{year}</option>\n) :
+ %(<option value="#{year}">#{year}</option>\n)
+ )
+ end
+ select_html(options[:field_name] || 'year', year_options, options)
+ end
+ end
+
+ private
+
+ def select_html(type, html_options, options)
+ name_and_id_from_options(options, type)
+ select_html = %(<select id="#{options[:id]}" name="#{options[:name]}")
+ select_html << %( disabled="disabled") if options[:disabled]
+ select_html << %(>\n)
+ select_html << %(<option value=""></option>\n) if options[:include_blank]
+ select_html << html_options.to_s
+ select_html << "</select>\n"
+ end
+
+ def hidden_html(type, value, options)
+ name_and_id_from_options(options, type)
+ hidden_html = %(<input type="hidden" id="#{options[:id]}" name="#{options[:name]}" value="#{value}" />\n)
+ end
+
+ def name_and_id_from_options(options, type)
+ options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]")
+ options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
+ end
+
+ def leading_zero_on_single_digits(number)
+ number > 9 ? number : "0#{number}"
+ end
+ end
+
+ class InstanceTag #:nodoc:
+ include DateHelper
+
+ def to_date_select_tag(options = {})
+ date_or_time_select(options.merge(:discard_hour => true))
+ end
+
+ def to_time_select_tag(options = {})
+ date_or_time_select options.merge(:discard_year => true, :discard_month => true)
+ end
+
+ def to_datetime_select_tag(options = {})
+ date_or_time_select options
+ end
+
+ private
+ def date_or_time_select(options)
+ defaults = { :discard_type => true }
+ options = defaults.merge(options)
+ datetime = value(object)
+ datetime ||= default_time_from_options(options[:default]) unless options[:include_blank]
+
+ position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
+
+ order = (options[:order] ||= [:year, :month, :day])
+
+ # Discard explicit and implicit by not being included in the :order
+ discard = {}
+ discard[:year] = true if options[:discard_year] or !order.include?(:year)
+ discard[:month] = true if options[:discard_month] or !order.include?(:month)
+ discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day)
+ discard[:hour] = true if options[:discard_hour]
+ discard[:minute] = true if options[:discard_minute] or discard[:hour]
+ discard[:second] = true unless options[:include_seconds] && !discard[:minute]
+
+ # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid
+ # (otherwise it could be 31 and february wouldn't be a valid date)
+ if datetime && discard[:day] && !discard[:month]
+ datetime = datetime.change(:day => 1)
+ end
+
+ # Maintain valid dates by including hidden fields for discarded elements
+ [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
+
+ # Ensure proper ordering of :hour, :minute and :second
+ [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) }
+
+ date_or_time_select = ''
+ order.reverse.each do |param|
+ # Send hidden fields for discarded elements once output has started
+ # This ensures AR can reconstruct valid dates using ParseDate
+ next if discard[param] && date_or_time_select.empty?
+
+ date_or_time_select.insert(0, self.send("select_#{param}", datetime, options_with_prefix(position[param], options.merge(:use_hidden => discard[param]))))
+ date_or_time_select.insert(0,
+ case param
+ when :hour then (discard[:year] && discard[:day] ? "" : " &mdash; ")
+ when :minute then " : "
+ when :second then options[:include_seconds] ? " : " : ""
+ else ""
+ end)
+
+ end
+
+ date_or_time_select
+ end
+
+ def options_with_prefix(position, options)
+ prefix = "#{@object_name}"
+ if options[:index]
+ prefix << "[#{options[:index]}]"
+ elsif @auto_index
+ prefix << "[#{@auto_index}]"
+ end
+ options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]")
+ end
+
+ def default_time_from_options(default)
+ case default
+ when nil
+ Time.now
+ when Date, Time
+ default
+ else
+ # Rename :minute and :second to :min and :sec
+ default[:min] ||= default[:minute]
+ default[:sec] ||= default[:second]
+
+ [:year, :month, :day, :hour, :min, :sec].each do |key|
+ default[key] ||= Time.now.send(key)
+ end
+
+ Time.mktime(default[:year], default[:month], default[:day],
+ default[:hour], default[:min], default[:sec])
+ end
+ end
+ end
+
+ class FormBuilder
+ def date_select(method, options = {})
+ @template.date_select(@object_name, method, options.merge(:object => @object))
+ end
+
+ def time_select(method, options = {})
+ @template.time_select(@object_name, method, options.merge(:object => @object))
+ end
+
+ def datetime_select(method, options = {})
+ @template.datetime_select(@object_name, method, options.merge(:object => @object))
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/debug_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/debug_helper.rb
new file mode 100644
index 000000000..20de7e465
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/debug_helper.rb
@@ -0,0 +1,31 @@
+module ActionView
+ module Helpers
+ # Provides a set of methods for making it easier to debug Rails objects.
+ module DebugHelper
+ # Returns a <pre>-tag that has +object+ dumped by YAML. This creates a very
+ # readable way to inspect an object.
+ #
+ # ==== Example
+ # my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]}
+ # debug(my_hash)
+ #
+ # => <pre class='debug_dump'>---
+ # first: 1
+ # second: two
+ # third:
+ # - 1
+ # - 2
+ # - 3
+ # </pre>
+ def debug(object)
+ begin
+ Marshal::dump(object)
+ "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", "&nbsp; ")}</pre>"
+ rescue Exception => e # errors from Marshal or YAML
+ # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
+ "<code class='debug_dump'>#{h(object.inspect)}</code>"
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_helper.rb
new file mode 100644
index 000000000..2bfb89663
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_helper.rb
@@ -0,0 +1,689 @@
+require 'cgi'
+require 'action_view/helpers/date_helper'
+require 'action_view/helpers/tag_helper'
+
+module ActionView
+ module Helpers
+ # Form helpers are designed to make working with models much easier compared to using just standard HTML
+ # elements by providing a set of methods for creating forms based on your models. This helper generates the HTML
+ # for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form
+ # is submitted (i.e., when the user hits the submit button or <tt>form.submit</tt> is called via JavaScript), the form inputs will be bundled into the <tt>params</tt> object and passed back to the controller.
+ #
+ # There are two types of form helpers: those that specifically work with model attributes and those that don't.
+ # This helper deals with those that work with model attributes; to see an example of form helpers that don't work
+ # with model attributes, check the ActionView::Helpers::FormTagHelper documentation.
+ #
+ # The core method of this helper, form_for, gives you the ability to create a form for a model instance;
+ # for example, let's say that you have a model <tt>Person</tt> and want to create a new instance of it:
+ #
+ # # Note: a @person variable will have been created in the controller.
+ # # For example: @person = Person.new
+ # <% form_for :person, @person, :url => { :action => "create" } do |f| %>
+ # <%= f.text_field :first_name %>
+ # <%= f.text_field :last_name %>
+ # <%= submit_tag 'Create' %>
+ # <% end %>
+ #
+ # The HTML generated for this would be:
+ #
+ # <form action="/persons/create" method="post">
+ # <input id="person_first_name" name="person[first_name]" size="30" type="text" />
+ # <input id="person_last_name" name="person[last_name]" size="30" type="text" />
+ # <input name="commit" type="submit" value="Create" />
+ # </form>
+ #
+ # The <tt>params</tt> object created when this form is submitted would look like:
+ #
+ # {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}}
+ #
+ # The params hash has a nested <tt>person</tt> value, which can therefore be accessed with <tt>params[:person]</tt> in the controller.
+ # If were editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than <tt>Person.new</tt> in the controller), the objects
+ # attribute values are filled into the form (e.g., the <tt>person_first_name</tt> field would have that person's first name in it).
+ #
+ # If the object name contains square brackets the id for the object will be inserted. For example:
+ #
+ # <%= text_field "person[]", "name" %>
+ #
+ # ...will generate the following ERb.
+ #
+ # <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" />
+ #
+ # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial
+ # used by <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may come in handy. Example:
+ #
+ # <%= text_field "person", "name", "index" => 1 %>
+ #
+ # ...becomes...
+ #
+ # <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" />
+ #
+ # There are also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
+ # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
+ module FormHelper
+ # Creates a form and a scope around a specific model object that is used as a base for questioning about
+ # values for the fields.
+ #
+ # <% form_for :person, @person, :url => { :action => "update" } do |f| %>
+ # First name: <%= f.text_field :first_name %>
+ # Last name : <%= f.text_field :last_name %>
+ # Biography : <%= f.text_area :biography %>
+ # Admin? : <%= f.check_box :admin %>
+ # <% end %>
+ #
+ # Worth noting is that the form_for tag is called in a ERb evaluation block, not an ERb output block. So that's <tt><% %></tt>,
+ # not <tt><%= %></tt>. Also worth noting is that form_for yields a <tt>form_builder</tt> object, in this example as <tt>f</tt>, which emulates
+ # the API for the stand-alone FormHelper methods, but without the object name. So instead of <tt>text_field :person, :name</tt>,
+ # you get away with <tt>f.text_field :name</tt>.
+ #
+ # Even further, the form_for method allows you to more easily escape the instance variable convention. So while the stand-alone
+ # approach would require <tt>text_field :person, :name, :object => person</tt>
+ # to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with
+ # <tt>:person, person</tt> and all subsequent field calls save <tt>:person</tt> and <tt>:object => person</tt>.
+ #
+ # Also note that form_for doesn't create an exclusive scope. It's still possible to use both the stand-alone FormHelper methods
+ # and methods from FormTagHelper. For example:
+ #
+ # <% form_for :person, @person, :url => { :action => "update" } do |f| %>
+ # First name: <%= f.text_field :first_name %>
+ # Last name : <%= f.text_field :last_name %>
+ # Biography : <%= text_area :person, :biography %>
+ # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %>
+ # <% end %>
+ #
+ # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base,
+ # like FormOptionHelper#collection_select and DateHelper#datetime_select.
+ #
+ # HTML attributes for the form tag can be given as :html => {...}. For example:
+ #
+ # <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %>
+ # ...
+ # <% end %>
+ #
+ # The above form will then have the <tt>id</tt> attribute with the value </tt>person_form</tt>, which you can then
+ # style with CSS or manipulate with JavaScript.
+ #
+ # === Relying on record identification
+ #
+ # In addition to manually configuring the form_for call, you can also rely on record identification, which will use
+ # the conventions and named routes of that approach. Examples:
+ #
+ # <% form_for(@post) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # This will expand to be the same as:
+ #
+ # <% form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
+ # ...
+ # <% end %>
+ #
+ # And for new records:
+ #
+ # <% form_for(Post.new) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # This will expand to be the same as:
+ #
+ # <% form_for :post, @post, :url => posts_path, :html => { :class => "new_post", :id => "new_post" } do |f| %>
+ # ...
+ # <% end %>
+ #
+ # You can also overwrite the individual conventions, like this:
+ #
+ # <% form_for(@post, :url => super_post_path(@post)) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # And for namespaced routes, like admin_post_url:
+ #
+ # <% form_for([:admin, @post]) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # === Customized form builders
+ #
+ # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
+ # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs.
+ #
+ # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %>
+ # <%= f.text_field :first_name %>
+ # <%= f.text_field :last_name %>
+ # <%= text_area :person, :biography %>
+ # <%= check_box_tag "person[admin]", @person.company.admin? %>
+ # <% end %>
+ #
+ # In many cases you will want to wrap the above in another helper, so you could do something like the following:
+ #
+ # def labelled_form_for(name, object, options, &proc)
+ # form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc)
+ # end
+ #
+ # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag.
+ def form_for(record_or_name_or_array, *args, &proc)
+ raise ArgumentError, "Missing block" unless block_given?
+
+ options = args.extract_options!
+
+ case record_or_name_or_array
+ when String, Symbol
+ object_name = record_or_name_or_array
+ when Array
+ object = record_or_name_or_array.last
+ object_name = ActionController::RecordIdentifier.singular_class_name(object)
+ apply_form_for_options!(record_or_name_or_array, options)
+ args.unshift object
+ else
+ object = record_or_name_or_array
+ object_name = ActionController::RecordIdentifier.singular_class_name(object)
+ apply_form_for_options!([object], options)
+ args.unshift object
+ end
+
+ concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding)
+ fields_for(object_name, *(args << options), &proc)
+ concat('</form>', proc.binding)
+ end
+
+ def apply_form_for_options!(object_or_array, options) #:nodoc:
+ object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array
+
+ html_options =
+ if object.respond_to?(:new_record?) && object.new_record?
+ { :class => dom_class(object, :new), :id => dom_id(object), :method => :post }
+ else
+ { :class => dom_class(object, :edit), :id => dom_id(object, :edit), :method => :put }
+ end
+
+ options[:html] ||= {}
+ options[:html].reverse_merge!(html_options)
+ options[:url] ||= polymorphic_path(object_or_array)
+ end
+
+ # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes
+ # fields_for suitable for specifying additional model objects in the same form:
+ #
+ # ==== Examples
+ # <% form_for @person, :url => { :action => "update" } do |person_form| %>
+ # First name: <%= person_form.text_field :first_name %>
+ # Last name : <%= person_form.text_field :last_name %>
+ #
+ # <% fields_for @person.permission do |permission_fields| %>
+ # Admin? : <%= permission_fields.check_box :admin %>
+ # <% end %>
+ # <% end %>
+ #
+ # ...or if you have an object that needs to be represented as a different parameter, like a Client that acts as a Person:
+ #
+ # <% fields_for :person, @client do |permission_fields| %>
+ # Admin?: <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # ...or if you don't have an object, just a name of the parameter
+ #
+ # <% fields_for :person do |permission_fields| %>
+ # Admin?: <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base,
+ # like FormOptionHelper#collection_select and DateHelper#datetime_select.
+ def fields_for(record_or_name_or_array, *args, &block)
+ raise ArgumentError, "Missing block" unless block_given?
+ options = args.extract_options!
+
+ case record_or_name_or_array
+ when String, Symbol
+ object_name = record_or_name_or_array
+ object = args.first
+ when Array
+ object = record_or_name_or_array.last
+ object_name = ActionController::RecordIdentifier.singular_class_name(object)
+ apply_form_for_options!(record_or_name_or_array, options)
+ else
+ object = record_or_name_or_array
+ object_name = ActionController::RecordIdentifier.singular_class_name(object)
+ end
+
+ builder = options[:builder] || ActionView::Base.default_form_builder
+ yield builder.new(object_name, object, self, options, block)
+ end
+
+ # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). The text of label will default to the attribute name unless you specify
+ # it explicitly. Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
+ # onto the HTML as an HTML element attribute as in the example shown.
+ #
+ # ==== Examples
+ # label(:post, :title)
+ # #=> <label for="post_title">Title</label>
+ #
+ # label(:post, :title, "A short title")
+ # #=> <label for="post_title">A short title</label>
+ #
+ # label(:post, :title, "A short title", :class => "title_label")
+ # #=> <label for="post_title" class="title_label">A short title</label>
+ #
+ def label(object_name, method, text = nil, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_label_tag(text, options)
+ end
+
+ # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # ==== Examples
+ # text_field(:post, :title, :size => 20)
+ # # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
+ #
+ # text_field(:post, :title, :class => "create_input")
+ # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
+ #
+ # text_field(:session, :user, :onchange => "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }")
+ # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange = "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }"/>
+ #
+ # text_field(:snippet, :code, :size => 20, :class => 'code_input')
+ # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
+ #
+ def text_field(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options)
+ end
+
+ # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # ==== Examples
+ # password_field(:login, :pass, :size => 20)
+ # # => <input type="text" id="login_pass" name="login[pass]" size="20" value="#{@login.pass}" />
+ #
+ # password_field(:account, :secret, :class => "form_input")
+ # # => <input type="text" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
+ #
+ # password_field(:user, :password, :onchange => "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }")
+ # # => <input type="text" id="user_password" name="user[password]" value="#{@user.password}" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/>
+ #
+ # password_field(:account, :pin, :size => 20, :class => 'form_input')
+ # # => <input type="text" id="account_pin" name="account[pin]" size="20" value="#{@account.pin}" class="form_input" />
+ #
+ def password_field(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options)
+ end
+
+ # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # ==== Examples
+ # hidden_field(:signup, :pass_confirm)
+ # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
+ #
+ # hidden_field(:post, :tag_list)
+ # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
+ #
+ # hidden_field(:user, :token)
+ # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+ def hidden_field(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options)
+ end
+
+ # Returns an file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # ==== Examples
+ # file_field(:user, :avatar)
+ # # => <input type="file" id="user_avatar" name="user[avatar]" />
+ #
+ # file_field(:post, :attached, :accept => 'text/html')
+ # # => <input type="file" id="post_attached" name="post[attached]" />
+ #
+ # file_field(:attachment, :file, :class => 'file_input')
+ # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
+ #
+ def file_field(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options)
+ end
+
+ # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
+ # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+.
+ #
+ # ==== Examples
+ # text_area(:post, :body, :cols => 20, :rows => 40)
+ # # => <textarea cols="20" rows="40" id="post_body" name="post[body]">
+ # # #{@post.body}
+ # # </textarea>
+ #
+ # text_area(:comment, :text, :size => "20x30")
+ # # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
+ # # #{@comment.text}
+ # # </textarea>
+ #
+ # text_area(:application, :notes, :cols => 40, :rows => 15, :class => 'app_input')
+ # # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input">
+ # # #{@application.notes}
+ # # </textarea>
+ #
+ # text_area(:entry, :body, :size => "20x20", :disabled => 'disabled')
+ # # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled">
+ # # #{@entry.body}
+ # # </textarea>
+ def text_area(object_name, method, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options)
+ end
+
+ # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that
+ # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a
+ # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+
+ # is set to 0 which is convenient for boolean values. Since HTTP standards say that unchecked checkboxes don't post anything,
+ # we add a hidden value with the same name as the checkbox as a work around.
+ #
+ # ==== Examples
+ # # Let's say that @post.validated? is 1:
+ # check_box("post", "validated")
+ # # => <input type="checkbox" id="post_validate" name="post[validated]" value="1" checked="checked" />
+ # # <input name="post[validated]" type="hidden" value="0" />
+ #
+ # # Let's say that @puppy.gooddog is "no":
+ # check_box("puppy", "gooddog", {}, "yes", "no")
+ # # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
+ # # <input name="puppy[gooddog]" type="hidden" value="no" />
+ #
+ # check_box("eula", "accepted", {}, "yes", "no", :class => 'eula_check')
+ # # => <input type="checkbox" id="eula_accepted" name="eula[accepted]" value="no" />
+ # # <input name="eula[accepted]" type="hidden" value="no" />
+ #
+ def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
+ end
+
+ # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
+ # radio button will be checked. Additional options on the input tag can be passed as a
+ # hash with +options+.
+ #
+ # ==== Examples
+ # # Let's say that @post.category returns "rails":
+ # radio_button("post", "category", "rails")
+ # radio_button("post", "category", "java")
+ # # => <input type="radio" id="post_category" name="post[category]" value="rails" checked="checked" />
+ # # <input type="radio" id="post_category" name="post[category]" value="java" />
+ #
+ # radio_button("user", "receive_newsletter", "yes")
+ # radio_button("user", "receive_newsletter", "no")
+ # # => <input type="radio" id="user_receive_newsletter" name="user[receive_newsletter]" value="yes" />
+ # # <input type="radio" id="user_receive_newsletter" name="user[receive_newsletter]" value="no" checked="checked" />
+ def radio_button(object_name, method, tag_value, options = {})
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options)
+ end
+ end
+
+ class InstanceTag #:nodoc:
+ include Helpers::TagHelper
+
+ attr_reader :method_name, :object_name
+
+ DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS)
+ DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS)
+ DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS)
+ DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS)
+
+ def initialize(object_name, method_name, template_object, local_binding = nil, object = nil)
+ @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
+ @template_object, @local_binding = template_object, local_binding
+ @object = object
+ if @object_name.sub!(/\[\]$/,"")
+ if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
+ @auto_index = object.to_param
+ else
+ raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
+ end
+ end
+ end
+
+ def to_label_tag(text = nil, options = {})
+ name_and_id = options.dup
+ add_default_name_and_id(name_and_id)
+ options["for"] = name_and_id["id"]
+ content = (text.blank? ? nil : text.to_s) || method_name.humanize
+ content_tag("label", content, options)
+ end
+
+ def to_input_field_tag(field_type, options = {})
+ options = options.stringify_keys
+ options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
+ options = DEFAULT_FIELD_OPTIONS.merge(options)
+ if field_type == "hidden"
+ options.delete("size")
+ end
+ options["type"] = field_type
+ options["value"] ||= value_before_type_cast(object) unless field_type == "file"
+ add_default_name_and_id(options)
+ tag("input", options)
+ end
+
+ def to_radio_button_tag(tag_value, options = {})
+ options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
+ options["type"] = "radio"
+ options["value"] = tag_value
+ if options.has_key?("checked")
+ cv = options.delete "checked"
+ checked = cv == true || cv == "checked"
+ else
+ checked = self.class.radio_button_checked?(value(object), tag_value)
+ end
+ options["checked"] = "checked" if checked
+ pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
+ options["id"] ||= defined?(@auto_index) ?
+ "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
+ "#{@object_name}_#{@method_name}_#{pretty_tag_value}"
+ add_default_name_and_id(options)
+ tag("input", options)
+ end
+
+ def to_text_area_tag(options = {})
+ options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
+ add_default_name_and_id(options)
+
+ if size = options.delete("size")
+ options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
+ end
+
+ content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
+ end
+
+ def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
+ options = options.stringify_keys
+ options["type"] = "checkbox"
+ options["value"] = checked_value
+ if options.has_key?("checked")
+ cv = options.delete "checked"
+ checked = cv == true || cv == "checked"
+ else
+ checked = self.class.check_box_checked?(value(object), checked_value)
+ end
+ options["checked"] = "checked" if checked
+ add_default_name_and_id(options)
+ tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
+ end
+
+ def to_date_tag()
+ defaults = DEFAULT_DATE_OPTIONS.dup
+ date = value(object) || Date.today
+ options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
+ html_day_select(date, options.call(3)) +
+ html_month_select(date, options.call(2)) +
+ html_year_select(date, options.call(1))
+ end
+
+ def to_boolean_select_tag(options = {})
+ options = options.stringify_keys
+ add_default_name_and_id(options)
+ value = value(object)
+ tag_text = "<select"
+ tag_text << tag_options(options)
+ tag_text << "><option value=\"false\""
+ tag_text << " selected" if value == false
+ tag_text << ">False</option><option value=\"true\""
+ tag_text << " selected" if value
+ tag_text << ">True</option></select>"
+ end
+
+ def to_content_tag(tag_name, options = {})
+ content_tag(tag_name, value(object), options)
+ end
+
+ def object
+ @object || (@template_object.instance_variable_get("@#{@object_name}") rescue nil)
+ end
+
+ def value(object)
+ self.class.value(object, @method_name)
+ end
+
+ def value_before_type_cast(object)
+ self.class.value_before_type_cast(object, @method_name)
+ end
+
+ class << self
+ def value(object, method_name)
+ object.send method_name unless object.nil?
+ end
+
+ def value_before_type_cast(object, method_name)
+ unless object.nil?
+ object.respond_to?(method_name + "_before_type_cast") ?
+ object.send(method_name + "_before_type_cast") :
+ object.send(method_name)
+ end
+ end
+
+ def check_box_checked?(value, checked_value)
+ case value
+ when TrueClass, FalseClass
+ value
+ when NilClass
+ false
+ when Integer
+ value != 0
+ when String
+ value == checked_value
+ else
+ value.to_i != 0
+ end
+ end
+
+ def radio_button_checked?(value, checked_value)
+ value.to_s == checked_value.to_s
+ end
+ end
+
+ private
+ def add_default_name_and_id(options)
+ if options.has_key?("index")
+ options["name"] ||= tag_name_with_index(options["index"])
+ options["id"] ||= tag_id_with_index(options["index"])
+ options.delete("index")
+ elsif defined?(@auto_index)
+ options["name"] ||= tag_name_with_index(@auto_index)
+ options["id"] ||= tag_id_with_index(@auto_index)
+ else
+ options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
+ options["id"] ||= tag_id
+ end
+ end
+
+ def tag_name
+ "#{@object_name}[#{@method_name}]"
+ end
+
+ def tag_name_with_index(index)
+ "#{@object_name}[#{index}][#{@method_name}]"
+ end
+
+ def tag_id
+ "#{sanitized_object_name}_#{@method_name}"
+ end
+
+ def tag_id_with_index(index)
+ "#{sanitized_object_name}_#{index}_#{@method_name}"
+ end
+
+ def sanitized_object_name
+ @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
+ end
+ end
+
+ class FormBuilder #:nodoc:
+ # The methods which wrap a form helper call.
+ class_inheritable_accessor :field_helpers
+ self.field_helpers = (FormHelper.instance_methods - ['form_for'])
+
+ attr_accessor :object_name, :object, :options
+
+ def initialize(object_name, object, template, options, proc)
+ @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
+ end
+
+ (field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
+ src = <<-end_src
+ def #{selector}(method, options = {})
+ @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object))
+ end
+ end_src
+ class_eval src, __FILE__, __LINE__
+ end
+
+ def fields_for(record_or_name_or_array, *args, &block)
+ case record_or_name_or_array
+ when String, Symbol
+ name = "#{object_name}[#{record_or_name_or_array}]"
+ when Array
+ object = record_or_name_or_array.last
+ name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
+ args.unshift(object)
+ else
+ object = record_or_name_or_array
+ name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
+ args.unshift(object)
+ end
+
+ @template.fields_for(name, *args, &block)
+ end
+
+ def label(method, text = nil, options = {})
+ @template.label(@object_name, method, text, options.merge(:object => @object))
+ end
+
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
+ @template.check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value)
+ end
+
+ def radio_button(method, tag_value, options = {})
+ @template.radio_button(@object_name, method, tag_value, options.merge(:object => @object))
+ end
+
+ def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError")
+ @template.error_message_on(@object, method, prepend_text, append_text, css_class)
+ end
+
+ def error_messages(options = {})
+ @template.error_messages_for(@object_name, options.merge(:object => @object))
+ end
+
+ def submit(value = "Save changes", options = {})
+ @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit"))
+ end
+ end
+ end
+
+ class Base
+ cattr_accessor :default_form_builder
+ self.default_form_builder = ::ActionView::Helpers::FormBuilder
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_options_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_options_helper.rb
new file mode 100644
index 000000000..0f1d2b02b
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_options_helper.rb
@@ -0,0 +1,425 @@
+require 'cgi'
+require 'erb'
+require 'action_view/helpers/form_helper'
+
+module ActionView
+ module Helpers
+ # Provides a number of methods for turning different kinds of containers into a set of option tags.
+ # == Options
+ # The <tt>collection_select</tt>, <tt>country_select</tt>, <tt>select</tt>,
+ # and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter,
+ # a hash.
+ #
+ # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
+ #
+ # For example,
+ #
+ # select("post", "category", Post::CATEGORIES, {:include_blank => true})
+ #
+ # could become:
+ #
+ # <select name="post[category]">
+ # <option></option>
+ # <option>joke</option>
+ # <option>poem</option>
+ # </select>
+ #
+ # Another common case is a select tag for an <tt>belongs_to</tt>-associated object.
+ #
+ # Example with @post.person_id => 2:
+ #
+ # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
+ #
+ # could become:
+ #
+ # <select name="post[person_id]">
+ # <option value="">None</option>
+ # <option value="1">David</option>
+ # <option value="2" selected="selected">Sam</option>
+ # <option value="3">Tobias</option>
+ # </select>
+ #
+ # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
+ #
+ # Example:
+ #
+ # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
+ #
+ # could become:
+ #
+ # <select name="post[person_id]">
+ # <option value="">Select Person</option>
+ # <option value="1">David</option>
+ # <option value="2">Sam</option>
+ # <option value="3">Tobias</option>
+ # </select>
+ module FormOptionsHelper
+ include ERB::Util
+
+ # Create a select tag and a series of contained option tags for the provided object and method.
+ # The option currently held by the object will be selected, provided that the object is available.
+ # See options_for_select for the required format of the choices parameter.
+ #
+ # Example with @post.person_id => 1:
+ # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true })
+ #
+ # could become:
+ #
+ # <select name="post[person_id]">
+ # <option value=""></option>
+ # <option value="1" selected="selected">David</option>
+ # <option value="2">Sam</option>
+ # <option value="3">Tobias</option>
+ # </select>
+ #
+ # This can be used to provide a default set of options in the standard way: before rendering the create form, a
+ # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
+ # to the database. Instead, a second model object is created when the create request is received.
+ # This allows the user to submit a form page more than once with the expected results of creating multiple records.
+ # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
+ #
+ # By default, post.person_id is the selected option. Specify :selected => value to use a different selection
+ # or :selected => nil to leave all options unselected.
+ def select(object, method, choices, options = {}, html_options = {})
+ InstanceTag.new(object, method, self, nil, options.delete(:object)).to_select_tag(choices, options, html_options)
+ end
+
+ # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
+ # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
+ # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
+ # or <tt>:include_blank</tt> in the +options+ hash.
+ #
+ # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
+ # of +collection+. The return values are used as the +value+ attribute and contents of each
+ # <tt><option></tt> tag, respectively.
+ #
+ # Example object structure for use with this method:
+ # class Post < ActiveRecord::Base
+ # belongs_to :author
+ # end
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # def name_with_initial
+ # "#{first_name.first}. #{last_name}"
+ # end
+ # end
+ #
+ # Sample usage (selecting the associated +Author+ for an instance of +Post+, <tt>@post</tt>):
+ # collection_select(:post, :author_id, Author.find(:all), :id, :name_with_initial, {:prompt => true})
+ #
+ # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
+ # <select name="post[author_id]">
+ # <option value="">Please select</option>
+ # <option value="1" selected="selected">D. Heinemeier Hansson</option>
+ # <option value="2">D. Thomas</option>
+ # <option value="3">M. Clark</option>
+ # </select>
+ def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
+ InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
+ end
+
+ # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
+ def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
+ InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options)
+ end
+
+ # Return select and option tags for the given object and method, using
+ # #time_zone_options_for_select to generate the list of option tags.
+ #
+ # In addition to the <tt>:include_blank</tt> option documented above,
+ # this method also supports a <tt>:model</tt> option, which defaults
+ # to TimeZone. This may be used by users to specify a different time
+ # zone model object. (See #time_zone_options_for_select for more
+ # information.)
+ def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
+ InstanceTag.new(object, method, self, nil, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options)
+ end
+
+ # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
+ # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
+ # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
+ # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+
+ # may also be an array of values to be selected when using a multiple select.
+ #
+ # Examples (call, result):
+ # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
+ # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
+ #
+ # options_for_select([ "VISA", "MasterCard" ], "MasterCard")
+ # <option>VISA</option>\n<option selected="selected">MasterCard</option>
+ #
+ # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
+ # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option>
+ #
+ # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
+ # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option>
+ #
+ # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+ def options_for_select(container, selected = nil)
+ container = container.to_a if Hash === container
+
+ options_for_select = container.inject([]) do |options, element|
+ text, value = option_text_and_value(element)
+ selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
+ options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}>#{html_escape(text.to_s)}</option>)
+ end
+
+ options_for_select.join("\n")
+ end
+
+ # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
+ # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
+ # If +selected+ is specified, the element returning a match on +value_method+ will get the selected option tag.
+ #
+ # Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate an input tag:
+ # options_from_collection_for_select(@project.people, "id", "name")
+ # <option value="#{person.id}">#{person.name}</option>
+ #
+ # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+ def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
+ options = collection.map do |element|
+ [element.send(text_method), element.send(value_method)]
+ end
+ options_for_select(options, selected)
+ end
+
+ # Returns a string of <tt><option></tt> tags, like <tt>#options_from_collection_for_select</tt>, but
+ # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
+ #
+ # Parameters:
+ # +collection+:: An array of objects representing the <tt><optgroup></tt> tags
+ # +group_method+:: The name of a method which, when called on a member of +collection+, returns an
+ # array of child objects representing the <tt><option></tt> tags
+ # +group_label_method+:: The name of a method which, when called on a member of +collection+, returns a
+ # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag
+ # +option_key_method+:: The name of a method which, when called on a child object of a member of
+ # +collection+, returns a value to be used as the +value+ attribute for its
+ # <tt><option></tt> tag
+ # +option_value_method+:: The name of a method which, when called on a child object of a member of
+ # +collection+, returns a value to be used as the contents of its
+ # <tt><option></tt> tag
+ # +selected_key+:: A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
+ # which will have the +selected+ attribute set. Corresponds to the return value
+ # of one of the calls to +option_key_method+. If +nil+, no selection is made.
+ #
+ # Example object structure for use with this method:
+ # class Continent < ActiveRecord::Base
+ # has_many :countries
+ # # attribs: id, name
+ # end
+ # class Country < ActiveRecord::Base
+ # belongs_to :continent
+ # # attribs: id, name, continent_id
+ # end
+ #
+ # Sample usage:
+ # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
+ #
+ # Possible output:
+ # <optgroup label="Africa">
+ # <option value="1">Egypt</option>
+ # <option value="4">Rwanda</option>
+ # ...
+ # </optgroup>
+ # <optgroup label="Asia">
+ # <option value="3" selected="selected">China</option>
+ # <option value="12">India</option>
+ # <option value="5">Japan</option>
+ # ...
+ # </optgroup>
+ #
+ # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
+ # wrap the output in an appropriate <tt><select></tt> tag.
+ def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
+ collection.inject("") do |options_for_select, group|
+ group_label_string = eval("group.#{group_label_method}")
+ options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
+ options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
+ options_for_select += '</optgroup>'
+ end
+ end
+
+ # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to
+ # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so
+ # that they will be listed above the rest of the (long) list.
+ #
+ # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+ def country_options_for_select(selected = nil, priority_countries = nil)
+ country_options = ""
+
+ if priority_countries
+ country_options += options_for_select(priority_countries, selected)
+ country_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
+ end
+
+ return country_options + options_for_select(COUNTRIES, selected)
+ end
+
+ # Returns a string of option tags for pretty much any time zone in the
+ # world. Supply a TimeZone name as +selected+ to have it marked as the
+ # selected option tag. You can also supply an array of TimeZone objects
+ # as +priority_zones+, so that they will be listed above the rest of the
+ # (long) list. (You can use TimeZone.us_zones as a convenience for
+ # obtaining a list of the US time zones.)
+ #
+ # The +selected+ parameter must be either +nil+, or a string that names
+ # a TimeZone.
+ #
+ # By default, +model+ is the TimeZone constant (which can be obtained
+ # in ActiveRecord as a value object). The only requirement is that the
+ # +model+ parameter be an object that responds to #all, and returns
+ # an array of objects that represent time zones.
+ #
+ # NOTE: Only the option tags are returned, you have to wrap this call in
+ # a regular HTML select tag.
+ def time_zone_options_for_select(selected = nil, priority_zones = nil, model = TimeZone)
+ zone_options = ""
+
+ zones = model.all
+ convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
+
+ if priority_zones
+ zone_options += options_for_select(convert_zones[priority_zones], selected)
+ zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
+
+ zones = zones.reject { |z| priority_zones.include?( z ) }
+ end
+
+ zone_options += options_for_select(convert_zones[zones], selected)
+ zone_options
+ end
+
+ private
+ def option_text_and_value(option)
+ # Options are [text, value] pairs or strings used for both.
+ if !option.is_a?(String) and option.respond_to?(:first) and option.respond_to?(:last)
+ [option.first, option.last]
+ else
+ [option, option]
+ end
+ end
+
+ def option_value_selected?(value, selected)
+ if selected.respond_to?(:include?) && !selected.is_a?(String)
+ selected.include? value
+ else
+ value == selected
+ end
+ end
+
+ # All the countries included in the country_options output.
+ COUNTRIES = ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola",
+ "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria",
+ "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin",
+ "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil",
+ "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia",
+ "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
+ "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
+ "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba",
+ "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt",
+ "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)",
+ "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia",
+ "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea",
+ "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)",
+ "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq",
+ "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya",
+ "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan",
+ "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya",
+ "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of",
+ "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
+ "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of",
+ "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru",
+ "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger",
+ "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau",
+ "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines",
+ "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation",
+ "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia",
+ "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino",
+ "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
+ "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa",
+ "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname",
+ "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic",
+ "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste",
+ "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan",
+ "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom",
+ "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela",
+ "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara",
+ "Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES")
+ end
+
+ class InstanceTag #:nodoc:
+ include FormOptionsHelper
+
+ def to_select_tag(choices, options, html_options)
+ html_options = html_options.stringify_keys
+ add_default_name_and_id(html_options)
+ value = value(object)
+ selected_value = options.has_key?(:selected) ? options[:selected] : value
+ content_tag("select", add_options(options_for_select(choices, selected_value), options, selected_value), html_options)
+ end
+
+ def to_collection_select_tag(collection, value_method, text_method, options, html_options)
+ html_options = html_options.stringify_keys
+ add_default_name_and_id(html_options)
+ value = value(object)
+ content_tag(
+ "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options
+ )
+ end
+
+ def to_country_select_tag(priority_countries, options, html_options)
+ html_options = html_options.stringify_keys
+ add_default_name_and_id(html_options)
+ value = value(object)
+ content_tag("select",
+ add_options(
+ country_options_for_select(value, priority_countries),
+ options, value
+ ), html_options
+ )
+ end
+
+ def to_time_zone_select_tag(priority_zones, options, html_options)
+ html_options = html_options.stringify_keys
+ add_default_name_and_id(html_options)
+ value = value(object)
+ content_tag("select",
+ add_options(
+ time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone),
+ options, value
+ ), html_options
+ )
+ end
+
+ private
+ def add_options(option_tags, options, value = nil)
+ if options[:include_blank]
+ option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
+ end
+ if value.blank? && options[:prompt]
+ ("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags
+ else
+ option_tags
+ end
+ end
+ end
+
+ class FormBuilder
+ def select(method, choices, options = {}, html_options = {})
+ @template.select(@object_name, method, choices, options.merge(:object => @object), html_options)
+ end
+
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
+ @template.collection_select(@object_name, method, collection, value_method, text_method, options.merge(:object => @object), html_options)
+ end
+
+ def country_select(method, priority_countries = nil, options = {}, html_options = {})
+ @template.country_select(@object_name, method, priority_countries, options.merge(:object => @object), html_options)
+ end
+
+ def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
+ @template.time_zone_select(@object_name, method, priority_zones, options.merge(:object => @object), html_options)
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_tag_helper.rb
new file mode 100644
index 000000000..077465086
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_tag_helper.rb
@@ -0,0 +1,432 @@
+require 'cgi'
+require 'action_view/helpers/tag_helper'
+
+module ActionView
+ module Helpers
+ # Provides a number of methods for creating form tags that doesn't rely on an ActiveRecord object assigned to the template like
+ # FormHelper does. Instead, you provide the names and values manually.
+ #
+ # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying
+ # <tt>:disabled => true</tt> will give <tt>disabled="disabled"</tt>.
+ module FormTagHelper
+ # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
+ # ActionController::Base#url_for. The method for the form defaults to POST.
+ #
+ # ==== Options
+ # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
+ # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
+ # If "put", "delete", or another verb is used, a hidden input with name _method
+ # is added to simulate the verb over post.
+ # * A list of parameters to feed to the URL the form will be posted to.
+ #
+ # ==== Examples
+ # form_tag('/posts')
+ # # => <form action="/posts" method="post">
+ #
+ # form_tag('/posts/1', :method => :put)
+ # # => <form action="/posts/1" method="put">
+ #
+ # form_tag('/upload', :multipart => true)
+ # # => <form action="/upload" method="post" enctype="multipart/form-data">
+ #
+ # <% form_tag '/posts' do -%>
+ # <div><%= submit_tag 'Save' %></div>
+ # <% end -%>
+ # # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
+ def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
+ html_options = html_options_for_form(url_for_options, options, *parameters_for_url)
+ if block_given?
+ form_tag_in_block(html_options, &block)
+ else
+ form_tag_html(html_options)
+ end
+ end
+
+ # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
+ # choice selection box.
+ #
+ # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or
+ # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box.
+ #
+ # ==== Options
+ # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices.
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # select_tag "people", "<option>David</option>"
+ # # => <select id="people" name="people"><option>David</option></select>
+ #
+ # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>"
+ # # => <select id="count" name="count"><option>1</option><option>2</option>
+ # # <option>3</option><option>4</option></select>
+ #
+ # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>", :multiple => true
+ # # => <select id="colors" multiple="multiple" name="colors"><option>Red</option>
+ # # <option>Green</option><option>Blue</option></select>
+ #
+ # select_tag "locations", "<option>Home</option><option selected="selected">Work</option><option>Out</option>"
+ # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option>
+ # # <option>Out</option></select>
+ #
+ # select_tag "access", "<option>Read</option><option>Write</option>", :multiple => true, :class => 'form_input'
+ # # => <select class="form_input" id="access" multiple="multiple" name="access"><option>Read</option>
+ # # <option>Write</option></select>
+ #
+ # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>", :disabled => true
+ # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
+ # # <option>Paris</option><option>Rome</option></select>
+ def select_tag(name, option_tags = nil, options = {})
+ content_tag :select, option_tags, { "name" => name, "id" => name }.update(options.stringify_keys)
+ end
+
+ # Creates a standard text field; use these text fields to input smaller chunks of text like a username
+ # or a search query.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:size</tt> - The number of visible characters that will fit in the input.
+ # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # text_field_tag 'name'
+ # # => <input id="name" name="name" type="text" />
+ #
+ # text_field_tag 'query', 'Enter your search query here'
+ # # => <input id="query" name="query" type="text" value="Enter your search query here" />
+ #
+ # text_field_tag 'request', nil, :class => 'special_input'
+ # # => <input class="special_input" id="request" name="request" type="text" />
+ #
+ # text_field_tag 'address', '', :size => 75
+ # # => <input id="address" name="address" size="75" type="text" value="" />
+ #
+ # text_field_tag 'zip', nil, :maxlength => 5
+ # # => <input id="zip" maxlength="5" name="zip" type="text" />
+ #
+ # text_field_tag 'payment_amount', '$0.00', :disabled => true
+ # # => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" />
+ #
+ # text_field_tag 'ip', '0.0.0.0', :maxlength => 15, :size => 20, :class => "ip-input"
+ # # => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" />
+ def text_field_tag(name, value = nil, options = {})
+ tag :input, { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys)
+ end
+
+ # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or
+ # data that should be hidden from the user.
+ #
+ # ==== Options
+ # * Creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # hidden_field_tag 'tags_list'
+ # # => <input id="tags_list" name="tags_list" type="hidden" />
+ #
+ # hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
+ # # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
+ #
+ # hidden_field_tag 'collected_input', '', :onchange => "alert('Input collected!')"
+ # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
+ # # type="hidden" value="" />
+ def hidden_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "hidden"))
+ end
+
+ # Creates a file upload field. If you are using file uploads then you will also need
+ # to set the multipart option for the form tag:
+ #
+ # <%= form_tag { :action => "post" }, { :multipart => true } %>
+ # <label for="file">File to Upload</label> <%= file_field_tag "file" %>
+ # <%= submit_tag %>
+ # <%= end_form_tag %>
+ #
+ # The specified URL will then be passed a File object containing the selected file, or if the field
+ # was left blank, a StringIO object.
+ #
+ # ==== Options
+ # * Creates standard HTML attributes for the tag.
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ #
+ # ==== Examples
+ # file_field_tag 'attachment'
+ # # => <input id="attachment" name="attachment" type="file" />
+ #
+ # file_field_tag 'avatar', :class => 'profile-input'
+ # # => <input class="profile-input" id="avatar" name="avatar" type="file" />
+ #
+ # file_field_tag 'picture', :disabled => true
+ # # => <input disabled="disabled" id="picture" name="picture" type="file" />
+ #
+ # file_field_tag 'resume', :value => '~/resume.doc'
+ # # => <input id="resume" name="resume" type="file" value="~/resume.doc" />
+ #
+ # file_field_tag 'user_pic', :accept => 'image/png,image/gif,image/jpeg'
+ # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" />
+ #
+ # file_field_tag 'file', :accept => 'text/html', :class => 'upload', :value => 'index.html'
+ # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
+ def file_field_tag(name, options = {})
+ text_field_tag(name, nil, options.update("type" => "file"))
+ end
+
+ # Creates a password field, a masked text field that will hide the users input behind a mask character.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:size</tt> - The number of visible characters that will fit in the input.
+ # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # password_field_tag 'pass'
+ # # => <input id="pass" name="pass" type="password" />
+ #
+ # password_field_tag 'secret', 'Your secret here'
+ # # => <input id="secret" name="secret" type="password" value="Your secret here" />
+ #
+ # password_field_tag 'masked', nil, :class => 'masked_input_field'
+ # # => <input class="masked_input_field" id="masked" name="masked" type="password" />
+ #
+ # password_field_tag 'token', '', :size => 15
+ # # => <input id="token" name="token" size="15" type="password" value="" />
+ #
+ # password_field_tag 'key', nil, :maxlength => 16
+ # # => <input id="key" maxlength="16" name="key" type="password" />
+ #
+ # password_field_tag 'confirm_pass', nil, :disabled => true
+ # # => <input disabled="disabled" id="confirm_pass" name="confirm_pass" type="password" />
+ #
+ # password_field_tag 'pin', '1234', :maxlength => 4, :size => 6, :class => "pin-input"
+ # # => <input class="pin-input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" />
+ def password_field_tag(name = "password", value = nil, options = {})
+ text_field_tag(name, value, options.update("type" => "password"))
+ end
+
+ # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
+ #
+ # ==== Options
+ # * <tt>:size</tt> - A string specifying the dimensions (columns by rows) of the textarea (e.g., "25x10").
+ # * <tt>:rows</tt> - Specify the number of rows in the textarea
+ # * <tt>:cols</tt> - Specify the number of columns in the textarea
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # text_area_tag 'post'
+ # # => <textarea id="post" name="post"></textarea>
+ #
+ # text_area_tag 'bio', @user.bio
+ # # => <textarea id="bio" name="bio">This is my biography.</textarea>
+ #
+ # text_area_tag 'body', nil, :rows => 10, :cols => 25
+ # # => <textarea cols="25" id="body" name="body" rows="10"></textarea>
+ #
+ # text_area_tag 'body', nil, :size => "25x10"
+ # # => <textarea name="body" id="body" cols="25" rows="10"></textarea>
+ #
+ # text_area_tag 'description', "Description goes here.", :disabled => true
+ # # => <textarea disabled="disabled" id="description" name="description">Description goes here.</textarea>
+ #
+ # text_area_tag 'comment', nil, :class => 'comment_input'
+ # # => <textarea class="comment_input" id="comment" name="comment"></textarea>
+ def text_area_tag(name, content = nil, options = {})
+ options.stringify_keys!
+
+ if size = options.delete("size")
+ options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
+ end
+
+ content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys)
+ end
+
+ # Creates a check box form input tag.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Examples
+ # check_box_tag 'accept'
+ # # => <input id="accept" name="accept" type="checkbox" value="1" />
+ #
+ # check_box_tag 'rock', 'rock music'
+ # # => <input id="rock" name="rock" type="checkbox" value="rock music" />
+ #
+ # check_box_tag 'receive_email', 'yes', true
+ # # => <input checked="checked" id="receive_email" name="receive_email" type="checkbox" value="yes" />
+ #
+ # check_box_tag 'tos', 'yes', false, :class => 'accept_tos'
+ # # => <input class="accept_tos" id="tos" name="tos" type="checkbox" value="yes" />
+ #
+ # check_box_tag 'eula', 'accepted', false, :disabled => true
+ # # => <input disabled="disabled" id="eula" name="eula" type="checkbox" value="accepted" />
+ def check_box_tag(name, value = "1", checked = false, options = {})
+ html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys)
+ html_options["checked"] = "checked" if checked
+ tag :input, html_options
+ end
+
+ # Creates a radio button; use groups of radio buttons named the same to allow users to
+ # select from a group of options.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Examples
+ # radio_button_tag 'gender', 'male'
+ # # => <input id="gender_male" name="gender" type="radio" value="male" />
+ #
+ # radio_button_tag 'receive_updates', 'no', true
+ # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" />
+ #
+ # radio_button_tag 'time_slot', "3:00 p.m.", false, :disabled => true
+ # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." />
+ #
+ # radio_button_tag 'color', "green", true, :class => "color_input"
+ # # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" />
+ def radio_button_tag(name, value, checked = false, options = {})
+ pretty_tag_value = value.to_s.gsub(/\s/, "_").gsub(/(?!-)\W/, "").downcase
+ pretty_name = name.to_s.gsub(/\[/, "_").gsub(/\]/, "")
+ html_options = { "type" => "radio", "name" => name, "id" => "#{pretty_name}_#{pretty_tag_value}", "value" => value }.update(options.stringify_keys)
+ html_options["checked"] = "checked" if checked
+ tag :input, html_options
+ end
+
+ # Creates a submit button with the text <tt>value</tt> as the caption.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a disabled version
+ # of the submit button when the form is submitted.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Examples
+ # submit_tag
+ # # => <input name="commit" type="submit" value="Save changes" />
+ #
+ # submit_tag "Edit this article"
+ # # => <input name="commit" type="submit" value="Edit this article" />
+ #
+ # submit_tag "Save edits", :disabled => true
+ # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" />
+ #
+ # submit_tag "Complete sale", :disable_with => "Please wait..."
+ # # => <input name="commit" onclick="this.disabled=true;this.value='Please wait...';this.form.submit();"
+ # # type="submit" value="Complete sale" />
+ #
+ # submit_tag nil, :class => "form_submit"
+ # # => <input class="form_submit" name="commit" type="submit" />
+ #
+ # submit_tag "Edit", :disable_with => "Editing...", :class => 'edit-button'
+ # # => <input class="edit-button" disable_with="Editing..." name="commit" type="submit" value="Edit" />
+ def submit_tag(value = "Save changes", options = {})
+ options.stringify_keys!
+
+ if disable_with = options.delete("disable_with")
+ options["onclick"] = [
+ "this.setAttribute('originalValue', this.value)",
+ "this.disabled=true",
+ "this.value='#{disable_with}'",
+ "#{options["onclick"]}",
+ "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit())",
+ "if (result == false) { this.value = this.getAttribute('originalValue'); this.disabled = false }",
+ "return result",
+ ].join(";")
+ end
+
+ tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys)
+ end
+
+ # Displays an image which when clicked will submit the form.
+ #
+ # <tt>source</tt> is passed to AssetTagHelper#image_path
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Examples
+ # image_submit_tag("login.png")
+ # # => <input src="/images/login.png" type="image" />
+ #
+ # image_submit_tag("purchase.png"), :disabled => true
+ # # => <input disabled="disabled" src="/images/purchase.png" type="image" />
+ #
+ # image_submit_tag("search.png"), :class => 'search-button'
+ # # => <input class="search-button" src="/images/search.png" type="image" />
+ #
+ # image_submit_tag("agree.png"), :disabled => true, :class => "agree-disagree-button"
+ # # => <input class="agree-disagree-button" disabled="disabled" src="/images/agree.png" type="image" />
+ def image_submit_tag(source, options = {})
+ tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys)
+ end
+
+ # Creates a field set for grouping HTML form elements.
+ #
+ # <tt>legend</tt> will become the fieldset's title (optional as per W3C).
+ #
+ # === Examples
+ # <% field_set_tag do %>
+ # <p><%= text_field_tag 'name' %></p>
+ # <% end %>
+ # # => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
+ #
+ # <% field_set_tag 'Your details' do %>
+ # <p><%= text_field_tag 'name' %></p>
+ # <% end %>
+ # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset>
+ def field_set_tag(legend = nil, &block)
+ content = capture(&block)
+ concat(tag(:fieldset, {}, true), block.binding)
+ concat(content_tag(:legend, legend), block.binding) unless legend.blank?
+ concat(content, block.binding)
+ concat("</fieldset>", block.binding)
+ end
+
+ private
+ def html_options_for_form(url_for_options, options, *parameters_for_url)
+ returning options.stringify_keys do |html_options|
+ html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
+ html_options["action"] = url_for(url_for_options, *parameters_for_url)
+ end
+ end
+
+ def extra_tags_for_form(html_options)
+ case method = html_options.delete("method").to_s
+ when /^get$/i # must be case-insentive, but can't use downcase as might be nil
+ html_options["method"] = "get"
+ ''
+ when /^post$/i, "", nil
+ html_options["method"] = "post"
+ protect_against_forgery? ? content_tag(:div, token_tag, :style => 'margin:0;padding:0') : ''
+ else
+ html_options["method"] = "post"
+ content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0')
+ end
+ end
+
+ def form_tag_html(html_options)
+ extra_tags = extra_tags_for_form(html_options)
+ tag(:form, html_options, true) + extra_tags
+ end
+
+ def form_tag_in_block(html_options, &block)
+ content = capture(&block)
+ concat(form_tag_html(html_options), block.binding)
+ concat(content, block.binding)
+ concat("</form>", block.binding)
+ end
+
+ def token_tag
+ unless protect_against_forgery?
+ ''
+ else
+ tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascript_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascript_helper.rb
new file mode 100644
index 000000000..1ea3cbd74
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascript_helper.rb
@@ -0,0 +1,217 @@
+require 'action_view/helpers/tag_helper'
+require 'action_view/helpers/prototype_helper'
+
+module ActionView
+ module Helpers
+ # Provides functionality for working with JavaScript in your views.
+ #
+ # == Ajax, controls and visual effects
+ #
+ # * For information on using Ajax, see
+ # ActionView::Helpers::PrototypeHelper.
+ # * For information on using controls and visual effects, see
+ # ActionView::Helpers::ScriptaculousHelper.
+ #
+ # == Including the JavaScript libraries into your pages
+ #
+ # Rails includes the Prototype JavaScript framework and the Scriptaculous
+ # JavaScript controls and visual effects library. If you wish to use
+ # these libraries and their helpers (ActionView::Helpers::PrototypeHelper
+ # and ActionView::Helpers::ScriptaculousHelper), you must do one of the
+ # following:
+ #
+ # * Use <tt><%= javascript_include_tag :defaults %></tt> in the HEAD
+ # section of your page (recommended): This function will return
+ # references to the JavaScript files created by the +rails+ command in
+ # your <tt>public/javascripts</tt> directory. Using it is recommended as
+ # the browser can then cache the libraries instead of fetching all the
+ # functions anew on every request.
+ # * Use <tt><%= javascript_include_tag 'prototype' %></tt>: As above, but
+ # will only include the Prototype core library, which means you are able
+ # to use all basic AJAX functionality. For the Scriptaculous-based
+ # JavaScript helpers, like visual effects, autocompletion, drag and drop
+ # and so on, you should use the method described above.
+ # * Use <tt><%= define_javascript_functions %></tt>: this will copy all the
+ # JavaScript support functions within a single script block. Not
+ # recommended.
+ #
+ # For documentation on +javascript_include_tag+ see
+ # ActionView::Helpers::AssetTagHelper.
+ module JavaScriptHelper
+ unless const_defined? :JAVASCRIPT_PATH
+ JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
+ end
+
+ include PrototypeHelper
+
+ # Returns a link that will trigger a JavaScript +function+ using the
+ # onclick handler and return false after the fact.
+ #
+ # The +function+ argument can be omitted in favor of an +update_page+
+ # block, which evaluates to a string when the template is rendered
+ # (instead of making an Ajax request first).
+ #
+ # Examples:
+ # link_to_function "Greeting", "alert('Hello world!')"
+ # Produces:
+ # <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a>
+ #
+ # link_to_function(image_tag("delete"), "if (confirm('Really?')) do_delete()")
+ # Produces:
+ # <a onclick="if (confirm('Really?')) do_delete(); return false;" href="#">
+ # <img src="/images/delete.png?" alt="Delete"/>
+ # </a>
+ #
+ # link_to_function("Show me more", nil, :id => "more_link") do |page|
+ # page[:details].visual_effect :toggle_blind
+ # page[:more_link].replace_html "Show me less"
+ # end
+ # Produces:
+ # <a href="#" id="more_link" onclick="try {
+ # $(&quot;details&quot;).visualEffect(&quot;toggle_blind&quot;);
+ # $(&quot;more_link&quot;).update(&quot;Show me less&quot;);
+ # }
+ # catch (e) {
+ # alert('RJS error:\n\n' + e.toString());
+ # alert('$(\&quot;details\&quot;).visualEffect(\&quot;toggle_blind\&quot;);
+ # \n$(\&quot;more_link\&quot;).update(\&quot;Show me less\&quot;);');
+ # throw e
+ # };
+ # return false;">Show me more</a>
+ #
+ def link_to_function(name, *args, &block)
+ html_options = args.extract_options!
+ function = args[0] || ''
+
+ html_options.symbolize_keys!
+ function = update_page(&block) if block_given?
+ content_tag(
+ "a", name,
+ html_options.merge({
+ :href => html_options[:href] || "#",
+ :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;"
+ })
+ )
+ end
+
+ # Returns a button that'll trigger a JavaScript +function+ using the
+ # onclick handler.
+ #
+ # The +function+ argument can be omitted in favor of an +update_page+
+ # block, which evaluates to a string when the template is rendered
+ # (instead of making an Ajax request first).
+ #
+ # Examples:
+ # button_to_function "Greeting", "alert('Hello world!')"
+ # button_to_function "Delete", "if (confirm('Really?')) do_delete()"
+ # button_to_function "Details" do |page|
+ # page[:details].visual_effect :toggle_slide
+ # end
+ # button_to_function "Details", :class => "details_button" do |page|
+ # page[:details].visual_effect :toggle_slide
+ # end
+ def button_to_function(name, *args, &block)
+ html_options = args.extract_options!
+ function = args[0] || ''
+
+ html_options.symbolize_keys!
+ function = update_page(&block) if block_given?
+ tag(:input, html_options.merge({
+ :type => "button", :value => name,
+ :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
+ }))
+ end
+
+ # Includes the Action Pack JavaScript libraries inside a single <script>
+ # tag. The function first includes prototype.js and then its core extensions,
+ # (determined by filenames starting with "prototype").
+ # Afterwards, any additional scripts will be included in undefined order.
+ #
+ # Note: The recommended approach is to copy the contents of
+ # lib/action_view/helpers/javascripts/ into your application's
+ # public/javascripts/ directory, and use +javascript_include_tag+ to
+ # create remote <script> links.
+ def define_javascript_functions
+ javascript = "<script type=\"#{Mime::JS}\">"
+
+ # load prototype.js and its extensions first
+ prototype_libs = Dir.glob(File.join(JAVASCRIPT_PATH, 'prototype*')).sort.reverse
+ prototype_libs.each do |filename|
+ javascript << "\n" << IO.read(filename)
+ end
+
+ # load other libraries
+ (Dir.glob(File.join(JAVASCRIPT_PATH, '*')) - prototype_libs).each do |filename|
+ javascript << "\n" << IO.read(filename)
+ end
+ javascript << '</script>'
+ end
+
+ # Escape carrier returns and single and double quotes for JavaScript segments.
+ def escape_javascript(javascript)
+ (javascript || '').gsub('\\','\0\0').gsub('</','<\/').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
+ end
+
+ # Returns a JavaScript tag with the +content+ inside. Example:
+ # javascript_tag "alert('All is good')"
+ #
+ # Returns:
+ # <script type="text/javascript">
+ # //<![CDATA[
+ # alert('All is good')
+ # //]]>
+ # </script>
+ #
+ # +html_options+ may be a hash of attributes for the <script> tag. Example:
+ # javascript_tag "alert('All is good')", :defer => 'defer'
+ # # => <script defer="defer" type="text/javascript">alert('All is good')</script>
+ #
+ # Instead of passing the content as an argument, you can also use a block
+ # in which case, you pass your +html_options+ as the first parameter.
+ # <% javascript_tag :defer => 'defer' do -%>
+ # alert('All is good')
+ # <% end -%>
+ def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
+ if block_given?
+ html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
+ content = capture(&block)
+ else
+ content = content_or_options_with_block
+ end
+
+ javascript_tag = content_tag("script", javascript_cdata_section(content), html_options.merge(:type => Mime::JS))
+
+ if block_given? && block_is_within_action_view?(block)
+ concat(javascript_tag, block.binding)
+ else
+ javascript_tag
+ end
+ end
+
+ def javascript_cdata_section(content) #:nodoc:
+ "\n//#{cdata_section("\n#{content}\n//")}\n"
+ end
+
+ protected
+ def options_for_javascript(options)
+ '{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}'
+ end
+
+ def array_or_string_for_javascript(option)
+ js_option = if option.kind_of?(Array)
+ "['#{option.join('\',\'')}']"
+ elsif !option.nil?
+ "'#{option}'"
+ end
+ js_option
+ end
+
+ private
+ def block_is_within_action_view?(block)
+ eval("defined? _erbout", block.binding)
+ end
+ end
+
+ JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/controls.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/controls.js
new file mode 100644
index 000000000..fbc4418b8
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/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,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {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("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || { });
+ }
+});
+
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (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/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/dragdrop.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/dragdrop.js
new file mode 100644
index 000000000..ccf4a1e45
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/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/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/effects.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/effects.js
new file mode 100644
index 000000000..65aed2395
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/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<len;i++)
+ this.effects[i] && this.effects[i].loop(timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if (!Object.isString(queueName)) return queueName;
+
+ return this.instances.get(queueName) ||
+ this.instances.set(queueName, new Effect.ScopedQueue());
+ }
+};
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.Base = Class.create({
+ position: null,
+ start: function(options) {
+ function codeForEvent(options,eventName){
+ return (
+ (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
+ (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
+ );
+ }
+ if (options && options.transition === false) options.transition = Effect.Transitions.linear;
+ this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn+(this.options.duration*1000);
+ this.fromToDelta = this.options.to-this.options.from;
+ this.totalTime = this.finishOn-this.startOn;
+ this.totalFrames = this.options.fps*this.options.duration;
+
+ eval('this.render = function(pos){ '+
+ 'if (this.state=="idle"){this.state="running";'+
+ codeForEvent(this.options,'beforeSetup')+
+ (this.setup ? 'this.setup();':'')+
+ codeForEvent(this.options,'afterSetup')+
+ '};if (this.state=="running"){'+
+ 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
+ 'this.position=pos;'+
+ codeForEvent(this.options,'beforeUpdate')+
+ (this.update ? 'this.update(pos);':'')+
+ codeForEvent(this.options,'afterUpdate')+
+ '}}');
+
+ this.event('beforeStart');
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if (timePos >= this.startOn) {
+ if (timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if (this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / this.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:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+});
+
+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 = '<div style="' + this + '"></div>';
+ 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/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/prototype.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/prototype.js
new file mode 100644
index 000000000..546f9fe44
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/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: '<script[^>]*>([\\S\\s]*?)<\/script>',
+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+ emptyFunction: function() { },
+ K: function(x) { return x }
+};
+
+if (Prototype.Browser.MobileSafari)
+ Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+
+/* Based on Alex Arnell's inheritance implementation. */
+var Class = {
+ create: function() {
+ var parent = null, properties = $A(arguments);
+ if (Object.isFunction(properties[0]))
+ parent = properties.shift();
+
+ function klass() {
+ this.initialize.apply(this, arguments);
+ }
+
+ Object.extend(klass, Class.Methods);
+ klass.superclass = parent;
+ klass.subclasses = [];
+
+ if (parent) {
+ var subclass = function() { };
+ subclass.prototype = parent.prototype;
+ klass.prototype = new subclass;
+ parent.subclasses.push(klass);
+ }
+
+ for (var i = 0; i < properties.length; i++)
+ klass.addMethods(properties[i]);
+
+ if (!klass.prototype.initialize)
+ klass.prototype.initialize = Prototype.emptyFunction;
+
+ klass.prototype.constructor = klass;
+
+ return klass;
+ }
+};
+
+Class.Methods = {
+ addMethods: function(source) {
+ var ancestor = this.superclass && this.superclass.prototype;
+ var properties = Object.keys(source);
+
+ if (!Object.keys({ toString: true }).length)
+ properties.push("toString", "valueOf");
+
+ for (var i = 0, length = properties.length; i < length; i++) {
+ var property = properties[i], value = source[property];
+ if (ancestor && Object.isFunction(value) &&
+ value.argumentNames().first() == "$super") {
+ var method = value, value = Object.extend((function(m) {
+ return function() { return ancestor[m].apply(this, arguments) };
+ })(property).wrap(method), {
+ valueOf: function() { return method },
+ toString: function() { return method.toString() }
+ });
+ }
+ this.prototype[property] = value;
+ }
+
+ return this;
+ }
+};
+
+var Abstract = { };
+
+Object.extend = function(destination, source) {
+ for (var property in source)
+ destination[property] = source[property];
+ return destination;
+};
+
+Object.extend(Object, {
+ inspect: function(object) {
+ try {
+ if (Object.isUndefined(object)) return 'undefined';
+ if (object === null) return 'null';
+ return object.inspect ? object.inspect() : object.toString();
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+
+ toJSON: function(object) {
+ var type = typeof object;
+ switch (type) {
+ case 'undefined':
+ case 'function':
+ case 'unknown': return;
+ case 'boolean': return object.toString();
+ }
+
+ if (object === null) return 'null';
+ if (object.toJSON) return object.toJSON();
+ if (Object.isElement(object)) return;
+
+ var results = [];
+ for (var property in object) {
+ var value = Object.toJSON(object[property]);
+ if (!Object.isUndefined(value))
+ results.push(property.toJSON() + ': ' + value);
+ }
+
+ return '{' + results.join(', ') + '}';
+ },
+
+ toQueryString: function(object) {
+ return $H(object).toQueryString();
+ },
+
+ toHTML: function(object) {
+ return object && object.toHTML ? object.toHTML() : String.interpret(object);
+ },
+
+ keys: function(object) {
+ var keys = [];
+ for (var property in object)
+ keys.push(property);
+ return keys;
+ },
+
+ values: function(object) {
+ var values = [];
+ for (var property in object)
+ values.push(object[property]);
+ return values;
+ },
+
+ clone: function(object) {
+ return Object.extend({ }, object);
+ },
+
+ isElement: function(object) {
+ return object && object.nodeType == 1;
+ },
+
+ isArray: function(object) {
+ return object && object.constructor === Array;
+ },
+
+ isHash: function(object) {
+ return object instanceof Hash;
+ },
+
+ isFunction: function(object) {
+ return typeof object == "function";
+ },
+
+ isString: function(object) {
+ return typeof object == "string";
+ },
+
+ isNumber: function(object) {
+ return typeof object == "number";
+ },
+
+ isUndefined: function(object) {
+ return typeof object == "undefined";
+ }
+});
+
+Object.extend(Function.prototype, {
+ argumentNames: function() {
+ var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
+ return names.length == 1 && !names[0] ? [] : names;
+ },
+
+ bind: function() {
+ if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+ },
+
+ bindAsEventListener: function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function(event) {
+ return __method.apply(object, [event || window.event].concat(args));
+ }
+ },
+
+ curry: function() {
+ if (!arguments.length) return this;
+ var __method = this, args = $A(arguments);
+ return function() {
+ return __method.apply(this, args.concat($A(arguments)));
+ }
+ },
+
+ delay: function() {
+ var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+ return window.setTimeout(function() {
+ return __method.apply(__method, args);
+ }, timeout);
+ },
+
+ wrap: function(wrapper) {
+ var __method = this;
+ return function() {
+ return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+ }
+ },
+
+ methodize: function() {
+ if (this._methodized) return this._methodized;
+ var __method = this;
+ return this._methodized = function() {
+ return __method.apply(null, [this].concat($A(arguments)));
+ };
+ }
+});
+
+Function.prototype.defer = Function.prototype.delay.curry(0.01);
+
+Date.prototype.toJSON = function() {
+ return '"' + this.getUTCFullYear() + '-' +
+ (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+ this.getUTCDate().toPaddedString(2) + 'T' +
+ this.getUTCHours().toPaddedString(2) + ':' +
+ this.getUTCMinutes().toPaddedString(2) + ':' +
+ this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) { }
+ }
+
+ return returnValue;
+ }
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+ return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create({
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ execute: function() {
+ this.callback(this);
+ },
+
+ stop: function() {
+ if (!this.timer) return;
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.execute();
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+});
+Object.extend(String, {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ },
+ specialChar: {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '\\': '\\\\'
+ }
+});
+
+Object.extend(String.prototype, {
+ gsub: function(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+
+ sub: function(pattern, replacement, count) {
+ replacement = this.gsub.prepareReplacement(replacement);
+ count = Object.isUndefined(count) ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ },
+
+ scan: function(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return String(this);
+ },
+
+ truncate: function(length, truncation) {
+ length = length || 30;
+ truncation = Object.isUndefined(truncation) ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : String(this);
+ },
+
+ strip: function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ },
+
+ escapeHTML: function() {
+ var self = arguments.callee;
+ self.text.data = this;
+ return self.div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = new Element('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? (div.childNodes.length > 1 ?
+ $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+ div.childNodes[0].nodeValue) : '';
+ },
+
+ toQueryParams: function(separator) {
+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
+ if (!match) return { };
+
+ return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+ if ((pair = pair.split('='))[0]) {
+ var key = decodeURIComponent(pair.shift());
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ if (value != undefined) value = decodeURIComponent(value);
+
+ if (key in hash) {
+ if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+ hash[key].push(value);
+ }
+ else hash[key] = value;
+ }
+ return hash;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ succ: function() {
+ return this.slice(0, this.length - 1) +
+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+ },
+
+ times: function(count) {
+ return count < 1 ? '' : new Array(count + 1).join(this);
+ },
+
+ camelize: function() {
+ var parts = this.split('-'), len = parts.length;
+ if (len == 1) return parts[0];
+
+ var camelized = this.charAt(0) == '-'
+ ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+ : parts[0];
+
+ for (var i = 1; i < len; i++)
+ camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+ return camelized;
+ },
+
+ capitalize: function() {
+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+ },
+
+ underscore: function() {
+ return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+ },
+
+ dasherize: function() {
+ return this.gsub(/_/,'-');
+ },
+
+ inspect: function(useDoubleQuotes) {
+ var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ },
+
+ toJSON: function() {
+ return this.inspect(true);
+ },
+
+ unfilterJSON: function(filter) {
+ return this.sub(filter || Prototype.JSONFilter, '#{1}');
+ },
+
+ isJSON: function() {
+ var str = this;
+ if (str.blank()) return false;
+ str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+ return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+ },
+
+ evalJSON: function(sanitize) {
+ var json = this.unfilterJSON();
+ try {
+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+ } catch (e) { }
+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+ },
+
+ include: function(pattern) {
+ return this.indexOf(pattern) > -1;
+ },
+
+ startsWith: function(pattern) {
+ return this.indexOf(pattern) === 0;
+ },
+
+ endsWith: function(pattern) {
+ var d = this.length - pattern.length;
+ return d >= 0 && this.lastIndexOf(pattern) === d;
+ },
+
+ empty: function() {
+ return this == '';
+ },
+
+ blank: function() {
+ return /^\s*$/.test(this);
+ },
+
+ interpolate: function(object, pattern) {
+ return new Template(this, pattern).evaluate(object);
+ }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+ escapeHTML: function() {
+ return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ unescapeHTML: function() {
+ return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+ }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+ if (Object.isFunction(replacement)) return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+};
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+ div: document.createElement('div'),
+ text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create({
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ if (Object.isFunction(object.toTemplateReplacements))
+ object = object.toTemplateReplacements();
+
+ return this.template.gsub(this.pattern, function(match) {
+ if (object == null) return '';
+
+ var before = match[1] || '';
+ if (before == '\\') return match[2];
+
+ var ctx = object, expr = match[3];
+ var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+ match = pattern.exec(expr);
+ if (match == null) return before;
+
+ while (match != null) {
+ var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+ ctx = ctx[comp];
+ if (null == ctx || '' == match[3]) break;
+ expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+ match = pattern.exec(expr);
+ }
+
+ return before + String.interpret(ctx);
+ }.bind(this));
+ }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
+
+var Enumerable = {
+ each: function(iterator, context) {
+ var index = 0;
+ iterator = iterator.bind(context);
+ try {
+ this._each(function(value) {
+ iterator(value, index++);
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ return this;
+ },
+
+ eachSlice: function(number, iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var index = -number, slices = [], array = this.toArray();
+ while ((index += number) < array.length)
+ slices.push(array.slice(index, index+number));
+ return slices.collect(iterator, context);
+ },
+
+ all: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!iterator(value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var result = false;
+ this.each(function(value, index) {
+ if (result = !!iterator(value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var results = [];
+ this.each(function(value, index) {
+ results.push(iterator(value, index));
+ });
+ return results;
+ },
+
+ detect: function(iterator, context) {
+ iterator = iterator.bind(context);
+ var result;
+ this.each(function(value, index) {
+ if (iterator(value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator, context) {
+ iterator = iterator.bind(context);
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(filter, iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var results = [];
+
+ if (Object.isString(filter))
+ filter = new RegExp(filter);
+
+ this.each(function(value, index) {
+ if (filter.match(value))
+ results.push(iterator(value, index));
+ });
+ return results;
+ },
+
+ include: function(object) {
+ if (Object.isFunction(this.indexOf))
+ if (this.indexOf(object) != -1) return true;
+
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inGroupsOf: function(number, fillWith) {
+ fillWith = Object.isUndefined(fillWith) ? null : fillWith;
+ return this.eachSlice(number, function(slice) {
+ while(slice.length < number) slice.push(fillWith);
+ return slice;
+ });
+ },
+
+ inject: function(memo, iterator, context) {
+ iterator = iterator.bind(context);
+ this.each(function(value, index) {
+ memo = iterator(memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.map(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator(value, index);
+ if (result == null || value >= result)
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator(value, index);
+ if (result == null || value < result)
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ (iterator(value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator, context) {
+ iterator = iterator.bind(context);
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator, context) {
+ iterator = iterator.bind(context);
+ return this.map(function(value, index) {
+ return {value: value, criteria: iterator(value, index)};
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.map();
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (Object.isFunction(args.last()))
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ },
+
+ size: function() {
+ return this.toArray().length;
+ },
+
+ inspect: function() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+};
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ filter: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray,
+ every: Enumerable.all,
+ some: Enumerable.any
+});
+function $A(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) return iterable.toArray();
+ var length = iterable.length, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+}
+
+if (Prototype.Browser.WebKit) {
+ function $A(iterable) {
+ if (!iterable) return [];
+ if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
+ iterable.toArray) return iterable.toArray();
+ var length = iterable.length, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+ }
+}
+
+Array.from = $A;
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0, length = this.length; i < length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(Object.isArray(value) ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ reduce: function() {
+ return this.length > 1 ? this : this[0];
+ },
+
+ uniq: function(sorted) {
+ return this.inject([], function(array, value, index) {
+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+ array.push(value);
+ return array;
+ });
+ },
+
+ intersect: function(array) {
+ return this.uniq().findAll(function(item) {
+ return array.detect(function(value) { return item === value });
+ });
+ },
+
+ clone: function() {
+ return [].concat(this);
+ },
+
+ size: function() {
+ return this.length;
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ },
+
+ toJSON: function() {
+ var results = [];
+ this.each(function(object) {
+ var value = Object.toJSON(object);
+ if (!Object.isUndefined(value)) results.push(value);
+ });
+ return '[' + results.join(', ') + ']';
+ }
+});
+
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+ Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+ i || (i = 0);
+ var length = this.length;
+ if (i < 0) i = length + i;
+ for (; i < length; i++)
+ if (this[i] === item) return i;
+ return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+ i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+ var n = this.slice(0, i).reverse().indexOf(item);
+ return (n < 0) ? n : i - n - 1;
+};
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+ if (!Object.isString(string)) return [];
+ string = string.strip();
+ return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+ Array.prototype.concat = function() {
+ var array = [];
+ for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ if (Object.isArray(arguments[i])) {
+ for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+ array.push(arguments[i][j]);
+ } else {
+ array.push(arguments[i]);
+ }
+ }
+ return array;
+ };
+}
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ return this.toPaddedString(2, 16);
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator) {
+ $R(0, this, true).each(iterator);
+ return this;
+ },
+
+ toPaddedString: function(length, radix) {
+ var string = this.toString(radix || 10);
+ return '0'.times(length - string.length) + string;
+ },
+
+ toJSON: function() {
+ return isFinite(this) ? this.toString() : 'null';
+ }
+});
+
+$w('abs round ceil floor').each(function(method){
+ Number.prototype[method] = Math[method].methodize();
+});
+function $H(object) {
+ return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+
+ function toQueryPair(key, value) {
+ if (Object.isUndefined(value)) return key;
+ return key + '=' + encodeURIComponent(String.interpret(value));
+ }
+
+ return {
+ initialize: function(object) {
+ this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+ },
+
+ _each: function(iterator) {
+ for (var key in this._object) {
+ var value = this._object[key], pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ set: function(key, value) {
+ return this._object[key] = value;
+ },
+
+ get: function(key) {
+ return this._object[key];
+ },
+
+ unset: function(key) {
+ var value = this._object[key];
+ delete this._object[key];
+ return value;
+ },
+
+ toObject: function() {
+ return Object.clone(this._object);
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ index: function(value) {
+ var match = this.detect(function(pair) {
+ return pair.value === value;
+ });
+ return match && match.key;
+ },
+
+ merge: function(object) {
+ return this.clone().update(object);
+ },
+
+ update: function(object) {
+ return new Hash(object).inject(this, function(result, pair) {
+ result.set(pair.key, pair.value);
+ return result;
+ });
+ },
+
+ toQueryString: function() {
+ return this.map(function(pair) {
+ var key = encodeURIComponent(pair.key), values = pair.value;
+
+ if (values && typeof values == 'object') {
+ if (Object.isArray(values))
+ return values.map(toQueryPair.curry(key)).join('&');
+ }
+ return toQueryPair(key, values);
+ }).join('&');
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ },
+
+ toJSON: function() {
+ return Object.toJSON(this.toObject());
+ },
+
+ clone: function() {
+ return new Hash(this);
+ }
+ }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ while (this.include(value)) {
+ iterator(value);
+ value = value.succ();
+ }
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+};
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new XMLHttpRequest()},
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+};
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responder) {
+ if (!this.include(responder))
+ this.responders.push(responder);
+ },
+
+ unregister: function(responder) {
+ this.responders = this.responders.without(responder);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (Object.isFunction(responder[callback])) {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) { }
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() { Ajax.activeRequestCount++ },
+ onComplete: function() { Ajax.activeRequestCount-- }
+});
+
+Ajax.Base = Class.create({
+ initialize: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ encoding: 'UTF-8',
+ parameters: '',
+ evalJSON: true,
+ evalJS: true
+ };
+ Object.extend(this.options, options || { });
+
+ this.options.method = this.options.method.toLowerCase();
+
+ if (Object.isString(this.options.parameters))
+ this.options.parameters = this.options.parameters.toQueryParams();
+ else if (Object.isHash(this.options.parameters))
+ this.options.parameters = this.options.parameters.toObject();
+ }
+});
+
+Ajax.Request = Class.create(Ajax.Base, {
+ _complete: false,
+
+ initialize: function($super, url, options) {
+ $super(options);
+ this.transport = Ajax.getTransport();
+ this.request(url);
+ },
+
+ request: function(url) {
+ this.url = url;
+ this.method = this.options.method;
+ var params = Object.clone(this.options.parameters);
+
+ if (!['get', 'post'].include(this.method)) {
+ // simulate other verbs over post
+ params['_method'] = this.method;
+ this.method = 'post';
+ }
+
+ this.parameters = params;
+
+ if (params = Object.toQueryString(params)) {
+ // when GET, append parameters to URL
+ if (this.method == 'get')
+ this.url += (this.url.include('?') ? '&' : '?') + params;
+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ params += '&_=';
+ }
+
+ try {
+ var response = new Ajax.Response(this);
+ if (this.options.onCreate) this.options.onCreate(response);
+ Ajax.Responders.dispatch('onCreate', this, response);
+
+ this.transport.open(this.method.toUpperCase(), this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ this.setRequestHeaders();
+
+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+ this.transport.send(this.body);
+
+ /* Force Firefox to handle ready state 4 for synchronous requests */
+ if (!this.options.asynchronous && this.transport.overrideMimeType)
+ this.onStateChange();
+
+ }
+ catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState > 1 && !((readyState == 4) && this._complete))
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ setRequestHeaders: function() {
+ var headers = {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-Prototype-Version': Prototype.Version,
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ };
+
+ if (this.method == 'post') {
+ headers['Content-type'] = this.options.contentType +
+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+ /* Force "Connection: close" for older Mozilla browsers to work
+ * around a bug where XMLHttpRequest sends an incorrect
+ * Content-length header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType &&
+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+ headers['Connection'] = 'close';
+ }
+
+ // user-defined headers
+ if (typeof this.options.requestHeaders == 'object') {
+ var extras = this.options.requestHeaders;
+
+ if (Object.isFunction(extras.push))
+ for (var i = 0, length = extras.length; i < length; i += 2)
+ headers[extras[i]] = extras[i+1];
+ else
+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+ }
+
+ for (var name in headers)
+ this.transport.setRequestHeader(name, headers[name]);
+ },
+
+ success: function() {
+ var status = this.getStatus();
+ return !status || (status >= 200 && status < 300);
+ },
+
+ getStatus: function() {
+ try {
+ return this.transport.status || 0;
+ } catch (e) { return 0 }
+ },
+
+ respondToReadyState: function(readyState) {
+ var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
+
+ if (state == 'Complete') {
+ try {
+ this._complete = true;
+ (this.options['on' + response.status]
+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ var contentType = response.getHeader('Content-type');
+ if (this.options.evalJS == 'force'
+ || (this.options.evalJS && contentType
+ && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+ Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if (state == 'Complete') {
+ // avoid memory leak in MSIE: clean up
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ }
+ },
+
+ getHeader: function(name) {
+ try {
+ return this.transport.getResponseHeader(name);
+ } catch (e) { return null }
+ },
+
+ evalResponse: function() {
+ try {
+ return eval((this.transport.responseText || '').unfilterJSON());
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+ initialize: function(request){
+ this.request = request;
+ var transport = this.transport = request.transport,
+ readyState = this.readyState = transport.readyState;
+
+ if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+ this.status = this.getStatus();
+ this.statusText = this.getStatusText();
+ this.responseText = String.interpret(transport.responseText);
+ this.headerJSON = this._getHeaderJSON();
+ }
+
+ if(readyState == 4) {
+ var xml = transport.responseXML;
+ this.responseXML = Object.isUndefined(xml) ? null : xml;
+ this.responseJSON = this._getResponseJSON();
+ }
+ },
+
+ status: 0,
+ statusText: '',
+
+ getStatus: Ajax.Request.prototype.getStatus,
+
+ getStatusText: function() {
+ try {
+ return this.transport.statusText || '';
+ } catch (e) { return '' }
+ },
+
+ getHeader: Ajax.Request.prototype.getHeader,
+
+ getAllHeaders: function() {
+ try {
+ return this.getAllResponseHeaders();
+ } catch (e) { return null }
+ },
+
+ getResponseHeader: function(name) {
+ return this.transport.getResponseHeader(name);
+ },
+
+ getAllResponseHeaders: function() {
+ return this.transport.getAllResponseHeaders();
+ },
+
+ _getHeaderJSON: function() {
+ var json = this.getHeader('X-JSON');
+ if (!json) return null;
+ json = decodeURIComponent(escape(json));
+ try {
+ return json.evalJSON(this.request.options.sanitizeJSON);
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ },
+
+ _getResponseJSON: function() {
+ var options = this.request.options;
+ if (!options.evalJSON || (options.evalJSON != 'force' &&
+ !(this.getHeader('Content-type') || '').include('application/json')) ||
+ this.responseText.blank())
+ return null;
+ try {
+ return this.responseText.evalJSON(options.sanitizeJSON);
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+ initialize: function($super, container, url, options) {
+ this.container = {
+ success: (container.success || container),
+ failure: (container.failure || (container.success ? null : container))
+ };
+
+ options = Object.clone(options);
+ var onComplete = options.onComplete;
+ options.onComplete = (function(response, json) {
+ this.updateContent(response.responseText);
+ if (Object.isFunction(onComplete)) onComplete(response, json);
+ }).bind(this);
+
+ $super(url, options);
+ },
+
+ updateContent: function(responseText) {
+ var receiver = this.container[this.success() ? 'success' : 'failure'],
+ options = this.options;
+
+ if (!options.evalScripts) responseText = responseText.stripScripts();
+
+ if (receiver = $(receiver)) {
+ if (options.insertion) {
+ if (Object.isString(options.insertion)) {
+ var insertion = { }; insertion[options.insertion] = responseText;
+ receiver.insert(insertion);
+ }
+ else options.insertion(receiver, responseText);
+ }
+ else receiver.update(responseText);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+ initialize: function($super, container, url, options) {
+ $super(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = { };
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.options.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(response) {
+ if (this.options.decay) {
+ this.decay = (response.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = response.responseText;
+ }
+ this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+function $(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push($(arguments[i]));
+ return elements;
+ }
+ if (Object.isString(element))
+ element = document.getElementById(element);
+ return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+ document._getElementsByXPath = function(expression, parentElement) {
+ var results = [];
+ var query = document.evaluate(expression, $(parentElement) || document,
+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0, length = query.snapshotLength; i < length; i++)
+ results.push(Element.extend(query.snapshotItem(i)));
+ return results;
+ };
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+ // DOM level 2 ECMAScript Language Binding
+ Object.extend(Node, {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+ });
+}
+
+(function() {
+ var element = this.Element;
+ this.Element = function(tagName, attributes) {
+ attributes = attributes || { };
+ tagName = tagName.toLowerCase();
+ var cache = Element.cache;
+ if (Prototype.Browser.IE && attributes.name) {
+ tagName = '<' + tagName + ' name="' + attributes.name + '">';
+ delete attributes.name;
+ return Element.writeAttribute(document.createElement(tagName), attributes);
+ }
+ if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+ return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+ };
+ Object.extend(this.Element, element || { });
+}).call(window);
+
+Element.cache = { };
+
+Element.Methods = {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function(element) {
+ element = $(element);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ return element;
+ },
+
+ hide: function(element) {
+ $(element).style.display = 'none';
+ return element;
+ },
+
+ show: function(element) {
+ $(element).style.display = '';
+ return element;
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ return element;
+ },
+
+ update: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+ content = Object.toHTML(content);
+ element.innerHTML = content.stripScripts();
+ content.evalScripts.bind(content).defer();
+ return element;
+ },
+
+ replace: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ else if (!Object.isElement(content)) {
+ content = Object.toHTML(content);
+ var range = element.ownerDocument.createRange();
+ range.selectNode(element);
+ content.evalScripts.bind(content).defer();
+ content = range.createContextualFragment(content.stripScripts());
+ }
+ element.parentNode.replaceChild(content, element);
+ return element;
+ },
+
+ insert: function(element, insertions) {
+ element = $(element);
+
+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+ insertions = {bottom:insertions};
+
+ var content, t, range;
+
+ for (position in insertions) {
+ content = insertions[position];
+ position = position.toLowerCase();
+ t = Element._insertionTranslations[position];
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ t.insert(element, content);
+ continue;
+ }
+
+ content = Object.toHTML(content);
+
+ range = element.ownerDocument.createRange();
+ t.initializeRange(element, range);
+ t.insert(element, range.createContextualFragment(content.stripScripts()));
+
+ content.evalScripts.bind(content).defer();
+ }
+
+ return element;
+ },
+
+ wrap: function(element, wrapper, attributes) {
+ element = $(element);
+ if (Object.isElement(wrapper))
+ $(wrapper).writeAttribute(attributes || { });
+ else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+ else wrapper = new Element('div', wrapper);
+ if (element.parentNode)
+ element.parentNode.replaceChild(wrapper, element);
+ wrapper.appendChild(element);
+ return wrapper;
+ },
+
+ inspect: function(element) {
+ element = $(element);
+ var result = '<' + element.tagName.toLowerCase();
+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+ var property = pair.first(), attribute = pair.last();
+ var value = (element[property] || '').toString();
+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
+ });
+ return result + '>';
+ },
+
+ recursivelyCollect: function(element, property) {
+ element = $(element);
+ var elements = [];
+ while (element = element[property])
+ if (element.nodeType == 1)
+ elements.push(Element.extend(element));
+ return elements;
+ },
+
+ ancestors: function(element) {
+ return $(element).recursivelyCollect('parentNode');
+ },
+
+ descendants: function(element) {
+ return $(element).getElementsBySelector("*");
+ },
+
+ firstDescendant: function(element) {
+ element = $(element).firstChild;
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ return $(element);
+ },
+
+ immediateDescendants: function(element) {
+ if (!(element = $(element).firstChild)) return [];
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ if (element) return [element].concat($(element).nextSiblings());
+ return [];
+ },
+
+ previousSiblings: function(element) {
+ return $(element).recursivelyCollect('previousSibling');
+ },
+
+ nextSiblings: function(element) {
+ return $(element).recursivelyCollect('nextSibling');
+ },
+
+ siblings: function(element) {
+ element = $(element);
+ return element.previousSiblings().reverse().concat(element.nextSiblings());
+ },
+
+ match: function(element, selector) {
+ if (Object.isString(selector))
+ selector = new Selector(selector);
+ return selector.match($(element));
+ },
+
+ up: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(element.parentNode);
+ var ancestors = element.ancestors();
+ return expression ? Selector.findElement(ancestors, expression, index) :
+ ancestors[index || 0];
+ },
+
+ down: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return element.firstDescendant();
+ var descendants = element.descendants();
+ return expression ? Selector.findElement(descendants, expression, index) :
+ descendants[index || 0];
+ },
+
+ previous: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+ var previousSiblings = element.previousSiblings();
+ return expression ? Selector.findElement(previousSiblings, expression, index) :
+ previousSiblings[index || 0];
+ },
+
+ next: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+ var nextSiblings = element.nextSiblings();
+ return expression ? Selector.findElement(nextSiblings, expression, index) :
+ nextSiblings[index || 0];
+ },
+
+ select: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element, args);
+ },
+
+ adjacent: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element.parentNode, args).without(element);
+ },
+
+ identify: function(element) {
+ element = $(element);
+ var id = element.readAttribute('id'), self = arguments.callee;
+ if (id) return id;
+ do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+ element.writeAttribute('id', id);
+ return id;
+ },
+
+ readAttribute: function(element, name) {
+ element = $(element);
+ if (Prototype.Browser.IE) {
+ var t = Element._attributeTranslations.read;
+ if (t.values[name]) return t.values[name](element, name);
+ if (t.names[name]) name = t.names[name];
+ if (name.include(':')) {
+ return (!element.attributes || !element.attributes[name]) ? null :
+ element.attributes[name].value;
+ }
+ }
+ return element.getAttribute(name);
+ },
+
+ writeAttribute: function(element, name, value) {
+ element = $(element);
+ var attributes = { }, t = Element._attributeTranslations.write;
+
+ if (typeof name == 'object') attributes = name;
+ else attributes[name] = Object.isUndefined(value) ? true : value;
+
+ for (var attr in attributes) {
+ name = t.names[attr] || attr;
+ value = attributes[attr];
+ if (t.values[attr]) name = t.values[attr](element, value);
+ if (value === false || value === null)
+ element.removeAttribute(name);
+ else if (value === true)
+ element.setAttribute(name, name);
+ else element.setAttribute(name, value);
+ }
+ return element;
+ },
+
+ getHeight: function(element) {
+ return $(element).getDimensions().height;
+ },
+
+ getWidth: function(element) {
+ return $(element).getDimensions().width;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ var elementClassName = element.className;
+ return (elementClassName.length > 0 && (elementClassName == className ||
+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ if (!element.hasClassName(className))
+ element.className += (element.className ? ' ' : '') + className;
+ return element;
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ element.className = element.className.replace(
+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+ return element;
+ },
+
+ toggleClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return element[element.hasClassName(className) ?
+ 'removeClassName' : 'addClassName'](className);
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ var node = element.firstChild;
+ while (node) {
+ var nextNode = node.nextSibling;
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ element.removeChild(node);
+ node = nextNode;
+ }
+ return element;
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.blank();
+ },
+
+ descendantOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+ var originalAncestor = ancestor;
+
+ if (element.compareDocumentPosition)
+ return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+ if (element.sourceIndex && !Prototype.Browser.Opera) {
+ var e = element.sourceIndex, a = ancestor.sourceIndex,
+ nextAncestor = ancestor.nextSibling;
+ if (!nextAncestor) {
+ do { ancestor = ancestor.parentNode; }
+ while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
+ }
+ if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex);
+ }
+
+ while (element = element.parentNode)
+ if (element == originalAncestor) return true;
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var pos = element.cumulativeOffset();
+ window.scrollTo(pos[0], pos[1]);
+ return element;
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ style = style == 'float' ? 'cssFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value) {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
+ }
+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+ return value == 'auto' ? null : value;
+ },
+
+ getOpacity: function(element) {
+ return $(element).getStyle('opacity');
+ },
+
+ setStyle: function(element, styles) {
+ element = $(element);
+ var elementStyle = element.style, match;
+ if (Object.isString(styles)) {
+ element.style.cssText += ';' + styles;
+ return styles.include('opacity') ?
+ element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+ }
+ for (var property in styles)
+ if (property == 'opacity') element.setOpacity(styles[property]);
+ else
+ elementStyle[(property == 'float' || property == 'cssFloat') ?
+ (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+ property] = styles[property];
+
+ return element;
+ },
+
+ setOpacity: function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+ return element;
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ var display = $(element).getStyle('display');
+ if (display != 'none' && display != null) // Safari bug
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ var originalDisplay = els.display;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = 'block';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = originalDisplay;
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (window.opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ return element;
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ return element;
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return element;
+ element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+ if (element._overflow !== 'hidden')
+ element.style.overflow = 'hidden';
+ return element;
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (!element._overflow) return element;
+ element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+ element._overflow = null;
+ return element;
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if (element.tagName == 'BODY') break;
+ var p = Element.getStyle(element, 'position');
+ if (p == 'relative' || p == 'absolute') break;
+ }
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'absolute') return;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+ var offsets = element.positionedOffset();
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.width = width + 'px';
+ element.style.height = height + 'px';
+ return element;
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'relative') return;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ return element;
+ },
+
+ cumulativeScrollOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ getOffsetParent: function(element) {
+ if (element.offsetParent) return $(element.offsetParent);
+ if (element == document.body) return $(element);
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return $(element);
+
+ return $(document.body);
+ },
+
+ viewportOffset: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent == document.body &&
+ Element.getStyle(element, 'position') == 'absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ clonePosition: function(element, source) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || { });
+
+ // find page position of source
+ source = $(source);
+ var p = source.viewportOffset();
+
+ // find coordinate system to use
+ element = $(element);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(element, 'position') == 'absolute') {
+ parent = element.getOffsetParent();
+ delta = parent.viewportOffset();
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if (options.setWidth) element.style.width = source.offsetWidth + 'px';
+ if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+ return element;
+ }
+};
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+ getElementsBySelector: Element.Methods.select,
+ childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+ write: {
+ names: {
+ className: 'class',
+ htmlFor: 'for'
+ },
+ values: { }
+ }
+};
+
+
+if (!document.createRange || Prototype.Browser.Opera) {
+ Element.Methods.insert = function(element, insertions) {
+ element = $(element);
+
+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+ insertions = { bottom: insertions };
+
+ var t = Element._insertionTranslations, content, position, pos, tagName;
+
+ for (position in insertions) {
+ content = insertions[position];
+ position = position.toLowerCase();
+ pos = t[position];
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ pos.insert(element, content);
+ continue;
+ }
+
+ content = Object.toHTML(content);
+ tagName = ((position == 'before' || position == 'after')
+ ? element.parentNode : element).tagName.toUpperCase();
+
+ if (t.tags[tagName]) {
+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+ if (position == 'top' || position == 'after') fragments.reverse();
+ fragments.each(pos.insert.curry(element));
+ }
+ else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
+
+ content.evalScripts.bind(content).defer();
+ }
+
+ return element;
+ };
+}
+
+if (Prototype.Browser.Opera) {
+ Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+ function(proceed, element, style) {
+ switch (style) {
+ case 'left': case 'top': case 'right': case 'bottom':
+ if (proceed(element, 'position') === 'static') return null;
+ case 'height': case 'width':
+ // returns '0px' for hidden elements; we want it to return null
+ if (!Element.visible(element)) return null;
+
+ // returns the border-box dimensions rather than the content-box
+ // dimensions, so we subtract padding and borders from the value
+ var dim = parseInt(proceed(element, style), 10);
+
+ if (dim !== element['offset' + style.capitalize()])
+ return dim + 'px';
+
+ var properties;
+ if (style === 'height') {
+ properties = ['border-top-width', 'padding-top',
+ 'padding-bottom', 'border-bottom-width'];
+ }
+ else {
+ properties = ['border-left-width', 'padding-left',
+ 'padding-right', 'border-right-width'];
+ }
+ return properties.inject(dim, function(memo, property) {
+ var val = proceed(element, property);
+ return val === null ? memo : memo - parseInt(val, 10);
+ }) + 'px';
+ default: return proceed(element, style);
+ }
+ }
+ );
+
+ Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+ function(proceed, element, attribute) {
+ if (attribute === 'title') return element.title;
+ return proceed(element, attribute);
+ }
+ );
+}
+
+else if (Prototype.Browser.IE) {
+ $w('positionedOffset getOffsetParent viewportOffset').each(function(method) {
+ Element.Methods[method] = Element.Methods[method].wrap(
+ function(proceed, element) {
+ element = $(element);
+ var position = element.getStyle('position');
+ if (position != 'static') return proceed(element);
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+ });
+
+ Element.Methods.getStyle = function(element, style) {
+ element = $(element);
+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value && element.currentStyle) value = element.currentStyle[style];
+
+ if (style == 'opacity') {
+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if (value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
+ }
+
+ if (value == 'auto') {
+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+ return element['offset' + style.capitalize()] + 'px';
+ return null;
+ }
+ return value;
+ };
+
+ Element.Methods.setOpacity = function(element, value) {
+ function stripAlpha(filter){
+ return filter.replace(/alpha\([^\)]*\)/gi,'');
+ }
+ element = $(element);
+ var currentStyle = element.currentStyle;
+ if ((currentStyle && !currentStyle.hasLayout) ||
+ (!currentStyle && element.style.zoom == 'normal'))
+ element.style.zoom = 1;
+
+ var filter = element.getStyle('filter'), style = element.style;
+ if (value == 1 || value === '') {
+ (filter = stripAlpha(filter)) ?
+ style.filter = filter : style.removeAttribute('filter');
+ return element;
+ } else if (value < 0.00001) value = 0;
+ style.filter = stripAlpha(filter) +
+ 'alpha(opacity=' + (value * 100) + ')';
+ return element;
+ };
+
+ Element._attributeTranslations = {
+ read: {
+ names: {
+ 'class': 'className',
+ 'for': 'htmlFor'
+ },
+ values: {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+ _getAttrNode: function(element, attribute) {
+ var node = element.getAttributeNode(attribute);
+ return node ? node.value : "";
+ },
+ _getEv: function(element, attribute) {
+ attribute = element.getAttribute(attribute);
+ return attribute ? attribute.toString().slice(23, -2) : null;
+ },
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+ title: function(element) {
+ return element.title;
+ }
+ }
+ }
+ };
+
+ Element._attributeTranslations.write = {
+ names: Object.clone(Element._attributeTranslations.read.names),
+ values: {
+ checked: function(element, value) {
+ element.checked = !!value;
+ },
+
+ style: function(element, value) {
+ element.style.cssText = value ? value : '';
+ }
+ }
+ };
+
+ Element._attributeTranslations.has = {};
+
+ $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+ 'encType maxLength readOnly longDesc').each(function(attr) {
+ Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+ Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+ });
+
+ (function(v) {
+ Object.extend(v, {
+ href: v._getAttr,
+ src: v._getAttr,
+ type: v._getAttr,
+ action: v._getAttrNode,
+ disabled: v._flag,
+ checked: v._flag,
+ readonly: v._flag,
+ multiple: v._flag,
+ onload: v._getEv,
+ onunload: v._getEv,
+ onclick: v._getEv,
+ ondblclick: v._getEv,
+ onmousedown: v._getEv,
+ onmouseup: v._getEv,
+ onmouseover: v._getEv,
+ onmousemove: v._getEv,
+ onmouseout: v._getEv,
+ onfocus: v._getEv,
+ onblur: v._getEv,
+ onkeypress: v._getEv,
+ onkeydown: v._getEv,
+ onkeyup: v._getEv,
+ onsubmit: v._getEv,
+ onreset: v._getEv,
+ onselect: v._getEv,
+ onchange: v._getEv
+ });
+ })(Element._attributeTranslations.read.values);
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1) ? 0.999999 :
+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
+ return element;
+ };
+}
+
+else if (Prototype.Browser.WebKit) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+
+ if (value == 1)
+ if(element.tagName == 'IMG' && element.width) {
+ element.width++; element.width--;
+ } else try {
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch (e) { }
+
+ return element;
+ };
+
+ // Safari returns margins on body which is incorrect if the child is absolutely
+ // positioned. For performance reasons, redefine Element#cumulativeOffset for
+ // KHTML/WebKit only.
+ Element.Methods.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return Element._returnOffset(valueL, valueT);
+ };
+}
+
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+ // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+ Element.Methods.update = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+
+ content = Object.toHTML(content);
+ var tagName = element.tagName.toUpperCase();
+
+ if (tagName in Element._insertionTranslations.tags) {
+ $A(element.childNodes).each(function(node) { element.removeChild(node) });
+ Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+ .each(function(node) { element.appendChild(node) });
+ }
+ else element.innerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+if (document.createElement('div').outerHTML) {
+ Element.Methods.replace = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ element.parentNode.replaceChild(content, element);
+ return element;
+ }
+
+ content = Object.toHTML(content);
+ var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+ if (Element._insertionTranslations.tags[tagName]) {
+ var nextSibling = element.next();
+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+ parent.removeChild(element);
+ if (nextSibling)
+ fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+ else
+ fragments.each(function(node) { parent.appendChild(node) });
+ }
+ else element.outerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+Element._returnOffset = function(l, t) {
+ var result = [l, t];
+ result.left = l;
+ result.top = t;
+ return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+ var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+ div.innerHTML = t[0] + html + t[1];
+ t[2].times(function() { div = div.firstChild });
+ return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+ before: {
+ adjacency: 'beforeBegin',
+ insert: function(element, node) {
+ element.parentNode.insertBefore(node, element);
+ },
+ initializeRange: function(element, range) {
+ range.setStartBefore(element);
+ }
+ },
+ top: {
+ adjacency: 'afterBegin',
+ insert: function(element, node) {
+ element.insertBefore(node, element.firstChild);
+ },
+ initializeRange: function(element, range) {
+ range.selectNodeContents(element);
+ range.collapse(true);
+ }
+ },
+ bottom: {
+ adjacency: 'beforeEnd',
+ insert: function(element, node) {
+ element.appendChild(node);
+ }
+ },
+ after: {
+ adjacency: 'afterEnd',
+ insert: function(element, node) {
+ element.parentNode.insertBefore(node, element.nextSibling);
+ },
+ initializeRange: function(element, range) {
+ range.setStartAfter(element);
+ }
+ },
+ tags: {
+ TABLE: ['<table>', '</table>', 1],
+ TBODY: ['<table><tbody>', '</tbody></table>', 2],
+ TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
+ TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+ SELECT: ['<select>', '</select>', 1]
+ }
+};
+
+(function() {
+ this.bottom.initializeRange = this.top.initializeRange;
+ Object.extend(this.tags, {
+ THEAD: this.tags.TBODY,
+ TFOOT: this.tags.TBODY,
+ TH: this.tags.TD
+ });
+}).call(Element._insertionTranslations);
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ attribute = Element._attributeTranslations.has[attribute] || attribute;
+ var node = $(element).getAttributeNode(attribute);
+ return node && node.specified;
+ }
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div').__proto__) {
+ window.HTMLElement = { };
+ window.HTMLElement.prototype = document.createElement('div').__proto__;
+ Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.extend = (function() {
+ if (Prototype.BrowserFeatures.SpecificElementExtensions)
+ return Prototype.K;
+
+ var Methods = { }, ByTag = Element.Methods.ByTag;
+
+ var extend = Object.extend(function(element) {
+ if (!element || element._extendedByPrototype ||
+ element.nodeType != 1 || element == window) return element;
+
+ var methods = Object.clone(Methods),
+ tagName = element.tagName, property, value;
+
+ // extend methods for specific tags
+ if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+ for (property in methods) {
+ value = methods[property];
+ if (Object.isFunction(value) && !(property in element))
+ element[property] = value.methodize();
+ }
+
+ element._extendedByPrototype = Prototype.emptyFunction;
+ return element;
+
+ }, {
+ refresh: function() {
+ // extend methods for all tags (Safari doesn't need this)
+ if (!Prototype.BrowserFeatures.ElementExtensions) {
+ Object.extend(Methods, Element.Methods);
+ Object.extend(Methods, Element.Methods.Simulated);
+ }
+ }
+ });
+
+ extend.refresh();
+ return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+ if (element.hasAttribute) return element.hasAttribute(attribute);
+ return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+ if (!methods) {
+ Object.extend(Form, Form.Methods);
+ Object.extend(Form.Element, Form.Element.Methods);
+ Object.extend(Element.Methods.ByTag, {
+ "FORM": Object.clone(Form.Methods),
+ "INPUT": Object.clone(Form.Element.Methods),
+ "SELECT": Object.clone(Form.Element.Methods),
+ "TEXTAREA": Object.clone(Form.Element.Methods)
+ });
+ }
+
+ if (arguments.length == 2) {
+ var tagName = methods;
+ methods = arguments[1];
+ }
+
+ if (!tagName) Object.extend(Element.Methods, methods || { });
+ else {
+ if (Object.isArray(tagName)) tagName.each(extend);
+ else extend(tagName);
+ }
+
+ function extend(tagName) {
+ tagName = tagName.toUpperCase();
+ if (!Element.Methods.ByTag[tagName])
+ Element.Methods.ByTag[tagName] = { };
+ Object.extend(Element.Methods.ByTag[tagName], methods);
+ }
+
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!Object.isFunction(value)) continue;
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = value.methodize();
+ }
+ }
+
+ function findDOMClass(tagName) {
+ var klass;
+ var trans = {
+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+ "FrameSet", "IFRAME": "IFrame"
+ };
+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName.capitalize() + 'Element';
+ if (window[klass]) return window[klass];
+
+ window[klass] = { };
+ window[klass].prototype = document.createElement(tagName).__proto__;
+ return window[klass];
+ }
+
+ if (F.ElementExtensions) {
+ copy(Element.Methods, HTMLElement.prototype);
+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+ }
+
+ if (F.SpecificElementExtensions) {
+ for (var tag in Element.Methods.ByTag) {
+ var klass = findDOMClass(tag);
+ if (Object.isUndefined(klass)) continue;
+ copy(T[tag], klass.prototype);
+ }
+ }
+
+ Object.extend(Element, Element.Methods);
+ delete Element.ByTag;
+
+ if (Element.extend.refresh) Element.extend.refresh();
+ Element.cache = { };
+};
+
+document.viewport = {
+ getDimensions: function() {
+ var dimensions = { };
+ var B = Prototype.Browser;
+ $w('width height').each(function(d) {
+ var D = d.capitalize();
+ dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :
+ (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];
+ });
+ return dimensions;
+ },
+
+ getWidth: function() {
+ return this.getDimensions().width;
+ },
+
+ getHeight: function() {
+ return this.getDimensions().height;
+ },
+
+ getScrollOffsets: function() {
+ return Element._returnOffset(
+ window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+ window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+ }
+};
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license. Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create({
+ initialize: function(expression) {
+ this.expression = expression.strip();
+ this.compileMatcher();
+ },
+
+ shouldUseXPath: function() {
+ if (!Prototype.BrowserFeatures.XPath) return false;
+
+ var e = this.expression;
+
+ // Safari 3 chokes on :*-of-type and :empty
+ if (Prototype.Browser.WebKit &&
+ (e.include("-of-type") || e.include(":empty")))
+ return false;
+
+ // XPath can't do namespaced attributes, nor can it read
+ // the "checked" property from DOM nodes
+ if ((/(\[[\w-]*?:|:checked)/).test(this.expression))
+ return false;
+
+ return true;
+ },
+
+ compileMatcher: function() {
+ if (this.shouldUseXPath())
+ return this.compileXPathMatcher();
+
+ var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+ c = Selector.criteria, le, p, m;
+
+ if (Selector._cache[e]) {
+ this.matcher = Selector._cache[e];
+ return;
+ }
+
+ this.matcher = ["this.matcher = function(root) {",
+ "var r = root, h = Selector.handlers, c = false, n;"];
+
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+ new Template(c[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.matcher.push("return h.unique(n);\n}");
+ eval(this.matcher.join('\n'));
+ Selector._cache[this.expression] = this.matcher;
+ },
+
+ compileXPathMatcher: function() {
+ var e = this.expression, ps = Selector.patterns,
+ x = Selector.xpath, le, m;
+
+ if (Selector._cache[e]) {
+ this.xpath = Selector._cache[e]; return;
+ }
+
+ this.matcher = ['.//*'];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ if (m = e.match(ps[i])) {
+ this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+ new Template(x[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.xpath = this.matcher.join('');
+ Selector._cache[this.expression] = this.xpath;
+ },
+
+ findElements: function(root) {
+ root = root || document;
+ if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+ return this.matcher(root);
+ },
+
+ match: function(element) {
+ this.tokens = [];
+
+ var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+ var le, p, m;
+
+ while (e && le !== e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ // use the Selector.assertions methods unless the selector
+ // is too complex.
+ if (as[i]) {
+ this.tokens.push([i, Object.clone(m)]);
+ e = e.replace(m[0], '');
+ } else {
+ // reluctantly do a document-wide search
+ // and look for a match in the array
+ return this.findElements(document).include(element);
+ }
+ }
+ }
+ }
+
+ var match = true, name, matches;
+ for (var i = 0, token; token = this.tokens[i]; i++) {
+ name = token[0], matches = token[1];
+ if (!Selector.assertions[name](element, matches)) {
+ match = false; break;
+ }
+ }
+
+ return match;
+ },
+
+ toString: function() {
+ return this.expression;
+ },
+
+ inspect: function() {
+ return "#<Selector:" + this.expression.inspect() + ">";
+ }
+});
+
+Object.extend(Selector, {
+ _cache: { },
+
+ xpath: {
+ descendant: "//*",
+ child: "/*",
+ adjacent: "/following-sibling::*[1]",
+ laterSibling: '/following-sibling::*',
+ tagName: function(m) {
+ if (m[1] == '*') return '';
+ return "[local-name()='" + m[1].toLowerCase() +
+ "' or local-name()='" + m[1].toUpperCase() + "']";
+ },
+ className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+ id: "[@id='#{1}']",
+ attrPresence: function(m) {
+ m[1] = m[1].toLowerCase();
+ return new Template("[@#{1}]").evaluate(m);
+ },
+ attr: function(m) {
+ m[1] = m[1].toLowerCase();
+ m[3] = m[5] || m[6];
+ return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+ },
+ pseudo: function(m) {
+ var h = Selector.xpath.pseudos[m[1]];
+ if (!h) return '';
+ if (Object.isFunction(h)) return h(m);
+ return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+ },
+ operators: {
+ '=': "[@#{1}='#{3}']",
+ '!=': "[@#{1}!='#{3}']",
+ '^=': "[starts-with(@#{1}, '#{3}')]",
+ '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+ '*=': "[contains(@#{1}, '#{3}')]",
+ '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+ '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+ },
+ pseudos: {
+ 'first-child': '[not(preceding-sibling::*)]',
+ 'last-child': '[not(following-sibling::*)]',
+ 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
+ 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+ 'checked': "[@checked]",
+ 'disabled': "[@disabled]",
+ 'enabled': "[not(@disabled)]",
+ 'not': function(m) {
+ var e = m[6], p = Selector.patterns,
+ x = Selector.xpath, le, v;
+
+ var exclusion = [];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in p) {
+ if (m = e.match(p[i])) {
+ v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+ exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+ return "[not(" + exclusion.join(" and ") + ")]";
+ },
+ 'nth-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+ },
+ 'nth-last-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+ },
+ 'nth-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("position() ", m);
+ },
+ 'nth-last-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+ },
+ 'first-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+ },
+ 'last-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+ },
+ 'only-of-type': function(m) {
+ var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+ },
+ nth: function(fragment, m) {
+ var mm, formula = m[6], predicate;
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ if (mm = formula.match(/^(\d+)$/)) // digit only
+ return '[' + fragment + "= " + mm[1] + ']';
+ if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (mm[1] == "-") mm[1] = -1;
+ var a = mm[1] ? Number(mm[1]) : 1;
+ var b = mm[2] ? Number(mm[2]) : 0;
+ predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+ "((#{fragment} - #{b}) div #{a} >= 0)]";
+ return new Template(predicate).evaluate({
+ fragment: fragment, a: a, b: b });
+ }
+ }
+ }
+ },
+
+ criteria: {
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+ attr: function(m) {
+ m[3] = (m[5] || m[6]);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+ },
+ pseudo: function(m) {
+ if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+ return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+ },
+ descendant: 'c = "descendant";',
+ child: 'c = "child";',
+ adjacent: 'c = "adjacent";',
+ laterSibling: 'c = "laterSibling";'
+ },
+
+ patterns: {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([\w\-\*]+)(\b|$)/,
+ className: /^\.([\w\-\*]+)(\b|$)/,
+ pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/,
+ attrPresence: /^\[([\w]+)\]/,
+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+ },
+
+ // for Selector.match and Element#match
+ assertions: {
+ tagName: function(element, matches) {
+ return matches[1].toUpperCase() == element.tagName.toUpperCase();
+ },
+
+ className: function(element, matches) {
+ return Element.hasClassName(element, matches[1]);
+ },
+
+ id: function(element, matches) {
+ return element.id === matches[1];
+ },
+
+ attrPresence: function(element, matches) {
+ return Element.hasAttribute(element, matches[1]);
+ },
+
+ attr: function(element, matches) {
+ var nodeValue = Element.readAttribute(element, matches[1]);
+ return Selector.operators[matches[2]](nodeValue, matches[3]);
+ }
+ },
+
+ handlers: {
+ // UTILITY FUNCTIONS
+ // joins two collections
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ a.push(node);
+ return a;
+ },
+
+ // marks an array of nodes for counting
+ mark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = true;
+ return nodes;
+ },
+
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = undefined;
+ return nodes;
+ },
+
+ // mark each child node with its position (for nth calls)
+ // "ofType" flag indicates whether we're indexing for nth-of-type
+ // rather than nth-child
+ index: function(parentNode, reverse, ofType) {
+ parentNode._counted = true;
+ if (reverse) {
+ for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+ var node = nodes[i];
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ } else {
+ for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ },
+
+ // filters out duplicates and extends all nodes
+ unique: function(nodes) {
+ if (nodes.length == 0) return nodes;
+ var results = [], n;
+ for (var i = 0, l = nodes.length; i < l; i++)
+ if (!(n = nodes[i])._counted) {
+ n._counted = true;
+ results.push(Element.extend(n));
+ }
+ return Selector.handlers.unmark(results);
+ },
+
+ // COMBINATOR FUNCTIONS
+ descendant: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName('*'));
+ return results;
+ },
+
+ child: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ for (var j = 0, child; child = node.childNodes[j]; j++)
+ if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+ }
+ return results;
+ },
+
+ adjacent: function(nodes) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ var next = this.nextElementSibling(node);
+ if (next) results.push(next);
+ }
+ return results;
+ },
+
+ laterSibling: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, Element.nextSiblings(node));
+ return results;
+ },
+
+ nextElementSibling: function(node) {
+ while (node = node.nextSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ previousElementSibling: function(node) {
+ while (node = node.previousSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ // TOKEN FUNCTIONS
+ tagName: function(nodes, root, tagName, combinator) {
+ tagName = tagName.toUpperCase();
+ var results = [], h = Selector.handlers;
+ if (nodes) {
+ if (combinator) {
+ // fastlane for ordinary descendant combinators
+ if (combinator == "descendant") {
+ for (var i = 0, node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName(tagName));
+ return results;
+ } else nodes = this[combinator](nodes);
+ if (tagName == "*") return nodes;
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.tagName.toUpperCase() == tagName) results.push(node);
+ return results;
+ } else return root.getElementsByTagName(tagName);
+ },
+
+ id: function(nodes, root, id, combinator) {
+ var targetNode = $(id), h = Selector.handlers;
+ if (!targetNode) return [];
+ if (!nodes && root == document) return [targetNode];
+ if (nodes) {
+ if (combinator) {
+ if (combinator == 'child') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (targetNode.parentNode == node) return [targetNode];
+ } else if (combinator == 'descendant') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.descendantOf(targetNode, node)) return [targetNode];
+ } else if (combinator == 'adjacent') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Selector.handlers.previousElementSibling(targetNode) == node)
+ return [targetNode];
+ } else nodes = h[combinator](nodes);
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node == targetNode) return [targetNode];
+ return [];
+ }
+ return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+ },
+
+ className: function(nodes, root, className, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ return Selector.handlers.byClassName(nodes, root, className);
+ },
+
+ byClassName: function(nodes, root, className) {
+ if (!nodes) nodes = Selector.handlers.descendant([root]);
+ var needle = ' ' + className + ' ';
+ for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+ nodeClassName = node.className;
+ if (nodeClassName.length == 0) continue;
+ if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+ results.push(node);
+ }
+ return results;
+ },
+
+ attrPresence: function(nodes, root, attr) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ var results = [];
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.hasAttribute(node, attr)) results.push(node);
+ return results;
+ },
+
+ attr: function(nodes, root, attr, value, operator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ var handler = Selector.operators[operator], results = [];
+ for (var i = 0, node; node = nodes[i]; i++) {
+ var nodeValue = Element.readAttribute(node, attr);
+ if (nodeValue === null) continue;
+ if (handler(nodeValue, value)) results.push(node);
+ }
+ return results;
+ },
+
+ pseudo: function(nodes, name, value, root, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ return Selector.pseudos[name](nodes, value, root);
+ }
+ },
+
+ pseudos: {
+ 'first-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.previousElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'last-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.nextElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'only-child': function(nodes, value, root) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+ results.push(node);
+ return results;
+ },
+ 'nth-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root);
+ },
+ 'nth-last-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true);
+ },
+ 'nth-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, false, true);
+ },
+ 'nth-last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true, true);
+ },
+ 'first-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, false, true);
+ },
+ 'last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, true, true);
+ },
+ 'only-of-type': function(nodes, formula, root) {
+ var p = Selector.pseudos;
+ return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+ },
+
+ // handles the an+b logic
+ getIndices: function(a, b, total) {
+ if (a == 0) return b > 0 ? [b] : [];
+ return $R(1, total).inject([], function(memo, i) {
+ if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+ return memo;
+ });
+ },
+
+ // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+ nth: function(nodes, formula, root, reverse, ofType) {
+ if (nodes.length == 0) return [];
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ var h = Selector.handlers, results = [], indexed = [], m;
+ h.mark(nodes);
+ for (var i = 0, node; node = nodes[i]; i++) {
+ if (!node.parentNode._counted) {
+ h.index(node.parentNode, reverse, ofType);
+ indexed.push(node.parentNode);
+ }
+ }
+ if (formula.match(/^\d+$/)) { // just a number
+ formula = Number(formula);
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.nodeIndex == formula) results.push(node);
+ } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (m[1] == "-") m[1] = -1;
+ var a = m[1] ? Number(m[1]) : 1;
+ var b = m[2] ? Number(m[2]) : 0;
+ var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+ for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+ for (var j = 0; j < l; j++)
+ if (node.nodeIndex == indices[j]) results.push(node);
+ }
+ }
+ h.unmark(nodes);
+ h.unmark(indexed);
+ return results;
+ },
+
+ 'empty': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ // IE treats comments as element nodes
+ if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+ results.push(node);
+ }
+ return results;
+ },
+
+ 'not': function(nodes, selector, root) {
+ var h = Selector.handlers, selectorType, m;
+ var exclusions = new Selector(selector).findElements(root);
+ h.mark(exclusions);
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node._counted) results.push(node);
+ h.unmark(exclusions);
+ return results;
+ },
+
+ 'enabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node.disabled) results.push(node);
+ return results;
+ },
+
+ 'disabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.disabled) results.push(node);
+ return results;
+ },
+
+ 'checked': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.checked) results.push(node);
+ return results;
+ }
+ },
+
+ operators: {
+ '=': function(nv, v) { return nv == v; },
+ '!=': function(nv, v) { return nv != v; },
+ '^=': function(nv, v) { return nv.startsWith(v); },
+ '$=': function(nv, v) { return nv.endsWith(v); },
+ '*=': function(nv, v) { return nv.include(v); },
+ '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+ '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+ },
+
+ matchElements: function(elements, expression) {
+ var matches = new Selector(expression).findElements(), h = Selector.handlers;
+ h.mark(matches);
+ for (var i = 0, results = [], element; element = elements[i]; i++)
+ if (element._counted) results.push(element);
+ h.unmark(matches);
+ return results;
+ },
+
+ findElement: function(elements, expression, index) {
+ if (Object.isNumber(expression)) {
+ index = expression; expression = false;
+ }
+ return Selector.matchElements(elements, expression || '*')[index || 0];
+ },
+
+ findChildElements: function(element, expressions) {
+ var exprs = expressions.join(',');
+ expressions = [];
+ exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ var results = [], h = Selector.handlers;
+ for (var i = 0, l = expressions.length, selector; i < l; i++) {
+ selector = new Selector(expressions[i].strip());
+ h.concat(results, selector.findElements(element));
+ }
+ return (l > 1) ? h.unique(results) : results;
+ }
+});
+
+if (Prototype.Browser.IE) {
+ // IE returns comment nodes on getElementsByTagName("*").
+ // Filter them out.
+ Selector.handlers.concat = function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ if (node.tagName !== "!") a.push(node);
+ return a;
+ };
+}
+
+function $$() {
+ return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+ reset: function(form) {
+ $(form).reset();
+ return form;
+ },
+
+ serializeElements: function(elements, options) {
+ if (typeof options != 'object') options = { hash: !!options };
+ else if (Object.isUndefined(options.hash)) options.hash = true;
+ var key, value, submitted = false, submit = options.submit;
+
+ var data = elements.inject({ }, function(result, element) {
+ if (!element.disabled && element.name) {
+ key = element.name; value = $(element).getValue();
+ if (value != null && (element.type != 'submit' || (!submitted &&
+ submit !== false && (!submit || key == submit) && (submitted = true)))) {
+ if (key in result) {
+ // a key is already present; construct an array of values
+ if (!Object.isArray(result[key])) result[key] = [result[key]];
+ result[key].push(value);
+ }
+ else result[key] = value;
+ }
+ }
+ return result;
+ });
+
+ return options.hash ? data : Object.toQueryString(data);
+ }
+};
+
+Form.Methods = {
+ serialize: function(form, options) {
+ return Form.serializeElements(Form.getElements(form), options);
+ },
+
+ getElements: function(form) {
+ return $A($(form).getElementsByTagName('*')).inject([],
+ function(elements, child) {
+ if (Form.Element.Serializers[child.tagName.toLowerCase()])
+ elements.push(Element.extend(child));
+ return elements;
+ }
+ );
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) || (name && input.name != name))
+ continue;
+ matchingInputs.push(Element.extend(input));
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('disable');
+ return form;
+ },
+
+ enable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('enable');
+ return form;
+ },
+
+ findFirstElement: function(form) {
+ var elements = $(form).getElements().findAll(function(element) {
+ return 'hidden' != element.type && !element.disabled;
+ });
+ var firstByIndex = elements.findAll(function(element) {
+ return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+ }).sortBy(function(element) { return element.tabIndex }).first();
+
+ return firstByIndex ? firstByIndex : elements.find(function(element) {
+ return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ form = $(form);
+ form.findFirstElement().activate();
+ return form;
+ },
+
+ request: function(form, options) {
+ form = $(form), options = Object.clone(options || { });
+
+ var params = options.parameters, action = form.readAttribute('action') || '';
+ if (action.blank()) action = window.location.href;
+ options.parameters = form.serialize(true);
+
+ if (params) {
+ if (Object.isString(params)) params = params.toQueryParams();
+ Object.extend(options.parameters, params);
+ }
+
+ if (form.hasAttribute('method') && !options.method)
+ options.method = form.method;
+
+ return new Ajax.Request(action, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+ focus: function(element) {
+ $(element).focus();
+ return element;
+ },
+
+ select: function(element) {
+ $(element).select();
+ return element;
+ }
+};
+
+Form.Element.Methods = {
+ serialize: function(element) {
+ element = $(element);
+ if (!element.disabled && element.name) {
+ var value = element.getValue();
+ if (value != undefined) {
+ var pair = { };
+ pair[element.name] = value;
+ return Object.toQueryString(pair);
+ }
+ }
+ return '';
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ return Form.Element.Serializers[method](element);
+ },
+
+ setValue: function(element, value) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ Form.Element.Serializers[method](element, value);
+ return element;
+ },
+
+ clear: function(element) {
+ $(element).value = '';
+ return element;
+ },
+
+ present: function(element) {
+ return $(element).value != '';
+ },
+
+ activate: function(element) {
+ element = $(element);
+ try {
+ element.focus();
+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
+ !['button', 'reset', 'submit'].include(element.type)))
+ element.select();
+ } catch (e) { }
+ return element;
+ },
+
+ disable: function(element) {
+ element = $(element);
+ element.blur();
+ element.disabled = true;
+ return element;
+ },
+
+ enable: function(element) {
+ element = $(element);
+ element.disabled = false;
+ return element;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+ input: function(element, value) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element, value);
+ default:
+ return Form.Element.Serializers.textarea(element, value);
+ }
+ },
+
+ inputSelector: function(element, value) {
+ if (Object.isUndefined(value)) return element.checked ? element.value : null;
+ else element.checked = !!value;
+ },
+
+ textarea: function(element, value) {
+ if (Object.isUndefined(value)) return element.value;
+ else element.value = value;
+ },
+
+ select: function(element, index) {
+ if (Object.isUndefined(index))
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ else {
+ var opt, value, single = !Object.isArray(index);
+ for (var i = 0, length = element.length; i < length; i++) {
+ opt = element.options[i];
+ value = this.optionValue(opt);
+ if (single) {
+ if (value == index) {
+ opt.selected = true;
+ return;
+ }
+ }
+ else opt.selected = index.include(value);
+ }
+ }
+ },
+
+ selectOne: function(element) {
+ var index = element.selectedIndex;
+ return index >= 0 ? this.optionValue(element.options[index]) : null;
+ },
+
+ selectMany: function(element) {
+ var values, length = element.length;
+ if (!length) return null;
+
+ for (var i = 0, values = []; i < length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) values.push(this.optionValue(opt));
+ }
+ return values;
+ },
+
+ optionValue: function(opt) {
+ // extend element because hasAttribute may not be native
+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+ initialize: function($super, element, frequency, callback) {
+ $super(callback, frequency);
+ this.element = $(element);
+ this.lastValue = this.getValue();
+ },
+
+ execute: function() {
+ var value = this.getValue();
+ if (Object.isString(this.lastValue) && Object.isString(value) ?
+ this.lastValue != value : String(this.lastValue) != String(value)) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = Class.create({
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ Form.getElements(this.element).each(this.registerCallback, this);
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ default:
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+});
+
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) var Event = { };
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+ KEY_HOME: 36,
+ KEY_END: 35,
+ KEY_PAGEUP: 33,
+ KEY_PAGEDOWN: 34,
+ KEY_INSERT: 45,
+
+ cache: { },
+
+ relatedTarget: function(event) {
+ var element;
+ switch(event.type) {
+ case 'mouseover': element = event.fromElement; break;
+ case 'mouseout': element = event.toElement; break;
+ default: return null;
+ }
+ return Element.extend(element);
+ }
+});
+
+Event.Methods = (function() {
+ var isButton;
+
+ if (Prototype.Browser.IE) {
+ var buttonMap = { 0: 1, 1: 4, 2: 2 };
+ isButton = function(event, code) {
+ return event.button == buttonMap[code];
+ };
+
+ } else if (Prototype.Browser.WebKit) {
+ isButton = function(event, code) {
+ switch (code) {
+ case 0: return event.which == 1 && !event.metaKey;
+ case 1: return event.which == 1 && event.metaKey;
+ default: return false;
+ }
+ };
+
+ } else {
+ isButton = function(event, code) {
+ return event.which ? (event.which === code + 1) : (event.button === code);
+ };
+ }
+
+ return {
+ isLeftClick: function(event) { return isButton(event, 0) },
+ isMiddleClick: function(event) { return isButton(event, 1) },
+ isRightClick: function(event) { return isButton(event, 2) },
+
+ element: function(event) {
+ var node = Event.extend(event).target;
+ return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
+ },
+
+ findElement: function(event, expression) {
+ var element = Event.element(event);
+ if (!expression) return element;
+ var elements = [element].concat(element.ancestors());
+ return Selector.findElement(elements, expression, 0);
+ },
+
+ pointer: function(event) {
+ return {
+ x: event.pageX || (event.clientX +
+ (document.documentElement.scrollLeft || document.body.scrollLeft)),
+ y: event.pageY || (event.clientY +
+ (document.documentElement.scrollTop || document.body.scrollTop))
+ };
+ },
+
+ pointerX: function(event) { return Event.pointer(event).x },
+ pointerY: function(event) { return Event.pointer(event).y },
+
+ stop: function(event) {
+ Event.extend(event);
+ event.preventDefault();
+ event.stopPropagation();
+ event.stopped = true;
+ }
+ };
+})();
+
+Event.extend = (function() {
+ var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+ m[name] = Event.Methods[name].methodize();
+ return m;
+ });
+
+ if (Prototype.Browser.IE) {
+ Object.extend(methods, {
+ stopPropagation: function() { this.cancelBubble = true },
+ preventDefault: function() { this.returnValue = false },
+ inspect: function() { return "[object Event]" }
+ });
+
+ return function(event) {
+ if (!event) return false;
+ if (event._extendedByPrototype) return event;
+
+ event._extendedByPrototype = Prototype.emptyFunction;
+ var pointer = Event.pointer(event);
+ Object.extend(event, {
+ target: event.srcElement,
+ relatedTarget: Event.relatedTarget(event),
+ pageX: pointer.x,
+ pageY: pointer.y
+ });
+ return Object.extend(event, methods);
+ };
+
+ } else {
+ Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;
+ Object.extend(Event.prototype, methods);
+ return Prototype.K;
+ }
+})();
+
+Object.extend(Event, (function() {
+ var cache = Event.cache;
+
+ function getEventID(element) {
+ if (element._eventID) return element._eventID;
+ arguments.callee.id = arguments.callee.id || 1;
+ return element._eventID = ++arguments.callee.id;
+ }
+
+ function getDOMEventName(eventName) {
+ if (eventName && eventName.include(':')) return "dataavailable";
+ return eventName;
+ }
+
+ function getCacheForID(id) {
+ return cache[id] = cache[id] || { };
+ }
+
+ function getWrappersForEventName(id, eventName) {
+ var c = getCacheForID(id);
+ return c[eventName] = c[eventName] || [];
+ }
+
+ function createWrapper(element, eventName, handler) {
+ var id = getEventID(element);
+ var c = getWrappersForEventName(id, eventName);
+ if (c.pluck("handler").include(handler)) return false;
+
+ var wrapper = function(event) {
+ if (!Event || !Event.extend ||
+ (event.eventName && event.eventName != eventName))
+ return false;
+
+ Event.extend(event);
+ handler.call(element, event)
+ };
+
+ wrapper.handler = handler;
+ c.push(wrapper);
+ return wrapper;
+ }
+
+ function findWrapper(id, eventName, handler) {
+ var c = getWrappersForEventName(id, eventName);
+ return c.find(function(wrapper) { return wrapper.handler == handler });
+ }
+
+ function destroyWrapper(id, eventName, handler) {
+ var c = getCacheForID(id);
+ if (!c[eventName]) return false;
+ c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+ }
+
+ function destroyCache() {
+ for (var id in cache)
+ for (var eventName in cache[id])
+ cache[id][eventName] = null;
+ }
+
+ if (window.attachEvent) {
+ window.attachEvent("onunload", destroyCache);
+ }
+
+ return {
+ observe: function(element, eventName, handler) {
+ element = $(element);
+ var name = getDOMEventName(eventName);
+
+ var wrapper = createWrapper(element, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.addEventListener) {
+ element.addEventListener(name, wrapper, false);
+ } else {
+ element.attachEvent("on" + name, wrapper);
+ }
+
+ return element;
+ },
+
+ stopObserving: function(element, eventName, handler) {
+ element = $(element);
+ var id = getEventID(element), name = getDOMEventName(eventName);
+
+ if (!handler && eventName) {
+ getWrappersForEventName(id, eventName).each(function(wrapper) {
+ element.stopObserving(eventName, wrapper.handler);
+ });
+ return element;
+
+ } else if (!eventName) {
+ Object.keys(getCacheForID(id)).each(function(eventName) {
+ element.stopObserving(eventName);
+ });
+ return element;
+ }
+
+ var wrapper = findWrapper(id, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, wrapper, false);
+ } else {
+ element.detachEvent("on" + name, wrapper);
+ }
+
+ destroyWrapper(id, eventName, handler);
+
+ return element;
+ },
+
+ fire: function(element, eventName, memo) {
+ element = $(element);
+ if (element == document && document.createEvent && !element.dispatchEvent)
+ element = document.documentElement;
+
+ if (document.createEvent) {
+ var event = document.createEvent("HTMLEvents");
+ event.initEvent("dataavailable", true, true);
+ } else {
+ var event = document.createEventObject();
+ event.eventType = "ondataavailable";
+ }
+
+ event.eventName = eventName;
+ event.memo = memo || { };
+
+ if (document.createEvent) {
+ element.dispatchEvent(event);
+ } else {
+ element.fireEvent(event.eventType, event);
+ }
+
+ return Event.extend(event);
+ }
+ };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+ fire: Event.fire,
+ observe: Event.observe,
+ stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+ fire: Element.Methods.fire.methodize(),
+ observe: Element.Methods.observe.methodize(),
+ stopObserving: Element.Methods.stopObserving.methodize()
+});
+
+(function() {
+ /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+ Matthias Miller, Dean Edwards and John Resig. */
+
+ var timer, fired = false;
+
+ function fireContentLoadedEvent() {
+ if (fired) return;
+ if (timer) window.clearInterval(timer);
+ document.fire("dom:loaded");
+ fired = true;
+ }
+
+ if (document.addEventListener) {
+ if (Prototype.Browser.WebKit) {
+ timer = window.setInterval(function() {
+ if (/loaded|complete/.test(document.readyState))
+ fireContentLoadedEvent();
+ }, 0);
+
+ Event.observe(window, "load", fireContentLoadedEvent);
+
+ } else {
+ document.addEventListener("DOMContentLoaded",
+ fireContentLoadedEvent, false);
+ }
+
+ } else {
+ document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+ $("__onDOMContentLoaded").onreadystatechange = function() {
+ if (this.readyState == "complete") {
+ this.onreadystatechange = null;
+ fireContentLoadedEvent();
+ }
+ };
+ }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+ Before: function(element, content) {
+ return Element.insert(element, {before:content});
+ },
+
+ Top: function(element, content) {
+ return Element.insert(element, {top:content});
+ },
+
+ Bottom: function(element, content) {
+ return Element.insert(element, {bottom:content});
+ },
+
+ After: function(element, content) {
+ return Element.insert(element, {after:content});
+ }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = Element.cumulativeScrollOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ // Deprecation layer -- use newer Element methods now (1.5.2).
+
+ cumulativeOffset: Element.Methods.cumulativeOffset,
+
+ positionedOffset: Element.Methods.positionedOffset,
+
+ absolutize: function(element) {
+ Position.prepare();
+ return Element.absolutize(element);
+ },
+
+ relativize: function(element) {
+ Position.prepare();
+ return Element.relativize(element);
+ },
+
+ realOffset: Element.Methods.cumulativeScrollOffset,
+
+ offsetParent: Element.Methods.getOffsetParent,
+
+ page: Element.Methods.viewportOffset,
+
+ clone: function(source, target, options) {
+ options = options || { };
+ return Element.clonePosition(target, source, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+ function iter(name) {
+ return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+ }
+
+ instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+ function(element, className) {
+ className = className.toString().strip();
+ var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+ return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+ } : function(element, className) {
+ className = className.toString().strip();
+ var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+ if (!classNames && !className) return elements;
+
+ var nodes = $(element).getElementsByTagName('*');
+ className = ' ' + className + ' ';
+
+ for (var i = 0, child, cn; child = nodes[i]; i++) {
+ if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+ (classNames && classNames.all(function(name) {
+ return !name.toString().blank() && cn.include(' ' + name + ' ');
+ }))))
+ elements.push(Element.extend(child));
+ }
+ return elements;
+ };
+
+ return function(className, parentElement) {
+ return $(parentElement || document.body).getElementsByClassName(className);
+ };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
+
+ toString: function() {
+ return $A(this).join(' ');
+ }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
+
+Element.addMethods(); \ No newline at end of file
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/number_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/number_helper.rb
new file mode 100644
index 000000000..eae230ad9
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/number_helper.rb
@@ -0,0 +1,179 @@
+module ActionView
+ module Helpers #:nodoc:
+ # Provides methods for converting numbers into formatted strings.
+ # Methods are provided for phone numbers, currency, percentage,
+ # precision, positional notation, and file size.
+ module NumberHelper
+ # Formats a +number+ into a US phone number (e.g., (555) 123-9876). You can customize the format
+ # in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:area_code</tt> - Adds parentheses around the area code.
+ # * <tt>:delimiter</tt> - Specifies the delimiter to use (defaults to "-").
+ # * <tt>:extension</tt> - Specifies an extension to add to the end of the
+ # generated number.
+ # * <tt>:country_code</tt> - Sets the country code for the phone number.
+ #
+ # ==== Examples
+ # number_to_phone(1235551234) # => 123-555-1234
+ # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234
+ # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234
+ # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555
+ # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234
+ #
+ # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".")
+ # => +1.123.555.1234 x 1343
+ def number_to_phone(number, options = {})
+ number = number.to_s.strip unless number.nil?
+ options = options.stringify_keys
+ area_code = options["area_code"] || nil
+ delimiter = options["delimiter"] || "-"
+ extension = options["extension"].to_s.strip || nil
+ country_code = options["country_code"] || nil
+
+ begin
+ str = ""
+ str << "+#{country_code}#{delimiter}" unless country_code.blank?
+ str << if area_code
+ number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ else
+ number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ end
+ str << " x #{extension}" unless extension.blank?
+ str
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format
+ # in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 2).
+ # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$").
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
+ #
+ # ==== Examples
+ # number_to_currency(1234567890.50) # => $1,234,567,890.50
+ # number_to_currency(1234567890.506) # => $1,234,567,890.51
+ # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506
+ #
+ # number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "")
+ # # => &pound;1234567890,50
+ def number_to_currency(number, options = {})
+ options = options.stringify_keys
+ precision = options["precision"] || 2
+ unit = options["unit"] || "$"
+ separator = precision > 0 ? options["separator"] || "." : ""
+ delimiter = options["delimiter"] || ","
+
+ begin
+ parts = number_with_precision(number, precision).split('.')
+ unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ as a percentage string (e.g., 65%). You can customize the
+ # format in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 3).
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ #
+ # ==== Examples
+ # number_to_percentage(100) # => 100.000%
+ # number_to_percentage(100, :precision => 0) # => 100%
+ #
+ # number_to_percentage(302.24398923423, :precision => 5)
+ # # => 302.24399%
+ def number_to_percentage(number, options = {})
+ options = options.stringify_keys
+ precision = options["precision"] || 3
+ separator = options["separator"] || "."
+
+ begin
+ number = number_with_precision(number, precision)
+ parts = number.split('.')
+ if parts.at(1).nil?
+ parts[0] + "%"
+ else
+ parts[0] + separator + parts[1].to_s + "%"
+ end
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You
+ # can customize the format using optional <em>delimiter</em> and <em>separator</em> parameters.
+ #
+ # ==== Options
+ # * <tt>delimiter</tt> - Sets the thousands delimiter (defaults to ",").
+ # * <tt>separator</tt> - Sets the separator between the units (defaults to ".").
+ #
+ # ==== Examples
+ # number_with_delimiter(12345678) # => 12,345,678
+ # number_with_delimiter(12345678.05) # => 12,345,678.05
+ # number_with_delimiter(12345678, ".") # => 12.345.678
+ #
+ # number_with_delimiter(98765432.98, " ", ",")
+ # # => 98 765 432,98
+ def number_with_delimiter(number, delimiter=",", separator=".")
+ begin
+ parts = number.to_s.split('.')
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
+ parts.join separator
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ with the specified level of +precision+ (e.g., 112.32 has a precision of 2). The default
+ # level of precision is 3.
+ #
+ # ==== Examples
+ # number_with_precision(111.2345) # => 111.235
+ # number_with_precision(111.2345, 2) # => 111.24
+ # number_with_precision(13, 5) # => 13.00000
+ # number_with_precision(389.32314, 0) # => 389
+ def number_with_precision(number, precision=3)
+ "%01.#{precision}f" % number
+ rescue
+ number
+ end
+
+ # Formats the bytes in +size+ into a more understandable representation
+ # (e.g., giving it 1500 yields 1.5 KB). This method is useful for
+ # reporting file sizes to users. This method returns nil if
+ # +size+ cannot be converted into a number. You can change the default
+ # precision of 1 using the precision parameter +precision+.
+ #
+ # ==== Examples
+ # number_to_human_size(123) # => 123 Bytes
+ # number_to_human_size(1234) # => 1.2 KB
+ # number_to_human_size(12345) # => 12.1 KB
+ # number_to_human_size(1234567) # => 1.2 MB
+ # number_to_human_size(1234567890) # => 1.1 GB
+ # number_to_human_size(1234567890123) # => 1.1 TB
+ # number_to_human_size(1234567, 2) # => 1.18 MB
+ # number_to_human_size(483989, 0) # => 4 MB
+ def number_to_human_size(size, precision=1)
+ size = Kernel.Float(size)
+ case
+ when size.to_i == 1; "1 Byte"
+ when size < 1.kilobyte; "%d Bytes" % size
+ when size < 1.megabyte; "%.#{precision}f KB" % (size / 1.0.kilobyte)
+ when size < 1.gigabyte; "%.#{precision}f MB" % (size / 1.0.megabyte)
+ when size < 1.terabyte; "%.#{precision}f GB" % (size / 1.0.gigabyte)
+ else "%.#{precision}f TB" % (size / 1.0.terabyte)
+ end.sub(/([0-9])\.?0+ /, '\1 ' )
+ rescue
+ nil
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/prototype_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/prototype_helper.rb
new file mode 100644
index 000000000..0435f5c42
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/prototype_helper.rb
@@ -0,0 +1,1268 @@
+require 'set'
+
+module ActionView
+ module Helpers
+ # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides
+ # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation,
+ # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]
+ # functionality, and more traditional object-oriented facilities for JavaScript.
+ # This module provides a set of helpers to make it more convenient to call
+ # functions from Prototype using Rails, including functionality to call remote
+ # Rails methods (that is, making a background request to a Rails action) using Ajax.
+ # This means that you can call actions in your controllers without
+ # reloading the page, but still update certain parts of it using
+ # injections into the DOM. A common use case is having a form that adds
+ # a new element to a list without reloading the page or updating a shopping
+ # cart total when a new item is added.
+ #
+ # == Usage
+ # To be able to use these helpers, you must first include the Prototype
+ # JavaScript framework in your pages.
+ #
+ # javascript_include_tag 'prototype'
+ #
+ # (See the documentation for
+ # ActionView::Helpers::JavaScriptHelper for more information on including
+ # this and other JavaScript files in your Rails templates.)
+ #
+ # Now you're ready to call a remote action either through a link...
+ #
+ # link_to_remote "Add to cart",
+ # :url => { :action => "add", :id => product.id },
+ # :update => { :success => "cart", :failure => "error" }
+ #
+ # ...through a form...
+ #
+ # <% form_remote_tag :url => '/shipping' do -%>
+ # <div><%= submit_tag 'Recalculate Shipping' %></div>
+ # <% end -%>
+ #
+ # ...periodically...
+ #
+ # periodically_call_remote(:url => 'update', :frequency => '5', :update => 'ticker')
+ #
+ # ...or through an observer (i.e., a form or field that is observed and calls a remote
+ # action when changed).
+ #
+ # <%= observe_field(:searchbox,
+ # :url => { :action => :live_search }),
+ # :frequency => 0.5,
+ # :update => :hits,
+ # :with => 'query'
+ # %>
+ #
+ # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than
+ # are listed here); check out the documentation for each method to find out more about its usage and options.
+ #
+ # === Common Options
+ # See link_to_remote for documentation of options common to all Ajax
+ # helpers; any of the options specified by link_to_remote can be used
+ # by the other helpers.
+ #
+ # == Designing your Rails actions for Ajax
+ # When building your action handlers (that is, the Rails actions that receive your background requests), it's
+ # important to remember a few things. First, whatever your action would normall return to the browser, it will
+ # return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause
+ # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up.
+ # You can turn the layout off on particular actions by doing the following:
+ #
+ # class SiteController < ActionController::Base
+ # layout "standard", :except => [:ajax_method, :more_ajax, :another_ajax]
+ # end
+ #
+ # Optionally, you could do this in the method you wish to lack a layout:
+ #
+ # render :layout => false
+ #
+ # You can tell the type of request from within your action using the <tt>request.xhr?</tt> (XmlHttpRequest, the
+ # method that Ajax uses to make background requests) method.
+ # def name
+ # # Is this an XmlHttpRequest request?
+ # if (request.xhr?)
+ # render :text => @name.to_s
+ # else
+ # # No? Then render an action.
+ # render :action => 'view_attribute', :attr => @name
+ # end
+ # end
+ #
+ # The else clause can be left off and the current action will render with full layout and template. An extension
+ # to this solution was posted to Ryan Heneise's blog at ArtOfMission["http://www.artofmission.com/"].
+ #
+ # layout proc{ |c| c.request.xhr? ? false : "application" }
+ #
+ # Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request.
+ #
+ # If you are just returning a little data or don't want to build a template for your output, you may opt to simply
+ # render text output, like this:
+ #
+ # render :text => 'Return this from my method!'
+ #
+ # Since whatever the method returns is injected into the DOM, this will simply inject some text (or HTML, if you
+ # tell it to). This is usually how small updates, such updating a cart total or a file count, are handled.
+ #
+ # == Updating multiple elements
+ # See JavaScriptGenerator for information on updating multiple elements
+ # on the page in an Ajax response.
+ module PrototypeHelper
+ unless const_defined? :CALLBACKS
+ CALLBACKS = Set.new([ :uninitialized, :loading, :loaded,
+ :interactive, :complete, :failure, :success ] +
+ (100..599).to_a)
+ AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
+ :asynchronous, :method, :insertion, :position,
+ :form, :with, :update, :script ]).merge(CALLBACKS)
+ end
+
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
+ # (using the url_for format) that's called in the background using
+ # XMLHttpRequest. The result of that request can then be inserted into a
+ # DOM object whose id can be specified with <tt>options[:update]</tt>.
+ # Usually, the result would be a partial prepared by the controller with
+ # render :partial.
+ #
+ # Examples:
+ # # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true});
+ # # return false;">Delete this post</a>
+ # link_to_remote "Delete this post", :update => "posts",
+ # :url => { :action => "destroy", :id => post.id }
+ #
+ # # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true});
+ # # return false;"><img alt="Refresh" src="/images/refresh.png?" /></a>
+ # link_to_remote(image_tag("refresh"), :update => "emails",
+ # :url => { :action => "list_emails" })
+ #
+ # You can override the generated HTML options by specifying a hash in
+ # <tt>options[:html]</tt>.
+ #
+ # link_to_remote "Delete this post", :update => "posts",
+ # :url => post_url(@post), :method => :delete,
+ # :html => { :class => "destructive" }
+ #
+ # You can also specify a hash for <tt>options[:update]</tt> to allow for
+ # easy redirection of output to an other DOM element if a server-side
+ # error occurs:
+ #
+ # Example:
+ # # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5',
+ # # {asynchronous:true, evalScripts:true}); return false;">Delete this post</a>
+ # link_to_remote "Delete this post",
+ # :url => { :action => "destroy", :id => post.id },
+ # :update => { :success => "posts", :failure => "error" }
+ #
+ # Optionally, you can use the <tt>options[:position]</tt> parameter to
+ # influence how the target DOM element is updated. It must be one of
+ # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
+ #
+ # The method used is by default POST. You can also specify GET or you
+ # can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt>
+ #
+ # Example:
+ # # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'});
+ # # return false;">Destroy</a>
+ # link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
+ #
+ # By default, these remote requests are processed asynchronous during
+ # which various JavaScript callbacks can be triggered (for progress
+ # indicators and the likes). All callbacks get access to the
+ # <tt>request</tt> object, which holds the underlying XMLHttpRequest.
+ #
+ # To access the server response, use <tt>request.responseText</tt>, to
+ # find out the HTTP status, use <tt>request.status</tt>.
+ #
+ # Example:
+ # # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true,
+ # # onComplete:function(request){undoRequestCompleted(request)}}); return false;">hello</a>
+ # word = 'hello'
+ # link_to_remote word,
+ # :url => { :action => "undo", :n => word_counter },
+ # :complete => "undoRequestCompleted(request)"
+ #
+ # The callbacks that may be specified are (in order):
+ #
+ # <tt>:loading</tt>:: Called when the remote document is being
+ # loaded with data by the browser.
+ # <tt>:loaded</tt>:: Called when the browser has finished loading
+ # the remote document.
+ # <tt>:interactive</tt>:: Called when the user can interact with the
+ # remote document, even though it has not
+ # finished loading.
+ # <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
+ # and the HTTP status code is in the 2XX range.
+ # <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
+ # and the HTTP status code is not in the 2XX
+ # range.
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
+ # (fires after success/failure if they are
+ # present).
+ #
+ # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
+ # adding additional callbacks for specific status codes.
+ #
+ # Example:
+ # # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
+ # # on404:function(request){alert('Not found...? Wrong URL...?')},
+ # # onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
+ # link_to_remote word,
+ # :url => { :action => "action" },
+ # 404 => "alert('Not found...? Wrong URL...?')",
+ # :failure => "alert('HTTP Error ' + request.status + '!')"
+ #
+ # A status code callback overrides the success/failure handlers if
+ # present.
+ #
+ # If you for some reason or another need synchronous processing (that'll
+ # block the browser while the request is happening), you can specify
+ # <tt>options[:type] = :synchronous</tt>.
+ #
+ # You can customize further browser side call logic by passing in
+ # JavaScript code snippets via some optional parameters. In their order
+ # of use these are:
+ #
+ # <tt>:confirm</tt>:: Adds confirmation dialog.
+ # <tt>:condition</tt>:: Perform remote request conditionally
+ # by this expression. Use this to
+ # describe browser-side conditions when
+ # request should not be initiated.
+ # <tt>:before</tt>:: Called before request is initiated.
+ # <tt>:after</tt>:: Called immediately after request was
+ # initiated and before <tt>:loading</tt>.
+ # <tt>:submit</tt>:: Specifies the DOM element ID that's used
+ # as the parent of the form elements. By
+ # default this is the current form, but
+ # it could just as well be the ID of a
+ # table row or any other DOM element.
+ # <tt>:with</tt>:: A JavaScript expression specifying
+ # the parameters for the XMLHttpRequest.
+ # Any expressions should return a valid
+ # URL query string.
+ #
+ # Example:
+ #
+ # :with => "'name=' + $('name').value"
+ #
+ # You can generate a link that uses AJAX in the general case, while
+ # degrading gracefully to plain link behavior in the absence of
+ # JavaScript by setting <tt>html_options[:href]</tt> to an alternate URL.
+ # Note the extra curly braces around the <tt>options</tt> hash separate
+ # it as the second parameter from <tt>html_options</tt>, the third.
+ #
+ # Example:
+ # link_to_remote "Delete this post",
+ # { :update => "posts", :url => { :action => "destroy", :id => post.id } },
+ # :href => url_for(:action => "destroy", :id => post.id)
+ def link_to_remote(name, options = {}, html_options = nil)
+ link_to_function(name, remote_function(options), html_options || options.delete(:html))
+ end
+
+ # Periodically calls the specified url (<tt>options[:url]</tt>) every
+ # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
+ # update a specified div (<tt>options[:update]</tt>) with the results
+ # of the remote call. The options for specifying the target with :url
+ # and defining callbacks is the same as link_to_remote.
+ # Examples:
+ # # Call get_averages and put its results in 'avg' every 10 seconds
+ # # Generates:
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
+ # # {asynchronous:true, evalScripts:true})}, 10)
+ # periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg')
+ #
+ # # Call invoice every 10 seconds with the id of the customer
+ # # If it succeeds, update the invoice DIV; if it fails, update the error DIV
+ # # Generates:
+ # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
+ # # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10)
+ # periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
+ # :update => { :success => "invoice", :failure => "error" }
+ #
+ # # Call update every 20 seconds and update the new_block DIV
+ # # Generates:
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('news_block', 'update', {asynchronous:true, evalScripts:true})}, 20)
+ # periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block')
+ #
+ def periodically_call_remote(options = {})
+ frequency = options[:frequency] || 10 # every ten seconds by default
+ code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
+ javascript_tag(code)
+ end
+
+ # Returns a form tag that will submit using XMLHttpRequest in the
+ # background instead of the regular reloading POST arrangement. Even
+ # though it's using JavaScript to serialize the form elements, the form
+ # submission will work just like a regular submission as viewed by the
+ # receiving side (all elements available in <tt>params</tt>). The options for
+ # specifying the target with :url and defining callbacks is the same as
+ # link_to_remote.
+ #
+ # A "fall-through" target for browsers that doesn't do JavaScript can be
+ # specified with the :action/:method options on :html.
+ #
+ # Example:
+ # # Generates:
+ # # <form action="/some/place" method="post" onsubmit="new Ajax.Request('',
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
+ # form_remote_tag :html => { :action =>
+ # url_for(:controller => "some", :action => "place") }
+ #
+ # The Hash passed to the :html key is equivalent to the options (2nd)
+ # argument in the FormTagHelper.form_tag method.
+ #
+ # By default the fall-through action is the same as the one specified in
+ # the :url (and the default method is :post).
+ #
+ # form_remote_tag also takes a block, like form_tag:
+ # # Generates:
+ # # <form action="/" method="post" onsubmit="new Ajax.Request('/',
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});
+ # # return false;"> <div><input name="commit" type="submit" value="Save" /></div>
+ # # </form>
+ # <% form_remote_tag :url => '/posts' do -%>
+ # <div><%= submit_tag 'Save' %></div>
+ # <% end -%>
+ def form_remote_tag(options = {}, &block)
+ options[:form] = true
+
+ options[:html] ||= {}
+ options[:html][:onsubmit] =
+ (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
+ "#{remote_function(options)}; return false;"
+
+ form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
+ end
+
+ # Creates a form that will submit using XMLHttpRequest in the background
+ # instead of the regular reloading POST arrangement and a scope around a
+ # specific resource that is used as a base for questioning about
+ # values for the fields.
+ #
+ # === Resource
+ #
+ # Example:
+ # <% remote_form_for(@post) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # This will expand to be the same as:
+ #
+ # <% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
+ # ...
+ # <% end %>
+ #
+ # === Nested Resource
+ #
+ # Example:
+ # <% remote_form_for([@post, @comment]) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # This will expand to be the same as:
+ #
+ # <% remote_form_for :comment, @comment, :url => post_comment_path(@post, @comment), :html => { :method => :put, :class => "edit_comment", :id => "edit_comment_45" } do |f| %>
+ # ...
+ # <% end %>
+ #
+ # If you don't need to attach a form to a resource, then check out form_remote_tag.
+ #
+ # See FormHelper#form_for for additional semantics.
+ def remote_form_for(record_or_name_or_array, *args, &proc)
+ options = args.extract_options!
+
+ case record_or_name_or_array
+ when String, Symbol
+ object_name = record_or_name_or_array
+ when Array
+ object = record_or_name_or_array.last
+ object_name = ActionController::RecordIdentifier.singular_class_name(object)
+ apply_form_for_options!(record_or_name_or_array, options)
+ args.unshift object
+ else
+ object = record_or_name_or_array
+ object_name = ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array)
+ apply_form_for_options!(object, options)
+ args.unshift object
+ end
+
+ concat(form_remote_tag(options), proc.binding)
+ fields_for(object_name, *(args << options), &proc)
+ concat('</form>', proc.binding)
+ end
+ alias_method :form_remote_for, :remote_form_for
+
+ # Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+
+ # that will submit form using XMLHttpRequest in the background instead of a regular POST request that
+ # reloads the page.
+ #
+ # # Create a button that submits to the create action
+ # #
+ # # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create',
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
+ # # return false;" type="button" value="Create" />
+ # <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %>
+ #
+ # # Submit to the remote action update and update the DIV succeed or fail based
+ # # on the success or failure of the request
+ # #
+ # # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'},
+ # # '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
+ # # return false;" type="button" value="Update" />
+ # <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' },
+ # :update => { :success => "succeed", :failure => "fail" }
+ #
+ # <tt>options</tt> argument is the same as in form_remote_tag.
+ def submit_to_remote(name, value, options = {})
+ options[:with] ||= 'Form.serialize(this.form)'
+
+ options[:html] ||= {}
+ options[:html][:type] = 'button'
+ options[:html][:onclick] = "#{remote_function(options)}; return false;"
+ options[:html][:name] = name
+ options[:html][:value] = value
+
+ tag("input", options[:html], false)
+ end
+
+ # Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function
+ # that form_remote_tag can call in :complete to evaluate a multiple
+ # update return document using update_element_function calls.
+ def evaluate_remote_response
+ "eval(request.responseText)"
+ end
+
+ # Returns the JavaScript needed for a remote function.
+ # Takes the same arguments as link_to_remote.
+ #
+ # Example:
+ # # Generates: <select id="options" onchange="new Ajax.Updater('options',
+ # # '/testing/update_options', {asynchronous:true, evalScripts:true})">
+ # <select id="options" onchange="<%= remote_function(:update => "options",
+ # :url => { :action => :update_options }) %>">
+ # <option value="0">Hello</option>
+ # <option value="1">World</option>
+ # </select>
+ def remote_function(options)
+ javascript_options = options_for_ajax(options)
+
+ update = ''
+ if options[:update] && options[:update].is_a?(Hash)
+ update = []
+ update << "success:'#{options[:update][:success]}'" if options[:update][:success]
+ update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
+ update = '{' + update.join(',') + '}'
+ elsif options[:update]
+ update << "'#{options[:update]}'"
+ end
+
+ function = update.empty? ?
+ "new Ajax.Request(" :
+ "new Ajax.Updater(#{update}, "
+
+ url_options = options[:url]
+ url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash)
+ function << "'#{url_for(url_options)}'"
+ function << ", #{javascript_options})"
+
+ function = "#{options[:before]}; #{function}" if options[:before]
+ function = "#{function}; #{options[:after]}" if options[:after]
+ function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
+ function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
+
+ return function
+ end
+
+ # Observes the field with the DOM ID specified by +field_id+ and calls a
+ # callback when its contents have changed. The default callback is an
+ # Ajax call. By default the value of the observed field is sent as a
+ # parameter with the Ajax call.
+ #
+ # Example:
+ # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest',
+ # # '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})})
+ # <%= observe_field :suggest, :url => { :action => :find_suggestion },
+ # :frequency => 0.25,
+ # :update => :suggest,
+ # :with => 'q'
+ # %>
+ #
+ # Required +options+ are either of:
+ # <tt>:url</tt>:: +url_for+-style options for the action to call
+ # when the field has changed.
+ # <tt>:function</tt>:: Instead of making a remote call to a URL, you
+ # can specify javascript code to be called instead.
+ # Note that the value of this option is used as the
+ # *body* of the javascript function, a function definition
+ # with parameters named element and value will be generated for you
+ # for example:
+ # observe_field("glass", :frequency => 1, :function => "alert('Element changed')")
+ # will generate:
+ # new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')})
+ # The element parameter is the DOM element being observed, and the value is its value at the
+ # time the observer is triggered.
+ #
+ # Additional options are:
+ # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
+ # this field will be detected. Not setting this
+ # option at all or to a value equal to or less than
+ # zero will use event based observation instead of
+ # time based observation.
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
+ # innerHTML should be updated with the
+ # XMLHttpRequest response text.
+ # <tt>:with</tt>:: A JavaScript expression specifying the parameters
+ # for the XMLHttpRequest. The default is to send the
+ # key and value of the observed field. Any custom
+ # expressions should return a valid URL query string.
+ # The value of the field is stored in the JavaScript
+ # variable +value+.
+ #
+ # Examples
+ #
+ # :with => "'my_custom_key=' + value"
+ # :with => "'person[name]=' + prompt('New name')"
+ # :with => "Form.Element.serialize('other-field')"
+ #
+ # Finally
+ # :with => 'name'
+ # is shorthand for
+ # :with => "'name=' + value"
+ # This essentially just changes the key of the parameter.
+ # <tt>:on</tt>:: Specifies which event handler to observe. By default,
+ # it's set to "changed" for text fields and areas and
+ # "click" for radio buttons and checkboxes. With this,
+ # you can specify it instead to be "blur" or "focus" or
+ # any other event.
+ #
+ # Additionally, you may specify any of the options documented in the
+ # <em>Common options</em> section at the top of this document.
+ #
+ # Example:
+ #
+ # # Sends params: {:title => 'Title of the book'} when the book_title input
+ # # field is changed.
+ # observe_field 'book_title',
+ # :url => 'http://example.com/books/edit/1',
+ # :with => 'title'
+ #
+ # # Sends params: {:book_title => 'Title of the book'} when the focus leaves
+ # # the input field.
+ # observe_field 'book_title',
+ # :url => 'http://example.com/books/edit/1',
+ # :on => 'blur'
+ #
+ def observe_field(field_id, options = {})
+ if options[:frequency] && options[:frequency] > 0
+ build_observer('Form.Element.Observer', field_id, options)
+ else
+ build_observer('Form.Element.EventObserver', field_id, options)
+ end
+ end
+
+ # Observes the form with the DOM ID specified by +form_id+ and calls a
+ # callback when its contents have changed. The default callback is an
+ # Ajax call. By default all fields of the observed field are sent as
+ # parameters with the Ajax call.
+ #
+ # The +options+ for +observe_form+ are the same as the options for
+ # +observe_field+. The JavaScript variable +value+ available to the
+ # <tt>:with</tt> option is set to the serialized form by default.
+ def observe_form(form_id, options = {})
+ if options[:frequency]
+ build_observer('Form.Observer', form_id, options)
+ else
+ build_observer('Form.EventObserver', form_id, options)
+ end
+ end
+
+ # All the methods were moved to GeneratorMethods so that
+ # #include_helpers_from_context has nothing to overwrite.
+ class JavaScriptGenerator #:nodoc:
+ def initialize(context, &block) #:nodoc:
+ @context, @lines = context, []
+ include_helpers_from_context
+ @context.instance_exec(self, &block)
+ end
+
+ private
+ def include_helpers_from_context
+ @context.extended_by.each do |mod|
+ extend mod unless mod.name =~ /^ActionView::Helpers/
+ end
+ extend GeneratorMethods
+ end
+
+ # JavaScriptGenerator generates blocks of JavaScript code that allow you
+ # to change the content and presentation of multiple DOM elements. Use
+ # this in your Ajax response bodies, either in a <script> tag or as plain
+ # JavaScript sent with a Content-type of "text/javascript".
+ #
+ # Create new instances with PrototypeHelper#update_page or with
+ # ActionController::Base#render, then call #insert_html, #replace_html,
+ # #remove, #show, #hide, #visual_effect, or any other of the built-in
+ # methods on the yielded generator in any order you like to modify the
+ # content and appearance of the current page.
+ #
+ # Example:
+ #
+ # # Generates:
+ # # new Insertion.Bottom("list", "<li>Some item</li>");
+ # # new Effect.Highlight("list");
+ # # ["status-indicator", "cancel-link"].each(Element.hide);
+ # update_page do |page|
+ # page.insert_html :bottom, 'list', "<li>#{@item.name}</li>"
+ # page.visual_effect :highlight, 'list'
+ # page.hide 'status-indicator', 'cancel-link'
+ # end
+ #
+ #
+ # Helper methods can be used in conjunction with JavaScriptGenerator.
+ # When a helper method is called inside an update block on the +page+
+ # object, that method will also have access to a +page+ object.
+ #
+ # Example:
+ #
+ # module ApplicationHelper
+ # def update_time
+ # page.replace_html 'time', Time.now.to_s(:db)
+ # page.visual_effect :highlight, 'time'
+ # end
+ # end
+ #
+ # # Controller action
+ # def poll
+ # render(:update) { |page| page.update_time }
+ # end
+ #
+ # You can also use PrototypeHelper#update_page_tag instead of
+ # PrototypeHelper#update_page to wrap the generated JavaScript in a
+ # <script> tag.
+ module GeneratorMethods
+ def to_s #:nodoc:
+ returning javascript = @lines * $/ do
+ if ActionView::Base.debug_rjs
+ source = javascript.dup
+ javascript.replace "try {\n#{source}\n} catch (e) "
+ javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
+ end
+ end
+ end
+
+ # Returns a element reference by finding it through +id+ in the DOM. This element can then be
+ # used for further method calls. Examples:
+ #
+ # page['blank_slate'] # => $('blank_slate');
+ # page['blank_slate'].show # => $('blank_slate').show();
+ # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
+ #
+ # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
+ # the correct id:
+ #
+ # page[@post] # => $('post_45')
+ # page[Post.new] # => $('new_post')
+ def [](id)
+ case id
+ when String, Symbol, NilClass
+ JavaScriptElementProxy.new(self, id)
+ else
+ JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
+ end
+ end
+
+ # Returns an object whose <tt>#to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
+ # expression as an argument to another JavaScriptGenerator method.
+ def literal(code)
+ ActiveSupport::JSON::Variable.new(code.to_s)
+ end
+
+ # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
+ # used for further method calls. Examples:
+ #
+ # page.select('p') # => $$('p');
+ # page.select('p.welcome b').first # => $$('p.welcome b').first();
+ # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
+ #
+ # You can also use prototype enumerations with the collection. Observe:
+ #
+ # # Generates: $$('#items li').each(function(value) { value.hide(); });
+ # page.select('#items li').each do |value|
+ # value.hide
+ # end
+ #
+ # Though you can call the block param anything you want, they are always rendered in the
+ # javascript as 'value, index.' Other enumerations, like collect() return the last statement:
+ #
+ # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
+ # page.select('#items li').collect('hidden') do |item|
+ # item.hide
+ # end
+ #
+ def select(pattern)
+ JavaScriptElementCollectionProxy.new(self, pattern)
+ end
+
+ # Inserts HTML at the specified +position+ relative to the DOM element
+ # identified by the given +id+.
+ #
+ # +position+ may be one of:
+ #
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
+ # element's existing content.
+ # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
+ # element's existing content.
+ # <tt>:before</tt>:: HTML is inserted immediately preceding the element.
+ # <tt>:after</tt>:: HTML is inserted immediately following the element.
+ #
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
+ # of options to be passed to ActionView::Base#render. For example:
+ #
+ # # Insert the rendered 'navigation' partial just before the DOM
+ # # element with ID 'content'.
+ # # Generates: new Insertion.Before("content", "-- Contents of 'navigation' partial --");
+ # insert_html :before, 'content', :partial => 'navigation'
+ #
+ # # Add a list item to the bottom of the <ul> with ID 'list'.
+ # # Generates: new Insertion.Bottom("list", "<li>Last item</li>");
+ # insert_html :bottom, 'list', '<li>Last item</li>'
+ #
+ def insert_html(position, id, *options_for_render)
+ insertion = position.to_s.camelize
+ call "new Insertion.#{insertion}", id, render(*options_for_render)
+ end
+
+ # Replaces the inner HTML of the DOM element with the given +id+.
+ #
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
+ # of options to be passed to ActionView::Base#render. For example:
+ #
+ # # Replace the HTML of the DOM element having ID 'person-45' with the
+ # # 'person' partial for the appropriate object.
+ # # Generates: Element.update("person-45", "-- Contents of 'person' partial --");
+ # replace_html 'person-45', :partial => 'person', :object => @person
+ #
+ def replace_html(id, *options_for_render)
+ call 'Element.update', id, render(*options_for_render)
+ end
+
+ # Replaces the "outer HTML" (i.e., the entire element, not just its
+ # contents) of the DOM element with the given +id+.
+ #
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
+ # of options to be passed to ActionView::Base#render. For example:
+ #
+ # # Replace the DOM element having ID 'person-45' with the
+ # # 'person' partial for the appropriate object.
+ # replace 'person-45', :partial => 'person', :object => @person
+ #
+ # This allows the same partial that is used for the +insert_html+ to
+ # be also used for the input to +replace+ without resorting to
+ # the use of wrapper elements.
+ #
+ # Examples:
+ #
+ # <div id="people">
+ # <%= render :partial => 'person', :collection => @people %>
+ # </div>
+ #
+ # # Insert a new person
+ # #
+ # # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
+ # page.insert_html :bottom, :partial => 'person', :object => @person
+ #
+ # # Replace an existing person
+ #
+ # # Generates: Element.replace("person_45", "-- Contents of partial --");
+ # page.replace 'person_45', :partial => 'person', :object => @person
+ #
+ def replace(id, *options_for_render)
+ call 'Element.replace', id, render(*options_for_render)
+ end
+
+ # Removes the DOM elements with the given +ids+ from the page.
+ #
+ # Example:
+ #
+ # # Remove a few people
+ # # Generates: ["person_23", "person_9", "person_2"].each(Element.remove);
+ # page.remove 'person_23', 'person_9', 'person_2'
+ #
+ def remove(*ids)
+ loop_on_multiple_args 'Element.remove', ids
+ end
+
+ # Shows hidden DOM elements with the given +ids+.
+ #
+ # Example:
+ #
+ # # Show a few people
+ # # Generates: ["person_6", "person_13", "person_223"].each(Element.show);
+ # page.show 'person_6', 'person_13', 'person_223'
+ #
+ def show(*ids)
+ loop_on_multiple_args 'Element.show', ids
+ end
+
+ # Hides the visible DOM elements with the given +ids+.
+ #
+ # Example:
+ #
+ # # Hide a few people
+ # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
+ # page.hide 'person_29', 'person_9', 'person_0'
+ #
+ def hide(*ids)
+ loop_on_multiple_args 'Element.hide', ids
+ end
+
+ # Toggles the visibility of the DOM elements with the given +ids+.
+ # Example:
+ #
+ # # Show a few people
+ # # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle);
+ # page.toggle 'person_14', 'person_12', 'person_23' # Hides the elements
+ # page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements
+ #
+ def toggle(*ids)
+ loop_on_multiple_args 'Element.toggle', ids
+ end
+
+ # Displays an alert dialog with the given +message+.
+ #
+ # Example:
+ #
+ # # Generates: alert('This message is from Rails!')
+ # page.alert('This message is from Rails!')
+ def alert(message)
+ call 'alert', message
+ end
+
+ # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
+ #
+ # Examples:
+ #
+ # # Generates: window.location.href = "/mycontroller";
+ # page.redirect_to(:action => 'index')
+ #
+ # # Generates: window.location.href = "/account/signup";
+ # page.redirect_to(:controller => 'account', :action => 'signup')
+ def redirect_to(location)
+ assign 'window.location.href', @context.url_for(location)
+ end
+
+ # Calls the JavaScript +function+, optionally with the given +arguments+.
+ #
+ # If a block is given, the block will be passed to a new JavaScriptGenerator;
+ # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
+ # and passed as the called function's final argument.
+ #
+ # Examples:
+ #
+ # # Generates: Element.replace(my_element, "My content to replace with.")
+ # page.call 'Element.replace', 'my_element', "My content to replace with."
+ #
+ # # Generates: alert('My message!')
+ # page.call 'alert', 'My message!'
+ #
+ def call(function, *arguments, &block)
+ record "#{function}(#{arguments_for_call(arguments, block)})"
+ end
+
+ # Assigns the JavaScript +variable+ the given +value+.
+ #
+ # Examples:
+ #
+ # # Generates: my_string = "This is mine!";
+ # page.assign 'my_string', 'This is mine!'
+ #
+ # # Generates: record_count = 33;
+ # page.assign 'record_count', 33
+ #
+ # # Generates: tabulated_total = 47
+ # page.assign 'tabulated_total', @total_from_cart
+ #
+ def assign(variable, value)
+ record "#{variable} = #{javascript_object_for(value)}"
+ end
+
+ # Writes raw JavaScript to the page.
+ #
+ # Example:
+ #
+ # page << "alert('JavaScript with Prototype.');"
+ def <<(javascript)
+ @lines << javascript
+ end
+
+ # Executes the content of the block after a delay of +seconds+. Example:
+ #
+ # # Generates:
+ # # setTimeout(function() {
+ # # ;
+ # # new Effect.Fade("notice",{});
+ # # }, 20000);
+ # page.delay(20) do
+ # page.visual_effect :fade, 'notice'
+ # end
+ def delay(seconds = 1)
+ record "setTimeout(function() {\n\n"
+ yield
+ record "}, #{(seconds * 1000).to_i})"
+ end
+
+ # Starts a script.aculo.us visual effect. See
+ # ActionView::Helpers::ScriptaculousHelper for more information.
+ def visual_effect(name, id = nil, options = {})
+ record @context.send(:visual_effect, name, id, options)
+ end
+
+ # Creates a script.aculo.us sortable element. Useful
+ # to recreate sortable elements after items get added
+ # or deleted.
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
+ def sortable(id, options = {})
+ record @context.send(:sortable_element_js, id, options)
+ end
+
+ # Creates a script.aculo.us draggable element.
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
+ def draggable(id, options = {})
+ record @context.send(:draggable_element_js, id, options)
+ end
+
+ # Creates a script.aculo.us drop receiving element.
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
+ def drop_receiving(id, options = {})
+ record @context.send(:drop_receiving_element_js, id, options)
+ end
+
+ private
+ def loop_on_multiple_args(method, ids)
+ record(ids.size>1 ?
+ "#{javascript_object_for(ids)}.each(#{method})" :
+ "#{method}(#{ids.first.to_json})")
+ end
+
+ def page
+ self
+ end
+
+ def record(line)
+ returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do
+ self << line
+ end
+ end
+
+ def render(*options_for_render)
+ old_format = @context && @context.template_format
+ @context.template_format = :html if @context
+ Hash === options_for_render.first ?
+ @context.render(*options_for_render) :
+ options_for_render.first.to_s
+ ensure
+ @context.template_format = old_format if @context
+ end
+
+ def javascript_object_for(object)
+ object.respond_to?(:to_json) ? object.to_json : object.inspect
+ end
+
+ def arguments_for_call(arguments, block = nil)
+ arguments << block_to_function(block) if block
+ arguments.map { |argument| javascript_object_for(argument) }.join ', '
+ end
+
+ def block_to_function(block)
+ generator = self.class.new(@context, &block)
+ literal("function() { #{generator.to_s} }")
+ end
+
+ def method_missing(method, *arguments)
+ JavaScriptProxy.new(self, method.to_s.camelize)
+ end
+ end
+ end
+
+ # Yields a JavaScriptGenerator and returns the generated JavaScript code.
+ # Use this to update multiple elements on a page in an Ajax response.
+ # See JavaScriptGenerator for more information.
+ #
+ # Example:
+ #
+ # update_page do |page|
+ # page.hide 'spinner'
+ # end
+ def update_page(&block)
+ JavaScriptGenerator.new(@template, &block).to_s
+ end
+
+ # Works like update_page but wraps the generated JavaScript in a <script>
+ # tag. Use this to include generated JavaScript in an ERb template.
+ # See JavaScriptGenerator for more information.
+ #
+ # +html_options+ may be a hash of <script> attributes to be passed
+ # to ActionView::Helpers::JavaScriptHelper#javascript_tag.
+ def update_page_tag(html_options = {}, &block)
+ javascript_tag update_page(&block), html_options
+ end
+
+ protected
+ def options_for_ajax(options)
+ js_options = build_callbacks(options)
+
+ js_options['asynchronous'] = options[:type] != :synchronous
+ js_options['method'] = method_option_to_s(options[:method]) if options[:method]
+ js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position]
+ js_options['evalScripts'] = options[:script].nil? || options[:script]
+
+ if options[:form]
+ js_options['parameters'] = 'Form.serialize(this)'
+ elsif options[:submit]
+ js_options['parameters'] = "Form.serialize('#{options[:submit]}')"
+ elsif options[:with]
+ js_options['parameters'] = options[:with]
+ end
+
+ if protect_against_forgery?
+ if js_options['parameters']
+ js_options['parameters'] << " + '&"
+ else
+ js_options['parameters'] = "'"
+ end
+ js_options['parameters'] << "#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}')"
+ end
+
+ options_for_javascript(js_options)
+ end
+
+ def method_option_to_s(method)
+ (method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'"
+ end
+
+ def build_observer(klass, name, options = {})
+ if options[:with] && (options[:with] !~ /[\{=(.]/)
+ options[:with] = "'#{options[:with]}=' + value"
+ else
+ options[:with] ||= 'value' unless options[:function]
+ end
+
+ callback = options[:function] || remote_function(options)
+ javascript = "new #{klass}('#{name}', "
+ javascript << "#{options[:frequency]}, " if options[:frequency]
+ javascript << "function(element, value) {"
+ javascript << "#{callback}}"
+ javascript << ", '#{options[:on]}'" if options[:on]
+ javascript << ")"
+ javascript_tag(javascript)
+ end
+
+ def build_callbacks(options)
+ callbacks = {}
+ options.each do |callback, code|
+ if CALLBACKS.include?(callback)
+ name = 'on' + callback.to_s.capitalize
+ callbacks[name] = "function(request){#{code}}"
+ end
+ end
+ callbacks
+ end
+ end
+
+ # Converts chained method calls on DOM proxy elements into JavaScript chains
+ class JavaScriptProxy < BasicObject #:nodoc:
+ def initialize(generator, root = nil)
+ @generator = generator
+ @generator << root if root
+ end
+
+ private
+ def method_missing(method, *arguments, &block)
+ if method.to_s =~ /(.*)=$/
+ assign($1, arguments.first)
+ else
+ call("#{method.to_s.camelize(:lower)}", *arguments, &block)
+ end
+ end
+
+ def call(function, *arguments, &block)
+ append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
+ self
+ end
+
+ def assign(variable, value)
+ append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}")
+ end
+
+ def function_chain
+ @function_chain ||= @generator.instance_variable_get(:@lines)
+ end
+
+ def append_to_function_chain!(call)
+ function_chain[-1].chomp!(';')
+ function_chain[-1] += ".#{call};"
+ end
+ end
+
+ class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
+ def initialize(generator, id)
+ @id = id
+ super(generator, "$(#{id.to_json})")
+ end
+
+ # Allows access of element attributes through +attribute+. Examples:
+ #
+ # page['foo']['style'] # => $('foo').style;
+ # page['foo']['style']['color'] # => $('blank_slate').style.color;
+ # page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
+ # page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
+ def [](attribute)
+ append_to_function_chain!(attribute)
+ self
+ end
+
+ def []=(variable, value)
+ assign(variable, value)
+ end
+
+ def replace_html(*options_for_render)
+ call 'update', @generator.send(:render, *options_for_render)
+ end
+
+ def replace(*options_for_render)
+ call 'replace', @generator.send(:render, *options_for_render)
+ end
+
+ def reload(options_for_replace = {})
+ replace(options_for_replace.merge({ :partial => @id.to_s }))
+ end
+
+ end
+
+ class JavaScriptVariableProxy < JavaScriptProxy #:nodoc:
+ def initialize(generator, variable)
+ @variable = variable
+ @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks
+ super(generator)
+ end
+
+ # The JSON Encoder calls this to check for the #to_json method
+ # Since it's a blank slate object, I suppose it responds to anything.
+ def respond_to?(method)
+ true
+ end
+
+ def to_json(options = nil)
+ @variable
+ end
+
+ private
+ def append_to_function_chain!(call)
+ @generator << @variable if @empty
+ @empty = false
+ super
+ end
+ end
+
+ class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc:
+ ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :find_all, :select, :max, :min, :partition, :reject, :sort_by, :in_groups_of, :each_slice] unless defined? ENUMERABLE_METHODS_WITH_RETURN
+ ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] unless defined? ENUMERABLE_METHODS
+ attr_reader :generator
+ delegate :arguments_for_call, :to => :generator
+
+ def initialize(generator, pattern)
+ super(generator, @pattern = pattern)
+ end
+
+ def each_slice(variable, number, &block)
+ if block
+ enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
+ else
+ add_variable_assignment!(variable)
+ append_enumerable_function!("eachSlice(#{number.to_json});")
+ end
+ end
+
+ def grep(variable, pattern, &block)
+ enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block
+ end
+
+ def in_groups_of(variable, number, fill_with = nil)
+ arguments = [number]
+ arguments << fill_with unless fill_with.nil?
+ add_variable_assignment!(variable)
+ append_enumerable_function!("inGroupsOf(#{arguments_for_call arguments});")
+ end
+
+ def inject(variable, memo, &block)
+ enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block
+ end
+
+ def pluck(variable, property)
+ add_variable_assignment!(variable)
+ append_enumerable_function!("pluck(#{property.to_json});")
+ end
+
+ def zip(variable, *arguments, &block)
+ add_variable_assignment!(variable)
+ append_enumerable_function!("zip(#{arguments_for_call arguments}")
+ if block
+ function_chain[-1] += ", function(array) {"
+ yield ActiveSupport::JSON::Variable.new('array')
+ add_return_statement!
+ @generator << '});'
+ else
+ function_chain[-1] += ');'
+ end
+ end
+
+ private
+ def method_missing(method, *arguments, &block)
+ if ENUMERABLE_METHODS.include?(method)
+ returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method)
+ variable = arguments.first if returnable
+ enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block)
+ else
+ super
+ end
+ end
+
+ # Options
+ # * variable - name of the variable to set the result of the enumeration to
+ # * method_args - array of the javascript enumeration method args that occur before the function
+ # * yield_args - array of the javascript yield args
+ # * return - true if the enumeration should return the last statement
+ def enumerate(enumerable, options = {}, &block)
+ options[:method_args] ||= []
+ options[:yield_args] ||= []
+ yield_args = options[:yield_args] * ', '
+ method_args = arguments_for_call options[:method_args] # foo, bar, function
+ method_args << ', ' unless method_args.blank?
+ add_variable_assignment!(options[:variable]) if options[:variable]
+ append_enumerable_function!("#{enumerable.to_s.camelize(:lower)}(#{method_args}function(#{yield_args}) {")
+ # only yield as many params as were passed in the block
+ yield(*options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1])
+ add_return_statement! if options[:return]
+ @generator << '});'
+ end
+
+ def add_variable_assignment!(variable)
+ function_chain.push("var #{variable} = #{function_chain.pop}")
+ end
+
+ def add_return_statement!
+ unless function_chain.last =~ /return/
+ function_chain.push("return #{function_chain.pop.chomp(';')};")
+ end
+ end
+
+ def append_enumerable_function!(call)
+ function_chain[-1].chomp!(';')
+ function_chain[-1] += ".#{call}"
+ end
+ end
+
+ class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
+ def initialize(generator, pattern)
+ super(generator, "$$(#{pattern.to_json})")
+ end
+ end
+ end
+end
+
+require 'action_view/helpers/javascript_helper'
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_identification_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_identification_helper.rb
new file mode 100644
index 000000000..6c235bff3
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_identification_helper.rb
@@ -0,0 +1,20 @@
+module ActionView
+ module Helpers
+ module RecordIdentificationHelper
+ # See ActionController::RecordIdentifier.partial_path -- this is just a delegate to that for convenient access in the view.
+ def partial_path(*args, &block)
+ ActionController::RecordIdentifier.partial_path(*args, &block)
+ end
+
+ # See ActionController::RecordIdentifier.dom_class -- this is just a delegate to that for convenient access in the view.
+ def dom_class(*args, &block)
+ ActionController::RecordIdentifier.dom_class(*args, &block)
+ end
+
+ # See ActionController::RecordIdentifier.dom_id -- this is just a delegate to that for convenient access in the view.
+ def dom_id(*args, &block)
+ ActionController::RecordIdentifier.dom_id(*args, &block)
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_tag_helper.rb
new file mode 100644
index 000000000..7b82c38a0
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_tag_helper.rb
@@ -0,0 +1,59 @@
+module ActionView
+ module Helpers
+ module RecordTagHelper
+ # Produces a wrapper DIV element with id and class parameters that
+ # relate to the specified ActiveRecord object. Usage example:
+ #
+ # <% div_for(@person, :class => "foo") do %>
+ # <%=h @person.name %>
+ # <% end %>
+ #
+ # produces:
+ #
+ # <div id="person_123" class="person foo"> Joe Bloggs </div>
+ #
+ def div_for(record, *args, &block)
+ content_tag_for(:div, record, *args, &block)
+ end
+
+ # content_tag_for creates an HTML element with id and class parameters
+ # that relate to the specified ActiveRecord object. For example:
+ #
+ # <% content_tag_for(:tr, @person) do %>
+ # <td><%=h @person.first_name %></td>
+ # <td><%=h @person.last_name %></td>
+ # <% end %>
+ #
+ # would produce hthe following HTML (assuming @person is an instance of
+ # a Person object, with an id value of 123):
+ #
+ # <tr id="person_123" class="person">....</tr>
+ #
+ # If you require the HTML id attribute to have a prefix, you can specify it:
+ #
+ # <% content_tag_for(:tr, @person, :foo) do %> ...
+ #
+ # produces:
+ #
+ # <tr id="foo_person_123" class="person">...
+ #
+ # content_tag_for also accepts a hash of options, which will be converted to
+ # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined
+ # with the default class name for your object. For example:
+ #
+ # <% content_tag_for(:li, @person, :class => "bar") %>...
+ #
+ # produces:
+ #
+ # <li id="person_123" class="person bar">...
+ #
+ def content_tag_for(tag_name, record, *args, &block)
+ prefix = args.first.is_a?(Hash) ? nil : args.shift
+ options = args.first.is_a?(Hash) ? args.shift : {}
+ concat content_tag(tag_name, capture(&block),
+ options.merge({ :class => "#{dom_class(record)} #{options[:class]}".strip, :id => dom_id(record, prefix) })),
+ block.binding
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/sanitize_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/sanitize_helper.rb
new file mode 100644
index 000000000..47fbe3a27
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/sanitize_helper.rb
@@ -0,0 +1,223 @@
+require 'action_view/helpers/tag_helper'
+require 'html/document'
+
+module ActionView
+ module Helpers #:nodoc:
+ # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
+ # These helper methods extend ActionView making them callable within your template files.
+ module SanitizeHelper
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ # This #sanitize helper will html encode all tags and strip all attributes that aren't specifically allowed.
+ # It also strips href/src tags with invalid protocols, like javascript: especially. It does its best to counter any
+ # tricks that hackers may use, like throwing in unicode/ascii/hex values to get past the javascript: filters. Check out
+ # the extensive test suite.
+ #
+ # <%= sanitize @article.body %>
+ #
+ # You can add or remove tags/attributes if you want to customize it a bit. See ActionView::Base for full docs on the
+ # available options. You can add tags/attributes for single uses of #sanitize by passing either the :attributes or :tags options:
+ #
+ # Normal Use
+ #
+ # <%= sanitize @article.body %>
+ #
+ # Custom Use (only the mentioned tags and attributes are allowed, nothing else)
+ #
+ # <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style)
+ #
+ # Add table tags to the default allowed tags
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+ # end
+ #
+ # Remove tags to the default allowed tags
+ #
+ # Rails::Initializer.run do |config|
+ # config.after_initialize do
+ # ActionView::Base.sanitized_allowed_tags.delete 'div'
+ # end
+ # end
+ #
+ # Change allowed default attributes
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style'
+ # end
+ #
+ def sanitize(html, options = {})
+ self.class.white_list_sanitizer.sanitize(html, options)
+ end
+
+ # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
+ def sanitize_css(style)
+ self.class.white_list_sanitizer.sanitize_css(style)
+ end
+
+ # Strips all HTML tags from the +html+, including comments. This uses the
+ # html-scanner tokenizer and so its HTML parsing ability is limited by
+ # that of html-scanner.
+ #
+ # ==== Examples
+ #
+ # strip_tags("Strip <i>these</i> tags!")
+ # # => Strip these tags!
+ #
+ # strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
+ # # => Bold no more! See more here...
+ #
+ # strip_tags("<div id='top-bar'>Welcome to my website!</div>")
+ # # => Welcome to my website!
+ def strip_tags(html)
+ self.class.full_sanitizer.sanitize(html)
+ end
+
+ # Strips all link tags from +text+ leaving just the link text.
+ #
+ # ==== Examples
+ # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
+ # # => Ruby on Rails
+ #
+ # strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.')
+ # # => Please e-mail me at me@email.com.
+ #
+ # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
+ # # => Blog: Visit
+ def strip_links(html)
+ self.class.link_sanitizer.sanitize(html)
+ end
+
+ module ClassMethods #:nodoc:
+ def self.extended(base)
+ class << base
+ attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
+
+ # we want these to be class methods on ActionView::Base, they'll get mattr_readers for these below.
+ helper_def = [:sanitized_protocol_separator, :sanitized_uri_attributes, :sanitized_bad_tags, :sanitized_allowed_tags,
+ :sanitized_allowed_attributes, :sanitized_allowed_css_properties, :sanitized_allowed_css_keywords,
+ :sanitized_shorthand_css_properties, :sanitized_allowed_protocols, :sanitized_protocol_separator=].collect! do |prop|
+ prop = prop.to_s
+ "def #{prop}(#{:value if prop =~ /=$/}) white_list_sanitizer.#{prop.sub /sanitized_/, ''} #{:value if prop =~ /=$/} end"
+ end.join("\n")
+ eval helper_def
+ end
+ end
+
+ # Gets the HTML::FullSanitizer instance used by strip_tags. Replace with
+ # any object that responds to #sanitize
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.full_sanitizer = MySpecialSanitizer.new
+ # end
+ #
+ def full_sanitizer
+ @full_sanitizer ||= HTML::FullSanitizer.new
+ end
+
+ # Gets the HTML::LinkSanitizer instance used by strip_links. Replace with
+ # any object that responds to #sanitize
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.link_sanitizer = MySpecialSanitizer.new
+ # end
+ #
+ def link_sanitizer
+ @link_sanitizer ||= HTML::LinkSanitizer.new
+ end
+
+ # Gets the HTML::WhiteListSanitizer instance used by sanitize and sanitize_css.
+ # Replace with any object that responds to #sanitize
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.white_list_sanitizer = MySpecialSanitizer.new
+ # end
+ #
+ def white_list_sanitizer
+ @white_list_sanitizer ||= HTML::WhiteListSanitizer.new
+ end
+
+ # Adds valid HTML attributes that the #sanitize helper checks for URIs.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target'
+ # end
+ #
+ def sanitized_uri_attributes=(attributes)
+ HTML::WhiteListSanitizer.uri_attributes.merge(attributes)
+ end
+
+ # Adds to the Set of 'bad' tags for the #sanitize helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_bad_tags = 'embed', 'object'
+ # end
+ #
+ def sanitized_bad_tags=(attributes)
+ HTML::WhiteListSanitizer.bad_tags.merge(attributes)
+ end
+ # Adds to the Set of allowed tags for the #sanitize helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+ # end
+ #
+ def sanitized_allowed_tags=(attributes)
+ HTML::WhiteListSanitizer.allowed_tags.merge(attributes)
+ end
+
+ # Adds to the Set of allowed html attributes for the #sanitize helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc'
+ # end
+ #
+ def sanitized_allowed_attributes=(attributes)
+ HTML::WhiteListSanitizer.allowed_attributes.merge(attributes)
+ end
+
+ # Adds to the Set of allowed css properties for the #sanitize and #sanitize_css heleprs.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_css_properties = 'expression'
+ # end
+ #
+ def sanitized_allowed_css_properties=(attributes)
+ HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes)
+ end
+
+ # Adds to the Set of allowed css keywords for the #sanitize and #sanitize_css helpers.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_css_keywords = 'expression'
+ # end
+ #
+ def sanitized_allowed_css_keywords=(attributes)
+ HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes)
+ end
+
+ # Adds to the Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_shorthand_css_properties = 'expression'
+ # end
+ #
+ def sanitized_shorthand_css_properties=(attributes)
+ HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes)
+ end
+
+ # Adds to the Set of allowed protocols for the #sanitize helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed'
+ # end
+ #
+ def sanitized_allowed_protocols=(attributes)
+ HTML::WhiteListSanitizer.allowed_protocols.merge(attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/scriptaculous_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/scriptaculous_helper.rb
new file mode 100644
index 000000000..f7a9daf60
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/scriptaculous_helper.rb
@@ -0,0 +1,199 @@
+require 'action_view/helpers/javascript_helper'
+
+module ActionView
+ module Helpers
+ # Provides a set of helpers for calling Scriptaculous JavaScript
+ # functions, including those which create Ajax controls and visual effects.
+ #
+ # To be able to use these helpers, you must include the Prototype
+ # JavaScript framework and the Scriptaculous JavaScript library in your
+ # pages. See the documentation for ActionView::Helpers::JavaScriptHelper
+ # for more information on including the necessary JavaScript.
+ #
+ # The Scriptaculous helpers' behavior can be tweaked with various options.
+ # See the documentation at http://script.aculo.us for more information on
+ # using these helpers in your application.
+ module ScriptaculousHelper
+ unless const_defined? :TOGGLE_EFFECTS
+ TOGGLE_EFFECTS = [:toggle_appear, :toggle_slide, :toggle_blind]
+ end
+
+ # Returns a JavaScript snippet to be used on the Ajax callbacks for
+ # starting visual effects.
+ #
+ # Example:
+ # <%= link_to_remote "Reload", :update => "posts",
+ # :url => { :action => "reload" },
+ # :complete => visual_effect(:highlight, "posts", :duration => 0.5)
+ #
+ # If no element_id is given, it assumes "element" which should be a local
+ # variable in the generated JavaScript execution context. This can be
+ # used for example with drop_receiving_element:
+ #
+ # <%= drop_receiving_element (...), :loading => visual_effect(:fade) %>
+ #
+ # This would fade the element that was dropped on the drop receiving
+ # element.
+ #
+ # For toggling visual effects, you can use :toggle_appear, :toggle_slide, and
+ # :toggle_blind which will alternate between appear/fade, slidedown/slideup, and
+ # blinddown/blindup respectively.
+ #
+ # You can change the behaviour with various options, see
+ # http://script.aculo.us for more documentation.
+ def visual_effect(name, element_id = false, js_options = {})
+ element = element_id ? element_id.to_json : "element"
+
+ js_options[:queue] = if js_options[:queue].is_a?(Hash)
+ '{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}'
+ elsif js_options[:queue]
+ "'#{js_options[:queue]}'"
+ end if js_options[:queue]
+
+ [:endcolor, :direction, :startcolor, :scaleMode, :restorecolor].each do |option|
+ js_options[option] = "'#{js_options[option]}'" if js_options[option]
+ end
+
+ if TOGGLE_EFFECTS.include? name.to_sym
+ "Effect.toggle(#{element},'#{name.to_s.gsub(/^toggle_/,'')}',#{options_for_javascript(js_options)});"
+ else
+ "new Effect.#{name.to_s.camelize}(#{element},#{options_for_javascript(js_options)});"
+ end
+ end
+
+ # Makes the element with the DOM ID specified by +element_id+ sortable
+ # by drag-and-drop and make an Ajax call whenever the sort order has
+ # changed. By default, the action called gets the serialized sortable
+ # element as parameters.
+ #
+ # Example:
+ # <%= sortable_element("my_list", :url => { :action => "order" }) %>
+ #
+ # In the example, the action gets a "my_list" array parameter
+ # containing the values of the ids of elements the sortable consists
+ # of, in the current order.
+ #
+ # Important: For this to work, the sortable elements must have id
+ # attributes in the form "string_identifier". For example, "item_1". Only
+ # the identifier part of the id attribute will be serialized.
+ #
+ # Additional +options+ are:
+ #
+ # <tt>:format</tt>:: A regular expression to determine what to send
+ # as the serialized id to the server (the default
+ # is <tt>/^[^_]*_(.*)$/</tt>).
+ #
+ # <tt>:constraint</tt>:: Whether to constrain the dragging to either <tt>:horizontal</tt>
+ # or <tt>:vertical</tt> (or false to make it unconstrained).
+ #
+ # <tt>:overlap</tt>:: Calculate the item overlap in the <tt>:horizontal</tt> or
+ # <tt>:vertical</tt> direction.
+ #
+ # <tt>:tag</tt>:: Which children of the container element to treat as
+ # sortable (default is <tt>li</tt>).
+ #
+ # <tt>:containment</tt>:: Takes an element or array of elements to treat as
+ # potential drop targets (defaults to the original
+ # target element).
+ #
+ # <tt>:only</tt>:: A CSS class name or arry of class names used to filter
+ # out child elements as candidates.
+ #
+ # <tt>:scroll</tt>:: Determines whether to scroll the list during drag
+ # operationsif the list runs past the visual border.
+ #
+ # <tt>:tree</tt>:: Determines whether to treat nested lists as part of the
+ # main sortable list. This means that you can create multi-
+ # layer lists, and not only sort items at the same level,
+ # but drag and sort items between levels.
+ #
+ # <tt>:hoverclass</tt>:: If set, the Droppable will have this additional CSS class
+ # when an accepted Draggable is hovered over it.
+ #
+ # <tt>:handle</tt>:: Sets whether the element should only be draggable by an
+ # embedded handle. The value may be a string referencing a
+ # CSS class value (as of script.aculo.us V1.5). The first
+ # child/grandchild/etc. element found within the element
+ # that has this CSS class value will be used as the handle.
+ #
+ # <tt>:ghosting</tt>:: Clones the element and drags the clone, leaving the original
+ # in place until the clone is dropped (defaut is <tt>false</tt>).
+ #
+ # <tt>:dropOnEmpty</tt>:: If set to true, the Sortable container will be made into
+ # a Droppable, that can receive a Draggable (as according to
+ # the containment rules) as a child element when there are no
+ # more elements inside (defaut is <tt>false</tt>).
+ #
+ # <tt>:onChange</tt>:: Called whenever the sort order changes while dragging. When
+ # dragging from one Sortable to another, the callback is
+ # called once on each Sortable. Gets the affected element as
+ # its parameter.
+ #
+ # <tt>:onUpdate</tt>:: Called when the drag ends and the Sortable's order is
+ # changed in any way. When dragging from one Sortable to
+ # another, the callback is called once on each Sortable. Gets
+ # the container as its parameter.
+ #
+ # See http://script.aculo.us for more documentation.
+ def sortable_element(element_id, options = {})
+ javascript_tag(sortable_element_js(element_id, options).chop!)
+ end
+
+ def sortable_element_js(element_id, options = {}) #:nodoc:
+ options[:with] ||= "Sortable.serialize(#{element_id.to_json})"
+ options[:onUpdate] ||= "function(){" + remote_function(options) + "}"
+ options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) }
+
+ [:tag, :overlap, :constraint, :handle].each do |option|
+ options[option] = "'#{options[option]}'" if options[option]
+ end
+
+ options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment]
+ options[:only] = array_or_string_for_javascript(options[:only]) if options[:only]
+
+ %(Sortable.create(#{element_id.to_json}, #{options_for_javascript(options)});)
+ end
+
+ # Makes the element with the DOM ID specified by +element_id+ draggable.
+ #
+ # Example:
+ # <%= draggable_element("my_image", :revert => true)
+ #
+ # You can change the behaviour with various options, see
+ # http://script.aculo.us for more documentation.
+ def draggable_element(element_id, options = {})
+ javascript_tag(draggable_element_js(element_id, options).chop!)
+ end
+
+ def draggable_element_js(element_id, options = {}) #:nodoc:
+ %(new Draggable(#{element_id.to_json}, #{options_for_javascript(options)});)
+ end
+
+ # Makes the element with the DOM ID specified by +element_id+ receive
+ # dropped draggable elements (created by draggable_element).
+ # and make an AJAX call By default, the action called gets the DOM ID
+ # of the element as parameter.
+ #
+ # Example:
+ # <%= drop_receiving_element("my_cart", :url =>
+ # { :controller => "cart", :action => "add" }) %>
+ #
+ # You can change the behaviour with various options, see
+ # http://script.aculo.us for more documentation.
+ def drop_receiving_element(element_id, options = {})
+ javascript_tag(drop_receiving_element_js(element_id, options).chop!)
+ end
+
+ def drop_receiving_element_js(element_id, options = {}) #:nodoc:
+ options[:with] ||= "'id=' + encodeURIComponent(element.id)"
+ options[:onDrop] ||= "function(element){" + remote_function(options) + "}"
+ options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) }
+
+ options[:accept] = array_or_string_for_javascript(options[:accept]) if options[:accept]
+ options[:hoverclass] = "'#{options[:hoverclass]}'" if options[:hoverclass]
+
+ %(Droppables.add(#{element_id.to_json}, #{options_for_javascript(options)});)
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/tag_helper.rb
new file mode 100644
index 000000000..999cbfb52
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/tag_helper.rb
@@ -0,0 +1,133 @@
+require 'cgi'
+require 'erb'
+
+module ActionView
+ module Helpers #:nodoc:
+ # Provides methods to generate HTML tags programmatically when you can't use
+ # a Builder. By default, they output XHTML compliant tags.
+ module TagHelper
+ include ERB::Util
+
+ BOOLEAN_ATTRIBUTES = Set.new(%w(disabled readonly multiple))
+
+ # Returns an empty HTML tag of type +name+ which by default is XHTML
+ # compliant. Set +open+ to true to create an open tag compatible
+ # with HTML 4.0 and below. Add HTML attributes by passing an attributes
+ # hash to +options+. Set +escape+ to false to disable attribute value
+ # escaping.
+ #
+ # ==== Options
+ # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and
+ # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
+ # symbols or strings for the attribute names.
+ #
+ # ==== Examples
+ # tag("br")
+ # # => <br />
+ #
+ # tag("br", nil, true)
+ # # => <br>
+ #
+ # tag("input", { :type => 'text', :disabled => true })
+ # # => <input type="text" disabled="disabled" />
+ #
+ # tag("img", { :src => "open & shut.png" })
+ # # => <img src="open &amp; shut.png" />
+ #
+ # tag("img", { :src => "open &amp; shut.png" }, false, false)
+ # # => <img src="open &amp; shut.png" />
+ def tag(name, options = nil, open = false, escape = true)
+ "<#{name}#{tag_options(options, escape) if options}" + (open ? ">" : " />")
+ end
+
+ # Returns an HTML block tag of type +name+ surrounding the +content+. Add
+ # HTML attributes by passing an attributes hash to +options+.
+ # Instead of passing the content as an argument, you can also use a block
+ # in which case, you pass your +options+ as the second parameter.
+ # Set escape to false to disable attribute value escaping.
+ #
+ # ==== Options
+ # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and
+ # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
+ # symbols or strings for the attribute names.
+ #
+ # ==== Examples
+ # content_tag(:p, "Hello world!")
+ # # => <p>Hello world!</p>
+ # content_tag(:div, content_tag(:p, "Hello world!"), :class => "strong")
+ # # => <div class="strong"><p>Hello world!</p></div>
+ # content_tag("select", options, :multiple => true)
+ # # => <select multiple="multiple">...options...</select>
+ #
+ # <% content_tag :div, :class => "strong" do -%>
+ # Hello world!
+ # <% end -%>
+ # # => <div class="strong"><p>Hello world!</p></div>
+ def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
+ if block_given?
+ options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
+ content = capture(&block)
+ content_tag = content_tag_string(name, content, options, escape)
+ block_is_within_action_view?(block) ? concat(content_tag, block.binding) : content_tag
+ else
+ content = content_or_options_with_block
+ content_tag_string(name, content, options, escape)
+ end
+ end
+
+ # Returns a CDATA section with the given +content+. CDATA sections
+ # are used to escape blocks of text containing characters which would
+ # otherwise be recognized as markup. CDATA sections begin with the string
+ # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
+ #
+ # ==== Examples
+ # cdata_section("<hello world>")
+ # # => <![CDATA[<hello world>]]>
+ #
+ # cdata_section(File.read("hello_world.txt"))
+ # # => <![CDATA[<hello from a text file]]>
+ def cdata_section(content)
+ "<![CDATA[#{content}]]>"
+ end
+
+ # Returns an escaped version of +html+ without affecting existing escaped entities.
+ #
+ # ==== Examples
+ # escape_once("1 > 2 &amp; 3")
+ # # => "1 &lt; 2 &amp; 3"
+ #
+ # escape_once("&lt;&lt; Accept & Checkout")
+ # # => "&lt;&lt; Accept &amp; Checkout"
+ def escape_once(html)
+ html.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] }
+ end
+
+ private
+ def content_tag_string(name, content, options, escape = true)
+ tag_options = tag_options(options, escape) if options
+ "<#{name}#{tag_options}>#{content}</#{name}>"
+ end
+
+ def tag_options(options, escape = true)
+ unless options.blank?
+ attrs = []
+ if escape
+ options.each do |key, value|
+ next unless value
+ key = key.to_s
+ value = BOOLEAN_ATTRIBUTES.include?(key) ? key : escape_once(value)
+ attrs << %(#{key}="#{value}")
+ end
+ else
+ attrs = options.map { |key, value| %(#{key}="#{value}") }
+ end
+ " #{attrs.sort * ' '}" unless attrs.empty?
+ end
+ end
+
+ def block_is_within_action_view?(block)
+ eval("defined? _erbout", block.binding)
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/text_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/text_helper.rb
new file mode 100644
index 000000000..e5747f28f
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/text_helper.rb
@@ -0,0 +1,479 @@
+require 'action_view/helpers/tag_helper'
+require 'html/document'
+
+module ActionView
+ module Helpers #:nodoc:
+ # The TextHelper module provides a set of methods for filtering, formatting
+ # and transforming strings, which can reduce the amount of inline Ruby code in
+ # your views. These helper methods extend ActionView making them callable
+ # within your template files.
+ module TextHelper
+ # The preferred method of outputting text in your views is to use the
+ # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
+ # do not operate as expected in an eRuby code block. If you absolutely must
+ # output text within a non-output code block (i.e., <% %>), you can use the concat method.
+ #
+ # ==== Examples
+ # <%
+ # concat "hello", binding
+ # # is the equivalent of <%= "hello" %>
+ #
+ # if (logged_in == true):
+ # concat "Logged in!", binding
+ # else
+ # concat link_to('login', :action => login), binding
+ # end
+ # # will either display "Logged in!" or a login link
+ # %>
+ def concat(string, binding)
+ eval(ActionView::Base.erb_variable, binding) << string
+ end
+
+ # If +text+ is longer than +length+, +text+ will be truncated to the length of
+ # +length+ (defaults to 30) and the last characters will be replaced with the +truncate_string+
+ # (defaults to "...").
+ #
+ # ==== Examples
+ # truncate("Once upon a time in a world far far away", 14)
+ # # => Once upon a...
+ #
+ # truncate("Once upon a time in a world far far away")
+ # # => Once upon a time in a world f...
+ #
+ # truncate("And they found that many people were sleeping better.", 25, "(clipped)")
+ # # => And they found that many (clipped)
+ #
+ # truncate("And they found that many people were sleeping better.", 15, "... (continued)")
+ # # => And they found... (continued)
+ def truncate(text, length = 30, truncate_string = "...")
+ if text.nil? then return end
+ l = length - truncate_string.chars.length
+ (text.chars.length > length ? text.chars[0...l] + truncate_string : text).to_s
+ end
+
+ # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
+ # a +highlighter+ string. The highlighter can be specialized by passing +highlighter+
+ # as a single-quoted string with \1 where the phrase is to be inserted (defaults to
+ # '<strong class="highlight">\1</strong>')
+ #
+ # ==== Examples
+ # highlight('You searched for: rails', 'rails')
+ # # => You searched for: <strong class="highlight">rails</strong>
+ #
+ # highlight('You searched for: ruby, rails, dhh', 'actionpack')
+ # # => You searched for: ruby, rails, dhh
+ #
+ # highlight('You searched for: rails', ['for', 'rails'], '<em>\1</em>')
+ # # => You searched <em>for</em>: <em>rails</em>
+ #
+ # highlight('You searched for: rails', 'rails', "<a href='search?q=\1'>\1</a>")
+ # # => You searched for: <a href='search?q=rails>rails</a>
+ def highlight(text, phrases, highlighter = '<strong class="highlight">\1</strong>')
+ if text.blank? || phrases.blank?
+ text
+ else
+ match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
+ text.gsub(/(#{match})/i, highlighter)
+ end
+ end
+
+ # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
+ # The +radius+ expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
+ # defined in +radius+ (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
+ # then the +excerpt_string+ will be prepended/appended accordingly. If the +phrase+
+ # isn't found, nil is returned.
+ #
+ # ==== Examples
+ # excerpt('This is an example', 'an', 5)
+ # # => "...s is an examp..."
+ #
+ # excerpt('This is an example', 'is', 5)
+ # # => "This is an..."
+ #
+ # excerpt('This is an example', 'is')
+ # # => "This is an example"
+ #
+ # excerpt('This next thing is an example', 'ex', 2)
+ # # => "...next t..."
+ #
+ # excerpt('This is also an example', 'an', 8, '<chop> ')
+ # # => "<chop> is also an example"
+ def excerpt(text, phrase, radius = 100, excerpt_string = "...")
+ if text.nil? || phrase.nil? then return end
+ phrase = Regexp.escape(phrase)
+
+ if found_pos = text.chars =~ /(#{phrase})/i
+ start_pos = [ found_pos - radius, 0 ].max
+ end_pos = [ found_pos + phrase.chars.length + radius, text.chars.length ].min
+
+ prefix = start_pos > 0 ? excerpt_string : ""
+ postfix = end_pos < text.chars.length ? excerpt_string : ""
+
+ prefix + text.chars[start_pos..end_pos].strip + postfix
+ else
+ nil
+ end
+ end
+
+ # Attempts to pluralize the +singular+ word unless +count+ is 1. If +plural+
+ # is supplied, it will use that when count is > 1, if the ActiveSupport Inflector
+ # is loaded, it will use the Inflector to determine the plural form, otherwise
+ # it will just add an 's' to the +singular+ word.
+ #
+ # ==== Examples
+ # pluralize(1, 'person')
+ # # => 1 person
+ #
+ # pluralize(2, 'person')
+ # # => 2 people
+ #
+ # pluralize(3, 'person', 'users')
+ # # => 3 users
+ #
+ # pluralize(0, 'person')
+ # # => 0 people
+ def pluralize(count, singular, plural = nil)
+ "#{count || 0} " + if count == 1 || count == '1'
+ singular
+ elsif plural
+ plural
+ elsif Object.const_defined?("Inflector")
+ Inflector.pluralize(singular)
+ else
+ singular + "s"
+ end
+ end
+
+ # Wraps the +text+ into lines no longer than +line_width+ width. This method
+ # breaks on the first whitespace character that does not exceed +line_width+
+ # (which is 80 by default).
+ #
+ # ==== Examples
+ # word_wrap('Once upon a time', 4)
+ # # => Once\nupon\na\ntime
+ #
+ # word_wrap('Once upon a time', 8)
+ # # => Once upon\na time
+ #
+ # word_wrap('Once upon a time')
+ # # => Once upon a time
+ #
+ # word_wrap('Once upon a time', 1)
+ # # => Once\nupon\na\ntime
+ def word_wrap(text, line_width = 80)
+ text.split("\n").collect do |line|
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
+ end * "\n"
+ end
+
+ begin
+ require_library_or_gem "redcloth" unless Object.const_defined?(:RedCloth)
+
+ # Returns the text with all the Textile[http://www.textism.com/tools/textile] codes turned into HTML tags.
+ #
+ # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
+ # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
+ # is available</i>.
+ #
+ # ==== Examples
+ # textilize("*This is Textile!* Rejoice!")
+ # # => "<p><strong>This is Textile!</strong> Rejoice!</p>"
+ #
+ # textilize("I _love_ ROR(Ruby on Rails)!")
+ # # => "<p>I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!</p>"
+ #
+ # textilize("h2. Textile makes markup -easy- simple!")
+ # # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
+ #
+ # textilize("Visit the Rails website "here":http://www.rubyonrails.org/.)
+ # # => "<p>Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>.</p>"
+ def textilize(text)
+ if text.blank?
+ ""
+ else
+ textilized = RedCloth.new(text, [ :hard_breaks ])
+ textilized.hard_breaks = true if textilized.respond_to?("hard_breaks=")
+ textilized.to_html
+ end
+ end
+
+ # Returns the text with all the Textile codes turned into HTML tags,
+ # but without the bounding <p> tag that RedCloth adds.
+ #
+ # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
+ # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
+ # is available</i>.
+ #
+ # ==== Examples
+ # textilize_without_paragraph("*This is Textile!* Rejoice!")
+ # # => "<strong>This is Textile!</strong> Rejoice!"
+ #
+ # textilize_without_paragraph("I _love_ ROR(Ruby on Rails)!")
+ # # => "I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!"
+ #
+ # textilize_without_paragraph("h2. Textile makes markup -easy- simple!")
+ # # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
+ #
+ # textilize_without_paragraph("Visit the Rails website "here":http://www.rubyonrails.org/.)
+ # # => "Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>."
+ def textilize_without_paragraph(text)
+ textiled = textilize(text)
+ if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
+ if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
+ return textiled
+ end
+ rescue LoadError
+ # We can't really help what's not there
+ end
+
+ begin
+ require_library_or_gem "bluecloth" unless Object.const_defined?(:BlueCloth)
+
+ # Returns the text with all the Markdown codes turned into HTML tags.
+ # <i>This method is only available if BlueCloth[http://www.deveiate.org/projects/BlueCloth]
+ # is available</i>.
+ #
+ # ==== Examples
+ # markdown("We are using __Markdown__ now!")
+ # # => "<p>We are using <strong>Markdown</strong> now!</p>"
+ #
+ # markdown("We like to _write_ `code`, not just _read_ it!")
+ # # => "<p>We like to <em>write</em> <code>code</code>, not just <em>read</em> it!</p>"
+ #
+ # markdown("The [Markdown website](http://daringfireball.net/projects/markdown/) has more information.")
+ # # => "<p>The <a href="http://daringfireball.net/projects/markdown/">Markdown website</a>
+ # # has more information.</p>"
+ #
+ # markdown('![The ROR logo](http://rubyonrails.com/images/rails.png "Ruby on Rails")')
+ # # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>'
+ def markdown(text)
+ text.blank? ? "" : BlueCloth.new(text).to_html
+ end
+ rescue LoadError
+ # We can't really help what's not there
+ end
+
+ # Returns +text+ transformed into HTML using simple formatting rules.
+ # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
+ # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
+ # considered as a linebreak and a <tt><br /></tt> tag is appended. This
+ # method does not remove the newlines from the +text+.
+ #
+ # ==== Examples
+ # my_text = """Here is some basic text...
+ # ...with a line break."""
+ #
+ # simple_format(my_text)
+ # # => "<p>Here is some basic text...<br />...with a line break.</p>"
+ #
+ # more_text = """We want to put a paragraph...
+ #
+ # ...right there."""
+ #
+ # simple_format(more_text)
+ # # => "<p>We want to put a paragraph...</p><p>...right there.</p>"
+ def simple_format(text)
+ content_tag 'p', text.to_s.
+ gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
+ gsub(/\n\n+/, "</p>\n\n<p>"). # 2+ newline -> paragraph
+ gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
+ end
+
+ # Turns all URLs and e-mail addresses into clickable links. The +link+ parameter
+ # will limit what should be linked. You can add HTML attributes to the links using
+ # +href_options+. Options for +link+ are <tt>:all</tt> (default),
+ # <tt>:email_addresses</tt>, and <tt>:urls</tt>. If a block is given, each URL and
+ # e-mail address is yielded and the result is used as the link text.
+ #
+ # ==== Examples
+ # auto_link("Go to http://www.rubyonrails.org and say hello to david@loudthinking.com")
+ # # => "Go to <a href=\"http://www.rubyonrails.org\">http://www.rubyonrails.org</a> and
+ # # say hello to <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
+ #
+ # auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :urls)
+ # # => "Visit <a href=\"http://www.loudthinking.com/\">http://www.loudthinking.com/</a>
+ # # or e-mail david@loudthinking.com"
+ #
+ # auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :email_addresses)
+ # # => "Visit http://www.loudthinking.com/ or e-mail <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
+ #
+ # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com."
+ # auto_link(post_body, :all, :target => '_blank') do |text|
+ # truncate(text, 15)
+ # end
+ # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>.
+ # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
+ #
+ def auto_link(text, link = :all, href_options = {}, &block)
+ return '' if text.blank?
+ case link
+ when :all then auto_link_email_addresses(auto_link_urls(text, href_options, &block), &block)
+ when :email_addresses then auto_link_email_addresses(text, &block)
+ when :urls then auto_link_urls(text, href_options, &block)
+ end
+ end
+
+ # Creates a Cycle object whose _to_s_ method cycles through elements of an
+ # array every time it is called. This can be used for example, to alternate
+ # classes for table rows. You can use named cycles to allow nesting in loops.
+ # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
+ # named cycle. You can manually reset a cycle by calling reset_cycle and passing the
+ # name of the cycle.
+ #
+ # ==== Examples
+ # # Alternate CSS classes for even and odd numbers...
+ # @items = [1,2,3,4]
+ # <table>
+ # <% @items.each do |item| %>
+ # <tr class="<%= cycle("even", "odd") -%>">
+ # <td>item</td>
+ # </tr>
+ # <% end %>
+ # </table>
+ #
+ #
+ # # Cycle CSS classes for rows, and text colors for values within each row
+ # @items = x = [{:first => 'Robert', :middle => 'Daniel', :last => 'James'},
+ # {:first => 'Emily', :middle => 'Shannon', :maiden => 'Pike', :last => 'Hicks'},
+ # {:first => 'June', :middle => 'Dae', :last => 'Jones'}]
+ # <% @items.each do |item| %>
+ # <tr class="<%= cycle("even", "odd", :name => "row_class") -%>">
+ # <td>
+ # <% item.values.each do |value| %>
+ # <%# Create a named cycle "colors" %>
+ # <span style="color:<%= cycle("red", "green", "blue", :name => "colors") -%>">
+ # <%= value %>
+ # </span>
+ # <% end %>
+ # <% reset_cycle("colors") %>
+ # </td>
+ # </tr>
+ # <% end %>
+ def cycle(first_value, *values)
+ if (values.last.instance_of? Hash)
+ params = values.pop
+ name = params[:name]
+ else
+ name = "default"
+ end
+ values.unshift(first_value)
+
+ cycle = get_cycle(name)
+ if (cycle.nil? || cycle.values != values)
+ cycle = set_cycle(name, Cycle.new(*values))
+ end
+ return cycle.to_s
+ end
+
+ # Resets a cycle so that it starts from the first element the next time
+ # it is called. Pass in +name+ to reset a named cycle.
+ #
+ # ==== Example
+ # # Alternate CSS classes for even and odd numbers...
+ # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
+ # <table>
+ # <% @items.each do |item| %>
+ # <tr class="<%= cycle("even", "odd") -%>">
+ # <% item.each do |value| %>
+ # <span style="color:<%= cycle("#333", "#666", "#999", :name => "colors") -%>">
+ # <%= value %>
+ # </span>
+ # <% end %>
+ #
+ # <% reset_cycle("colors") %>
+ # </tr>
+ # <% end %>
+ # </table>
+ def reset_cycle(name = "default")
+ cycle = get_cycle(name)
+ cycle.reset unless cycle.nil?
+ end
+
+ class Cycle #:nodoc:
+ attr_reader :values
+
+ def initialize(first_value, *values)
+ @values = values.unshift(first_value)
+ reset
+ end
+
+ def reset
+ @index = 0
+ end
+
+ def to_s
+ value = @values[@index].to_s
+ @index = (@index + 1) % @values.size
+ return value
+ end
+ end
+
+ private
+ # The cycle helpers need to store the cycles in a place that is
+ # guaranteed to be reset every time a page is rendered, so it
+ # uses an instance variable of ActionView::Base.
+ def get_cycle(name)
+ @_cycles = Hash.new unless defined?(@_cycles)
+ return @_cycles[name]
+ end
+
+ def set_cycle(name, cycle_object)
+ @_cycles = Hash.new unless defined?(@_cycles)
+ @_cycles[name] = cycle_object
+ end
+
+ AUTO_LINK_RE = %r{
+ ( # leading text
+ <\w+.*?>| # leading HTML tag, or
+ [^=!:'"/]| # leading punctuation, or
+ ^ # beginning of line
+ )
+ (
+ (?:https?://)| # protocol spec, or
+ (?:www\.) # www.*
+ )
+ (
+ [-\w]+ # subdomain or domain
+ (?:\.[-\w]+)* # remaining subdomains or domain
+ (?::\d+)? # port
+ (?:/(?:(?:[~\w\+@%-]|(?:[,.;:][^\s$]))+)?)* # path
+ (?:\?[\w\+@%&=.;-]+)? # query string
+ (?:\#[\w\-]*)? # trailing anchor
+ )
+ ([[:punct:]]|\s|<|$) # trailing text
+ }x unless const_defined?(:AUTO_LINK_RE)
+
+ # Turns all urls into clickable links. If a block is given, each url
+ # is yielded and the result is used as the link text.
+ def auto_link_urls(text, href_options = {})
+ extra_options = tag_options(href_options.stringify_keys) || ""
+ text.gsub(AUTO_LINK_RE) do
+ all, a, b, c, d = $&, $1, $2, $3, $4
+ if a =~ /<a\s/i # don't replace URL's that are already linked
+ all
+ else
+ text = b + c
+ text = yield(text) if block_given?
+ %(#{a}<a href="#{b=="www."?"http://www.":b}#{c}"#{extra_options}>#{text}</a>#{d})
+ end
+ end
+ end
+
+ # Turns all email addresses into clickable links. If a block is given,
+ # each email is yielded and the result is used as the link text.
+ def auto_link_email_addresses(text)
+ body = text.dup
+ text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
+ text = $1
+
+ if body.match(/<a\b[^>]*>(.*)(#{Regexp.escape(text)})(.*)<\/a>/)
+ text
+ else
+ display_text = (block_given?) ? yield(text) : text
+ %{<a href="mailto:#{text}">#{display_text}</a>}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/url_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/url_helper.rb
new file mode 100644
index 000000000..eb3391180
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/url_helper.rb
@@ -0,0 +1,524 @@
+require 'action_view/helpers/javascript_helper'
+
+module ActionView
+ module Helpers #:nodoc:
+ # Provides a set of methods for making links and getting URLs that
+ # depend on the routing subsystem (see ActionController::Routing).
+ # This allows you to use the same format for links in views
+ # and controllers.
+ module UrlHelper
+ include JavaScriptHelper
+
+ # Returns the URL for the set of +options+ provided. This takes the
+ # same options as url_for in ActionController (see the
+ # documentation for ActionController::Base#url_for). Note that by default
+ # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative /controller/action
+ # instead of the fully qualified URL like http://example.com/controller/action.
+ #
+ # When called from a view, url_for returns an HTML escaped url. If you
+ # need an unescaped url, pass :escape => false in the +options+.
+ #
+ # ==== Options
+ # * <tt>:anchor</tt> -- specifies the anchor name to be appended to the path.
+ # * <tt>:only_path</tt> -- if true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified)
+ # * <tt>:trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this
+ # is currently not recommended since it breaks caching.
+ # * <tt>:host</tt> -- overrides the default (current) host if provided
+ # * <tt>:protocol</tt> -- overrides the default (current) protocol if provided
+ # * <tt>:user</tt> -- Inline HTTP authentication (only plucked out if :password is also present)
+ # * <tt>:password</tt> -- Inline HTTP authentication (only plucked out if :user is also present)
+ # * <tt>:escape</tt> -- Determines whether the returned URL will be HTML escaped or not (<tt>true</tt> by default)
+ #
+ # ==== Relying on named routes
+ #
+ # If you instead of a hash pass a record (like an Active Record or Active Resource) as the options parameter,
+ # you'll trigger the named route for that record. The lookup will happen on the name of the class. So passing
+ # a Workshop object will attempt to use the workshop_path route. If you have a nested route, such as
+ # admin_workshop_path you'll have to call that explicitly (it's impossible for url_for to guess that route).
+ #
+ # ==== Examples
+ # <%= url_for(:action => 'index') %>
+ # # => /blog/
+ #
+ # <%= url_for(:action => 'find', :controller => 'books') %>
+ # # => /books/find
+ #
+ # <%= url_for(:action => 'login', :controller => 'members', :only_path => false, :protocol => 'https') %>
+ # # => https://www.railsapplication.com/members/login/
+ #
+ # <%= url_for(:action => 'play', :anchor => 'player') %>
+ # # => /messages/play/#player
+ #
+ # <%= url_for(:action => 'checkout', :anchor => 'tax&ship') %>
+ # # => /testing/jump/#tax&amp;ship
+ #
+ # <%= url_for(:action => 'checkout', :anchor => 'tax&ship', :escape => false) %>
+ # # => /testing/jump/#tax&ship
+ #
+ # <%= url_for(Workshop.new) %>
+ # # relies on Workshop answering a new_record? call (and in this case returning true)
+ # # => /workshops
+ #
+ # <%= url_for(@workshop) %>
+ # # calls @workshop.to_s
+ # # => /workshops/5
+ def url_for(options = {})
+ case options
+ when Hash
+ show_path = options[:host].nil? ? true : false
+ options = { :only_path => show_path }.update(options.symbolize_keys)
+ escape = options.key?(:escape) ? options.delete(:escape) : true
+ url = @controller.send(:url_for, options)
+ when String
+ escape = true
+ url = options
+ when NilClass
+ url = @controller.send(:url_for, nil)
+ else
+ escape = false
+ url = polymorphic_path(options)
+ end
+
+ escape ? escape_once(url) : url
+ end
+
+ # Creates a link tag of the given +name+ using a URL created by the set
+ # of +options+. See the valid options in the documentation for
+ # url_for. It's also possible to pass a string instead
+ # of an options hash to get a link tag that uses the value of the string as the
+ # href for the link, or use +:back+ to link to the referrer - a JavaScript back
+ # link will be used in place of a referrer if none exists. If nil is passed as
+ # a name, the link itself will become the name.
+ #
+ # ==== Options
+ # * <tt>:confirm => 'question?'</tt> -- This will add a JavaScript confirm
+ # prompt with the question specified. If the user accepts, the link is
+ # processed normally, otherwise no action is taken.
+ # * <tt>:popup => true || array of window options</tt> -- This will force the
+ # link to open in a popup window. By passing true, a default browser window
+ # will be opened with the URL. You can also specify an array of options
+ # that are passed-thru to JavaScripts window.open method.
+ # * <tt>:method => symbol of HTTP verb</tt> -- This modifier will dynamically
+ # create an HTML form and immediately submit the form for processing using
+ # the HTTP verb specified. Useful for having links perform a POST operation
+ # in dangerous actions like deleting a record (which search bots can follow
+ # while spidering your site). Supported verbs are :post, :delete and :put.
+ # Note that if the user has JavaScript disabled, the request will fall back
+ # to using GET. If you are relying on the POST behavior, you should check
+ # for it in your controller's action by using the request object's methods
+ # for post?, delete? or put?.
+ # * The +html_options+ will accept a hash of html attributes for the link tag.
+ #
+ # Note that if the user has JavaScript disabled, the request will fall back
+ # to using GET. If :href=>'#' is used and the user has JavaScript disabled
+ # clicking the link will have no effect. If you are relying on the POST
+ # behavior, your should check for it in your controller's action by using the
+ # request object's methods for post?, delete? or put?.
+ #
+ # You can mix and match the +html_options+ with the exception of
+ # :popup and :method which will raise an ActionView::ActionViewError
+ # exception.
+ #
+ # ==== Examples
+ # link_to "Visit Other Site", "http://www.rubyonrails.org/", :confirm => "Are you sure?"
+ # # => <a href="http://www.rubyonrails.org/" onclick="return confirm('Are you sure?');">Visit Other Site</a>
+ #
+ # link_to "Help", { :action => "help" }, :popup => true
+ # # => <a href="/testing/help/" onclick="window.open(this.href);return false;">Help</a>
+ #
+ # link_to "View Image", { :action => "view" }, :popup => ['new_window_name', 'height=300,width=600']
+ # # => <a href="/testing/view/" onclick="window.open(this.href,'new_window_name','height=300,width=600');return false;">View Image</a>
+ #
+ # link_to "Delete Image", { :action => "delete", :id => @image.id }, :confirm => "Are you sure?", :method => :delete
+ # # => <a href="/testing/delete/9/" onclick="if (confirm('Are you sure?')) { var f = document.createElement('form');
+ # f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;
+ # var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method');
+ # m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;">Delete Image</a>
+ def link_to(name, options = {}, html_options = nil)
+ url = case options
+ when String
+ options
+ when :back
+ @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()'
+ else
+ self.url_for(options)
+ end
+
+ if html_options
+ html_options = html_options.stringify_keys
+ href = html_options['href']
+ convert_options_to_javascript!(html_options, url)
+ tag_options = tag_options(html_options)
+ else
+ tag_options = nil
+ end
+
+ href_attr = "href=\"#{url}\"" unless href
+ "<a #{href_attr}#{tag_options}>#{name || url}</a>"
+ end
+
+ # Generates a form containing a single button that submits to the URL created
+ # by the set of +options+. This is the safest method to ensure links that
+ # cause changes to your data are not triggered by search bots or accelerators.
+ # If the HTML button does not work with your layout, you can also consider
+ # using the link_to method with the <tt>:method</tt> modifier as described in
+ # the link_to documentation.
+ #
+ # The generated FORM element has a class name of <tt>button-to</tt>
+ # to allow styling of the form itself and its children. You can control
+ # the form submission and input element behavior using +html_options+.
+ # This method accepts the <tt>:method</tt> and <tt>:confirm</tt> modifiers
+ # described in the link_to documentation. If no <tt>:method</tt> modifier
+ # is given, it will default to performing a POST operation. You can also
+ # disable the button by passing <tt>:disabled => true</tt> in +html_options+.
+ # If you are using RESTful routes, you can pass the <tt>:method</tt>
+ # to change the HTTP verb used to submit the form.
+ #
+ # ==== Options
+ # The +options+ hash accepts the same options at url_for.
+ #
+ # There are a few special +html_options+:
+ # * <tt>:method</tt> -- specifies the anchor name to be appended to the path.
+ # * <tt>:disabled</tt> -- specifies the anchor name to be appended to the path.
+ # * <tt>:confirm</tt> -- This will add a JavaScript confirm
+ # prompt with the question specified. If the user accepts, the link is
+ # processed normally, otherwise no action is taken.
+ #
+ # ==== Examples
+ # <%= button_to "New", :action => "new" %>
+ # # => "<form method="post" action="/controller/new" class="button-to">
+ # # <div><input value="New" type="submit" /></div>
+ # # </form>"
+ #
+ # button_to "Delete Image", { :action => "delete", :id => @image.id },
+ # :confirm => "Are you sure?", :method => :delete
+ # # => "<form method="post" action="/images/delete/1" class="button-to">
+ # # <div>
+ # # <input type="hidden" name="_method" value="delete" />
+ # # <input onclick="return confirm('Are you sure?');"
+ # # value="Delete" type="submit" />
+ # # </div>
+ # # </form>"
+ def button_to(name, options = {}, html_options = {})
+ html_options = html_options.stringify_keys
+ convert_boolean_attributes!(html_options, %w( disabled ))
+
+ method_tag = ''
+ if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
+ method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
+ end
+
+ form_method = method.to_s == 'get' ? 'get' : 'post'
+
+ request_token_tag = ''
+ if form_method == 'post' && protect_against_forgery?
+ request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
+ end
+
+ if confirm = html_options.delete("confirm")
+ html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
+ end
+
+ url = options.is_a?(String) ? options : self.url_for(options)
+ name ||= url
+
+ html_options.merge!("type" => "submit", "value" => name)
+
+ "<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
+ method_tag + tag("input", html_options) + request_token_tag + "</div></form>"
+ end
+
+
+ # Creates a link tag of the given +name+ using a URL created by the set of
+ # +options+ unless the current request URI is the same as the links, in
+ # which case only the name is returned (or the given block is yielded, if
+ # one exists). You can give link_to_unless_current a block which will
+ # specialize the default behavior (e.g., show a "Start Here" link rather
+ # than the link's text).
+ #
+ # ==== Examples
+ # Let's say you have a navigation menu...
+ #
+ # <ul id="navbar">
+ # <li><%= link_to_unless_current("Home", { :action => "index" }) %></li>
+ # <li><%= link_to_unless_current("About Us", { :action => "about" }) %></li>
+ # </ul>
+ #
+ # If in the "about" action, it will render...
+ #
+ # <ul id="navbar">
+ # <li><a href="/controller/index">Home</a></li>
+ # <li>About Us</li>
+ # </ul>
+ #
+ # ...but if in the "home" action, it will render:
+ #
+ # <ul id="navbar">
+ # <li><a href="/controller/index">Home</a></li>
+ # <li><a href="/controller/about">About Us</a></li>
+ # </ul>
+ #
+ # The implicit block given to link_to_unless_current is evaluated if the current
+ # action is the action given. So, if we had a comments page and wanted to render a
+ # "Go Back" link instead of a link to the comments page, we could do something like this...
+ #
+ # <%=
+ # link_to_unless_current("Comment", { :controller => 'comments', :action => 'new}) do
+ # link_to("Go back", { :controller => 'posts', :action => 'index' })
+ # end
+ # %>
+ def link_to_unless_current(name, options = {}, html_options = {}, &block)
+ link_to_unless current_page?(options), name, options, html_options, &block
+ end
+
+ # Creates a link tag of the given +name+ using a URL created by the set of
+ # +options+ unless +condition+ is true, in which case only the name is
+ # returned. To specialize the default behavior (i.e., show a login link rather
+ # than just the plaintext link text), you can pass a block that
+ # accepts the name or the full argument list for link_to_unless.
+ #
+ # ==== Examples
+ # <%= link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) %>
+ # # If the user is logged in...
+ # # => <a href="/controller/reply/">Reply</a>
+ #
+ # <%=
+ # link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) do |name|
+ # link_to(name, { :controller => "accounts", :action => "signup" })
+ # end
+ # %>
+ # # If the user is logged in...
+ # # => <a href="/controller/reply/">Reply</a>
+ # # If not...
+ # # => <a href="/accounts/signup">Reply</a>
+ def link_to_unless(condition, name, options = {}, html_options = {}, &block)
+ if condition
+ if block_given?
+ block.arity <= 1 ? yield(name) : yield(name, options, html_options)
+ else
+ name
+ end
+ else
+ link_to(name, options, html_options)
+ end
+ end
+
+ # Creates a link tag of the given +name+ using a URL created by the set of
+ # +options+ if +condition+ is true, in which case only the name is
+ # returned. To specialize the default behavior, you can pass a block that
+ # accepts the name or the full argument list for link_to_unless (see the examples
+ # in link_to_unless).
+ #
+ # ==== Examples
+ # <%= link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) %>
+ # # If the user isn't logged in...
+ # # => <a href="/sessions/new/">Login</a>
+ #
+ # <%=
+ # link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) do
+ # link_to(@current_user.login, { :controller => "accounts", :action => "show", :id => @current_user })
+ # end
+ # %>
+ # # If the user isn't logged in...
+ # # => <a href="/sessions/new/">Login</a>
+ # # If they are logged in...
+ # # => <a href="/accounts/show/3">my_username</a>
+ def link_to_if(condition, name, options = {}, html_options = {}, &block)
+ link_to_unless !condition, name, options, html_options, &block
+ end
+
+ # Creates a mailto link tag to the specified +email_address+, which is
+ # also used as the name of the link unless +name+ is specified. Additional
+ # HTML attributes for the link can be passed in +html_options+.
+ #
+ # mail_to has several methods for hindering email harvesters and customizing
+ # the email itself by passing special keys to +html_options+.
+ #
+ # ==== Options
+ # * <tt>:encode</tt> - This key will accept the strings "javascript" or "hex".
+ # Passing "javascript" will dynamically create and encode the mailto: link then
+ # eval it into the DOM of the page. This method will not show the link on
+ # the page if the user has JavaScript disabled. Passing "hex" will hex
+ # encode the +email_address+ before outputting the mailto: link.
+ # * <tt>:replace_at</tt> - When the link +name+ isn't provided, the
+ # +email_address+ is used for the link label. You can use this option to
+ # obfuscate the +email_address+ by substituting the @ sign with the string
+ # given as the value.
+ # * <tt>:replace_dot</tt> - When the link +name+ isn't provided, the
+ # +email_address+ is used for the link label. You can use this option to
+ # obfuscate the +email_address+ by substituting the . in the email with the
+ # string given as the value.
+ # * <tt>:subject</tt> - Preset the subject line of the email.
+ # * <tt>:body</tt> - Preset the body of the email.
+ # * <tt>:cc</tt> - Carbon Copy addition recipients on the email.
+ # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
+ #
+ # ==== Examples
+ # mail_to "me@domain.com"
+ # # => <a href="mailto:me@domain.com">me@domain.com</a>
+ #
+ # mail_to "me@domain.com", "My email", :encode => "javascript"
+ # # => <script type="text/javascript">eval(unescape('%64%6f%63...%6d%65%6e'))</script>
+ #
+ # mail_to "me@domain.com", "My email", :encode => "hex"
+ # # => <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>
+ #
+ # mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email"
+ # # => <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a>
+ #
+ # mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com",
+ # :subject => "This is an example email"
+ # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
+ def mail_to(email_address, name = nil, html_options = {})
+ html_options = html_options.stringify_keys
+ encode = html_options.delete("encode").to_s
+ cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body")
+
+ string = ''
+ extras = ''
+ extras << "cc=#{CGI.escape(cc).gsub("+", "%20")}&" unless cc.nil?
+ extras << "bcc=#{CGI.escape(bcc).gsub("+", "%20")}&" unless bcc.nil?
+ extras << "body=#{CGI.escape(body).gsub("+", "%20")}&" unless body.nil?
+ extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil?
+ extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty?
+
+ email_address = email_address.to_s
+
+ email_address_obfuscated = email_address.dup
+ email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at")
+ email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot")
+
+ if encode == "javascript"
+ tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');"
+ for i in 0...tmp.length
+ string << sprintf("%%%x",tmp[i])
+ end
+ "<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>"
+ elsif encode == "hex"
+ email_address_encoded = ''
+ email_address_obfuscated.each_byte do |c|
+ email_address_encoded << sprintf("&#%d;", c)
+ end
+
+ protocol = 'mailto:'
+ protocol.each_byte { |c| string << sprintf("&#%d;", c) }
+
+ for i in 0...email_address.length
+ if email_address[i,1] =~ /\w/
+ string << sprintf("%%%x",email_address[i])
+ else
+ string << email_address[i,1]
+ end
+ end
+ content_tag "a", name || email_address_encoded, html_options.merge({ "href" => "#{string}#{extras}" })
+ else
+ content_tag "a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:#{email_address}#{extras}" })
+ end
+ end
+
+ # True if the current request URI was generated by the given +options+.
+ #
+ # ==== Examples
+ # Let's say we're in the <tt>/shop/checkout</tt> action.
+ #
+ # current_page?(:action => 'process')
+ # # => false
+ #
+ # current_page?(:controller => 'shop', :action => 'checkout')
+ # # => true
+ #
+ # current_page?(:action => 'checkout')
+ # # => true
+ #
+ # current_page?(:controller => 'library', :action => 'checkout')
+ # # => false
+ def current_page?(options)
+ url_string = CGI.escapeHTML(url_for(options))
+ request = @controller.request
+ if url_string =~ /^\w+:\/\//
+ url_string == "#{request.protocol}#{request.host_with_port}#{request.request_uri}"
+ else
+ url_string == request.request_uri
+ end
+ end
+
+ private
+ def convert_options_to_javascript!(html_options, url = '')
+ confirm, popup = html_options.delete("confirm"), html_options.delete("popup")
+
+ method, href = html_options.delete("method"), html_options['href']
+
+ html_options["onclick"] = case
+ when popup && method
+ raise ActionView::ActionViewError, "You can't use :popup and :method in the same link"
+ when confirm && popup
+ "if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;"
+ when confirm && method
+ "if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;"
+ when confirm
+ "return #{confirm_javascript_function(confirm)};"
+ when method
+ "#{method_javascript_function(method, url, href)}return false;"
+ when popup
+ popup_javascript_function(popup) + 'return false;'
+ else
+ html_options["onclick"]
+ end
+ end
+
+ def confirm_javascript_function(confirm)
+ "confirm('#{escape_javascript(confirm)}')"
+ end
+
+ def popup_javascript_function(popup)
+ popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);"
+ end
+
+ def method_javascript_function(method, url = '', href = nil)
+ action = (href && url.size > 0) ? "'#{url}'" : 'this.href'
+ submit_function =
+ "var f = document.createElement('form'); f.style.display = 'none'; " +
+ "this.parentNode.appendChild(f); f.method = 'POST'; f.action = #{action};"
+
+ unless method == :post
+ submit_function << "var m = document.createElement('input'); m.setAttribute('type', 'hidden'); "
+ submit_function << "m.setAttribute('name', '_method'); m.setAttribute('value', '#{method}'); f.appendChild(m);"
+ end
+
+ if protect_against_forgery?
+ submit_function << "var s = document.createElement('input'); s.setAttribute('type', 'hidden'); "
+ submit_function << "s.setAttribute('name', '#{request_forgery_protection_token}'); s.setAttribute('value', '#{escape_javascript form_authenticity_token}'); f.appendChild(s);"
+ end
+ submit_function << "f.submit();"
+ end
+
+ # Processes the _html_options_ hash, converting the boolean
+ # attributes from true/false form into the form required by
+ # HTML/XHTML. (An attribute is considered to be boolean if
+ # its name is listed in the given _bool_attrs_ array.)
+ #
+ # More specifically, for each boolean attribute in _html_options_
+ # given as:
+ #
+ # "attr" => bool_value
+ #
+ # if the associated _bool_value_ evaluates to true, it is
+ # replaced with the attribute's name; otherwise the attribute is
+ # removed from the _html_options_ hash. (See the XHTML 1.0 spec,
+ # section 4.5 "Attribute Minimization" for more:
+ # http://www.w3.org/TR/xhtml1/#h-4.5)
+ #
+ # Returns the updated _html_options_ hash, which is also modified
+ # in place.
+ #
+ # Example:
+ #
+ # convert_boolean_attributes!( html_options,
+ # %w( checked disabled readonly ) )
+ def convert_boolean_attributes!(html_options, bool_attrs)
+ bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) }
+ html_options
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/partials.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/partials.rb
new file mode 100644
index 000000000..0795b8ec5
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/partials.rb
@@ -0,0 +1,200 @@
+module ActionView
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a single object
+ # (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being
+ # prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own.
+ #
+ # In a template for Advertiser#account:
+ #
+ # <%= render :partial => "account" %>
+ #
+ # This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable +account+ to
+ # the template for display.
+ #
+ # In another template for Advertiser#buy, we could have:
+ #
+ # <%= render :partial => "account", :locals => { :account => @buyer } %>
+ #
+ # <% for ad in @advertisements %>
+ # <%= render :partial => "ad", :locals => { :ad => ad } %>
+ # <% end %>
+ #
+ # This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then render
+ # "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display.
+ #
+ # == Rendering a collection of partials
+ #
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub
+ # template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders
+ # a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten
+ # with a single line:
+ #
+ # <%= render :partial => "ad", :collection => @advertisements %>
+ #
+ # This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An iteration counter
+ # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
+ # example above, the template would be fed +ad_counter+.
+ #
+ # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also just keep domain objects,
+ # like Active Records, in there.
+ #
+ # == Rendering shared partials
+ #
+ # Two controllers can share a set of partials and render them like this:
+ #
+ # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %>
+ #
+ # This will render the partial "advertisement/_ad.erb" regardless of which controller this is being called from.
+ #
+ # == Rendering partials with layouts
+ #
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are specified globally
+ # for the entire action, but they work in a similar fashion. Imagine a list with two types of users:
+ #
+ # <%# app/views/users/index.html.erb &>
+ # Here's the administrator:
+ # <%= render :partial => "user", :layout => "administrator", :locals => { :user => administrator } %>
+ #
+ # Here's the editor:
+ # <%= render :partial => "user", :layout => "editor", :locals => { :user => editor } %>
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # Name: <%= user.name %>
+ #
+ # <%# app/views/users/_administrator.html.erb &>
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # <%= yield %>
+ # </div>
+ #
+ # <%# app/views/users/_editor.html.erb &>
+ # <div id="editor">
+ # Deadline: $<%= user.deadline %>
+ # <%= yield %>
+ # </div>
+ #
+ # ...this will return:
+ #
+ # Here's the administrator:
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # Name: <%= user.name %>
+ # </div>
+ #
+ # Here's the editor:
+ # <div id="editor">
+ # Deadline: $<%= user.deadline %>
+ # Name: <%= user.name %>
+ # </div>
+ #
+ # You can also apply a layout to a block within any template:
+ #
+ # <%# app/views/users/_chief.html.erb &>
+ # <% render(:layout => "administrator", :locals => { :user => chief }) do %>
+ # Title: <%= chief.title %>
+ # <% end %>
+ #
+ # ...this will return:
+ #
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # Title: <%= chief.name %>
+ # </div>
+ #
+ # As you can see, the :locals hash is shared between both the partial and its layout.
+ module Partials
+ private
+ def render_partial(partial_path, object_assigns = nil, local_assigns = nil) #:nodoc:
+ case partial_path
+ when String, Symbol, NilClass
+ path, partial_name = partial_pieces(partial_path)
+ object = extracting_object(partial_name, object_assigns)
+ local_assigns = local_assigns ? local_assigns.clone : {}
+ add_counter_to_local_assigns!(partial_name, local_assigns)
+ add_object_to_local_assigns!(partial_name, local_assigns, object)
+
+ if logger && logger.debug?
+ ActionController::Base.benchmark("Rendered #{path}/_#{partial_name}", Logger::DEBUG, false) do
+ render("#{path}/_#{partial_name}", local_assigns)
+ end
+ else
+ render("#{path}/_#{partial_name}", local_assigns)
+ end
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Associations::HasManyThroughAssociation
+ if partial_path.any?
+ path = ActionController::RecordIdentifier.partial_path(partial_path.first)
+ collection = partial_path
+ render_partial_collection(path, collection, nil, object_assigns.value)
+ else
+ ""
+ end
+ else
+ render_partial(
+ ActionController::RecordIdentifier.partial_path(partial_path),
+ object_assigns, local_assigns)
+ end
+ end
+
+ def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = nil) #:nodoc:
+ collection_of_partials = Array.new
+ counter_name = partial_counter_name(partial_name)
+ local_assigns = local_assigns ? local_assigns.clone : {}
+ collection.each_with_index do |element, counter|
+ local_assigns[counter_name] = counter
+ collection_of_partials.push(render_partial(partial_name, element, local_assigns))
+ end
+
+ return " " if collection_of_partials.empty?
+
+ if partial_spacer_template
+ spacer_path, spacer_name = partial_pieces(partial_spacer_template)
+ collection_of_partials.join(render("#{spacer_path}/_#{spacer_name}"))
+ else
+ collection_of_partials.join
+ end
+ end
+
+ alias_method :render_collection_of_partials, :render_partial_collection
+
+ def partial_pieces(partial_path)
+ if partial_path.include?('/')
+ return File.dirname(partial_path), File.basename(partial_path)
+ else
+ return controller.class.controller_path, partial_path
+ end
+ end
+
+ def partial_counter_name(partial_name)
+ "#{partial_variable_name(partial_name)}_counter".intern
+ end
+
+ def partial_variable_name(partial_name)
+ partial_name.split('/').last.split('.').first.intern
+ end
+
+ def extracting_object(partial_name, object_assigns)
+ variable_name = partial_variable_name(partial_name)
+ if object_assigns.nil?
+ controller.instance_variable_get("@#{variable_name}")
+ else
+ object_assigns
+ end
+ end
+
+ def add_counter_to_local_assigns!(partial_name, local_assigns)
+ counter_name = partial_counter_name(partial_name)
+ local_assigns[counter_name] = 1 unless local_assigns.has_key?(counter_name)
+ end
+
+ def add_object_to_local_assigns!(partial_name, local_assigns, object)
+ variable_name = partial_variable_name(partial_name)
+
+ local_assigns[:object] ||=
+ local_assigns[variable_name] ||=
+ if object.is_a?(ActionView::Base::ObjectWrapper)
+ object.value
+ else
+ object
+ end || controller.instance_variable_get("@#{variable_name}")
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_error.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_error.rb
new file mode 100644
index 000000000..01b4b1555
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_error.rb
@@ -0,0 +1,102 @@
+module ActionView
+ # The TemplateError exception is raised when the compilation of the template fails. This exception then gathers a
+ # bunch of intimate details and uses it to report a very precise exception message.
+ class TemplateError < ActionViewError #:nodoc:
+ SOURCE_CODE_RADIUS = 3
+
+ attr_reader :original_exception
+
+ def initialize(base_path, file_path, assigns, source, original_exception)
+ @base_path, @assigns, @source, @original_exception =
+ base_path, assigns.dup, source, original_exception
+ @file_path = file_path
+ end
+
+ def message
+ ActiveSupport::Deprecation.silence { original_exception.message }
+ end
+
+ def clean_backtrace
+ original_exception.clean_backtrace
+ end
+
+ def sub_template_message
+ if @sub_templates
+ "Trace of template inclusion: " +
+ @sub_templates.collect { |template| strip_base_path(template) }.join(", ")
+ else
+ ""
+ end
+ end
+
+ def source_extract(indentation = 0)
+ return unless num = line_number
+ num = num.to_i
+
+ source_code = IO.readlines(@file_path)
+
+ start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
+ end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
+
+ indent = ' ' * indentation
+ line_counter = start_on_line
+
+ source_code[start_on_line..end_on_line].sum do |line|
+ line_counter += 1
+ "#{indent}#{line_counter}: #{line}"
+ end
+ end
+
+ def sub_template_of(template_path)
+ @sub_templates ||= []
+ @sub_templates << template_path
+ end
+
+ def line_number
+ @line_number ||=
+ if file_name
+ regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
+
+ $1 if message =~ regexp or clean_backtrace.find { |line| line =~ regexp }
+ end
+ end
+
+ def file_name
+ stripped = strip_base_path(@file_path)
+ stripped.slice!(0,1) if stripped[0] == ?/
+ stripped
+ end
+
+ def to_s
+ "\n\n#{self.class} (#{message}) #{source_location}:\n" +
+ "#{source_extract}\n #{clean_backtrace.join("\n ")}\n\n"
+ end
+
+ def backtrace
+ [
+ "#{source_location.capitalize}\n\n#{source_extract(4)}\n " +
+ clean_backtrace.join("\n ")
+ ]
+ end
+
+ private
+ def strip_base_path(path)
+ stripped_path = File.expand_path(path).gsub(@base_path, "")
+ stripped_path.gsub!(/^#{Regexp.escape File.expand_path(RAILS_ROOT)}/, '') if defined?(RAILS_ROOT)
+ stripped_path
+ end
+
+ def source_location
+ if line_number
+ "on line ##{line_number} of "
+ else
+ 'in '
+ end + file_name
+ end
+ end
+end
+
+if defined?(Exception::TraceSubstitutions)
+ Exception::TraceSubstitutions << [/:in\s+`_run_(html|xml).*'\s*$/, '']
+ Exception::TraceSubstitutions << [%r{^\s*#{Regexp.escape RAILS_ROOT}/}, ''] if defined?(RAILS_ROOT)
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handler.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handler.rb
new file mode 100644
index 000000000..b9f4330a2
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handler.rb
@@ -0,0 +1,17 @@
+module ActionView
+ class TemplateHandler
+ def self.line_offset
+ 0
+ end
+
+ def initialize(view)
+ @view = view
+ end
+
+ def render(template, local_assigns)
+ end
+
+ def compile(template)
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/builder.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/builder.rb
new file mode 100644
index 000000000..0f49d6ab3
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/builder.rb
@@ -0,0 +1,19 @@
+require 'builder'
+
+module ActionView
+ module TemplateHandlers
+ class Builder < TemplateHandler
+ def self.line_offset
+ 2
+ end
+
+ def compile(template)
+ content_type_handler = (@view.send!(:controller).respond_to?(:response) ? "controller.response" : "controller")
+ "#{content_type_handler}.content_type ||= Mime::XML\n" +
+ "xml = Builder::XmlMarkup.new(:indent => 2)\n" +
+ template +
+ "\nxml.target!\n"
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/erb.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/erb.rb
new file mode 100644
index 000000000..022fc362e
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/erb.rb
@@ -0,0 +1,21 @@
+require 'erb'
+
+class ERB
+ module Util
+ HTML_ESCAPE = { '&' => '&amp;', '"' => '&quot;', '>' => '&gt;', '<' => '&lt;' }
+
+ def html_escape(s)
+ s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
+ end
+ end
+end
+
+module ActionView
+ module TemplateHandlers
+ class ERB < TemplateHandler
+ def compile(template)
+ ::ERB.new(template, nil, @view.erb_trim_mode).src
+ end
+ end
+ end
+end
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/rjs.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/rjs.rb
new file mode 100644
index 000000000..4ca9fc327
--- /dev/null
+++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/rjs.rb
@@ -0,0 +1,14 @@
+module ActionView
+ module TemplateHandlers
+ class RJS < TemplateHandler
+ def self.line_offset
+ 2
+ end
+
+ def compile(template)
+ "controller.response.content_type ||= Mime::JS\n" +
+ "update_page do |page|\n#{template}\nend"
+ end
+ end
+ end
+end