roda-tags 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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