roda-tags 0.1.1 → 0.2.0

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.
@@ -1,82 +1,98 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'roda'
2
4
  require_relative '../../core_ext/string' unless ''.respond_to?(:titleize)
3
5
  # require_relative '../../core_ext/object' unless :symbol.respond_to?(:in?)
4
6
 
5
7
  class Roda
6
-
7
- #
8
+ # add module documentation
8
9
  module RodaPlugins
9
-
10
- # TODO: add documentation here
10
+ # add module documentation
11
11
  module RodaTagHelpers
12
12
  # default options
13
13
  OPTS = {
14
- #
15
- tags_label_required_str: '<span>*</span>',
16
- #
17
- tags_label_append_str: ':',
18
- # the default classes for various form tags. ie: shortcut to automatically add
14
+ tags_label_required_str: '<span>*</span>',
15
+ tags_label_append_str: ':',
16
+ # the default classes for various form tags. ie: shortcut to automatically add
19
17
  # BS3 'form-control'
20
- tags_forms_default_class: '', # 'form-control',
21
-
18
+ tags_forms_default_class: '' # 'form-control',
19
+
22
20
  }.freeze
23
-
24
-
21
+
25
22
  # Depend on the render plugin, since this plugin only makes
26
23
  # sense when the render plugin is used.
27
24
  def self.load_dependencies(app, opts = OPTS)
28
25
  app.plugin :tags, opts
29
26
  end
30
-
27
+
28
+ # Configure the tag_helpers plugin with given options.
29
+ #
30
+ # @param app [Class] The Roda app class
31
+ # @param opts [Hash] Configuration options to merge with defaults
32
+ # If tag_helpers is already configured, merges with existing options
33
+ # Otherwise merges with OPTS constant defaults
34
+ #
35
+ # @return [void]
36
+ #
31
37
  def self.configure(app, opts = {})
32
- if app.opts[:tag_helpers]
33
- opts = app.opts[:tag_helpers][:orig_opts].merge(opts)
34
- else
35
- opts = OPTS.merge(opts)
36
- end
37
-
38
+ opts = if app.opts[:tag_helpers]
39
+ app.opts[:tag_helpers][:orig_opts].merge(opts)
40
+ else
41
+ OPTS.merge(opts)
42
+ end
43
+
38
44
  app.opts[:tag_helpers] = opts.dup
39
45
  app.opts[:tag_helpers][:orig_opts] = opts
40
46
  end
41
-
42
- #
47
+
48
+ # add module documentation
43
49
  module ClassMethods
44
50
  # Return the uitags options for this class.
45
51
  def tag_helpers_opts
46
52
  opts[:tag_helpers]
47
53
  end
48
-
49
54
  end
50
-
51
- #
55
+
56
+ # add module documentation
57
+ # rubocop:disable Metrics/ModuleLength
52
58
  module InstanceMethods
53
-
54
- # Constructs a form without object based on options
55
- #
56
- # ==== Examples
57
- #
58
- # form_tag('/register') do
59
- # ...
59
+ # Constructs a form tag with given action and attributes
60
+ #
61
+ # @param action [String] The URL/path the form submits to
62
+ # @param attrs [Hash] HTML attributes for the form tag
63
+ # @param block [Block] Optional block containing form content
64
+ #
65
+ # @return [String] The generated HTML form tag and contents
66
+ #
67
+ # @example Basic usage
68
+ #
69
+ # form_tag('/register') do
70
+ # # form contents
60
71
  # end
61
- # #=>
62
- # <form action="/register" id="register-form" method="post">
63
- # ...
64
- # </form>
65
- #
66
- #
67
- # <% form_tag('/register', method: :put, id: 'register-form' ) %>
72
+ # #=> <form action="/register" method="post">
73
+ # # ...
74
+ # # </form>
75
+ #
76
+ # form_tag('/register', id: :register-form, class: 'form-control') do
68
77
  # ...
69
- # <% end %>
70
- # #=>
71
- # <form action="/register" id="register-form" method="post" >
72
- # <input name="_method" type="hidden" value="put"/>
73
- # ...
74
- # </form>
75
- #
76
- # Multipart support via:
77
- #
78
+ # end
79
+ # #=> <form action="/register" method="post" id="register-form" class="form-control">
80
+ # # ...
81
+ # # </form>
82
+ #
83
+ # @example Custom methods PUT/DELETE
84
+ #
85
+ # form_tag('/items', method: :put) do
86
+ # ...
87
+ # end
88
+ # #=> <form action="/items" method="post">
89
+ # # <input name="_method" type="hidden" value="PUT" />
90
+ # # </form>
91
+ #
92
+ # @example Multipart forms
93
+ #
78
94
  # <% form_tag('/register', multipart: true ) %>
79
- #
95
+ #
80
96
  # <% form_tag('/register', multipart: 'multipart/form-data' ) %>
81
97
  #
82
98
  # <% form_tag('/register', enctype: 'multipart/form-data' ) %>
@@ -84,61 +100,72 @@ class Roda
84
100
  # <form enctype="multipart/form-data" method="post" action="/register">
85
101
  # ...
86
102
  # </form>
87
- #
88
- def form_tag(action, attrs = {}, &block)
103
+ #
104
+ def form_tag(action, attrs = {}, &block)
89
105
  attrs.reverse_merge!(method: :post, action: action)
90
106
  method = attrs[:method]
107
+
91
108
  # Unless the method is :get, fake out the method using :post
92
109
  attrs[:method] = :post unless attrs[:method] == :get
93
- faux_method_tag = method.to_s =~ /post|get/ ? '' : faux_method(method)
110
+ faux_method_tag = /post|get/.match?(method.to_s) ? '' : faux_method(method)
111
+
94
112
  # set the enctype to multipart-form if we got a @multipart form
95
113
  attrs[:enctype] = 'multipart/form-data' if attrs.delete(:multipart) || @multipart
96
114
  captured_html = block_given? ? capture_html(&block) : ''
97
115
  concat_content(tag(:form, faux_method_tag + captured_html, attrs))
98
116
  end
99
-
117
+
100
118
  # Constructs a label tag from the given options
101
- #
102
- # ==== Examples
103
- #
119
+ #
120
+ # @example Basic usage
121
+ #
104
122
  # <%= label_tag(:name) %>
105
123
  # #=> <label for="name">Name:</label>
106
- #
107
- # Should accept a custom label text.
108
- #
124
+ #
125
+ # @example Should accept a custom label text.
126
+ #
109
127
  # <%= label_tag(:name, label: 'Custom label') %>
110
128
  # #=> <label for="name">Custom label:</label>
111
- #
112
- # If label value is nil, then renders the default label text.
113
- #
129
+ #
130
+ # @example If label value is nil, then renders the default label text.
131
+ #
114
132
  # <%= label_tag(:name, label: nil) %>
115
133
  # #=> <label for="name">Name:</label>
116
- #
117
- # Removes the label text when given :false.
118
- #
134
+ #
135
+ # @example Removes the label text when given :false.
136
+ #
119
137
  # <%= label_tag(:name, label: false) %>
120
138
  # #=> <label for="name"></label>
121
- #
122
- # Appends the <tt>app.forms_label_required_str</tt> value, when the label is required.
123
- #
139
+ #
140
+ # @example Appends the `app.forms_label_required_str` value, when the label is required.
141
+ #
124
142
  # <%= label_tag(:name, required: true) %>
125
143
  # #=> <label for="name">Name: <span>*</span></label>
126
- #
127
- def label_tag(field, attrs = {}, &block)
128
- attrs.reverse_merge!(label: field.to_s.titleize, for: field)
129
-
144
+ #
145
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
146
+ def label_tag(field, attrs = {}, &block)
147
+ field = field.to_s
148
+
149
+ for_text = attrs.delete(:for)
150
+ # handle FALSE & nil values
151
+ for_text = field if for_text == false
152
+ for_text = field if for_text.nil?
153
+
154
+ attrs.reverse_merge!(label: field.titleize, for: for_text)
155
+ # attrs.reverse_merge!(label: field.titleize, for: field)
156
+
130
157
  label_text = attrs.delete(:label)
131
158
  # handle FALSE & nil values
132
159
  label_text = '' if label_text == false
133
- label_text = field.to_s.titleize if label_text.nil?
134
-
160
+ label_text = field.titleize if label_text.nil?
161
+
135
162
  unless label_text.to_s.empty?
136
163
  label_text << opts_tag_helpers[:tags_label_append_str]
137
- if attrs.delete(:required)
164
+ if attrs.delete(:required)
138
165
  label_text = "#{label_text} #{opts_tag_helpers[:tags_label_required_str]}"
139
166
  end
140
167
  end
141
-
168
+
142
169
  if block_given? # label with inner content
143
170
  label_content = label_text + capture_html(&block)
144
171
  concat_content(tag(:label, label_content, attrs))
@@ -146,211 +173,220 @@ class Roda
146
173
  tag(:label, label_text, attrs)
147
174
  end
148
175
  end
149
-
176
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
177
+
150
178
  # Constructs a hidden field input from the given options
151
179
  #
152
- # ==== Examples
153
- #
180
+ # @param name [String] The field name used for :id & :name attributes
181
+ # @param attrs [Hash] HTML attributes for the input[@type=hidden] tag
182
+ #
183
+ # @return [String] The generated HTML input[@type=hidden] tag
184
+ #
185
+ # @example Basic usage
186
+ #
154
187
  # <%= hidden_field_tag(:snippet_name) %>
155
188
  # #=>
156
189
  # <input id="snippet_name" name="snippet_name" type="hidden">
157
- #
158
- # Providing a value:
159
- #
190
+ #
191
+ # @example Providing a value:
192
+ #
160
193
  # <%= hidden_field_tag(:snippet_name, value: 'myvalue') %>
161
194
  # #=>
162
195
  # <input id="snippet_name" name="snippet_name" type="hidden" value="myvalue">
163
- #
164
- # Setting a different <tt>:id</tt>
165
- #
196
+ #
197
+ # @example Setting a different `:id`
198
+ #
166
199
  # <%= hidden_field_tag(:snippet_name, id: 'some-id') %>
167
200
  # #=>
168
201
  # <input id="some-id" name="snippet_name" type="hidden">
169
- #
170
- # Removing the <tt>:id</tt> attribute completely.
171
- #
202
+ #
203
+ # @example Removing the `:id` attribute completely.
204
+ #
172
205
  # <%= hidden_field_tag(:snippet_name, id: false ) %>
173
206
  # #=>
174
207
  # <input name="snippet_name" type="hidden">
175
- #
176
- def hidden_field_tag(name, attrs = {})
208
+ #
209
+ def hidden_field_tag(name, attrs = {})
210
+ name = name.to_s
211
+ attrs = {} if attrs.nil?
212
+
177
213
  attrs.reverse_merge!(name: name, value: '', type: :hidden)
178
214
  attrs = add_css_id(attrs, name)
179
215
  tag(:input, attrs)
180
216
  end
181
- alias_method :hiddenfield_tag, :hidden_field_tag
182
-
183
- # Creates a standard text field; use these text fields to input smaller chunks of text like
217
+ alias hiddenfield_tag hidden_field_tag
218
+
219
+ # Creates a standard text field; use these text fields to input smaller chunks of text like
184
220
  # a username or a search query.
185
- #
186
- # ==== Examples
187
- #
221
+ #
222
+ # @param name [String] The field name used for :id & :name attributes
223
+ # @param attrs [Hash] HTML attributes for the input[@type=text] tag
224
+ #
225
+ # @return [String] The generated HTML input[@type=text] tag
226
+ #
227
+ # @example Basic usage
188
228
  # text_field_tag(:snippet_name)
189
- # #=>
190
- # <input class="text" id="snippet_name" name="snippet_name" type="text">
191
- #
192
- # Providing a value:
193
- #
229
+ # #=> <input class="text" id="snippet_name" name="snippet_name" type="text">
230
+ #
231
+ # @example With a value
194
232
  # text_field_tag(:snippet, value: 'some-value')
195
- # #=>
196
- # <input class="text" id="snippet" name="snippet" type="text" value="some-value">
197
- #
198
- # Setting a different <tt>:id</tt>
199
- #
233
+ # #=> <input class="text" id="snippet" name="snippet" type="text" value="some-value">
234
+ #
235
+ # @example Setting a different `:id`
200
236
  # text_field_tag(:snippet_name, id: 'some-id')
201
- # #=>
202
- # <input class="text" id="some-id" name="snippet_name" type="text">
203
- #
204
- # Removing the <tt>:id</tt> attribute completely. NB! bad practice.
205
- #
237
+ # #=> <input class="text" id="some-id" name="snippet_name" type="text">
238
+ #
239
+ # @example Removing the `:id` attribute completely.
240
+ # NOTE! bad practice.
241
+ #
206
242
  # text_field_tag(:snippet_name, id: false)
207
- # #=>
208
- # <input class="text" name="snippet_name" type="text">
209
- #
210
- # Adding another CSS class. NB! appends the the class to the default class <tt>.text</tt>.
211
- #
243
+ # #=> <input class="text" name="snippet_name" type="text">
244
+ #
245
+ # @example Adding another CSS class.
246
+ # NOTE! appends the the class to the default class `.text`.
247
+ #
212
248
  # text_field_tag(:snippet_name, class: :big )
213
- # #=>
214
- # <input class="big text" id="snippet_name" name="snippet_name" type="text">
215
- #
216
- # Adds a <tt>:title</tt> attribute when passed <tt>:ui_hint</tt>.
217
- #
249
+ # #=> <input class="big text" id="snippet_name" name="snippet_name" type="text">
250
+ #
251
+ # @example Adds a `:title` attribute when passed `:ui_hint`.
218
252
  # text_field_tag(:name, ui_hint: 'a user hint')
219
- # #=>
220
- # <input class="text" id="name" name="name" title="a user hint" type="text">
221
- #
222
- # Supports <tt>:maxlength</tt> & <tt>:size</tt> attributes.
223
- #
253
+ # #=> <input class="text" id="name" name="name" title="a user hint" type="text">
254
+ #
255
+ # @example Supports `:maxlength` & `:size` attributes.
224
256
  # text_field_tag(:ip, maxlength: 15, size: 20)
225
- # #=>
226
- # <input class="text" id="ip" maxlength="15" name="ip" size="20" type="text">
227
- #
228
- # Supports <tt>:disabled</tt> & <tt>:readonly</tt> attributes.
229
- #
257
+ # #=> <input class="text" id="ip" maxlength="15" name="ip" size="20" type="text">
258
+ #
259
+ # @example Supports `:disabled` & `:readonly` attributes.
230
260
  # text_field_tag(:name, disabled: true)
231
- # #=>
232
- # <input class="text" disabled="disabled" id="name" name="name" type="text" >
233
- #
261
+ # #=> <input class="text" disabled="disabled" id="name" name="name" type="text" >
262
+ #
234
263
  # text_field_tag(:name, readonly: true)
235
- # #=>
236
- # <input class="text" id="name" name="name" readonly="readonly" type="text">
237
- #
264
+ # #=> <input class="text" id="name" name="name" readonly="readonly" type="text">
238
265
  #
239
- def text_field_tag(name, attrs = {})
266
+ def text_field_tag(name, attrs = {})
267
+ name = name.to_s
268
+ attrs = {} if attrs.nil?
269
+
240
270
  attrs.reverse_merge!(name: name, type: :text)
241
271
  attrs = add_css_id(attrs, name)
242
272
  attrs = add_css_class(attrs, :text)
243
273
  attrs = add_ui_hint(attrs)
244
274
  tag(:input, attrs)
245
275
  end
246
- alias_method :textfield_tag, :text_field_tag
247
-
276
+ alias textfield_tag text_field_tag
277
+
248
278
  # Constructs a password field input from the given options
249
- #
250
- # ==== Examples
251
- #
279
+ #
280
+ # @param name [String] The field name used for :id & :name attributes
281
+ # @param attrs [Hash] HTML attributes for the input[@type=password] tag
282
+ #
283
+ # @return [String] The generated HTML input[@type=password] tag
284
+ #
285
+ # @example Basic usage
252
286
  # password_field_tag(:snippet_name)
253
287
  # #=> <input class="text" id="snippet_name" name="snippet_name" type="password">
254
- #
255
- # Providing a value:
256
- #
288
+ #
289
+ # @example With a value
257
290
  # password_field_tag(:snippet_name, value: 'some-value')
258
291
  # #=>
259
292
  # <input class="text" id="snippet" name="snippet" type="password" value="some-value">
260
- #
261
- # Setting a different <tt>:id</tt>
262
- #
293
+ #
294
+ # @example With custom `:id` attribute
263
295
  # password_field_tag(:snippet_name, id: 'some-id')
264
296
  # #=> <input class="text" id="some-id" name="snippet_name" type="password">
265
- #
266
- # Removing the <tt>:id</tt> attribute completely. NB! bad practice.
267
- #
297
+ #
298
+ # @example Without the `:id` attribute
299
+ # NOTE! bad practice.
300
+ #
268
301
  # password_field_tag(:snippet_name, id: false)
269
302
  # #=> <input class="text" name="snippet_name" type="password">
270
- #
271
- # Adding another CSS class. NB! appends the the class to the default class <tt>.text</tt>.
272
- #
303
+ #
304
+ # @example Adding another CSS class
305
+ # NOTE! appends the the class to the default class `.text`.
306
+ #
273
307
  # password_field_tag(:snippet_name, class: :big )
274
308
  # #=> <input class="big text" id="snippet_name" name="snippet_name" type="password">
275
- #
276
- # Adds a <tt>:title</tt> attribute when passed <tt>:ui_hint</tt>.
277
- #
309
+ #
310
+ # @example Adds a `:title` attribute when passed `:ui_hint`.
278
311
  # password_field_tag(:name, ui_hint: 'a user hint')
279
312
  # #=> <input class="text" id="name" name="name" title="a user hint" type="password">
280
- #
281
- # Supports <tt>:maxlength</tt>, <tt>:size</tt> & <tt>:disabled</tt> attributes.
282
- #
313
+ #
314
+ # @example Supports `:maxlength`, `:size` & `:disabled` attributes
283
315
  # password_field_tag(:ip, maxlength: 15, size: 20)
284
316
  # #=> <input class="text" id="ip" maxlength="15" name="ip" size="20" type="password">
285
- #
317
+ #
286
318
  # password_field_tag(:name, disabled: true)
287
319
  # #=> <input class="text" id="name" disabled="disabled" name="name" type="password">
288
- #
289
- def password_field_tag(name, attrs = {})
320
+ #
321
+ def password_field_tag(name, attrs = {})
322
+ name = name.to_s
323
+ attrs = {} if attrs.nil?
324
+
290
325
  attrs.reverse_merge!(name: name, type: :password)
291
326
  attrs = add_css_id(attrs, name)
292
327
  attrs = add_css_class(attrs, :text) # deliberately giving it the .text class
293
328
  attrs = add_ui_hint(attrs)
294
329
  tag(:input, attrs)
295
330
  end
296
- alias_method :passwordfield_tag, :password_field_tag
297
-
298
-
299
- # Creates a file upload field. If you are using file uploads then you will also
331
+ alias passwordfield_tag password_field_tag
332
+
333
+ # Creates a file upload field. If you are using file uploads then you will also
300
334
  # need to set the multipart option for the form tag:
301
- #
335
+ #
302
336
  # <% form_tag '/upload', :multipart => true do %>
303
337
  # <label for="file">File to Upload</label>
304
- # <%= file_field_tag "file" %>
338
+ # <%= file_field_tag 'file' %>
305
339
  # <%= submit_tag %>
306
340
  # <% end %>
307
- #
308
- # The specified URL will then be passed a File object containing the selected file,
341
+ #
342
+ # The specified URL will then be passed a File object containing the selected file,
309
343
  # or if the field was left blank, a StringIO object.
310
- #
311
- # ==== Examples
312
- #
344
+ #
345
+ # @param name [String] The field name used for :id & :name attributes
346
+ # @param attrs [Hash] HTML attributes for the input[@type=file] tag
347
+ #
348
+ # @return [String] The generated HTML input[@type=file] tag
349
+ #
350
+ # @example Basic usage
313
351
  # file_field_tag('attachment')
314
352
  # #=> <input class="file" id="attachment" name="attachment" type="file">
315
- #
316
- # Ignores the invalid <tt>:value</tt> attribute.
317
- #
353
+ #
354
+ # @example Ignores the invalid `:value` attribute.
318
355
  # file_field_tag(:photo, value: 'some-value')
319
356
  # #=> <input class="file" id="photo" name="photo" type="file">
320
- #
321
- # Setting a different <tt>:id</tt>
322
- #
357
+ #
358
+ # @example Setting a different `:id`
323
359
  # file_field_tag(:photo, id: 'some-id')
324
360
  # #=> <input class="file" id="some-id" name="photo" type="file">
325
- #
326
- # Removing the <tt>:id</tt> attribute completely. NB! bad practice.
327
- #
361
+ #
362
+ # @example Removing the `:id` attribute completely
363
+ # NOTE! bad practice.
364
+ #
328
365
  # file_field_tag(:photo, id: false)
329
366
  # #=> <input class="file" name="photo" type="file">
330
- #
331
- # Adding another CSS class. NB! appends the the class to the default class +.text+.
332
- #
367
+ #
368
+ # @example Adding another CSS class.
369
+ # NOTE! appends the the class to the default class +.text+.
370
+ #
333
371
  # file_field_tag(:photo, class: :big )
334
372
  # #=> <input class="big file" id="photo" name="photo" type="file">
335
- #
336
- # Adds a <tt>:title</tt> attribute when passed <tt>:ui_hint</tt>. Also works with +:title+.
337
- #
373
+ #
374
+ # @example Adds a `:title` attribute when passed `:ui_hint`. Also works with +:title+.
338
375
  # file_field_tag(:photo, ui_hint: 'a user hint')
339
376
  # #=> <input class="file" id="photo" name="photo" title="a user hint" type="file">
340
- #
341
- # Supports the <tt>:disabled</tt> attribute.
342
- #
377
+ #
378
+ # @example Supports the `:disabled` attribute.
343
379
  # file_field_tag(:photo, disabled: true)
344
380
  # #=> <input class="file" disabled="disabled" id="photo" name="photo" type="file">
345
- #
346
- #
347
- # Supports the <tt>:accept</tt> attribute, even though most browsers don't.
348
- #
381
+ #
382
+ # @example Supports the `:accept` attribute, even though most browsers don't.
349
383
  # file_field_tag(:photo, accept: 'image/png,image/jpeg')
350
- # #=>
351
- # <input accept="image/png,image/jpeg" class="file" ... type="file">
352
- #
353
- def file_field_tag(name, attrs = {})
384
+ # #=> <input accept="image/png,image/jpeg" class="file" ... type="file">
385
+ #
386
+ def file_field_tag(name, attrs = {})
387
+ name = name.to_s
388
+ attrs = {} if attrs.nil?
389
+
354
390
  attrs.reverse_merge!(name: name, type: :file)
355
391
  attrs.delete(:value) # can't use value, so delete it if present
356
392
  attrs = add_css_id(attrs, name)
@@ -358,240 +394,267 @@ class Roda
358
394
  attrs = add_ui_hint(attrs)
359
395
  tag(:input, attrs)
360
396
  end
361
- alias_method :filefield_tag, :file_field_tag
362
-
363
-
397
+ alias filefield_tag file_field_tag
398
+
364
399
  # Constructs a textarea input from the given options
365
- #
366
- # TODO: enable :escape functionality...
367
- #
368
- # * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped. If you
400
+ #
401
+ # * `:escape` - By default, the contents of the text input are HTML escaped. If you
369
402
  # need unescaped contents, set this to false.
370
- #
403
+ #
371
404
  # Any other key creates standard HTML attributes for the tag.
372
405
  #
373
- # ==== Examples
374
- #
406
+ # @param name [String] The field name used for :id & :name attributes
407
+ # @param attrs [Hash] HTML attributes for the textarea tag
408
+ #
409
+ # @return [String] The generated HTML textarea tag
410
+ #
411
+ # TODO: enable :escape functionality...
412
+ #
413
+ # @example Basic usage
375
414
  # textarea_tag('post')
376
415
  # #=> <textarea id="post" name="post">\n</textarea>
377
- #
378
- # Providing a value:
379
- #
416
+ #
417
+ # @example Providing a value:
380
418
  # textarea_tag(:bio, value: @actor.bio)
381
419
  # #=> <textarea id="bio" name="bio">This is my biography.\n</textarea>
382
- #
383
- # Setting a different <tt>:id</tt>
384
- #
420
+ #
421
+ # @example Setting a different `:id`
385
422
  # textarea_tag(:body, id: 'some-id')
386
423
  # #=> <textarea id="some-id" name="post">\n\n</textarea>
387
- #
388
- # Adding a CSS class. NB! textarea has no other class by default.
389
- #
424
+ #
425
+ # @example Adding a CSS class.
426
+ # NOTE! textarea has no other class by default.
427
+ #
390
428
  # textarea_tag(:body, class: 'big')
391
429
  # #=> <textarea class="big" id="post" name="post">\n</textarea>
392
- #
393
- # Adds a <tt>:title</tt> attribute when passed <tt>:ui_hint</tt>.
394
- #
430
+ #
431
+ # @example Adds a `:title` attribute when passed `:ui_hint`.
395
432
  # textarea_tag(:body, ui_hint: 'a user hint')
396
433
  # #=> <textarea id="post" name="post" title="a user hint">\n</textarea>
397
- #
398
- # Supports <tt>:rows</tt> & <tt>:cols</tt> attributes.
399
- #
434
+ #
435
+ # @example Supports `:rows` & `:cols` attributes.
400
436
  # textarea_tag('body', rows: 10, cols: 25)
401
437
  # #=> <textarea cols="25" id="body" name="body" rows="10">\n</textarea>
402
- #
403
- # Supports shortcut to +:rows+ & +:cols+ attributes, via the +:size+ attribute.
404
- #
438
+ #
439
+ # @example Supports shortcut to `:rows` & `:cols` attributes, via the `:size` attribute.
405
440
  # textarea_tag( 'body', size: "25x10")
406
441
  # #=> <textarea cols="25" id="body" name="body" rows="10"></textarea>
407
- #
408
- # Supports +:disabled+ & +:readonly+ attributes.
409
- #
442
+ #
443
+ # @example Supports `:disabled` & `:readonly` attributes.
410
444
  # textarea_tag(:description, disabled: true)
411
445
  # #=> <textarea disabled="disabled" id="description" name="description"></textarea>
412
- #
446
+ #
413
447
  # textarea_tag(:description, readonly: true)
414
448
  # #=> <textarea id="description" name="description" readonly="readonly"></textarea>
415
- #
416
- def textarea_tag(name, attrs = {})
449
+ #
450
+ def textarea_tag(name, attrs = {})
451
+ name = name.to_s
452
+ attrs = {} if attrs.nil?
453
+
417
454
  attrs.reverse_merge!(name: name)
418
455
  attrs = add_css_id(attrs, name)
419
- if size = attrs.delete(:size)
420
- attrs[:cols], attrs[:rows] = size.split('x') if size.respond_to?(:split)
456
+ if (size = attrs.delete(:size)) && size.respond_to?(:split) && size.match?(/^\d+x\d+$/)
457
+ attrs[:cols], attrs[:rows] = size.split('x')
421
458
  end
422
- content = attrs.delete(:value)
459
+
460
+ # TODO: add sanitation support of the value passed
461
+ # content = Rack::Utils.escape_html(attrs.delete(:value).to_s)
462
+ content = attrs.delete(:value).to_s
463
+
423
464
  attrs = add_ui_hint(attrs)
424
465
  tag(:textarea, content, attrs)
425
466
  end
426
- alias_method :text_area_tag, :textarea_tag
427
-
428
- # Creates a field set for grouping HTML form elements.
429
- #
430
- # ==== Examples
431
- #
432
- # <% field_set_tag(:actor) %>
433
- # #=>
434
- # <fieldset id="fieldset-actor">
435
- # ...
436
- # </fieldset>
437
- #
438
- # Sets the <tt><legend></tt> and <tt>:id</tt> attribute when given a single argument.
439
- #
440
- # <% field_set_tag 'User Details' do %>
441
- # <p><%= text_field_tag 'name' %></p>
467
+ alias text_area_tag textarea_tag
468
+
469
+ # Creates a fieldset tag wrapping the provided content
470
+ #
471
+ # @param args [Array] Arguments - first arg can be legend text or last arg can be attrs hash
472
+ # @param attrs [Hash] Optional HTML attributes hash if last arg is a hash
473
+ # @yield Optional block containing fieldset content
474
+ # @yieldreturn [String] The captured HTML content for the fieldset
475
+ #
476
+ # @return [String] The generated HTML fieldset with legend and content
477
+ #
478
+ # @example Basic usage with legend text
479
+ # fieldset_tag("User Details")
480
+ # #=> <fieldset id="fieldset-user-details">
481
+ # # <legend>User Details</legend>
482
+ # # </fieldset>
483
+ #
484
+ # @example With attributes and block content
485
+ # fieldset_tag("Details", class: "form-section") do
486
+ # text_field_tag(:name)
487
+ # end
488
+ # #=> <fieldset class="form-section" id="fieldset-details">
489
+ # # <legend>Details</legend>
490
+ # # <input type="text" name="name" id="name">
491
+ # # </fieldset>
492
+ #
493
+ # @example With legend in attributes
494
+ # fieldset_tag(legend: "Section", class: "bordered")
495
+ # #=> <fieldset class="bordered" id="fieldset">
496
+ # # <legend>Section</legend>
497
+ # # </fieldset>
498
+ #
499
+ # @example Sets the `<legend>` and `:id` attribute when given a single argument.
500
+ # <% fieldset_tag 'User Details' do %>
501
+ # <p><%= textfield_tag 'name' %></p>
442
502
  # <% end %>
443
- # #=>
444
- # <fieldset id="fieldset-user-details">
445
- # <legend>User Details</legend>
446
- # <p><input name="name" class="text" id="name" type="text"></p>
447
- # </fieldset>
448
- #
449
- # Supports <tt>:legend</tt> attribute for the <tt><legend></tt> tag.
450
- #
451
- # field_set_tag(:actor, legend: 'Your Details')
452
- # #=>
453
- # <fieldset id="fieldset-actor">
454
- # <legend>Your Details</legend>
455
- # <snip...>
456
- #
457
- # Adding a CSS class. NB! fieldset has no other class by default.
458
- #
459
- # field_set_tag(:actor, class: "legend-class")
460
- # #=>
461
- # <fieldset class="legend-class" id="fieldset-actor">
462
- # <snip...>
463
- #
464
- #
465
- # When passed +nil+ as the first argument the <tt>:id</tt> becomes 'fieldset'.
466
- #
467
- # field_set_tag( nil, class: 'format')
468
- # #=>
469
- # <fieldset class="format" id="fieldset">
470
- # <snip...>
471
- #
472
- # Removing the <tt>:id</tt> attribute completely.
473
- #
474
- # field_set_tag('User Details', id: false)
475
- # #=>
476
- # <fieldset>
477
- # <legend>User Details</legend>
478
- # <snip...>
479
- #
480
- # @api public
481
- def field_set_tag(*args, &block)
503
+ # #=> <fieldset id="fieldset-user-details">
504
+ # # <legend>User Details</legend>
505
+ # # <p><input name="name" class="text" id="name" type="text"></p>
506
+ # # </fieldset>
507
+ #
508
+ # @example Supports `:legend` attribute for the `<legend>` tag.
509
+ # fieldset_tag(:actor, legend: 'Your Details')
510
+ # #=> <fieldset id="fieldset-actor">
511
+ # # <legend>Your Details</legend>
512
+ # # <snip...>
513
+ #
514
+ # @example Adding a CSS class.
515
+ #
516
+ # NOTE! fieldset has no other class by default.
517
+ #
518
+ # fieldset_tag(:actor, class: "legend-class")
519
+ # #=> <fieldset class="legend-class" id="fieldset-actor">
520
+ # # <snip...>
521
+ #
522
+ # @example When passed +nil+ as the first argument the `:id` becomes 'fieldset'.
523
+ # fieldset_tag(nil, class: 'format')
524
+ # #=> <fieldset class="format" id="fieldset">
525
+ # # <snip...>
526
+ #
527
+ # @example Removing the `:id` attribute completely.
528
+ # fieldset_tag('User Details', id: false)
529
+ # #=> <fieldset>
530
+ # # <legend>User Details</legend>
531
+ # # <snip...>
532
+ #
533
+ # rubocop:disable Metrics/AbcSize
534
+ def fieldset_tag(*args, &block)
482
535
  attrs = args.last.is_a?(Hash) ? args.pop : {}
483
536
  attrs = add_css_id(attrs, ['fieldset', args.first].compact.join('-'))
537
+
484
538
  legend_text = args.first.is_a?(String || Symbol) ? args.first : attrs.delete(:legend)
485
539
  legend_html = legend_text.blank? ? '' : tag(:legend, legend_text)
486
540
  captured_html = block_given? ? capture_html(&block) : ''
487
541
  concat_content(tag(:fieldset, legend_html + captured_html, attrs))
488
542
  end
489
- alias_method :fieldset_tag, :field_set_tag
490
-
491
- # Return a legend with _contents_.
492
- #
493
- # ==== Examples
494
- #
543
+ # rubocop:enable Metrics/AbcSize
544
+ alias field_set_tag fieldset_tag
545
+
546
+ # Creates a legend tag with given contents and attributes
547
+ #
548
+ # @param contents [String] The text content for the legend
549
+ # @param attrs [Hash] HTML attributes for the legend tag
550
+ #
551
+ # @return [String] The generated HTML legend tag
552
+ #
553
+ # @example Basic usage
495
554
  # legend_tag('User Details')
496
555
  # #=> <legend>User Details</legend>
497
- #
498
- # Adding an :id attribute.
499
- #
500
- # legend_tag('User Details', id: 'some-id')
501
- # #=> <legend id="some-id">User Details</legend>
502
- #
503
- # Adding a CSS class. NB! legend has no other class by default.
504
- #
505
- # legend_tag('User Details', class: 'some-class')
506
- # #=> <legend class="some-class">User Details</legend>
507
- #
508
- def legend_tag(contents, attrs = {})
556
+ #
557
+ # @example With id attribute
558
+ # legend_tag('User Details', id: 'user-legend')
559
+ # #=> <legend id="user-legend">User Details</legend>
560
+ #
561
+ # @example With CSS class
562
+ # legend_tag('User Details', class: 'form-legend')
563
+ # #=> <legend class="form-legend">User Details</legend>
564
+ #
565
+ def legend_tag(contents, attrs = {})
509
566
  tag(:legend, contents, attrs)
510
567
  end
511
-
568
+
512
569
  # Creates a checkbox element.
513
- #
514
- # ==== Examples
515
- #
570
+ #
571
+ # @param name [String] The field name used for :id & :name attributes
572
+ # @param attrs [Hash] HTML attributes for the input[@type=checkbox] tag
573
+ #
574
+ # @return [String] The generated HTML input[@type=checkbox] tag
575
+ #
576
+ # @example Basic usage
516
577
  # check_box_tag(:accept)
517
578
  # #=> <input class="checkbox" id="accept" name="accept" type="checkbox" value="1">
518
- #
519
- # Providing a value:
520
- #
579
+ #
580
+ # @example Providing a value:
521
581
  # check_box_tag(:rock, value: 'rock music')
522
582
  # #=> <input class="checkbox" id="rock" name="rock" type="checkbox" value="rock music">
523
- #
524
- # Setting a different <tt>:id</tt>.
525
- #
583
+ #
584
+ # @example Setting a different `:id`.
526
585
  # check_box_tag(:rock, :id => 'some-id')
527
586
  # #=> <input class="checkbox" id="some-id" name="rock" type="checkbox" value="1">
528
- #
529
- # Adding another CSS class. NB! appends the the class to the default class +.checkbox+.
530
- #
587
+ #
588
+ # @example Adding another CSS class.
589
+ # NOTE! appends the the class to the default class `.checkbox`.
590
+ #
531
591
  # check_box_tag(:rock, class: 'small')
532
592
  # #=> <input class="small checkbox" id="rock" name="rock" type="checkbox" value="1">
533
- #
534
- # Adds a <tt>:title</tt> attribute when passed <tt>:ui_hint</tt>.
535
- #
593
+ #
594
+ # @example Adds a `:title` attribute when passed `:ui_hint`.
536
595
  # check_box_tag(:rock, ui_hint: 'a user hint')
537
596
  # #=> <input ... title="a user hint" type="checkbox" value="1">
538
- #
539
- # Supports the <tt>:disabled</tt> & <tt>:checked</tt> attributes.
540
- #
597
+ #
598
+ # @example Supports the `:disabled` & `:checked` attributes.
541
599
  # check_box_tag(:rock, checked: true)
542
600
  # #=> <input checked="checked" ... type="checkbox" value="1">
543
- #
601
+ #
544
602
  # check_box_tag(:rock, disabled: true)
545
603
  # #=> <input class="checkbox" disabled="disabled" ... type="checkbox" value="1">
546
- #
547
- def check_box_tag(name, attrs = {})
604
+ #
605
+ def check_box_tag(name, attrs = {})
606
+ name = name.to_s
607
+ attrs = {} if attrs.nil?
608
+
548
609
  attrs.reverse_merge!(name: name, type: :checkbox, checked: false, value: 1)
549
610
  attrs = add_css_id(attrs, name)
550
611
  attrs = add_css_class(attrs, :checkbox)
551
612
  attrs = add_ui_hint(attrs)
552
613
  tag(:input, attrs)
553
614
  end
554
- alias_method :checkbox_tag, :check_box_tag
555
-
556
- # Creates a radio button; use groups of radio buttons named the same to allow users to
615
+ alias checkbox_tag check_box_tag
616
+
617
+ # Creates a radio button; use groups of radio buttons named the same to allow users to
557
618
  # select from a group of options.
558
- #
559
- # ==== Examples
560
- #
619
+ #
620
+ # @param name [String] The field name used for :id & :name attributes
621
+ # @param attrs [Hash] HTML attributes for the input[@type=radio] tag
622
+ #
623
+ # @return [String] The generated HTML input[@type=radio] tag
624
+ #
625
+ # @example Basic usage
561
626
  # radio_button_tag(:accept)
562
627
  # #=> <input class="radio" id="accept_1" name="accept" type="radio" value="1">
563
- #
564
- # Providing a value:
565
- #
628
+ #
629
+ # @example Providing a value:
566
630
  # radio_button_tag(:rock, value: 'rock music')
567
631
  # #=> <input ... type="radio" value="rock music">
568
- #
569
- # Setting a different <tt>:id</tt>.
570
- #
571
- # radio_button_tag(:rock, id: 'some-id')
572
- # #=>
573
- # <input class="radio" id="some-id_1" name="rock" type="radio" value="1">
574
- #
575
- # Adding another CSS class. NB! appends the the class to the default class +.radio+.
576
- #
577
- # radio_button_tag(:rock, class: 'big')
632
+ #
633
+ # @example Setting a different `:id`.
634
+ # radio_button_tag(:rock, id: 'some-id')
635
+ # #=> <input class="radio" id="some-id_1" name="rock" type="radio" value="1">
636
+ #
637
+ # @example Adding another CSS class
638
+ # NOTE! appends the the class to the default class +.radio+.
639
+ #
640
+ # radio_button_tag(:rock, class: 'big')
578
641
  # #=> <input class="big radio" id="rock_1" name="rock" type="radio" value="1">
579
- #
580
- # Adds a <tt>:title</tt> attribute when passed <tt>:ui_hint</tt>.
581
- #
582
- # radio_button_tag(:rock, ui_hint: 'a user hint')
642
+ #
643
+ # @example Adds a `:title` attribute when passed `:ui_hint`.
644
+ # radio_button_tag(:rock, ui_hint: 'a user hint')
583
645
  # #=> <input ... title="a user hint" type="radio" value="1">
584
- #
585
- # Supports the <tt>:disabled</tt> & <tt>:checked</tt> attributes.
586
- #
646
+ #
647
+ # @example Supports the `:disabled` & `:checked` attributes.
587
648
  # radio_button_tag(:yes, checked: true)
588
649
  # #=> <input checked="checked" class="checkbox" id="yes_1"...value="1">
589
- #
650
+ #
590
651
  # radio_button_tag(:yes, disabled: true)
591
652
  # #=> <input disabled="disabled" class="checkbox" id="yes_1" ... value="1">
592
- #
593
- #
594
- def radio_button_tag(name, attrs = {})
653
+ #
654
+ def radio_button_tag(name, attrs = {})
655
+ name = name.to_s
656
+ attrs = {} if attrs.nil?
657
+
595
658
  attrs.reverse_merge!(name: name, type: :radio, checked: false, value: 1)
596
659
  attrs = add_css_id(attrs, name)
597
660
  # id_value = [field.to_s,'_',value].join
@@ -600,278 +663,411 @@ class Roda
600
663
  attrs = add_ui_hint(attrs)
601
664
  tag(:input, attrs)
602
665
  end
603
- alias_method :radiobutton_tag, :radio_button_tag
604
-
666
+ alias radiobutton_tag radio_button_tag
667
+
605
668
  # Creates a submit button with the text value as the caption.
606
- #
607
- # ==== Examples
608
- #
669
+ #
670
+ # @param name [String] The field name used for :id & :name attributes
671
+ # @param attrs [Hash] HTML attributes for the input[@type=submit] tag
672
+ #
673
+ # @return [String] The generated HTML input[@type=submit] tag
674
+ #
675
+ # @example Basic usage
609
676
  # <%= submit_tag %> || <%= submit_button %>
610
677
  # => <input name="submit" type="submit" value="Save Form">
611
678
  #
612
679
  # <%= submit_tag(nil) %>
613
680
  # => <input name="submit" type="submit" value="">
614
- #
681
+ #
615
682
  # <%= submit_tag("Custom Value") %>
616
683
  # => <input name="submit" type="submit" value="Custom Value">
617
- #
618
- # Adding a CSS class. NB! input[:submit] has no other class by default.
619
- #
684
+ #
685
+ # @example Adding a CSS class.
686
+ # NOTE! input[:submit] has no other class by default.
687
+ #
620
688
  # <%= submit_tag(class: 'some-class') %>
621
689
  # #=> <input class="some-class" name="submit" type="submit" value="Save Form">
622
- #
623
- # Supports the <tt>:disabled</tt> attribute.
624
- #
690
+ #
691
+ # @example Supports the `:disabled` attribute.
625
692
  # <%= submit_tag(disabled: true) %>
626
693
  # #=> <input disabled="disabled" name="submit" type="submit" value="Save Form">
627
- #
628
- # Adds a +:title+ attribute when passed +:ui_hint+. Also works with +:title+.
629
- #
694
+ #
695
+ # @example Adds a `:title` attribute when passed `:ui_hint`. Also works with `:title`.
630
696
  # <%= submit_tag(ui_hint: 'a user hint') %>
631
697
  # #=> <input name="submit" title="a user hint" type="submit" value="Save Form">
632
- #
633
- def submit_tag(value = 'Save Form', attrs = {})
634
- value, attrs = 'Save Form', value if value.is_a?(Hash)
698
+ #
699
+ def submit_tag(value = 'Save Form', attrs = {})
700
+ if value.is_a?(Hash)
701
+ attrs = value
702
+ value = 'Save Form'
703
+ end
635
704
  attrs.reverse_merge!(type: :submit, name: :submit, value: value)
636
705
  attrs = add_ui_hint(attrs)
637
706
  self_closing_tag(:input, attrs)
638
707
  end
639
- alias_method :submit_button, :submit_tag
640
-
641
- # Displays an image which when clicked will submit the form.
642
- #
643
- # ==== Examples
644
- #
645
- # @img = '/img/btn.png'
646
- #
647
- # image_submit_tag(@img)
648
- # #=> <input src="/img/btn.png" type="image">
649
- #
650
- # image_submit_tag(@img, disabled: true)
651
- # #=> <input disabled="disabled" src="/img/btn.png" type="image">
652
- #
653
- # image_submit_tag(@img, class 'search-button')
654
- # #=> <input class="search-button" src="/img/btn.png" type="image">
655
- #
708
+ alias submit_button submit_tag
709
+
710
+ # Creates a image submit button which submits the form when clicked.
711
+ #
712
+ # @param src [String] The URL/path to the image
713
+ # @param attrs [Hash] HTML attributes for the input[@type=image] tag
714
+ #
715
+ # @return [String] The generated HTML input[@type=image] tag
716
+ #
717
+ # @example Basic usage
718
+ # image_submit_tag('/images/submit.png')
719
+ # #=> <input src="/images/submit.png" type="image">
720
+ #
721
+ # @example With attributes
722
+ # image_submit_tag('/images/submit.png', class: 'submit-button')
723
+ # #=> <input class="submit-button" src="/images/submit.png" type="image">
724
+ #
725
+ # @example With disabled state
726
+ # image_submit_tag('/images/submit.png', disabled: true)
727
+ # #=> <input disabled="disabled" src="/images/submit.png" type="image">
728
+ #
656
729
  def image_submit_tag(src, attrs = {})
657
730
  tag(:input, { type: :image, src: src }.merge(attrs))
658
731
  end
659
- alias_method :imagesubmit_tag, :image_submit_tag
660
-
732
+ alias imagesubmit_tag image_submit_tag
733
+
661
734
  # Creates a reset button with the text value as the caption.
662
- #
663
- # ==== Examples
664
- #
735
+ #
736
+ # @param value [String] The button text
737
+ # @param attrs [Hash] HTML attributes for the input[@type=reset] tag
738
+ #
739
+ # @return [String] The generated HTML input[@type=reset] tag
740
+ #
741
+ # @example Basic usage
665
742
  # <%= reset_tag %>
666
743
  # => <input name="reset" type="reset" value="Reset Form">
667
744
  #
668
745
  # <%= reset_tag(nil) %>
669
746
  # => <input name="reset" type="reset" value="">
670
- #
671
- # Adding a CSS class. NB! input[:reset] has no other class by default.
672
- #
747
+ #
748
+ # @example Adding a CSS class
749
+ # NOTE! input[:reset] has no other class by default.
750
+ #
673
751
  # <%= reset_tag('Custom Value', class: 'some-class') %>
674
752
  # => <input class="some-class" name="reset" type="reset" value="Custom Value" >
675
- #
676
- # Supports the <tt>:disabled</tt> attribute.
677
- #
753
+ #
754
+ # @example Supports the `:disabled` attribute.
678
755
  # <%= reset_tag('Custom Value', disabled: true) %>
679
- # => <input disabled="disabled" name="reset" type="reset" value="Custom Value">
680
- #
681
- # Adds a <tt>:title</tt> attribute when passed <tt>:ui_hint</tt>.
682
- #
756
+ # => <input disabled="disabled" name="reset" type="reset" value="Custom Value">
757
+ #
758
+ # @example Adds a `:title` attribute when passed `:ui_hint`.
683
759
  # <%= reset_tag('Custom Value', ui_hint: 'a user hint') %>
684
- # => <input name="reset" title="a user hint" type="submit" value="Custom Value">
685
- #
760
+ # => <input name="reset" title="a user hint" type="reset" value="Custom Value">
761
+ #
686
762
  def reset_tag(value = 'Reset Form', attrs = {})
687
- value, attrs = 'Reset Form', value if value.is_a?(Hash)
763
+ if value.is_a?(Hash)
764
+ attrs = value
765
+ value = 'Reset Form'
766
+ end
688
767
  attrs.reverse_merge!(type: :reset, name: :reset, value: value)
689
768
  attrs = add_ui_hint(attrs)
690
769
  self_closing_tag(:input, attrs)
691
770
  end
692
- alias_method :reset_button, :reset_tag
693
-
694
- # Creates a dropdown selection menu.
695
- #
696
- # If the :multiple option is set to true, a multiple choice selection box
697
- # is created.
698
- #
699
- # ==== Attributes
700
- #
701
- # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices.
702
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
703
- #
704
- # Any other key creates standard HTML attributes for the tag.
705
- #
706
- #
707
- # ==== Examples
708
- #
709
- # NB! the format for the options values must be [value, key].
710
- #
711
- # With Options values as a Hash
712
- #
713
- # select_tag(:letters, {a: 'A', b: 'B' })
714
- # #=>
715
- # <select id="letters" name="letters">
716
- # <option value="a">A</option>
717
- # <option value="b">B</option>
718
- # </select>
719
- #
720
- # With Options values as an Array
721
- #
722
- # @letters = [[:a,'A'], [:b,'B']]
723
- #
724
- # select_tag(:letters, @letters)
725
- # #=>
726
- # <select id="letters" name="letters">
727
- # <option value="a">A</option>
728
- # <option value="b">B</option>
729
- # </select>
730
- #
731
- #
732
- # With Options values as an Array
733
- #
771
+ alias reset_button reset_tag
772
+
773
+ # Creates a select (dropdown menu) form element with options
774
+ # Automatically handles single vs multiple selection modes
775
+ #
776
+ # @param name [String, Symbol] The name/id for the select element
777
+ # @param options [Array, Hash] The options to populate the dropdown
778
+ # Can be array of [value, text] pairs or hash of value => text mappings
779
+ # @param attrs [Hash] HTML attributes to apply to the select element
780
+ # Special options include:
781
+ # - :selected => Value(s) to mark as selected
782
+ # - :multiple => Allow multiple selections
783
+ # - :prompt => Add placeholder prompt option
784
+ #
785
+ # NOTE! the format for the options values must be [value, key].
786
+ #
787
+ # @return [String] The generated HTML select element with options
788
+ #
789
+ # @example Basic dropdown with array options
790
+ # select_tag(:color, [['red', 'Red'], ['blue', 'Blue']])
791
+ # #=> <select id="color" name="color">
792
+ # # <option value="red">Red</option>
793
+ # # <option value="blue">Blue</option>
794
+ # # </select>
795
+ #
796
+ # @example Multiple select with hash options
797
+ #
798
+ # NOTE! the [] on the `:name` attribute
799
+ #
800
+ # select_tag(:colors, {red: 'Red', blue: 'Blue'}, multiple: true)
801
+ # #=> <select id="colors" name="colors[]" multiple="multiple">
802
+ # # <option value="red">Red</option>
803
+ # # <option value="blue">Blue</option>
804
+ # # <snip...>
805
+ #
806
+ # @example With Selected Option value
734
807
  # select_tag(:letters, @letters, selected: :a)
735
- # #=>
736
- # <select id="letters" name="letters">
737
- # <option selected="selected" value="a">A</option>
738
- # <snip...>
739
- #
740
- # When passing multiple items to :selected, the select menu automatically becomes
741
- # a multiple select box. <b>NB! the [] on the <tt>:name</tt> attribute</b>
742
- #
808
+ # #=> <select id="letters" name="letters">
809
+ # # <option selected="selected" value="a">A</option>
810
+ # # <snip...>
811
+ #
812
+ # @example With Multiple Selected Options Array
813
+ #
814
+ # NOTE! the [] on the `:name` attribute and the select menu automatically
815
+ # becomes a multiple select box.
816
+ #
743
817
  # select_tag(:letters, @letters, selected: [:a,'b'])
744
- # #=>
745
- # <select id="letters" multiple="multiple" name="letters[]">
746
- # <option selected="selected" value="a">A</option>
747
- # <option selected="selected" value="b">B</option>
748
- # </select>
749
- #
750
- # When { multiple: true }, the select menu becomes a select box allowing multiple choices.
751
- # <b>NB! the [] on the <tt>:name</tt> attribute</b>
752
- #
753
- # select_tag(:letters, @letters, multiple: true)
754
- # #=>
755
- # <select id="letters" name="letters[]" multiple="multiple">
756
- # <snip...>
757
- #
758
- # select_tag(:letters, @letters, disabled: true)
759
- # #=>
760
- # <select id="letters" disabled="disabled" name="letters">
761
- # <snip...>
762
- #
818
+ # #=> <select id="letters" multiple="multiple" name="letters[]">
819
+ # # <option selected="selected" value="a">A</option>
820
+ # # <option selected="selected" value="b">B</option>
821
+ # # <snip...>
822
+ #
823
+ # @example With custom `:id` attribute
763
824
  # select_tag(:letters, @letters, id: 'my-letters')
764
- # #=>
765
- # <select id="my-letters" name="letters">
766
- # <snip...>
767
- #
825
+ # #=> <select id="my-letters" name="letters">
826
+ # # <snip...>
827
+ #
828
+ # @example With custom `:class` attribute
768
829
  # select_tag(:letters, @letters, class: 'funky-select')
769
- # #=>
770
- # <select class="funky-select" id="my-letters" name="letters">
771
- # <snip...>
772
- #
830
+ # #=> <select class="funky-select" id="my-letters" name="letters">
831
+ # # <snip...>
832
+ #
833
+ # @example With `prompt: true` attribute
773
834
  # select_tag(:letters, @letters, prompt: true)
774
- # #=>
775
- # <select id="letters" name="letters">
776
- # <option selected="selected" value="">- Select -</option>
777
- # <snip...>
778
- #
779
- # select_tag(:letters, @letters, prompt: 'Top Letters', selected: 'a')
780
- # #=>
781
- # <select id="letters" name="letters">
782
- # <option value="">Top Letters</option>
783
- # <option selected="selected" value="a">A</option>
784
- # <snip...>
785
- #
786
- #
787
- def select_tag(name, options, attrs = {})
835
+ # #=> <select id="letters" name="letters">
836
+ # # <option selected="selected" value="">- Select -</option>
837
+ # # <snip...>
838
+ #
839
+ # @example With `prompt: 'Custom'` attribute
840
+ # select_tag(:letters, @letters, prompt: 'Top Letters')
841
+ # #=> <select id="letters" name="letters">
842
+ # # <option value="">Top Letters</option>
843
+ # # <snip...>
844
+
845
+ # @example With `disabled: true` attribute
846
+ # select_tag(:letters, @letters, disabled: true)
847
+ # #=> <select id="letters" disabled="disabled" name="letters">
848
+ # # <snip...>
849
+ #
850
+ # rubocop:disable Metrics/AbcSize
851
+ def select_tag(name, options, attrs = {})
852
+ name = name.to_s
853
+ attrs = {} if attrs.nil?
854
+
788
855
  options = options.to_a.reverse if options.is_a?(Hash)
789
856
  attrs[:multiple] = true if attrs[:selected].is_a?(Array)
790
857
  options_html = select_options(options, attrs)
791
858
  attrs.delete(:selected)
792
859
  # attrs = add_css_id(attrs, name)
793
860
  add_css_id(attrs, name)
794
- html_name = (attrs[:multiple] == true && !name.to_s.end_with?('[]')) ? "#{name}[]" : name
861
+
862
+ html_name = attrs[:multiple] == true && !name.to_s.end_with?('[]') ? "#{name}[]" : name
863
+
795
864
  tag(:select, options_html, { name: html_name }.merge(attrs))
796
865
  end
797
-
798
- # Return select option _contents_ with _value_.
799
- #
800
- # ==== Examples
801
- #
802
- # select_option('a', 'Letter A') #=> <option value="a">Letter A</option>
803
- #
804
- # select_option('on', '') #=> <option value="on">On</option>
805
- #
866
+ # rubocop:enable Metrics/AbcSize
867
+
868
+ # Creates an option tag for use in a select dropdown menu.
869
+ #
870
+ # @param value [String] The value attribute for the option
871
+ # @param key [String] The text content shown to the user
872
+ # @param attrs [Hash] Additional HTML attributes for the option tag
873
+ #
874
+ # @return [String] The generated HTML option tag
875
+ #
876
+ # @example Basic usage
877
+ # select_option('a', 'Letter A')
878
+ # #=> <option value="a">Letter A</option>
879
+ #
880
+ # @example When key is blank, titleizes value
881
+ # select_option('on', '')
882
+ # #=> <option value="on">On</option>
883
+ #
884
+ # @example With selected attribute
806
885
  # select_option('a', 'Letter A', selected: true)
807
886
  # #=> <option selected="selected" value="a">Letter A</option>
808
- #
809
- # select_option('a', 'Letter A', selected: false)
810
- # #=> <option value="a">Letter A</option>
811
- #
887
+ #
812
888
  def select_option(value, key, attrs = {})
813
889
  key = value.to_s.titleize if key.blank?
814
890
  tag(:option, key, { value: value }.merge(attrs))
815
891
  end
816
-
817
- # Support Rack::MethodOverride
818
- #
892
+
893
+ # Creates a hidden input field for HTTP method override support (e.g. PUT/DELETE requests)
894
+ # Used internally by form_tag for non-GET/POST methods.
895
+ # Supports `Rack::MethodOverride`
896
+ #
897
+ # @param method [String] The HTTP method to override with (default: 'PUT')
898
+ # @return [String] Hidden input field with _method override
899
+ #
900
+ # @example Basic usage
901
+ # faux_method('DELETE')
902
+ # #=> <input name="_method" type="hidden" value="DELETE">
903
+ #
904
+ # @example Default PUT method
905
+ # faux_method
906
+ # #=> <input name="_method" type="hidden" value="PUT">
907
+ #
819
908
  def faux_method(method = 'PUT')
820
909
  hidden_field_tag(:input, name: '_method', value: method.to_s.upcase)
821
910
  end
822
-
823
-
911
+
824
912
  private
825
-
826
- #
913
+
914
+ # Returns the tag helper options hash from Roda options.
915
+ # These options control default HTML attributes and formatting for generated tags.
916
+ #
917
+ # @return [Hash] The tag helper configuration options
918
+ #
919
+ # @example Basic usage
920
+ # opts_tag_helpers[:tags_label_required_str]
921
+ # #=> '<span>*</span>'
922
+ #
827
923
  def opts_tag_helpers
828
924
  opts[:tag_helpers]
829
925
  end
830
-
926
+
927
+ # Converts a string to an HTML safe ID attribute value
928
+ # - Converts to lowercase
929
+ # - Replaces non-word characters with hyphens
930
+ # - Collapses multiple hyphens into single hyphens
931
+ #
932
+ # @param id [String, #to_s] The value to convert to an HTML safe ID
933
+ #
934
+ # @return [String] The sanitized ID value
831
935
  #
832
- def html_safe_id(id)
936
+ # @example Basic usage
937
+ # html_safe_id("Hello World!")
938
+ # #=> "hello-world-"
939
+ #
940
+ # @example With symbols
941
+ # html_safe_id(:hello_world)
942
+ # #=> "hello-world"
943
+ #
944
+ # @example Collapsing multiple hyphens
945
+ # html_safe_id("too--many---hyphens")
946
+ # #=> "too-many-hyphens"
947
+ #
948
+ def html_safe_id(id)
833
949
  id.to_s.downcase.gsub(/\W/, '-').gsub('--', '-')
834
950
  end
835
-
836
- # do we have a class attrs already
951
+
952
+ # Adds CSS classes to the HTML attributes hash.
953
+ # Merges new classes with any existing classes, preserving existing ones.
954
+ #
955
+ # @param attrs [Hash] HTML attributes hash
956
+ # @param new_class [String, Symbol, nil] Additional CSS class(es) to add
957
+ #
958
+ # @return [Hash] Updated attributes hash with merged classes
959
+ #
960
+ # @example Basic usage
961
+ # add_css_class({}, 'btn')
962
+ # #=> { class: 'btn' }
963
+ #
964
+ # @example With existing classes
965
+ # add_css_class({class: 'red'}, 'btn')
966
+ # #=> { class: 'red btn' }
967
+ #
968
+ # @example With nil new_class
969
+ # add_css_class({class: 'red'}, nil)
970
+ # #=> { class: 'red' }
971
+ #
837
972
  def add_css_class(attrs, new_class = nil)
838
973
  merge_attr_classes(attrs, new_class)
839
974
  end
840
-
975
+
976
+ # Adds or updates the ID attribute in the HTML attributes hash.
977
+ # Handles nil/empty values and sanitizes the ID for HTML safety.
978
+ #
979
+ # @param attrs [Hash, nil] HTML attributes hash to modify
980
+ # @param new_id [String, Symbol, Hash] New ID value to set
981
+ #
982
+ # @return [Hash] Updated attributes hash with ID added/modified
983
+ #
984
+ # @example Basic usage
985
+ # add_css_id({}, 'my-id')
986
+ # #=> { id: 'my-id' }
987
+ #
988
+ # @example With existing ID
989
+ # add_css_id({id: 'old'}, 'new')
990
+ # #=> { id: 'old' }
841
991
  #
842
- def add_css_id(attrs, new_id)
992
+ # @example With false ID
993
+ # add_css_id({}, false)
994
+ # #=> {} # No ID added
995
+ #
996
+ # @example With empty ID
997
+ # add_css_id({}, '')
998
+ # #=> {} # ID removed
999
+ #
1000
+ def add_css_id(attrs, new_id)
843
1001
  attrs = {} if attrs.nil?
844
1002
  new_id = '' if new_id.is_a?(Hash)
845
- id_value = attrs[:id].nil? ? html_safe_id(new_id.to_s) : attrs.delete(:id)
1003
+ id_value = attrs[:id].nil? ? html_safe_id(new_id.to_s) : attrs.delete(:id)
846
1004
  attrs[:id] = id_value.to_s unless id_value == false
847
1005
  attrs[:id] = nil if attrs[:id] == '' # set to nil to remove from tag output
848
1006
  attrs
849
1007
  end
850
-
1008
+
1009
+ # Adds a title attribute to HTML elements based on ui_hint option
1010
+ # Moves the :ui_hint value to :title if present
1011
+ #
1012
+ # @param attrs [Hash] HTML attributes hash
1013
+ #
1014
+ # @return [Hash] Updated attributes hash with ui_hint moved to title
851
1015
  #
852
- def add_ui_hint(attrs)
1016
+ # @example Basic usage
1017
+ # add_ui_hint({ui_hint: 'Help text'})
1018
+ # #=> {title: 'Help text'}
1019
+ #
1020
+ # @example With no ui_hint
1021
+ # add_ui_hint({class: 'btn'})
1022
+ # #=> {class: 'btn'}
1023
+ #
1024
+ def add_ui_hint(attrs)
853
1025
  attrs[:title] = attrs.delete(:ui_hint) unless attrs[:ui_hint].nil?
854
1026
  attrs
855
1027
  end
856
-
857
- # Return select option elements from _values_ with _options_ passed.
858
- #
859
- # === Options
860
- #
861
- # :selected string, symbol, or array of options selected
862
- #
863
- # ==== Examples
864
- #
865
- #
1028
+
1029
+ # Creates and returns select option elements from an array of values or hash
1030
+ # Handles optgroups, selected values, and prompts
1031
+ #
1032
+ # @param values [Array, Hash] The values to create options from. Can be:
1033
+ # - Array of [value, text] pairs: `[["a", "Option A"], ["b", "Option B"]]`
1034
+ # - Hash of value => text pairs: `{"a" => "Option A", "b" => "Option B"}`
1035
+ # - Nested hash for optgroups: `{"Group 1" => {"a" => "Option A"}}`
1036
+ # @param attrs [Hash] HTML attributes hash containing selection options
1037
+ # @option attrs [String, Array] :selected Value(s) to mark as selected
1038
+ # @option attrs [String, true] :prompt Optional prompt text or true for default
1039
+ #
1040
+ # @return [String] Generated HTML option tags
1041
+ #
1042
+ # @example Basic array of options
1043
+ # select_options([["a", "Option A"], ["b", "Option B"]])
1044
+ # #=> <option value="a">Option A</option>
1045
+ # # <option value="b">Option B</option>
1046
+ #
1047
+ # @example With selected value
1048
+ # select_options([["a", "A"], ["b", "B"]], selected: "a")
1049
+ # #=> <option value="a" selected="selected">A</option>
1050
+ # # <option value="b">B</option>
1051
+ #
1052
+ # @example With optgroups
1053
+ # select_options({ 'Group 1' => {:a => 'A' }, 'Group 2' => { 'b' => 'B' } })
1054
+ # #=> <optgroup label="Group 1">
1055
+ # # <option value="a">A</option>
1056
+ # # </optgroup>
1057
+ # # <optgroup label="Group 2">
1058
+ # # <option value="b">B</option>
1059
+ # # </optgroup>
1060
+ #
1061
+ # rubocop:disable Metrics/MethodLength
866
1062
  def select_options(values, attrs = {})
867
- attrs = {} if attrs.blank?
1063
+ attrs = {} if attrs.blank?
868
1064
  values = [] if values.blank?
869
1065
  normalize_select_prompt(values, attrs)
870
1066
  # { :a => 'A' }
871
1067
  # [5, 'E']
872
- # FIXME:: when passed a Hash of values, they become reversed (last first and so on..)
873
-
874
- values.map do |value, key|
1068
+ # FIXME:: when passed a Hash of values, they become reversed (last first and so on..)
1069
+
1070
+ values.map do |value, key|
875
1071
  if value.is_a?(Hash)
876
1072
  tag(:optgroup, select_options(value, attrs), label: key)
877
1073
  elsif option_selected?(value, attrs[:selected])
@@ -881,39 +1077,63 @@ class Roda
881
1077
  end
882
1078
  end.join
883
1079
  end
884
-
885
- # Normalize select prompt.
886
- #
887
- # * When +attrs+ contains a :prompt string it is assigned as the prompt
888
- # * When :prompt is true the default of '- Select Model -' will become the prompt
889
- # * The prompt is selected unless a specific option is explicitly selected.
890
- #
891
- def normalize_select_prompt(values, attrs = {})
1080
+ # rubocop:enable Metrics/MethodLength
1081
+
1082
+ # Normalizes the prompt option for select dropdowns
1083
+ # - Removes prompt from attributes after processing
1084
+ # - Sets blank option as selected by default unless explicit selection
1085
+ # - Uses default "- Select -" text if prompt is true
1086
+ # - Adds prompt as first empty option in values array
1087
+ #
1088
+ # @param values [Array] Array of select options to prepend prompt to
1089
+ # @param attrs [Hash] Attributes hash containing prompt option
1090
+ #
1091
+ # @example With true prompt
1092
+ # normalize_select_prompt(values, prompt: true)
1093
+ # # Adds ["", "- Select -"] as first option
1094
+ #
1095
+ # @example With custom prompt text
1096
+ # normalize_select_prompt(values, prompt: "Choose one")
1097
+ # # Adds ["", "Choose one"] as first option
1098
+ #
1099
+ def normalize_select_prompt(values, attrs = {})
892
1100
  return unless attrs.key?(:prompt)
1101
+
893
1102
  prompt = attrs.delete(:prompt)
894
1103
  attrs[:selected] = '' unless attrs.include?(:selected)
895
1104
  prompt_text = prompt == true ? '- Select -' : prompt
896
1105
  values.unshift(['', prompt_text])
897
1106
  end
898
-
899
- # Check if option _key_ is _selected_.
900
- def option_selected?(key, selection)
901
- # if Array === selection
902
- if selection.is_a?(Array)
903
- # (selection.map { |s| s.to_s }).include?(key.to_s)
904
- (selection.map(&:to_s)).include?(key.to_s)
1107
+
1108
+ # Checks if a select option value should be marked as selected
1109
+ #
1110
+ # @param key [String, Symbol] The option value to check
1111
+ # @param selection [String, Symbol, Array] The currently selected value(s)
1112
+ #
1113
+ # @return [Boolean] true if option should be selected, false otherwise
1114
+ #
1115
+ # @example With single selection
1116
+ # option_selected?('a', 'a') #=> true
1117
+ # option_selected?('a', 'b') #=> false
1118
+ #
1119
+ # @example With array of selections
1120
+ # option_selected?('a', ['a', 'b']) #=> true
1121
+ # option_selected?('c', ['a', 'b']) #=> false
1122
+ #
1123
+ def option_selected?(key, selection)
1124
+ if selection.is_a?(Array)
1125
+ selection.map(&:to_s).include?(key.to_s)
905
1126
  else
906
1127
  selection.to_s == key.to_s
907
1128
  end
908
1129
  end
909
-
910
-
911
- end # /InstanceMethods
912
-
913
- end # /RodaTagHelpers
914
-
1130
+ end
1131
+ # rubocop:enable Metrics/ModuleLength
1132
+ # /InstanceMethods
1133
+ end
1134
+ # /RodaTagHelpers
1135
+
915
1136
  register_plugin(:tag_helpers, RodaTagHelpers)
916
-
917
- end # /RodaPlugins
918
-
1137
+ end
1138
+ # /RodaPlugins
919
1139
  end