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

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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));
}
});

View file

@ -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')];
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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