actionview 4.2.11.1 → 6.0.4.8

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +242 -186
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -8
  5. data/lib/action_view/base.rb +144 -37
  6. data/lib/action_view/buffers.rb +18 -1
  7. data/lib/action_view/cache_expiry.rb +53 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker.rb +54 -20
  10. data/lib/action_view/digestor.rb +88 -85
  11. data/lib/action_view/flows.rb +11 -12
  12. data/lib/action_view/gem_version.rb +6 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +16 -11
  14. data/lib/action_view/helpers/asset_tag_helper.rb +241 -82
  15. data/lib/action_view/helpers/asset_url_helper.rb +171 -67
  16. data/lib/action_view/helpers/atom_feed_helper.rb +19 -17
  17. data/lib/action_view/helpers/cache_helper.rb +112 -42
  18. data/lib/action_view/helpers/capture_helper.rb +20 -13
  19. data/lib/action_view/helpers/controller_helper.rb +15 -4
  20. data/lib/action_view/helpers/csp_helper.rb +26 -0
  21. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  22. data/lib/action_view/helpers/date_helper.rb +230 -129
  23. data/lib/action_view/helpers/debug_helper.rb +7 -6
  24. data/lib/action_view/helpers/form_helper.rb +755 -129
  25. data/lib/action_view/helpers/form_options_helper.rb +130 -75
  26. data/lib/action_view/helpers/form_tag_helper.rb +116 -71
  27. data/lib/action_view/helpers/javascript_helper.rb +30 -14
  28. data/lib/action_view/helpers/number_helper.rb +84 -59
  29. data/lib/action_view/helpers/output_safety_helper.rb +36 -4
  30. data/lib/action_view/helpers/rendering_helper.rb +11 -8
  31. data/lib/action_view/helpers/sanitize_helper.rb +30 -31
  32. data/lib/action_view/helpers/tag_helper.rb +232 -75
  33. data/lib/action_view/helpers/tags/base.rb +138 -98
  34. data/lib/action_view/helpers/tags/check_box.rb +20 -19
  35. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
  37. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  38. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  39. data/lib/action_view/helpers/tags/collection_select.rb +4 -2
  40. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  41. data/lib/action_view/helpers/tags/date_field.rb +2 -1
  42. data/lib/action_view/helpers/tags/date_select.rb +37 -36
  43. data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +2 -1
  45. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  47. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  48. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  49. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/label.rb +3 -2
  51. data/lib/action_view/helpers/tags/month_field.rb +2 -1
  52. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  54. data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
  55. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  56. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/search_field.rb +14 -9
  58. data/lib/action_view/helpers/tags/select.rb +11 -10
  59. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/text_area.rb +4 -2
  61. data/lib/action_view/helpers/tags/text_field.rb +8 -8
  62. data/lib/action_view/helpers/tags/time_field.rb +2 -1
  63. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  64. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  65. data/lib/action_view/helpers/tags/translator.rb +15 -16
  66. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  67. data/lib/action_view/helpers/tags/week_field.rb +2 -1
  68. data/lib/action_view/helpers/tags.rb +3 -1
  69. data/lib/action_view/helpers/text_helper.rb +56 -38
  70. data/lib/action_view/helpers/translation_helper.rb +91 -47
  71. data/lib/action_view/helpers/url_helper.rb +160 -105
  72. data/lib/action_view/helpers.rb +5 -3
  73. data/lib/action_view/layouts.rb +65 -61
  74. data/lib/action_view/log_subscriber.rb +61 -10
  75. data/lib/action_view/lookup_context.rb +147 -89
  76. data/lib/action_view/model_naming.rb +3 -1
  77. data/lib/action_view/path_set.rb +28 -23
  78. data/lib/action_view/railtie.rb +62 -6
  79. data/lib/action_view/record_identifier.rb +53 -26
  80. data/lib/action_view/renderer/abstract_renderer.rb +71 -13
  81. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +103 -0
  82. data/lib/action_view/renderer/partial_renderer.rb +239 -225
  83. data/lib/action_view/renderer/renderer.rb +22 -8
  84. data/lib/action_view/renderer/streaming_template_renderer.rb +54 -54
  85. data/lib/action_view/renderer/template_renderer.rb +79 -73
  86. data/lib/action_view/rendering.rb +68 -44
  87. data/lib/action_view/routing_url_for.rb +33 -22
  88. data/lib/action_view/tasks/cache_digests.rake +25 -0
  89. data/lib/action_view/template/error.rb +44 -29
  90. data/lib/action_view/template/handlers/builder.rb +12 -13
  91. data/lib/action_view/template/handlers/erb/erubi.rb +87 -0
  92. data/lib/action_view/template/handlers/erb.rb +24 -86
  93. data/lib/action_view/template/handlers/html.rb +11 -0
  94. data/lib/action_view/template/handlers/raw.rb +4 -4
  95. data/lib/action_view/template/handlers.rb +38 -8
  96. data/lib/action_view/template/html.rb +19 -10
  97. data/lib/action_view/template/inline.rb +22 -0
  98. data/lib/action_view/template/raw_file.rb +28 -0
  99. data/lib/action_view/template/resolver.rb +217 -193
  100. data/lib/action_view/template/sources/file.rb +17 -0
  101. data/lib/action_view/template/sources.rb +13 -0
  102. data/lib/action_view/template/text.rb +11 -10
  103. data/lib/action_view/template/types.rb +18 -18
  104. data/lib/action_view/template.rb +146 -90
  105. data/lib/action_view/test_case.rb +52 -32
  106. data/lib/action_view/testing/resolvers.rb +46 -34
  107. data/lib/action_view/unbound_template.rb +31 -0
  108. data/lib/action_view/version.rb +3 -1
  109. data/lib/action_view/view_paths.rb +48 -31
  110. data/lib/action_view.rb +11 -8
  111. data/lib/assets/compiled/rails-ujs.js +746 -0
  112. metadata +41 -32
  113. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  114. data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,38 +1,238 @@
1
- require 'active_support/core_ext/string/output_safety'
2
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/output_safety"
4
+ require "set"
3
5
 
4
6
  module ActionView
5
7
  # = Action View Tag Helpers
6
8
  module Helpers #:nodoc:
7
- # Provides methods to generate HTML tags programmatically when you can't use
8
- # a Builder. By default, they output XHTML compliant tags.
9
+ # Provides methods to generate HTML tags programmatically both as a modern
10
+ # HTML5 compliant builder style and legacy XHTML compliant tags.
9
11
  module TagHelper
10
12
  extend ActiveSupport::Concern
11
13
  include CaptureHelper
12
14
  include OutputSafetyHelper
13
15
 
14
- BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
15
- autoplay controls loop selected hidden scoped async
16
- defer reversed ismap seamless muted required
17
- autofocus novalidate formnovalidate open pubdate
18
- itemscope allowfullscreen default inert sortable
19
- truespeed typemustmatch).to_set
16
+ BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked
17
+ compact controls declare default defaultchecked
18
+ defaultmuted defaultselected defer disabled
19
+ enabled formnovalidate hidden indeterminate inert
20
+ ismap itemscope loop multiple muted nohref
21
+ noresize noshade novalidate nowrap open
22
+ pauseonexit readonly required reversed scoped
23
+ seamless selected sortable truespeed typemustmatch
24
+ visible).to_set
25
+
26
+ BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
27
+
28
+ TAG_PREFIXES = ["aria", "data", :aria, :data].to_set
29
+
30
+ PRE_CONTENT_STRINGS = Hash.new { "" }
31
+ PRE_CONTENT_STRINGS[:textarea] = "\n"
32
+ PRE_CONTENT_STRINGS["textarea"] = "\n"
33
+
34
+ class TagBuilder #:nodoc:
35
+ include CaptureHelper
36
+ include OutputSafetyHelper
37
+
38
+ VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set
39
+
40
+ def initialize(view_context)
41
+ @view_context = view_context
42
+ end
43
+
44
+ def tag_string(name, content = nil, **options, &block)
45
+ escape = handle_deprecated_escape_options(options)
46
+ content = @view_context.capture(self, &block) if block_given?
47
+
48
+ if VOID_ELEMENTS.include?(name) && content.nil?
49
+ "<#{name.to_s.dasherize}#{tag_options(options, escape)}>".html_safe
50
+ else
51
+ content_tag_string(name.to_s.dasherize, content || "", options, escape)
52
+ end
53
+ end
54
+
55
+ def content_tag_string(name, content, options, escape = true)
56
+ tag_options = tag_options(options, escape) if options
57
+
58
+ if escape
59
+ name = ERB::Util.xml_name_escape(name)
60
+ content = ERB::Util.unwrapped_html_escape(content)
61
+ end
62
+
63
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
64
+ end
65
+
66
+ def tag_options(options, escape = true)
67
+ return if options.blank?
68
+ output = +""
69
+ sep = " "
70
+ options.each_pair do |key, value|
71
+ if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
72
+ value.each_pair do |k, v|
73
+ next if v.nil?
74
+ output << sep
75
+ output << prefix_tag_option(key, k, v, escape)
76
+ end
77
+ elsif BOOLEAN_ATTRIBUTES.include?(key)
78
+ if value
79
+ output << sep
80
+ output << boolean_tag_option(key)
81
+ end
82
+ elsif !value.nil?
83
+ output << sep
84
+ output << tag_option(key, value, escape)
85
+ end
86
+ end
87
+ output unless output.empty?
88
+ end
89
+
90
+ def boolean_tag_option(key)
91
+ %(#{key}="#{key}")
92
+ end
20
93
 
21
- BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym })
94
+ def tag_option(key, value, escape)
95
+ key = ERB::Util.xml_name_escape(key) if escape
22
96
 
23
- TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set
97
+ if value.is_a?(Array)
98
+ value = escape ? safe_join(value, " ") : value.join(" ")
99
+ else
100
+ value = escape ? ERB::Util.unwrapped_html_escape(value).dup : value.to_s.dup
101
+ end
102
+ value.gsub!('"', "&quot;")
103
+ %(#{key}="#{value}")
104
+ end
105
+
106
+ private
107
+ def prefix_tag_option(prefix, key, value, escape)
108
+ key = "#{prefix}-#{key.to_s.dasherize}"
109
+ unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
110
+ value = value.to_json
111
+ end
112
+ tag_option(key, value, escape)
113
+ end
24
114
 
25
- PRE_CONTENT_STRINGS = {
26
- :textarea => "\n"
27
- }
115
+ def respond_to_missing?(*args)
116
+ true
117
+ end
118
+
119
+ def handle_deprecated_escape_options(options)
120
+ # The option :escape_attributes has been merged into the options hash to be
121
+ # able to warn when it is used, so we need to handle default values here.
122
+ escape_option_provided = options.has_key?(:escape)
123
+ escape_attributes_option_provided = options.has_key?(:escape_attributes)
124
+
125
+ if escape_attributes_option_provided
126
+ ActiveSupport::Deprecation.warn(<<~MSG)
127
+ Use of the option :escape_attributes is deprecated. It currently \
128
+ escapes both names and values of tags and attributes and it is \
129
+ equivalent to :escape. If any of them are enabled, the escaping \
130
+ is fully enabled.
131
+ MSG
132
+ end
28
133
 
29
- # Returns an empty HTML tag of type +name+ which by default is XHTML
134
+ return true unless escape_option_provided || escape_attributes_option_provided
135
+ escape_option = options.delete(:escape)
136
+ escape_attributes_option = options.delete(:escape_attributes)
137
+ escape_option || escape_attributes_option
138
+ end
139
+
140
+ def method_missing(called, *args, **options, &block)
141
+ tag_string(called, *args, **options, &block)
142
+ end
143
+ end
144
+
145
+ # Returns an HTML tag.
146
+ #
147
+ # === Building HTML tags
148
+ #
149
+ # Builds HTML5 compliant tags with a tag proxy. Every tag can be built with:
150
+ #
151
+ # tag.<tag name>(optional content, options)
152
+ #
153
+ # where tag name can be e.g. br, div, section, article, or any tag really.
154
+ #
155
+ # ==== Passing content
156
+ #
157
+ # Tags can pass content to embed within it:
158
+ #
159
+ # tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1>
160
+ #
161
+ # tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div>
162
+ #
163
+ # Content can also be captured with a block, which is useful in templates:
164
+ #
165
+ # <%= tag.p do %>
166
+ # The next great American novel starts here.
167
+ # <% end %>
168
+ # # => <p>The next great American novel starts here.</p>
169
+ #
170
+ # ==== Options
171
+ #
172
+ # Use symbol keyed options to add attributes to the generated tag.
173
+ #
174
+ # tag.section class: %w( kitties puppies )
175
+ # # => <section class="kitties puppies"></section>
176
+ #
177
+ # tag.section id: dom_id(@post)
178
+ # # => <section id="<generated dom id>"></section>
179
+ #
180
+ # Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+.
181
+ #
182
+ # tag.input type: 'text', disabled: true
183
+ # # => <input type="text" disabled="disabled">
184
+ #
185
+ # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
186
+ # pointing to a hash of sub-attributes.
187
+ #
188
+ # To play nicely with JavaScript conventions, sub-attributes are dasherized.
189
+ #
190
+ # tag.article data: { user_id: 123 }
191
+ # # => <article data-user-id="123"></article>
192
+ #
193
+ # Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>.
194
+ #
195
+ # Data attribute values are encoded to JSON, with the exception of strings, symbols and
196
+ # BigDecimals.
197
+ # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
198
+ # from 1.4.3.
199
+ #
200
+ # tag.div data: { city_state: %w( Chicago IL ) }
201
+ # # => <div data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
202
+ #
203
+ # The generated attributes are escaped by default. This can be disabled using
204
+ # +escape_attributes+.
205
+ #
206
+ # tag.img src: 'open & shut.png'
207
+ # # => <img src="open &amp; shut.png">
208
+ #
209
+ # tag.img src: 'open & shut.png', escape_attributes: false
210
+ # # => <img src="open & shut.png">
211
+ #
212
+ # The tag builder respects
213
+ # {HTML5 void elements}[https://www.w3.org/TR/html5/syntax.html#void-elements]
214
+ # if no content is passed, and omits closing tags for those elements.
215
+ #
216
+ # # A standard element:
217
+ # tag.div # => <div></div>
218
+ #
219
+ # # A void element:
220
+ # tag.br # => <br>
221
+ #
222
+ # === Legacy syntax
223
+ #
224
+ # The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
225
+ #
226
+ # tag(name, options = nil, open = false, escape = true)
227
+ #
228
+ # It returns an empty HTML tag of type +name+ which by default is XHTML
30
229
  # compliant. Set +open+ to true to create an open tag compatible
31
230
  # with HTML 4.0 and below. Add HTML attributes by passing an attributes
32
231
  # hash to +options+. Set +escape+ to false to disable attribute value
33
232
  # escaping.
34
233
  #
35
234
  # ==== Options
235
+ #
36
236
  # You can use symbols or strings for the attribute names.
37
237
  #
38
238
  # Use +true+ with boolean attributes that can render with no value, like
@@ -41,16 +241,8 @@ module ActionView
41
241
  # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
42
242
  # pointing to a hash of sub-attributes.
43
243
  #
44
- # To play nicely with JavaScript conventions sub-attributes are dasherized.
45
- # For example, a key +user_id+ would render as <tt>data-user-id</tt> and
46
- # thus accessed as <tt>dataset.userId</tt>.
47
- #
48
- # Values are encoded to JSON, with the exception of strings, symbols and
49
- # BigDecimals.
50
- # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
51
- # from 1.4.3.
52
- #
53
244
  # ==== Examples
245
+ #
54
246
  # tag("br")
55
247
  # # => <br />
56
248
  #
@@ -66,13 +258,18 @@ module ActionView
66
258
  # tag("img", src: "open & shut.png")
67
259
  # # => <img src="open &amp; shut.png" />
68
260
  #
69
- # tag("img", {src: "open &amp; shut.png"}, false, false)
261
+ # tag("img", { src: "open &amp; shut.png" }, false, false)
70
262
  # # => <img src="open &amp; shut.png" />
71
263
  #
72
- # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)})
264
+ # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
73
265
  # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
74
- def tag(name, options = nil, open = false, escape = true)
75
- "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
266
+ def tag(name = nil, options = nil, open = false, escape = true)
267
+ if name.nil?
268
+ tag_builder
269
+ else
270
+ name = ERB::Util.xml_name_escape(name) if escape
271
+ "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
272
+ end
76
273
  end
77
274
 
78
275
  # Returns an HTML block tag of type +name+ surrounding the +content+. Add
@@ -80,6 +277,7 @@ module ActionView
80
277
  # Instead of passing the content as an argument, you can also use a block
81
278
  # in which case, you pass your +options+ as the second parameter.
82
279
  # Set escape to false to disable attribute value escaping.
280
+ # Note: this is legacy syntax, see +tag+ method description for details.
83
281
  #
84
282
  # ==== Options
85
283
  # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and
@@ -103,9 +301,9 @@ module ActionView
103
301
  def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
104
302
  if block_given?
105
303
  options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
106
- content_tag_string(name, capture(&block), options, escape)
304
+ tag_builder.content_tag_string(name, capture(&block), options, escape)
107
305
  else
108
- content_tag_string(name, content_or_options_with_block, options, escape)
306
+ tag_builder.content_tag_string(name, content_or_options_with_block, options, escape)
109
307
  end
110
308
  end
111
309
 
@@ -123,7 +321,7 @@ module ActionView
123
321
  # cdata_section("hello]]>world")
124
322
  # # => <![CDATA[hello]]]]><![CDATA[>world]]>
125
323
  def cdata_section(content)
126
- splitted = content.to_s.gsub(/\]\]\>/, ']]]]><![CDATA[>')
324
+ splitted = content.to_s.gsub(/\]\]\>/, "]]]]><![CDATA[>")
127
325
  "<![CDATA[#{splitted}]]>".html_safe
128
326
  end
129
327
 
@@ -139,49 +337,8 @@ module ActionView
139
337
  end
140
338
 
141
339
  private
142
-
143
- def content_tag_string(name, content, options, escape = true)
144
- tag_options = tag_options(options, escape) if options
145
- content = ERB::Util.unwrapped_html_escape(content) if escape
146
- "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
147
- end
148
-
149
- def tag_options(options, escape = true)
150
- return if options.blank?
151
- attrs = []
152
- options.each_pair do |key, value|
153
- if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
154
- value.each_pair do |k, v|
155
- attrs << prefix_tag_option(key, k, v, escape)
156
- end
157
- elsif BOOLEAN_ATTRIBUTES.include?(key)
158
- attrs << boolean_tag_option(key) if value
159
- elsif !value.nil?
160
- attrs << tag_option(key, value, escape)
161
- end
162
- end
163
- " #{attrs * ' '}" unless attrs.empty?
164
- end
165
-
166
- def prefix_tag_option(prefix, key, value, escape)
167
- key = "#{prefix}-#{key.to_s.dasherize}"
168
- unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
169
- value = value.to_json
170
- end
171
- tag_option(key, value, escape)
172
- end
173
-
174
- def boolean_tag_option(key)
175
- %(#{key}="#{key}")
176
- end
177
-
178
- def tag_option(key, value, escape)
179
- if value.is_a?(Array)
180
- value = escape ? safe_join(value, " ") : value.join(" ")
181
- else
182
- value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
183
- end
184
- %(#{key}="#{value.gsub('"'.freeze, '&quot;'.freeze)}")
340
+ def tag_builder
341
+ @tag_builder ||= TagBuilder.new(self)
185
342
  end
186
343
  end
187
344
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -11,10 +13,19 @@ module ActionView
11
13
  @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
12
14
  @template_object = template_object
13
15
 
14
- @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
16
+ @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
15
17
  @object = retrieve_object(options.delete(:object))
18
+ @skip_default_ids = options.delete(:skip_default_ids)
19
+ @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object)
16
20
  @options = options
17
- @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
21
+
22
+ if Regexp.last_match
23
+ @generate_indexed_names = true
24
+ @auto_index = retrieve_autoindex(Regexp.last_match.pre_match)
25
+ else
26
+ @generate_indexed_names = false
27
+ @auto_index = nil
28
+ end
18
29
  end
19
30
 
20
31
  # This is what child classes implement.
@@ -23,132 +34,161 @@ module ActionView
23
34
  end
24
35
 
25
36
  private
37
+ def value
38
+ if @allow_method_names_outside_object
39
+ object.public_send @method_name if object && object.respond_to?(@method_name)
40
+ else
41
+ object.public_send @method_name if object
42
+ end
43
+ end
26
44
 
27
- def value(object)
28
- object.public_send @method_name if object
29
- end
30
-
31
- def value_before_type_cast(object)
32
- unless object.nil?
33
- method_before_type_cast = @method_name + "_before_type_cast"
45
+ def value_before_type_cast
46
+ unless object.nil?
47
+ method_before_type_cast = @method_name + "_before_type_cast"
34
48
 
35
- if value_came_from_user?(object) && object.respond_to?(method_before_type_cast)
36
- object.public_send(method_before_type_cast)
37
- else
38
- value(object)
49
+ if value_came_from_user? && object.respond_to?(method_before_type_cast)
50
+ object.public_send(method_before_type_cast)
51
+ else
52
+ value
53
+ end
39
54
  end
40
55
  end
41
- end
42
56
 
43
- def value_came_from_user?(object)
44
- method_name = "#{@method_name}_came_from_user?"
45
- !object.respond_to?(method_name) || object.public_send(method_name)
46
- end
57
+ def value_came_from_user?
58
+ method_name = "#{@method_name}_came_from_user?"
59
+ !object.respond_to?(method_name) || object.public_send(method_name)
60
+ end
47
61
 
48
- def retrieve_object(object)
49
- if object
50
- object
51
- elsif @template_object.instance_variable_defined?("@#{@object_name}")
52
- @template_object.instance_variable_get("@#{@object_name}")
62
+ def retrieve_object(object)
63
+ if object
64
+ object
65
+ elsif @template_object.instance_variable_defined?("@#{@object_name}")
66
+ @template_object.instance_variable_get("@#{@object_name}")
67
+ end
68
+ rescue NameError
69
+ # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
70
+ nil
53
71
  end
54
- rescue NameError
55
- # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
56
- nil
57
- end
58
72
 
59
- def retrieve_autoindex(pre_match)
60
- object = self.object || @template_object.instance_variable_get("@#{pre_match}")
61
- if object && object.respond_to?(:to_param)
62
- object.to_param
63
- else
64
- raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
73
+ def retrieve_autoindex(pre_match)
74
+ object = self.object || @template_object.instance_variable_get("@#{pre_match}")
75
+ if object && object.respond_to?(:to_param)
76
+ object.to_param
77
+ else
78
+ raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
79
+ end
65
80
  end
66
- end
67
81
 
68
- def add_default_name_and_id_for_value(tag_value, options)
69
- if tag_value.nil?
70
- add_default_name_and_id(options)
71
- else
72
- specified_id = options["id"]
73
- add_default_name_and_id(options)
82
+ def add_default_name_and_id_for_value(tag_value, options)
83
+ if tag_value.nil?
84
+ add_default_name_and_id(options)
85
+ else
86
+ specified_id = options["id"]
87
+ add_default_name_and_id(options)
74
88
 
75
- if specified_id.blank? && options["id"].present?
76
- options["id"] += "_#{sanitized_value(tag_value)}"
89
+ if specified_id.blank? && options["id"].present?
90
+ options["id"] += "_#{sanitized_value(tag_value)}"
91
+ end
77
92
  end
78
93
  end
79
- end
80
94
 
81
- def add_default_name_and_id(options)
82
- if options.has_key?("index")
83
- options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) }
84
- options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
85
- options.delete("index")
86
- elsif defined?(@auto_index)
87
- options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) }
88
- options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
89
- else
90
- options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) }
91
- options["id"] = options.fetch("id"){ tag_id }
95
+ def add_default_name_and_id(options)
96
+ index = name_and_id_index(options)
97
+ options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
98
+
99
+ if generate_ids?
100
+ options["id"] = options.fetch("id") { tag_id(index) }
101
+ if namespace = options.delete("namespace")
102
+ options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
103
+ end
104
+ end
92
105
  end
93
106
 
94
- options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
95
- end
107
+ def tag_name(multiple = false, index = nil)
108
+ # a little duplication to construct less strings
109
+ case
110
+ when @object_name.empty?
111
+ "#{sanitized_method_name}#{multiple ? "[]" : ""}"
112
+ when index
113
+ "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
114
+ else
115
+ "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
116
+ end
117
+ end
96
118
 
97
- def tag_name(multiple = false)
98
- "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
99
- end
119
+ def tag_id(index = nil)
120
+ # a little duplication to construct less strings
121
+ case
122
+ when @object_name.empty?
123
+ sanitized_method_name.dup
124
+ when index
125
+ "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
126
+ else
127
+ "#{sanitized_object_name}_#{sanitized_method_name}"
128
+ end
129
+ end
100
130
 
101
- def tag_name_with_index(index, multiple = false)
102
- "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
103
- end
131
+ def sanitized_object_name
132
+ @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
133
+ end
104
134
 
105
- def tag_id
106
- "#{sanitized_object_name}_#{sanitized_method_name}"
107
- end
135
+ def sanitized_method_name
136
+ @sanitized_method_name ||= @method_name.sub(/\?$/, "")
137
+ end
108
138
 
109
- def tag_id_with_index(index)
110
- "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
111
- end
139
+ def sanitized_value(value)
140
+ value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
141
+ end
112
142
 
113
- def sanitized_object_name
114
- @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
115
- end
143
+ def select_content_tag(option_tags, options, html_options)
144
+ html_options = html_options.stringify_keys
145
+ add_default_name_and_id(html_options)
116
146
 
117
- def sanitized_method_name
118
- @sanitized_method_name ||= @method_name.sub(/\?$/,"")
119
- end
147
+ if placeholder_required?(html_options)
148
+ raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
149
+ options[:include_blank] ||= true unless options[:prompt]
150
+ end
120
151
 
121
- def sanitized_value(value)
122
- value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
123
- end
152
+ value = options.fetch(:selected) { value() }
153
+ select = content_tag("select", add_options(option_tags, options, value), html_options)
124
154
 
125
- def select_content_tag(option_tags, options, html_options)
126
- html_options = html_options.stringify_keys
127
- add_default_name_and_id(html_options)
128
- options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options)
129
- value = options.fetch(:selected) { value(object) }
130
- select = content_tag("select", add_options(option_tags, options, value), html_options)
155
+ if html_options["multiple"] && options.fetch(:include_hidden, true)
156
+ tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select
157
+ else
158
+ select
159
+ end
160
+ end
131
161
 
132
- if html_options["multiple"] && options.fetch(:include_hidden, true)
133
- tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select
134
- else
135
- select
162
+ def placeholder_required?(html_options)
163
+ # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
164
+ html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
136
165
  end
137
- end
138
166
 
139
- def select_not_required?(html_options)
140
- !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1
141
- end
167
+ def add_options(option_tags, options, value = nil)
168
+ if options[:include_blank]
169
+ option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags
170
+ end
171
+ if value.blank? && options[:prompt]
172
+ tag_options = { value: "" }.tap do |prompt_opts|
173
+ prompt_opts[:disabled] = true if options[:disabled] == ""
174
+ prompt_opts[:selected] = true if options[:selected] == ""
175
+ end
176
+ option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
177
+ end
178
+ option_tags
179
+ end
142
180
 
143
- def add_options(option_tags, options, value = nil)
144
- if options[:include_blank]
145
- option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags
181
+ def name_and_id_index(options)
182
+ if options.key?("index")
183
+ options.delete("index") || ""
184
+ elsif @generate_indexed_names
185
+ @auto_index || ""
186
+ end
146
187
  end
147
- if value.blank? && options[:prompt]
148
- option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags
188
+
189
+ def generate_ids?
190
+ !@skip_default_ids
149
191
  end
150
- option_tags
151
- end
152
192
  end
153
193
  end
154
194
  end