actionview 8.0.5.2 → 8.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -171
  3. data/lib/action_view/base.rb +4 -2
  4. data/lib/action_view/buffers.rb +1 -1
  5. data/lib/action_view/dependency_tracker/erb_tracker.rb +1 -1
  6. data/lib/action_view/dependency_tracker.rb +6 -1
  7. data/lib/action_view/gem_version.rb +3 -3
  8. data/lib/action_view/helpers/asset_tag_helper.rb +20 -4
  9. data/lib/action_view/helpers/capture_helper.rb +2 -2
  10. data/lib/action_view/helpers/controller_helper.rb +6 -2
  11. data/lib/action_view/helpers/date_helper.rb +17 -0
  12. data/lib/action_view/helpers/form_helper.rb +2 -3
  13. data/lib/action_view/helpers/form_options_helper.rb +10 -12
  14. data/lib/action_view/helpers/form_tag_helper.rb +17 -10
  15. data/lib/action_view/helpers/javascript_helper.rb +5 -1
  16. data/lib/action_view/helpers/number_helper.rb +14 -0
  17. data/lib/action_view/helpers/sanitize_helper.rb +1 -6
  18. data/lib/action_view/helpers/tag_helper.rb +37 -52
  19. data/lib/action_view/helpers/tags/base.rb +9 -11
  20. data/lib/action_view/helpers/tags/check_box.rb +2 -2
  21. data/lib/action_view/helpers/tags/datetime_field.rb +1 -1
  22. data/lib/action_view/helpers/tags/file_field.rb +1 -4
  23. data/lib/action_view/helpers/tags/hidden_field.rb +1 -1
  24. data/lib/action_view/helpers/tags/label.rb +10 -3
  25. data/lib/action_view/helpers/tags/radio_button.rb +1 -1
  26. data/lib/action_view/helpers/tags/select.rb +6 -1
  27. data/lib/action_view/helpers/tags/select_renderer.rb +3 -3
  28. data/lib/action_view/helpers/tags/text_area.rb +1 -1
  29. data/lib/action_view/helpers/tags/text_field.rb +1 -1
  30. data/lib/action_view/helpers/text_helper.rb +7 -23
  31. data/lib/action_view/helpers/translation_helper.rb +6 -1
  32. data/lib/action_view/helpers/url_helper.rb +37 -9
  33. data/lib/action_view/locale/en.yml +3 -0
  34. data/lib/action_view/railtie.rb +11 -0
  35. data/lib/action_view/record_identifier.rb +21 -0
  36. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +1 -5
  37. data/lib/action_view/renderer/partial_renderer.rb +16 -0
  38. data/lib/action_view/template/error.rb +7 -3
  39. data/lib/action_view/template/handlers/erb.rb +36 -11
  40. data/lib/action_view/template.rb +2 -8
  41. data/lib/action_view/test_case.rb +50 -52
  42. data/lib/action_view.rb +3 -0
  43. metadata +11 -17
@@ -26,6 +26,8 @@ module ActionView
26
26
 
27
27
  # Delegates to ActiveSupport::NumberHelper#number_to_phone.
28
28
  #
29
+ # number_to_phone("1234567890") # => "123-456-7890"
30
+ #
29
31
  # Additionally, supports a +:raise+ option that will cause
30
32
  # InvalidNumberError to be raised if +number+ is not a valid number:
31
33
  #
@@ -42,6 +44,8 @@ module ActionView
42
44
 
43
45
  # Delegates to ActiveSupport::NumberHelper#number_to_currency.
44
46
  #
47
+ # number_to_currency("1234") # => "$1234.00"
48
+ #
45
49
  # Additionally, supports a +:raise+ option that will cause
46
50
  # InvalidNumberError to be raised if +number+ is not a valid number:
47
51
  #
@@ -54,6 +58,8 @@ module ActionView
54
58
 
55
59
  # Delegates to ActiveSupport::NumberHelper#number_to_percentage.
56
60
  #
61
+ # number_to_percentage("99") # => "99.000%"
62
+ #
57
63
  # Additionally, supports a +:raise+ option that will cause
58
64
  # InvalidNumberError to be raised if +number+ is not a valid number:
59
65
  #
@@ -66,6 +72,8 @@ module ActionView
66
72
 
67
73
  # Delegates to ActiveSupport::NumberHelper#number_to_delimited.
68
74
  #
75
+ # number_with_delimiter("1234") # => "1,234"
76
+ #
69
77
  # Additionally, supports a +:raise+ option that will cause
70
78
  # InvalidNumberError to be raised if +number+ is not a valid number:
71
79
  #
@@ -78,6 +86,8 @@ module ActionView
78
86
 
79
87
  # Delegates to ActiveSupport::NumberHelper#number_to_rounded.
80
88
  #
89
+ # number_with_precision("1234") # => "1234.000"
90
+ #
81
91
  # Additionally, supports a +:raise+ option that will cause
82
92
  # InvalidNumberError to be raised if +number+ is not a valid number:
83
93
  #
@@ -90,6 +100,8 @@ module ActionView
90
100
 
91
101
  # Delegates to ActiveSupport::NumberHelper#number_to_human_size.
92
102
  #
103
+ # number_to_human_size("1234") # => "1.21 KB"
104
+ #
93
105
  # Additionally, supports a +:raise+ option that will cause
94
106
  # InvalidNumberError to be raised if +number+ is not a valid number:
95
107
  #
@@ -102,6 +114,8 @@ module ActionView
102
114
 
103
115
  # Delegates to ActiveSupport::NumberHelper#number_to_human.
104
116
  #
117
+ # number_to_human("1234") # => "1.23 Thousand"
118
+ #
105
119
  # Additionally, supports a +:raise+ option that will cause
106
120
  # InvalidNumberError to be raised if +number+ is not a valid number:
107
121
  #
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string/output_safety"
4
3
  require "rails-html-sanitizer"
5
4
 
6
5
  module ActionView
@@ -34,9 +33,6 @@ module ActionView
34
33
  # Please note that sanitizing user-provided text does not guarantee that the
35
34
  # resulting markup is valid or even well-formed.
36
35
  #
37
- # The output is markup, which is safe only as element content. Do not use it in an attribute
38
- # value; use +strip_tags+ or a tag helper such as <tt>tag.a(title: ...)</tt> instead.
39
- #
40
36
  # ==== Options
41
37
  #
42
38
  # [+:tags+]
@@ -141,8 +137,7 @@ module ActionView
141
137
  # strip_tags("> A quote from Smith & Wesson")
142
138
  # # => &gt; A quote from Smith &amp; Wesson
143
139
  def strip_tags(html)
144
- stripped = self.class.full_sanitizer.sanitize(html)
145
- ERB::Util.html_escape_once(stripped) if stripped
140
+ self.class.full_sanitizer.sanitize(html)&.html_safe
146
141
  end
147
142
 
148
143
  # Strips all link tags from +html+ leaving just the link text.
@@ -44,9 +44,6 @@ module ActionView
44
44
  PRE_CONTENT_STRINGS["textarea"] = "\n"
45
45
 
46
46
  class TagBuilder # :nodoc:
47
- include CaptureHelper
48
- include OutputSafetyHelper
49
-
50
47
  def self.define_element(name, code_generator:, method_name: name)
51
48
  return if method_defined?(name)
52
49
 
@@ -226,17 +223,7 @@ module ActionView
226
223
  tag_options(attributes.to_h).to_s.strip.html_safe
227
224
  end
228
225
 
229
- def tag_string(name, content = nil, options, escape: true, &block)
230
- content = @view_context.capture(self, &block) if block
231
-
232
- content_tag_string(name, content, options, escape)
233
- end
234
-
235
- def self_closing_tag_string(name, options, escape = true, tag_suffix = " />")
236
- "<#{name}#{tag_options(options, escape)}#{tag_suffix}".html_safe
237
- end
238
-
239
- def content_tag_string(name, content, options, escape = true)
226
+ def content_tag_string(name, content, options, escape = true) # :nodoc:
240
227
  tag_options = tag_options(options, escape) if options
241
228
 
242
229
  if escape && content.present?
@@ -245,82 +232,80 @@ module ActionView
245
232
  "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
246
233
  end
247
234
 
248
- def tag_options(options, escape = true)
235
+ def tag_options(options, escape = true) # :nodoc:
249
236
  return if options.blank?
250
237
  output = +""
251
238
  sep = " "
252
239
  options.each_pair do |key, value|
253
- next if key.blank?
254
-
255
- name = Symbol === key ? key.name : key.to_s
256
- next if name.blank?
257
-
258
240
  type = TAG_TYPES[key]
259
241
  if type == :data && value.is_a?(Hash)
260
242
  value.each_pair do |k, v|
261
- next if k.blank? || v.nil?
262
-
263
- nested_name = Symbol === k ? k.name : k.to_s
264
- next if nested_name.blank?
265
-
243
+ next if v.nil?
266
244
  output << sep
267
- output << prefix_tag_option(name, nested_name, v, escape)
245
+ output << prefix_tag_option(key, k, v, escape)
268
246
  end
269
247
  elsif type == :aria && value.is_a?(Hash)
270
248
  value.each_pair do |k, v|
271
- next if k.blank? || v.nil?
272
-
273
- nested_name = Symbol === k ? k.name : k.to_s
274
- next if nested_name.blank?
249
+ next if v.nil?
275
250
 
276
251
  case v
277
252
  when Array, Hash
278
253
  tokens = TagHelper.build_tag_values(v)
279
254
  next if tokens.none?
280
255
 
281
- v = safe_join(tokens, " ")
256
+ v = @view_context.safe_join(tokens, " ")
282
257
  else
283
258
  v = v.to_s
284
259
  end
285
260
 
286
261
  output << sep
287
- output << prefix_tag_option(name, nested_name, v, escape)
262
+ output << prefix_tag_option(key, k, v, escape)
288
263
  end
289
264
  elsif type == :boolean
290
265
  if value
291
266
  output << sep
292
- output << boolean_tag_option(name)
267
+ output << boolean_tag_option(key)
293
268
  end
294
269
  elsif !value.nil?
295
270
  output << sep
296
- output << tag_option(name, value, escape)
271
+ output << tag_option(key, value, escape)
297
272
  end
298
273
  end
299
274
  output unless output.empty?
300
275
  end
301
276
 
302
- def boolean_tag_option(key)
303
- %(#{key}="#{key}")
304
- end
277
+ private
278
+ def tag_string(name, content = nil, options, escape: true, &block)
279
+ content = @view_context.capture(self, &block) if block
305
280
 
306
- def tag_option(key, value, escape)
307
- key = ERB::Util.xml_name_escape(key) if escape
308
-
309
- case value
310
- when Array, Hash
311
- value = TagHelper.build_tag_values(value) if key.to_s == "class"
312
- value = escape ? safe_join(value, " ") : value.join(" ")
313
- when Regexp
314
- value = escape ? ERB::Util.unwrapped_html_escape(value.source) : value.source
315
- else
316
- value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
281
+ content_tag_string(name, content, options, escape)
317
282
  end
318
- value = value.gsub('"', "&quot;") if value.include?('"')
319
283
 
320
- %(#{key}="#{value}")
321
- end
284
+ def self_closing_tag_string(name, options, escape = true, tag_suffix = " />")
285
+ "<#{name}#{tag_options(options, escape)}#{tag_suffix}".html_safe
286
+ end
287
+
288
+ def boolean_tag_option(key)
289
+ %(#{key}="#{key}")
290
+ end
291
+
292
+ def tag_option(key, value, escape)
293
+ key = ERB::Util.xml_name_escape(key) if escape
294
+
295
+ case value
296
+ when Array, Hash
297
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
298
+ value = escape ? @view_context.safe_join(value, " ") : value.join(" ")
299
+ when Regexp
300
+ value = escape ? ERB::Util.unwrapped_html_escape(value.source) : value.source
301
+ else
302
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
303
+ end
304
+ value = value.gsub('"', "&quot;") if value.include?('"')
305
+
306
+ %(#{key}="#{value}")
307
+ end
322
308
 
323
- private
324
309
  def prefix_tag_option(prefix, key, value, escape)
325
310
  key = "#{prefix}-#{key.to_s.dasherize}"
326
311
  unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
@@ -80,32 +80,30 @@ module ActionView
80
80
  end
81
81
  end
82
82
 
83
- def add_default_name_and_field_for_value(tag_value, options, field = "id")
83
+ def add_default_name_and_id_for_value(tag_value, options)
84
84
  if tag_value.nil?
85
- add_default_name_and_field(options, field)
85
+ add_default_name_and_id(options)
86
86
  else
87
- specified_field = options[field]
88
- add_default_name_and_field(options, field)
87
+ specified_id = options["id"]
88
+ add_default_name_and_id(options)
89
89
 
90
- if specified_field.blank? && options[field].present?
91
- options[field] += "_#{sanitized_value(tag_value)}"
90
+ if specified_id.blank? && options["id"].present?
91
+ options["id"] += "_#{sanitized_value(tag_value)}"
92
92
  end
93
93
  end
94
94
  end
95
- alias_method :add_default_name_and_id_for_value, :add_default_name_and_field_for_value
96
95
 
97
- def add_default_name_and_field(options, field = "id")
96
+ def add_default_name_and_id(options)
98
97
  index = name_and_id_index(options)
99
98
  options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
100
99
 
101
100
  if generate_ids?
102
- options[field] = options.fetch(field) { tag_id(index, options.delete("namespace")) }
101
+ options["id"] = options.fetch("id") { tag_id(index, options.delete("namespace")) }
103
102
  if namespace = options.delete("namespace")
104
- options[field] = options[field] ? "#{namespace}_#{options[field]}" : namespace
103
+ options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
105
104
  end
106
105
  end
107
106
  end
108
- alias_method :add_default_name_and_id, :add_default_name_and_field
109
107
 
110
108
  def tag_name(multiple = false, index = nil)
111
109
  @template_object.field_name(@object_name, sanitized_method_name, multiple: multiple, index: index)
@@ -21,10 +21,10 @@ module ActionView
21
21
  options["checked"] = "checked" if input_checked?(options)
22
22
 
23
23
  if options["multiple"]
24
- add_default_name_and_field_for_value(@checked_value, options)
24
+ add_default_name_and_id_for_value(@checked_value, options)
25
25
  options.delete("multiple")
26
26
  else
27
- add_default_name_and_field(options)
27
+ add_default_name_and_id(options)
28
28
  end
29
29
 
30
30
  include_hidden = options.delete("include_hidden") { true }
@@ -6,7 +6,7 @@ module ActionView
6
6
  class DatetimeField < TextField # :nodoc:
7
7
  def render
8
8
  options = @options.stringify_keys
9
- options["value"] = datetime_value(options["value"] || value)
9
+ options["value"] = datetime_value(options.fetch("value", value))
10
10
  options["min"] = format_datetime(parse_datetime(options["min"]))
11
11
  options["max"] = format_datetime(parse_datetime(options["max"]))
12
12
  @options = options
@@ -6,11 +6,8 @@ module ActionView
6
6
  class FileField < TextField # :nodoc:
7
7
  def render
8
8
  include_hidden = @options.delete(:include_hidden)
9
- if @options[:accept].is_a?(Array)
10
- @options[:accept] = @options[:accept].join(",")
11
- end
12
9
  options = @options.stringify_keys
13
- add_default_name_and_field(options)
10
+ add_default_name_and_id(options)
14
11
 
15
12
  if options["multiple"] && include_hidden
16
13
  hidden_field_for_multiple_file(options) + super
@@ -5,7 +5,7 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class HiddenField < TextField # :nodoc:
7
7
  def render
8
- @options[:autocomplete] = "off"
8
+ @options.reverse_merge!(autocomplete: "off")
9
9
  super
10
10
  end
11
11
  end
@@ -48,11 +48,18 @@ module ActionView
48
48
  def render(&block)
49
49
  options = @options.stringify_keys
50
50
  tag_value = options.delete("value")
51
+ name_and_id = options.dup
51
52
 
52
- add_default_name_and_field_for_value(tag_value, options, "for")
53
+ if name_and_id["for"]
54
+ name_and_id["id"] = name_and_id["for"]
55
+ else
56
+ name_and_id.delete("id")
57
+ end
58
+
59
+ add_default_name_and_id_for_value(tag_value, name_and_id)
53
60
  options.delete("index")
54
- options.delete("name")
55
61
  options.delete("namespace")
62
+ options["for"] = name_and_id["id"] unless options.key?("for")
56
63
 
57
64
  builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)
58
65
 
@@ -64,7 +71,7 @@ module ActionView
64
71
  render_component(builder)
65
72
  end
66
73
 
67
- label_tag(options["for"], content, options)
74
+ label_tag(name_and_id["id"], content, options)
68
75
  end
69
76
 
70
77
  private
@@ -18,7 +18,7 @@ module ActionView
18
18
  options["type"] = "radio"
19
19
  options["value"] = @tag_value
20
20
  options["checked"] = "checked" if input_checked?(options)
21
- add_default_name_and_field_for_value(@tag_value, options)
21
+ add_default_name_and_id_for_value(@tag_value, options)
22
22
  tag("input", options)
23
23
  end
24
24
 
@@ -37,7 +37,12 @@ module ActionView
37
37
  # [nil, []]
38
38
  # { nil => [] }
39
39
  def grouped_choices?
40
- !@choices.blank? && @choices.first.respond_to?(:second) && Array === @choices.first.second
40
+ return false if @choices.blank?
41
+
42
+ first_choice = @choices.first
43
+ return false unless first_choice.is_a?(Enumerable)
44
+
45
+ first_choice.second.is_a?(Array)
41
46
  end
42
47
  end
43
48
  end
@@ -11,7 +11,7 @@ module ActionView
11
11
  html_options[prop.to_s] = options.delete(prop) if options.key?(prop) && !html_options.key?(prop.to_s)
12
12
  end
13
13
 
14
- add_default_name_and_field(html_options)
14
+ add_default_name_and_id(html_options)
15
15
 
16
16
  if placeholder_required?(html_options)
17
17
  raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
@@ -37,7 +37,7 @@ module ActionView
37
37
  if options[:include_blank]
38
38
  content = (options[:include_blank] if options[:include_blank].is_a?(String))
39
39
  label = (" " unless content)
40
- option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
40
+ option_tags = tag_builder.option(content, value: "", label: label) + "\n" + option_tags
41
41
  end
42
42
 
43
43
  if value.blank? && options[:prompt]
@@ -45,7 +45,7 @@ module ActionView
45
45
  prompt_opts[:disabled] = true if options[:disabled] == ""
46
46
  prompt_opts[:selected] = true if options[:selected] == ""
47
47
  end
48
- option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
48
+ option_tags = tag_builder.option(prompt_text(options[:prompt]), **tag_options) + "\n" + option_tags
49
49
  end
50
50
 
51
51
  option_tags
@@ -10,7 +10,7 @@ module ActionView
10
10
 
11
11
  def render
12
12
  options = @options.stringify_keys
13
- add_default_name_and_field(options)
13
+ add_default_name_and_id(options)
14
14
 
15
15
  if size = options.delete("size")
16
16
  options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
@@ -13,7 +13,7 @@ module ActionView
13
13
  options["size"] = options["maxlength"] unless options.key?("size")
14
14
  options["type"] ||= field_type
15
15
  options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
16
- add_default_name_and_field(options)
16
+ add_default_name_and_id(options)
17
17
  tag("input", options)
18
18
  end
19
19
 
@@ -143,15 +143,8 @@ module ActionView
143
143
  #
144
144
  # [+:highlighter+]
145
145
  # The highlighter string. Uses <tt>\1</tt> as the placeholder for a
146
- # phrase. Defaults to <tt>"<mark>\1</mark>"</tt>. The numbered placeholders
147
- # <tt>\0</tt> to <tt>\9</tt> are expanded; other +String#sub+ replacement escapes
148
- # are not. The phrase is HTML escaped, quotes included. This option
149
- # is ignored if a block is specified.
150
- #
151
- # WARNING: Put a placeholder only in element content or inside a
152
- # quoted attribute value. HTML escaping does not make the phrase safe
153
- # in an unquoted attribute value, in a URL, JavaScript or CSS
154
- # context, or in an attribute whose value is HTML, such as +srcdoc+.
146
+ # phrase, similar to +String#sub+. Defaults to <tt>"<mark>\1</mark>"</tt>.
147
+ # This option is ignored if a block is specified.
155
148
  #
156
149
  # [+:sanitize+]
157
150
  # Whether to sanitize +text+ before highlighting. Defaults to true.
@@ -170,8 +163,8 @@ module ActionView
170
163
  # highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>')
171
164
  # # => "You searched <em>for</em>: <em>rails</em>"
172
165
  #
173
- # highlight('You searched for: rails', 'rails', highlighter: '<mark title="\1">\1</mark>')
174
- # # => "You searched for: <mark title=\"rails\">rails</mark>"
166
+ # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
167
+ # # => "You searched for: <a href=\"search?q=rails\">rails</a>"
175
168
  #
176
169
  # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match)) }
177
170
  # # => "You searched for: <a href=\"search?q=rails\">rails</a>"
@@ -186,23 +179,14 @@ module ActionView
186
179
  else
187
180
  patterns = Array(phrases).map { |phrase| Regexp === phrase ? phrase : Regexp.escape(phrase) }
188
181
  pattern = /(#{patterns.join("|")})/i
189
- # The text after a tag begins with the ">" that closes it. Leave it
190
- # in the segment so phrases can see it as context, but keep it out
191
- # of the match so the highlighter can't take the place of the ">".
192
- pattern_after_tag = /(?!\A)#{pattern}/
193
182
  highlighter = options.fetch(:highlighter, '<mark>\1</mark>') unless block
194
183
 
195
- text.scan(/<[^>]*|[^<]+/).each_with_index do |segment, index|
184
+ text.scan(/<[^>]*|[^<]+/).each do |segment|
196
185
  if !segment.start_with?("<")
197
- segment_pattern = index > 0 ? pattern_after_tag : pattern
198
-
199
186
  if block
200
- segment.gsub!(segment_pattern, &block)
187
+ segment.gsub!(pattern, &block)
201
188
  else
202
- segment.gsub!(segment_pattern) do
203
- match = Regexp.last_match
204
- highlighter.gsub(/\\(\d)/) { ERB::Util.html_escape_once(match[$1.to_i]) }
205
- end
189
+ segment.gsub!(pattern, highlighter)
206
190
  end
207
191
  end
208
192
  end.join
@@ -140,7 +140,12 @@ module ActionView
140
140
  end
141
141
 
142
142
  def missing_translation(key, options)
143
- keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
143
+ locale = options[:locale] || I18n.locale
144
+
145
+ i18n_exception = I18n::MissingTranslation.new(locale, key, options)
146
+ I18n.exception_handler.call(i18n_exception, locale, key, options)
147
+
148
+ keys = I18n.normalize_keys(locale, key, options[:scope])
144
149
 
145
150
  title = +"translation missing: #{keys.join(".")}"
146
151
 
@@ -341,8 +341,9 @@ module ActionView
341
341
  inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
342
342
  if params
343
343
  to_form_params(params).each do |param|
344
- inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value],
345
- autocomplete: "off")
344
+ options = { type: "hidden", name: param[:name], value: param[:value] }
345
+ options[:autocomplete] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
346
+ inner_tags.safe_concat tag(:input, **options)
346
347
  end
347
348
  end
348
349
  html = content_tag("form", inner_tags, form_options)
@@ -538,24 +539,47 @@ module ActionView
538
539
  # current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
539
540
  # # => true
540
541
  #
541
- # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
542
+ # Different actions may share the same URL path but have a different HTTP method. Let's say we
543
+ # sent a POST to <tt>http://www.example.com/products</tt> and rendered a validation error.
542
544
  #
543
545
  # current_page?(controller: 'product', action: 'index')
544
546
  # # => false
545
547
  #
548
+ # current_page?(controller: 'product', action: 'create')
549
+ # # => false
550
+ #
551
+ # current_page?(controller: 'product', action: 'create', method: :post)
552
+ # # => true
553
+ #
554
+ # current_page?(controller: 'product', action: 'index', method: [:get, :post])
555
+ # # => true
556
+ #
546
557
  # We can also pass in the symbol arguments instead of strings.
547
558
  #
548
- def current_page?(options = nil, check_parameters: false, **options_as_kwargs)
559
+ def current_page?(options = nil, check_parameters: false, method: :get, **options_as_kwargs)
549
560
  unless request
550
561
  raise "You cannot use helpers that need to determine the current " \
551
562
  "page unless your view context provides a Request object " \
552
563
  "in a #request method"
553
564
  end
554
565
 
555
- return false unless request.get? || request.head?
566
+ if options.is_a?(Hash)
567
+ check_parameters = options.delete(:check_parameters) { check_parameters }
568
+ method = options.delete(:method) { method }
569
+ else
570
+ options ||= options_as_kwargs
571
+ end
572
+
573
+ method_matches = case method
574
+ when :get
575
+ request.get? || request.head?
576
+ when Array
577
+ method.include?(request.method_symbol) || (method.include?(:get) && request.head?)
578
+ else
579
+ method == request.method_symbol
580
+ end
581
+ return false unless method_matches
556
582
 
557
- options ||= options_as_kwargs
558
- check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
559
583
  url_string = URI::RFC2396_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
560
584
 
561
585
  # We ignore any extra parameters in the request_uri if the
@@ -751,14 +775,18 @@ module ActionView
751
775
  else
752
776
  token
753
777
  end
754
- tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token, autocomplete: "off")
778
+ options = { type: "hidden", name: request_forgery_protection_token.to_s, value: token }
779
+ options[:autocomplete] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
780
+ tag(:input, **options)
755
781
  else
756
782
  ""
757
783
  end
758
784
  end
759
785
 
760
786
  def method_tag(method)
761
- tag("input", type: "hidden", name: "_method", value: method.to_s, autocomplete: "off")
787
+ options = { type: "hidden", name: "_method", value: method.to_s }
788
+ options[:autocomplete] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
789
+ tag("input", **options)
762
790
  end
763
791
 
764
792
  # Returns an array of hashes each containing :name and :value keys
@@ -43,6 +43,9 @@
43
43
  hour: "Hour"
44
44
  minute: "Minute"
45
45
  second: "Seconds"
46
+ relative:
47
+ future: "in %{time}"
48
+ past: "%{time} ago"
46
49
 
47
50
  helpers:
48
51
  select:
@@ -72,8 +72,19 @@ module ActionView
72
72
  end
73
73
 
74
74
  config.after_initialize do |app|
75
+ ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_scripts = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["script-src", "script-src-elem", "script-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
76
+ ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_styles = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["style-src", "style-src-elem", "style-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
77
+ ActionView::Helpers::JavaScriptHelper.auto_include_nonce = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["script-src", "script-src-elem", "script-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
78
+ end
79
+
80
+ config.after_initialize do |app|
81
+ config.after_initialize do
82
+ ActionView.render_tracker = config.action_view.render_tracker
83
+ end
84
+
75
85
  ActiveSupport.on_load(:action_view) do
76
86
  app.config.action_view.each do |k, v|
87
+ next if k == :render_tracker
77
88
  send "#{k}=", v
78
89
  end
79
90
  end
@@ -101,6 +101,27 @@ module ActionView
101
101
  end
102
102
  end
103
103
 
104
+ # The DOM target convention is to concatenate any number of parameters into a string.
105
+ # Records are passed through dom_id, while string and symbols are retained.
106
+ #
107
+ # dom_target(Post.find(45)) # => "post_45"
108
+ # dom_target(Post.find(45), :edit) # => "post_45_edit"
109
+ # dom_target(Post.find(45), :edit, :special) # => "post_45_edit_special"
110
+ # dom_target(Post.find(45), Comment.find(1)) # => "post_45_comment_1"
111
+ def dom_target(*objects)
112
+ objects.map! do |object|
113
+ case object
114
+ when Symbol, String
115
+ object
116
+ when Class
117
+ dom_class(object)
118
+ else
119
+ dom_id(object)
120
+ end
121
+ end
122
+ objects.join(JOIN)
123
+ end
124
+
104
125
  private
105
126
  # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
106
127
  # This can be overwritten to customize the default generated string representation if desired.