PageRenderTime 54ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/actionpack/lib/action_view/helpers/form_options_helper.rb

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