actionview 7.0.8.1 → 7.2.2.1

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -425
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +52 -14
  8. data/lib/action_view/buffers.rb +106 -8
  9. data/lib/action_view/cache_expiry.rb +44 -41
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/dependency_tracker/{ripper_tracker.rb → ruby_tracker.rb} +4 -3
  12. data/lib/action_view/dependency_tracker.rb +1 -1
  13. data/lib/action_view/deprecator.rb +7 -0
  14. data/lib/action_view/digestor.rb +1 -1
  15. data/lib/action_view/gem_version.rb +3 -3
  16. data/lib/action_view/helpers/active_model_helper.rb +1 -1
  17. data/lib/action_view/helpers/asset_tag_helper.rb +151 -55
  18. data/lib/action_view/helpers/asset_url_helper.rb +6 -5
  19. data/lib/action_view/helpers/atom_feed_helper.rb +5 -5
  20. data/lib/action_view/helpers/cache_helper.rb +7 -13
  21. data/lib/action_view/helpers/capture_helper.rb +30 -10
  22. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  23. data/lib/action_view/helpers/controller_helper.rb +6 -0
  24. data/lib/action_view/helpers/csp_helper.rb +2 -2
  25. data/lib/action_view/helpers/csrf_helper.rb +3 -3
  26. data/lib/action_view/helpers/date_helper.rb +17 -19
  27. data/lib/action_view/helpers/debug_helper.rb +3 -3
  28. data/lib/action_view/helpers/form_helper.rb +248 -214
  29. data/lib/action_view/helpers/form_options_helper.rb +2 -1
  30. data/lib/action_view/helpers/form_tag_helper.rb +125 -58
  31. data/lib/action_view/helpers/javascript_helper.rb +1 -0
  32. data/lib/action_view/helpers/number_helper.rb +37 -330
  33. data/lib/action_view/helpers/output_safety_helper.rb +6 -6
  34. data/lib/action_view/helpers/rendering_helper.rb +1 -1
  35. data/lib/action_view/helpers/sanitize_helper.rb +51 -21
  36. data/lib/action_view/helpers/tag_helper.rb +210 -42
  37. data/lib/action_view/helpers/tags/base.rb +11 -52
  38. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  39. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  40. data/lib/action_view/helpers/tags/collection_select.rb +3 -0
  41. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  42. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  43. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  45. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  46. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  47. data/lib/action_view/helpers/tags/select.rb +3 -0
  48. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  49. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  50. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  51. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  52. data/lib/action_view/helpers/tags/weekday_select.rb +3 -0
  53. data/lib/action_view/helpers/tags.rb +2 -0
  54. data/lib/action_view/helpers/text_helper.rb +157 -85
  55. data/lib/action_view/helpers/translation_helper.rb +3 -3
  56. data/lib/action_view/helpers/url_helper.rb +35 -80
  57. data/lib/action_view/helpers.rb +2 -0
  58. data/lib/action_view/layouts.rb +8 -8
  59. data/lib/action_view/log_subscriber.rb +57 -36
  60. data/lib/action_view/lookup_context.rb +29 -13
  61. data/lib/action_view/path_registry.rb +57 -0
  62. data/lib/action_view/path_set.rb +13 -14
  63. data/lib/action_view/railtie.rb +25 -3
  64. data/lib/action_view/record_identifier.rb +15 -8
  65. data/lib/action_view/render_parser/prism_render_parser.rb +127 -0
  66. data/lib/action_view/render_parser/ripper_render_parser.rb +341 -0
  67. data/lib/action_view/render_parser.rb +21 -169
  68. data/lib/action_view/renderer/abstract_renderer.rb +2 -2
  69. data/lib/action_view/renderer/collection_renderer.rb +10 -2
  70. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +2 -1
  71. data/lib/action_view/renderer/partial_renderer.rb +2 -1
  72. data/lib/action_view/renderer/renderer.rb +34 -38
  73. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -2
  74. data/lib/action_view/renderer/template_renderer.rb +3 -2
  75. data/lib/action_view/rendering.rb +26 -8
  76. data/lib/action_view/template/error.rb +14 -1
  77. data/lib/action_view/template/handlers/builder.rb +4 -4
  78. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  79. data/lib/action_view/template/handlers/erb.rb +73 -1
  80. data/lib/action_view/template/handlers.rb +1 -1
  81. data/lib/action_view/template/html.rb +1 -1
  82. data/lib/action_view/template/raw_file.rb +1 -1
  83. data/lib/action_view/template/renderable.rb +8 -2
  84. data/lib/action_view/template/resolver.rb +9 -3
  85. data/lib/action_view/template/text.rb +1 -1
  86. data/lib/action_view/template/types.rb +25 -34
  87. data/lib/action_view/template.rb +278 -55
  88. data/lib/action_view/template_path.rb +2 -0
  89. data/lib/action_view/test_case.rb +181 -28
  90. data/lib/action_view/unbound_template.rb +17 -7
  91. data/lib/action_view/version.rb +1 -1
  92. data/lib/action_view/view_paths.rb +15 -24
  93. data/lib/action_view.rb +4 -1
  94. metadata +31 -31
  95. data/lib/action_view/ripper_ast_parser.rb +0 -198
  96. data/lib/assets/compiled/rails-ujs.js +0 -777
@@ -1,14 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/code_generator"
3
4
  require "active_support/core_ext/enumerable"
4
5
  require "active_support/core_ext/string/output_safety"
6
+ require "active_support/core_ext/string/inflections"
5
7
  require "set"
6
8
  require "action_view/helpers/capture_helper"
7
9
  require "action_view/helpers/output_safety_helper"
8
10
 
9
11
  module ActionView
10
- # = Action View Tag Helpers
11
12
  module Helpers # :nodoc:
13
+ # = Action View Tag \Helpers
14
+ #
12
15
  # Provides methods to generate HTML tags programmatically both as a modern
13
16
  # HTML5 compliant builder style and legacy XHTML compliant tags.
14
17
  module TagHelper
@@ -45,8 +48,183 @@ module ActionView
45
48
  include CaptureHelper
46
49
  include OutputSafetyHelper
47
50
 
48
- HTML_VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set
49
- SVG_SELF_CLOSING_ELEMENTS = %i(animate animateMotion animateTransform circle ellipse line path polygon polyline rect set stop use view).to_set
51
+ def self.define_element(name, code_generator:, method_name: name.to_s.underscore)
52
+ code_generator.define_cached_method(method_name, namespace: :tag_builder) do |batch|
53
+ batch.push(<<~RUBY) unless instance_methods.include?(method_name.to_sym)
54
+ def #{method_name}(content = nil, escape: true, **options, &block)
55
+ tag_string("#{name}", content, options, escape: escape, &block)
56
+ end
57
+ RUBY
58
+ end
59
+ end
60
+
61
+ def self.define_void_element(name, code_generator:, method_name: name.to_s.underscore)
62
+ code_generator.define_cached_method(method_name, namespace: :tag_builder) do |batch|
63
+ batch.push(<<~RUBY)
64
+ def #{method_name}(content = nil, escape: true, **options, &block)
65
+ if content || block
66
+ ActionView.deprecator.warn <<~TEXT
67
+ Putting content inside a void element (#{name}) is invalid
68
+ according to the HTML5 spec, and so it is being deprecated
69
+ without replacement. In Rails 8.0, passing content as a
70
+ positional argument will raise, and using a block will have
71
+ no effect.
72
+ TEXT
73
+ tag_string("#{name}", content, options, escape: escape, &block)
74
+ else
75
+ self_closing_tag_string("#{name}", options, escape, ">")
76
+ end
77
+ end
78
+ RUBY
79
+ end
80
+ end
81
+
82
+ def self.define_self_closing_element(name, code_generator:, method_name: name.to_s.underscore)
83
+ code_generator.define_cached_method(method_name, namespace: :tag_builder) do |batch|
84
+ batch.push(<<~RUBY)
85
+ def #{method_name}(content = nil, escape: true, **options, &block)
86
+ if content || block
87
+ tag_string("#{name}", content, options, escape: escape, &block)
88
+ else
89
+ self_closing_tag_string("#{name}", options, escape)
90
+ end
91
+ end
92
+ RUBY
93
+ end
94
+ end
95
+
96
+ ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
97
+ define_void_element :area, code_generator: code_generator
98
+ define_void_element :base, code_generator: code_generator
99
+ define_void_element :br, code_generator: code_generator
100
+ define_void_element :col, code_generator: code_generator
101
+ define_void_element :embed, code_generator: code_generator
102
+ define_void_element :hr, code_generator: code_generator
103
+ define_void_element :img, code_generator: code_generator
104
+ define_void_element :input, code_generator: code_generator
105
+ define_void_element :keygen, code_generator: code_generator
106
+ define_void_element :link, code_generator: code_generator
107
+ define_void_element :meta, code_generator: code_generator
108
+ define_void_element :source, code_generator: code_generator
109
+ define_void_element :track, code_generator: code_generator
110
+ define_void_element :wbr, code_generator: code_generator
111
+
112
+ define_self_closing_element :animate, code_generator: code_generator
113
+ define_self_closing_element :animateMotion, code_generator: code_generator
114
+ define_self_closing_element :animateTransform, code_generator: code_generator
115
+ define_self_closing_element :circle, code_generator: code_generator
116
+ define_self_closing_element :ellipse, code_generator: code_generator
117
+ define_self_closing_element :line, code_generator: code_generator
118
+ define_self_closing_element :path, code_generator: code_generator
119
+ define_self_closing_element :polygon, code_generator: code_generator
120
+ define_self_closing_element :polyline, code_generator: code_generator
121
+ define_self_closing_element :rect, code_generator: code_generator
122
+ define_self_closing_element :set, code_generator: code_generator
123
+ define_self_closing_element :stop, code_generator: code_generator
124
+ define_self_closing_element :use, code_generator: code_generator
125
+ define_self_closing_element :view, code_generator: code_generator
126
+
127
+ define_element :a, code_generator: code_generator
128
+ define_element :abbr, code_generator: code_generator
129
+ define_element :address, code_generator: code_generator
130
+ define_element :article, code_generator: code_generator
131
+ define_element :aside, code_generator: code_generator
132
+ define_element :audio, code_generator: code_generator
133
+ define_element :b, code_generator: code_generator
134
+ define_element :bdi, code_generator: code_generator
135
+ define_element :bdo, code_generator: code_generator
136
+ define_element :blockquote, code_generator: code_generator
137
+ define_element :body, code_generator: code_generator
138
+ define_element :button, code_generator: code_generator
139
+ define_element :canvas, code_generator: code_generator
140
+ define_element :caption, code_generator: code_generator
141
+ define_element :cite, code_generator: code_generator
142
+ define_element :code, code_generator: code_generator
143
+ define_element :colgroup, code_generator: code_generator
144
+ define_element :data, code_generator: code_generator
145
+ define_element :datalist, code_generator: code_generator
146
+ define_element :dd, code_generator: code_generator
147
+ define_element :del, code_generator: code_generator
148
+ define_element :details, code_generator: code_generator
149
+ define_element :dfn, code_generator: code_generator
150
+ define_element :dialog, code_generator: code_generator
151
+ define_element :div, code_generator: code_generator
152
+ define_element :dl, code_generator: code_generator
153
+ define_element :dt, code_generator: code_generator
154
+ define_element :em, code_generator: code_generator
155
+ define_element :fieldset, code_generator: code_generator
156
+ define_element :figcaption, code_generator: code_generator
157
+ define_element :figure, code_generator: code_generator
158
+ define_element :footer, code_generator: code_generator
159
+ define_element :form, code_generator: code_generator
160
+ define_element :h1, code_generator: code_generator
161
+ define_element :h2, code_generator: code_generator
162
+ define_element :h3, code_generator: code_generator
163
+ define_element :h4, code_generator: code_generator
164
+ define_element :h5, code_generator: code_generator
165
+ define_element :h6, code_generator: code_generator
166
+ define_element :head, code_generator: code_generator
167
+ define_element :header, code_generator: code_generator
168
+ define_element :hgroup, code_generator: code_generator
169
+ define_element :html, code_generator: code_generator
170
+ define_element :i, code_generator: code_generator
171
+ define_element :iframe, code_generator: code_generator
172
+ define_element :ins, code_generator: code_generator
173
+ define_element :kbd, code_generator: code_generator
174
+ define_element :label, code_generator: code_generator
175
+ define_element :legend, code_generator: code_generator
176
+ define_element :li, code_generator: code_generator
177
+ define_element :main, code_generator: code_generator
178
+ define_element :map, code_generator: code_generator
179
+ define_element :mark, code_generator: code_generator
180
+ define_element :menu, code_generator: code_generator
181
+ define_element :meter, code_generator: code_generator
182
+ define_element :nav, code_generator: code_generator
183
+ define_element :noscript, code_generator: code_generator
184
+ define_element :object, code_generator: code_generator
185
+ define_element :ol, code_generator: code_generator
186
+ define_element :optgroup, code_generator: code_generator
187
+ define_element :option, code_generator: code_generator
188
+ define_element :output, code_generator: code_generator
189
+ define_element :p, code_generator: code_generator
190
+ define_element :picture, code_generator: code_generator
191
+ define_element :portal, code_generator: code_generator
192
+ define_element :pre, code_generator: code_generator
193
+ define_element :progress, code_generator: code_generator
194
+ define_element :q, code_generator: code_generator
195
+ define_element :rp, code_generator: code_generator
196
+ define_element :rt, code_generator: code_generator
197
+ define_element :ruby, code_generator: code_generator
198
+ define_element :s, code_generator: code_generator
199
+ define_element :samp, code_generator: code_generator
200
+ define_element :script, code_generator: code_generator
201
+ define_element :search, code_generator: code_generator
202
+ define_element :section, code_generator: code_generator
203
+ define_element :select, code_generator: code_generator
204
+ define_element :slot, code_generator: code_generator
205
+ define_element :small, code_generator: code_generator
206
+ define_element :span, code_generator: code_generator
207
+ define_element :strong, code_generator: code_generator
208
+ define_element :style, code_generator: code_generator
209
+ define_element :sub, code_generator: code_generator
210
+ define_element :summary, code_generator: code_generator
211
+ define_element :sup, code_generator: code_generator
212
+ define_element :table, code_generator: code_generator
213
+ define_element :tbody, code_generator: code_generator
214
+ define_element :td, code_generator: code_generator
215
+ define_element :template, code_generator: code_generator
216
+ define_element :textarea, code_generator: code_generator
217
+ define_element :tfoot, code_generator: code_generator
218
+ define_element :th, code_generator: code_generator
219
+ define_element :thead, code_generator: code_generator
220
+ define_element :time, code_generator: code_generator
221
+ define_element :title, code_generator: code_generator
222
+ define_element :tr, code_generator: code_generator
223
+ define_element :u, code_generator: code_generator
224
+ define_element :ul, code_generator: code_generator
225
+ define_element :var, code_generator: code_generator
226
+ define_element :video, code_generator: code_generator
227
+ end
50
228
 
51
229
  def initialize(view_context)
52
230
  @view_context = view_context
@@ -61,30 +239,22 @@ module ActionView
61
239
  tag_options(attributes.to_h).to_s.strip.html_safe
62
240
  end
63
241
 
64
- def p(*arguments, **options, &block)
65
- tag_string(:p, *arguments, **options, &block)
66
- end
242
+ def tag_string(name, content = nil, options, escape: true, &block)
243
+ content = @view_context.capture(self, &block) if block
67
244
 
68
- def tag_string(name, content = nil, **options, &block)
69
- escape = handle_deprecated_escape_options(options)
245
+ content_tag_string(name, content, options, escape)
246
+ end
70
247
 
71
- content = @view_context.capture(self, &block) if block_given?
72
- self_closing = SVG_SELF_CLOSING_ELEMENTS.include?(name)
73
- if (HTML_VOID_ELEMENTS.include?(name) || self_closing) && content.nil?
74
- "<#{name.to_s.dasherize}#{tag_options(options, escape)}#{self_closing ? " />" : ">"}".html_safe
75
- else
76
- content_tag_string(name.to_s.dasherize, content || "", options, escape)
77
- end
248
+ def self_closing_tag_string(name, options, escape = true, tag_suffix = " />")
249
+ "<#{name}#{tag_options(options, escape)}#{tag_suffix}".html_safe
78
250
  end
79
251
 
80
252
  def content_tag_string(name, content, options, escape = true)
81
253
  tag_options = tag_options(options, escape) if options
82
254
 
83
- if escape
84
- name = ERB::Util.xml_name_escape(name)
255
+ if escape && content.present?
85
256
  content = ERB::Util.unwrapped_html_escape(content)
86
257
  end
87
-
88
258
  "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
89
259
  end
90
260
 
@@ -164,29 +334,12 @@ module ActionView
164
334
  true
165
335
  end
166
336
 
167
- def handle_deprecated_escape_options(options)
168
- # The option :escape_attributes has been merged into the options hash to be
169
- # able to warn when it is used, so we need to handle default values here.
170
- escape_option_provided = options.has_key?(:escape)
171
- escape_attributes_option_provided = options.has_key?(:escape_attributes)
172
-
173
- if escape_attributes_option_provided
174
- ActiveSupport::Deprecation.warn(<<~MSG)
175
- Use of the option :escape_attributes is deprecated. It currently \
176
- escapes both names and values of tags and attributes and it is \
177
- equivalent to :escape. If any of them are enabled, the escaping \
178
- is fully enabled.
179
- MSG
180
- end
337
+ def method_missing(called, *args, escape: true, **options, &block)
338
+ name = called.name.dasherize
181
339
 
182
- return true unless escape_option_provided || escape_attributes_option_provided
183
- escape_option = options.delete(:escape)
184
- escape_attributes_option = options.delete(:escape_attributes)
185
- escape_option || escape_attributes_option
186
- end
340
+ TagHelper.ensure_valid_html5_tag_name(name)
187
341
 
188
- def method_missing(called, *args, **options, &block)
189
- tag_string(called, *args, **options, &block)
342
+ tag_string(name, *args, options, escape: escape, &block)
190
343
  end
191
344
  end
192
345
 
@@ -267,6 +420,14 @@ module ActionView
267
420
  # # A void element:
268
421
  # tag.br # => <br>
269
422
  #
423
+ # Note that when using the block form options should be wrapped in
424
+ # parenthesis.
425
+ #
426
+ # <%= tag.a(href: "/about", class: "font-bold") do %>
427
+ # About the author
428
+ # <% end %>
429
+ # # => <a href="/about" class="font-bold">About the author</a>
430
+ #
270
431
  # === Building HTML attributes
271
432
  #
272
433
  # Transforms a Hash into HTML attributes, ready to be interpolated into
@@ -283,7 +444,7 @@ module ActionView
283
444
  #
284
445
  # === Legacy syntax
285
446
  #
286
- # The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
447
+ # The following format is for legacy syntax support. It will be deprecated in future versions of \Rails.
287
448
  #
288
449
  # tag(name, options = nil, open = false, escape = true)
289
450
  #
@@ -332,7 +493,7 @@ module ActionView
332
493
  if name.nil?
333
494
  tag_builder
334
495
  else
335
- name = ERB::Util.xml_name_escape(name) if escape
496
+ ensure_valid_html5_tag_name(name)
336
497
  "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
337
498
  end
338
499
  end
@@ -366,6 +527,8 @@ module ActionView
366
527
  # <% end -%>
367
528
  # # => <div class="strong">Hello world!</div>
368
529
  def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
530
+ ensure_valid_html5_tag_name(name)
531
+
369
532
  if block_given?
370
533
  options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
371
534
  tag_builder.content_tag_string(name, capture(&block), options, escape)
@@ -386,7 +549,7 @@ module ActionView
386
549
  # token_list(nil, false, 123, "", "foo", { bar: true })
387
550
  # # => "123 foo bar"
388
551
  def token_list(*args)
389
- tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
552
+ tokens = build_tag_values(*args).flat_map { |value| CGI.unescape_html(value.to_s).split(/\s+/) }.uniq
390
553
 
391
554
  safe_join(tokens, " ")
392
555
  end
@@ -422,6 +585,11 @@ module ActionView
422
585
  end
423
586
 
424
587
  private
588
+ def ensure_valid_html5_tag_name(name)
589
+ raise ArgumentError, "Invalid HTML5 tag name: #{name.inspect}" unless /\A[a-zA-Z][^\s\/>]*\z/.match?(name)
590
+ end
591
+ module_function :ensure_valid_html5_tag_name
592
+
425
593
  def build_tag_values(*args)
426
594
  tag_values = []
427
595
 
@@ -5,7 +5,6 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class Base # :nodoc:
7
7
  include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
8
- include FormOptionsHelper
9
8
 
10
9
  attr_reader :object
11
10
 
@@ -35,22 +34,24 @@ module ActionView
35
34
 
36
35
  private
37
36
  def value
37
+ return unless object
38
+
38
39
  if @allow_method_names_outside_object
39
- object.public_send @method_name if object && object.respond_to?(@method_name)
40
+ object.public_send @method_name if object.respond_to?(@method_name)
40
41
  else
41
- object.public_send @method_name if object
42
+ object.public_send @method_name
42
43
  end
43
44
  end
44
45
 
45
46
  def value_before_type_cast
46
- unless object.nil?
47
- method_before_type_cast = @method_name + "_before_type_cast"
47
+ return unless object
48
48
 
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
49
+ method_before_type_cast = @method_name + "_before_type_cast"
50
+
51
+ if value_came_from_user? && object.respond_to?(method_before_type_cast)
52
+ object.public_send(method_before_type_cast)
53
+ else
54
+ value
54
55
  end
55
56
  end
56
57
 
@@ -120,48 +121,6 @@ module ActionView
120
121
  value.to_s.gsub(/[\s.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
121
122
  end
122
123
 
123
- def select_content_tag(option_tags, options, html_options)
124
- html_options = html_options.stringify_keys
125
- add_default_name_and_id(html_options)
126
-
127
- if placeholder_required?(html_options)
128
- raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
129
- options[:include_blank] ||= true unless options[:prompt]
130
- end
131
-
132
- value = options.fetch(:selected) { value() }
133
- select = content_tag("select", add_options(option_tags, options, value), html_options)
134
-
135
- if html_options["multiple"] && options.fetch(:include_hidden, true)
136
- tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "", autocomplete: "off") + select
137
- else
138
- select
139
- end
140
- end
141
-
142
- def placeholder_required?(html_options)
143
- # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
144
- html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
145
- end
146
-
147
- def add_options(option_tags, options, value = nil)
148
- if options[:include_blank]
149
- content = (options[:include_blank] if options[:include_blank].is_a?(String))
150
- label = (" " unless content)
151
- option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
152
- end
153
-
154
- if value.blank? && options[:prompt]
155
- tag_options = { value: "" }.tap do |prompt_opts|
156
- prompt_opts[:disabled] = true if options[:disabled] == ""
157
- prompt_opts[:selected] = true if options[:selected] == ""
158
- end
159
- option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
160
- end
161
-
162
- option_tags
163
- end
164
-
165
124
  def name_and_id_index(options)
166
125
  if options.key?("index")
167
126
  options.delete("index") || ""
@@ -7,6 +7,7 @@ module ActionView
7
7
  module Tags # :nodoc:
8
8
  class CollectionCheckBoxes < Base # :nodoc:
9
9
  include CollectionHelpers
10
+ include FormOptionsHelper
10
11
 
11
12
  class CheckBoxBuilder < Builder # :nodoc:
12
13
  def check_box(extra_html_options = {})
@@ -7,6 +7,7 @@ module ActionView
7
7
  module Tags # :nodoc:
8
8
  class CollectionRadioButtons < Base # :nodoc:
9
9
  include CollectionHelpers
10
+ include FormOptionsHelper
10
11
 
11
12
  class RadioButtonBuilder < Builder # :nodoc:
12
13
  def radio_button(extra_html_options = {})
@@ -4,6 +4,9 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class CollectionSelect < Base # :nodoc:
7
+ include SelectRenderer
8
+ include FormOptionsHelper
9
+
7
10
  def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
8
11
  @collection = collection
9
12
  @value_method = value_method
@@ -5,7 +5,7 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class DateField < DatetimeField # :nodoc:
7
7
  private
8
- def format_date(value)
8
+ def format_datetime(value)
9
9
  value&.strftime("%Y-%m-%d")
10
10
  end
11
11
  end
@@ -6,6 +6,8 @@ module ActionView
6
6
  module Helpers
7
7
  module Tags # :nodoc:
8
8
  class DateSelect < Base # :nodoc:
9
+ include SelectRenderer
10
+
9
11
  def initialize(object_name, method_name, template_object, options, html_options)
10
12
  @html_options = html_options
11
13
 
@@ -6,20 +6,28 @@ module ActionView
6
6
  class DatetimeField < TextField # :nodoc:
7
7
  def render
8
8
  options = @options.stringify_keys
9
- options["value"] ||= format_date(value)
10
- options["min"] = format_date(datetime_value(options["min"]))
11
- options["max"] = format_date(datetime_value(options["max"]))
9
+ options["value"] = datetime_value(options["value"] || value)
10
+ options["min"] = format_datetime(parse_datetime(options["min"]))
11
+ options["max"] = format_datetime(parse_datetime(options["max"]))
12
12
  @options = options
13
13
  super
14
14
  end
15
15
 
16
16
  private
17
- def format_date(value)
17
+ def datetime_value(value)
18
+ if value.is_a?(String)
19
+ value
20
+ else
21
+ format_datetime(value)
22
+ end
23
+ end
24
+
25
+ def format_datetime(value)
18
26
  raise NotImplementedError
19
27
  end
20
28
 
21
- def datetime_value(value)
22
- if value.is_a? String
29
+ def parse_datetime(value)
30
+ if value.is_a?(String)
23
31
  DateTime.parse(value) rescue nil
24
32
  else
25
33
  value
@@ -4,6 +4,11 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class DatetimeLocalField < DatetimeField # :nodoc:
7
+ def initialize(object_name, method_name, template_object, options = {})
8
+ @include_seconds = options.delete(:include_seconds) { true }
9
+ super
10
+ end
11
+
7
12
  class << self
8
13
  def field_type
9
14
  @field_type ||= "datetime-local"
@@ -11,8 +16,12 @@ module ActionView
11
16
  end
12
17
 
13
18
  private
14
- def format_date(value)
15
- value&.strftime("%Y-%m-%dT%T")
19
+ def format_datetime(value)
20
+ if @include_seconds
21
+ value&.strftime("%Y-%m-%dT%T")
22
+ else
23
+ value&.strftime("%Y-%m-%dT%H:%M")
24
+ end
16
25
  end
17
26
  end
18
27
  end
@@ -4,6 +4,9 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class GroupedCollectionSelect < Base # :nodoc:
7
+ include SelectRenderer
8
+ include FormOptionsHelper
9
+
7
10
  def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
8
11
  @collection = collection
9
12
  @group_method = group_method
@@ -5,7 +5,7 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class MonthField < DatetimeField # :nodoc:
7
7
  private
8
- def format_date(value)
8
+ def format_datetime(value)
9
9
  value&.strftime("%Y-%m")
10
10
  end
11
11
  end
@@ -4,6 +4,9 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class Select < Base # :nodoc:
7
+ include SelectRenderer
8
+ include FormOptionsHelper
9
+
7
10
  def initialize(object_name, method_name, template_object, choices, options, html_options)
8
11
  @choices = block_given? ? template_object.capture { yield || "" } : choices
9
12
  @choices = @choices.to_a if @choices.is_a?(Range)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ module SelectRenderer # :nodoc:
7
+ private
8
+ def select_content_tag(option_tags, options, html_options)
9
+ html_options = html_options.stringify_keys
10
+ [:required, :multiple, :size].each do |prop|
11
+ html_options[prop.to_s] = options.delete(prop) if options.key?(prop) && !html_options.key?(prop.to_s)
12
+ end
13
+
14
+ add_default_name_and_id(html_options)
15
+
16
+ if placeholder_required?(html_options)
17
+ raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
18
+ options[:include_blank] ||= true unless options[:prompt]
19
+ end
20
+
21
+ value = options.fetch(:selected) { value() }
22
+ select = content_tag("select", add_options(option_tags, options, value), html_options)
23
+
24
+ if html_options["multiple"] && options.fetch(:include_hidden, true)
25
+ tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "", autocomplete: "off") + select
26
+ else
27
+ select
28
+ end
29
+ end
30
+
31
+ def placeholder_required?(html_options)
32
+ # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
33
+ html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
34
+ end
35
+
36
+ def add_options(option_tags, options, value = nil)
37
+ if options[:include_blank]
38
+ content = (options[:include_blank] if options[:include_blank].is_a?(String))
39
+ label = (" " unless content)
40
+ option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
41
+ end
42
+
43
+ if value.blank? && options[:prompt]
44
+ tag_options = { value: "" }.tap do |prompt_opts|
45
+ prompt_opts[:disabled] = true if options[:disabled] == ""
46
+ prompt_opts[:selected] = true if options[:selected] == ""
47
+ end
48
+ option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
49
+ end
50
+
51
+ option_tags
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -10,7 +10,7 @@ module ActionView
10
10
  end
11
11
 
12
12
  private
13
- def format_date(value)
13
+ def format_datetime(value)
14
14
  if @include_seconds
15
15
  value&.strftime("%T.%L")
16
16
  else
@@ -4,6 +4,9 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class TimeZoneSelect < Base # :nodoc:
7
+ include SelectRenderer
8
+ include FormOptionsHelper
9
+
7
10
  def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
8
11
  @priority_zones = priority_zones
9
12
  @html_options = html_options
@@ -5,7 +5,7 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class WeekField < DatetimeField # :nodoc:
7
7
  private
8
- def format_date(value)
8
+ def format_datetime(value)
9
9
  value&.strftime("%Y-W%V")
10
10
  end
11
11
  end