6b385ef77dff920f9c72ad5e6a0e084f0671c2ab
[feedcatcher.git] / vendor / rails / actionpack / lib / action_view / helpers / form_options_helper.rb
1 require 'cgi'
2 require 'erb'
3 require 'action_view/helpers/form_helper'
4
5 module ActionView
6 module Helpers
7 # Provides a number of methods for turning different kinds of containers into a set of option tags.
8 # == Options
9 # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
10 #
11 # * <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.
12 #
13 # For example,
14 #
15 # select("post", "category", Post::CATEGORIES, {:include_blank => true})
16 #
17 # could become:
18 #
19 # <select name="post[category]">
20 # <option></option>
21 # <option>joke</option>
22 # <option>poem</option>
23 # </select>
24 #
25 # Another common case is a select tag for an <tt>belongs_to</tt>-associated object.
26 #
27 # Example with @post.person_id => 2:
28 #
29 # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
30 #
31 # could become:
32 #
33 # <select name="post[person_id]">
34 # <option value="">None</option>
35 # <option value="1">David</option>
36 # <option value="2" selected="selected">Sam</option>
37 # <option value="3">Tobias</option>
38 # </select>
39 #
40 # * <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.
41 #
42 # Example:
43 #
44 # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
45 #
46 # could become:
47 #
48 # <select name="post[person_id]">
49 # <option value="">Select Person</option>
50 # <option value="1">David</option>
51 # <option value="2">Sam</option>
52 # <option value="3">Tobias</option>
53 # </select>
54 #
55 # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
56 # option to be in the +html_options+ parameter.
57 #
58 # Example:
59 #
60 # select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
61 #
62 # becomes:
63 #
64 # <select name="album[][genre]" id="album__genre">
65 # <option value="rap">rap</option>
66 # <option value="rock">rock</option>
67 # <option value="country">country</option>
68 # </select>
69 #
70 # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
71 #
72 # Example:
73 #
74 # select("post", "category", Post::CATEGORIES, {:disabled => 'restricted'})
75 #
76 # could become:
77 #
78 # <select name="post[category]">
79 # <option></option>
80 # <option>joke</option>
81 # <option>poem</option>
82 # <option disabled="disabled">restricted</option>
83 # </select>
84 #
85 # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
86 #
87 # Example:
88 #
89 # collection_select(:post, :category_id, Category.all, :id, :name, {:disabled => lambda{|category| category.archived? }})
90 #
91 # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
92 # <select name="post[category_id]">
93 # <option value="1" disabled="disabled">2008 stuff</option>
94 # <option value="2" disabled="disabled">Christmas</option>
95 # <option value="3">Jokes</option>
96 # <option value="4">Poems</option>
97 # </select>
98 #
99 module FormOptionsHelper
100 include ERB::Util
101
102 # Create a select tag and a series of contained option tags for the provided object and method.
103 # The option currently held by the object will be selected, provided that the object is available.
104 # See options_for_select for the required format of the choices parameter.
105 #
106 # Example with @post.person_id => 1:
107 # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { :include_blank => true })
108 #
109 # could become:
110 #
111 # <select name="post[person_id]">
112 # <option value=""></option>
113 # <option value="1" selected="selected">David</option>
114 # <option value="2">Sam</option>
115 # <option value="3">Tobias</option>
116 # </select>
117 #
118 # This can be used to provide a default set of options in the standard way: before rendering the create form, a
119 # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
120 # to the database. Instead, a second model object is created when the create request is received.
121 # This allows the user to submit a form page more than once with the expected results of creating multiple records.
122 # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
123 #
124 # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection
125 # or <tt>:selected => nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
126 # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
127 def select(object, method, choices, options = {}, html_options = {})
128 InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options)
129 end
130
131 # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
132 # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
133 # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
134 # or <tt>:include_blank</tt> in the +options+ hash.
135 #
136 # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
137 # of +collection+. The return values are used as the +value+ attribute and contents of each
138 # <tt><option></tt> tag, respectively.
139 #
140 # Example object structure for use with this method:
141 # class Post < ActiveRecord::Base
142 # belongs_to :author
143 # end
144 # class Author < ActiveRecord::Base
145 # has_many :posts
146 # def name_with_initial
147 # "#{first_name.first}. #{last_name}"
148 # end
149 # end
150 #
151 # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
152 # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {:prompt => true})
153 #
154 # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
155 # <select name="post[author_id]">
156 # <option value="">Please select</option>
157 # <option value="1" selected="selected">D. Heinemeier Hansson</option>
158 # <option value="2">D. Thomas</option>
159 # <option value="3">M. Clark</option>
160 # </select>
161 def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
162 InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
163 end
164
165 # Return select and option tags for the given object and method, using
166 # #time_zone_options_for_select to generate the list of option tags.
167 #
168 # In addition to the <tt>:include_blank</tt> option documented above,
169 # this method also supports a <tt>:model</tt> option, which defaults
170 # to TimeZone. This may be used by users to specify a different time
171 # zone model object. (See +time_zone_options_for_select+ for more
172 # information.)
173 #
174 # You can also supply an array of TimeZone objects
175 # as +priority_zones+, so that they will be listed above the rest of the
176 # (long) list. (You can use TimeZone.us_zones as a convenience for
177 # obtaining a list of the US time zones, or a Regexp to select the zones
178 # of your choice)
179 #
180 # Finally, this method supports a <tt>:default</tt> option, which selects
181 # a default TimeZone if the object's time zone is +nil+.
182 #
183 # Examples:
184 # time_zone_select( "user", "time_zone", nil, :include_blank => true)
185 #
186 # time_zone_select( "user", "time_zone", nil, :default => "Pacific Time (US & Canada)" )
187 #
188 # time_zone_select( "user", 'time_zone', TimeZone.us_zones, :default => "Pacific Time (US & Canada)")
189 #
190 # time_zone_select( "user", 'time_zone', [ TimeZone['Alaska'], TimeZone['Hawaii'] ])
191 #
192 # time_zone_select( "user", 'time_zone', /Australia/)
193 #
194 # time_zone_select( "user", "time_zone", TZInfo::Timezone.all.sort, :model => TZInfo::Timezone)
195 def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
196 InstanceTag.new(object, method, self, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options)
197 end
198
199 # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
200 # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
201 # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
202 # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
203 # may also be an array of values to be selected when using a multiple select.
204 #
205 # Examples (call, result):
206 # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
207 # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
208 #
209 # options_for_select([ "VISA", "MasterCard" ], "MasterCard")
210 # <option>VISA</option>\n<option selected="selected">MasterCard</option>
211 #
212 # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
213 # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option>
214 #
215 # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
216 # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option>
217 #
218 # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
219 # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
220 #
221 # Examples:
222 # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum")
223 # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
224 #
225 # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"])
226 # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced" disabled="disabled">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
227 #
228 # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum")
229 # <option value="Free" selected="selected">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
230 #
231 # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
232 def options_for_select(container, selected = nil)
233 container = container.to_a if Hash === container
234 selected, disabled = extract_selected_and_disabled(selected)
235
236 options_for_select = container.inject([]) do |options, element|
237 text, value = option_text_and_value(element)
238 selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
239 disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled)
240 options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}>#{html_escape(text.to_s)}</option>)
241 end
242
243 options_for_select.join("\n")
244 end
245
246 # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
247 # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
248 # Example:
249 # options_from_collection_for_select(@people, 'id', 'name')
250 # This will output the same HTML as if you did this:
251 # <option value="#{person.id}">#{person.name}</option>
252 #
253 # This is more often than not used inside a #select_tag like this example:
254 # select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
255 #
256 # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
257 # will be selected option tag(s).
258 #
259 # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
260 # function are the selected values.
261 #
262 # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
263 #
264 # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
265 # Failure to do this will produce undesired results. Example:
266 # options_from_collection_for_select(@people, 'id', 'name', '1')
267 # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
268 # options_from_collection_for_select(@people, 'id', 'name', 1)
269 # should produce the desired results.
270 def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
271 options = collection.map do |element|
272 [element.send(text_method), element.send(value_method)]
273 end
274 selected, disabled = extract_selected_and_disabled(selected)
275 select_deselect = {}
276 select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected)
277 select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled)
278
279 options_for_select(options, select_deselect)
280 end
281
282 # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
283 # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
284 #
285 # Parameters:
286 # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
287 # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
288 # array of child objects representing the <tt><option></tt> tags.
289 # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
290 # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
291 # * +option_key_method+ - The name of a method which, when called on a child object of a member of
292 # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
293 # * +option_value_method+ - The name of a method which, when called on a child object of a member of
294 # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
295 # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
296 # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
297 # to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
298 # to be specified.
299 #
300 # Example object structure for use with this method:
301 # class Continent < ActiveRecord::Base
302 # has_many :countries
303 # # attribs: id, name
304 # end
305 # class Country < ActiveRecord::Base
306 # belongs_to :continent
307 # # attribs: id, name, continent_id
308 # end
309 #
310 # Sample usage:
311 # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
312 #
313 # Possible output:
314 # <optgroup label="Africa">
315 # <option value="1">Egypt</option>
316 # <option value="4">Rwanda</option>
317 # ...
318 # </optgroup>
319 # <optgroup label="Asia">
320 # <option value="3" selected="selected">China</option>
321 # <option value="12">India</option>
322 # <option value="5">Japan</option>
323 # ...
324 # </optgroup>
325 #
326 # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
327 # wrap the output in an appropriate <tt><select></tt> tag.
328 def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
329 collection.inject("") do |options_for_select, group|
330 group_label_string = eval("group.#{group_label_method}")
331 options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
332 options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
333 options_for_select += '</optgroup>'
334 end
335 end
336
337 # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
338 # wraps them with <tt><optgroup></tt> tags.
339 #
340 # Parameters:
341 # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
342 # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
343 # nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
344 # Ex. ["North America",[["United States","US"],["Canada","CA"]]]
345 # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
346 # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
347 # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
348 # * +prompt+ - set to true or a prompt string. When the select element doesn’t have a value yet, this
349 # prepends an option with a generic prompt — "Please select" — or the given prompt string.
350 #
351 # Sample usage (Array):
352 # grouped_options = [
353 # ['North America',
354 # [['United States','US'],'Canada']],
355 # ['Europe',
356 # ['Denmark','Germany','France']]
357 # ]
358 # grouped_options_for_select(grouped_options)
359 #
360 # Sample usage (Hash):
361 # grouped_options = {
362 # 'North America' => [['United States','US], 'Canada'],
363 # 'Europe' => ['Denmark','Germany','France']
364 # }
365 # grouped_options_for_select(grouped_options)
366 #
367 # Possible output:
368 # <optgroup label="Europe">
369 # <option value="Denmark">Denmark</option>
370 # <option value="Germany">Germany</option>
371 # <option value="France">France</option>
372 # </optgroup>
373 # <optgroup label="North America">
374 # <option value="US">United States</option>
375 # <option value="Canada">Canada</option>
376 # </optgroup>
377 #
378 # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
379 # wrap the output in an appropriate <tt><select></tt> tag.
380 def grouped_options_for_select(grouped_options, selected_key = nil, prompt = nil)
381 body = ''
382 body << content_tag(:option, prompt, :value => "") if prompt
383
384 grouped_options = grouped_options.sort if grouped_options.is_a?(Hash)
385
386 grouped_options.each do |group|
387 body << content_tag(:optgroup, options_for_select(group[1], selected_key), :label => group[0])
388 end
389
390 body
391 end
392
393 # Returns a string of option tags for pretty much any time zone in the
394 # world. Supply a TimeZone name as +selected+ to have it marked as the
395 # selected option tag. You can also supply an array of TimeZone objects
396 # as +priority_zones+, so that they will be listed above the rest of the
397 # (long) list. (You can use TimeZone.us_zones as a convenience for
398 # obtaining a list of the US time zones, or a Regexp to select the zones
399 # of your choice)
400 #
401 # The +selected+ parameter must be either +nil+, or a string that names
402 # a TimeZone.
403 #
404 # By default, +model+ is the TimeZone constant (which can be obtained
405 # in Active Record as a value object). The only requirement is that the
406 # +model+ parameter be an object that responds to +all+, and returns
407 # an array of objects that represent time zones.
408 #
409 # NOTE: Only the option tags are returned, you have to wrap this call in
410 # a regular HTML select tag.
411 def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
412 zone_options = ""
413
414 zones = model.all
415 convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
416
417 if priority_zones
418 if priority_zones.is_a?(Regexp)
419 priority_zones = model.all.find_all {|z| z =~ priority_zones}
420 end
421 zone_options += options_for_select(convert_zones[priority_zones], selected)
422 zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
423
424 zones = zones.reject { |z| priority_zones.include?( z ) }
425 end
426
427 zone_options += options_for_select(convert_zones[zones], selected)
428 zone_options
429 end
430
431 private
432 def option_text_and_value(option)
433 # Options are [text, value] pairs or strings used for both.
434 if !option.is_a?(String) and option.respond_to?(:first) and option.respond_to?(:last)
435 [option.first, option.last]
436 else
437 [option, option]
438 end
439 end
440
441 def option_value_selected?(value, selected)
442 if selected.respond_to?(:include?) && !selected.is_a?(String)
443 selected.include? value
444 else
445 value == selected
446 end
447 end
448
449 def extract_selected_and_disabled(selected)
450 if selected.is_a?(Hash)
451 [selected[:selected], selected[:disabled]]
452 else
453 [selected, nil]
454 end
455 end
456
457 def extract_values_from_collection(collection, value_method, selected)
458 if selected.is_a?(Proc)
459 collection.map do |element|
460 element.send(value_method) if selected.call(element)
461 end.compact
462 else
463 selected
464 end
465 end
466 end
467
468 class InstanceTag #:nodoc:
469 include FormOptionsHelper
470
471 def to_select_tag(choices, options, html_options)
472 html_options = html_options.stringify_keys
473 add_default_name_and_id(html_options)
474 value = value(object)
475 selected_value = options.has_key?(:selected) ? options[:selected] : value
476 disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil
477 content_tag("select", add_options(options_for_select(choices, :selected => selected_value, :disabled => disabled_value), options, selected_value), html_options)
478 end
479
480 def to_collection_select_tag(collection, value_method, text_method, options, html_options)
481 html_options = html_options.stringify_keys
482 add_default_name_and_id(html_options)
483 value = value(object)
484 disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil
485 selected_value = options.has_key?(:selected) ? options[:selected] : value
486 content_tag(
487 "select", add_options(options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => disabled_value), options, value), html_options
488 )
489 end
490
491 def to_time_zone_select_tag(priority_zones, options, html_options)
492 html_options = html_options.stringify_keys
493 add_default_name_and_id(html_options)
494 value = value(object)
495 content_tag("select",
496 add_options(
497 time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone),
498 options, value
499 ), html_options
500 )
501 end
502
503 private
504 def add_options(option_tags, options, value = nil)
505 if options[:include_blank]
506 option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
507 end
508 if value.blank? && options[:prompt]
509 ("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags
510 else
511 option_tags
512 end
513 end
514 end
515
516 class FormBuilder
517 def select(method, choices, options = {}, html_options = {})
518 @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
519 end
520
521 def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
522 @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
523 end
524
525 def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
526 @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
527 end
528 end
529 end
530 end