decidim-friendly_signup 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,915 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "foundation_rails_helper/form_builder"
4
+
5
+ module Decidim
6
+ # This custom FormBuilder adds fields needed to deal with translatable fields,
7
+ # following the conventions set on `Decidim::TranslatableAttributes`.
8
+ class FormBuilder < FoundationRailsHelper::FormBuilder
9
+ include ActionView::Context
10
+ include Decidim::TranslatableAttributes
11
+ include Decidim::Map::Autocomplete::FormBuilder
12
+
13
+ # Public: generates a check boxes input from a collection and adds help
14
+ # text and errors.
15
+ #
16
+ # attribute - the name of the field
17
+ # collection - the collection from which we will render the check boxes
18
+ # value_attribute - a Symbol or a Proc defining how to find the value
19
+ # attribute
20
+ # text_attribute - a Symbol or a Proc defining how to find the text
21
+ # attribute
22
+ # options - a Hash with options
23
+ # html_options - a Hash with options
24
+ #
25
+ # Renders a collection of check boxes.
26
+ # rubocop:disable Metrics/ParameterLists
27
+ def collection_check_boxes(attribute, collection, value_attribute, text_attribute, options = {}, html_options = {})
28
+ super + error_and_help_text(attribute, options)
29
+ end
30
+ # rubocop:enable Metrics/ParameterLists
31
+
32
+ # Public: generates a radio buttons input from a collection and adds help
33
+ # text and errors.
34
+ #
35
+ # attribute - the name of the field
36
+ # collection - the collection from which we will render the radio buttons
37
+ # value_attribute - a Symbol or a Proc defining how to find the value attribute
38
+ # text_attribute - a Symbol or a Proc defining how to find the text attribute
39
+ # options - a Hash with options
40
+ # html_options - a Hash with options
41
+ #
42
+ # Renders a collection of radio buttons.
43
+ # rubocop:disable Metrics/ParameterLists
44
+ def collection_radio_buttons(attribute, collection, value_attribute, text_attribute, options = {}, html_options = {})
45
+ super + error_and_help_text(attribute, options)
46
+ end
47
+ # rubocop:enable Metrics/ParameterLists
48
+
49
+ def create_language_selector(locales, tabs_id, name)
50
+ if Decidim.available_locales.count > 4
51
+ language_selector_select(locales, tabs_id, name)
52
+ else
53
+ language_tabs(locales, tabs_id, name)
54
+ end
55
+ end
56
+
57
+ # Public: Generates an form field for each locale.
58
+ #
59
+ # type - The form field's type, like `text_area` or `text_input`
60
+ # name - The name of the field
61
+ # options - The set of options to send to the field
62
+ #
63
+ # Renders form fields for each locale.
64
+ def translated(type, name, options = {})
65
+ return translated_one_locale(type, name, locales.first, options.merge(label: (options[:label] || label_for(name)))) if locales.count == 1
66
+
67
+ tabs_id = sanitize_tabs_selector(options[:tabs_id] || "#{object_name}-#{name}-tabs")
68
+
69
+ label_tabs = content_tag(:div, class: "label--tabs") do
70
+ field_label = label_i18n(name, options[:label] || label_for(name))
71
+
72
+ language_selector = "".html_safe
73
+ language_selector = create_language_selector(locales, tabs_id, name) if options[:label] != false
74
+
75
+ safe_join [field_label, language_selector]
76
+ end
77
+
78
+ hashtaggable = options.delete(:hashtaggable)
79
+ tabs_content = content_tag(:div, class: "tabs-content", data: { tabs_content: tabs_id }) do
80
+ locales.each_with_index.inject("".html_safe) do |string, (locale, index)|
81
+ tab_content_id = "#{tabs_id}-#{name}-panel-#{index}"
82
+ string + content_tag(:div, class: tab_element_class_for("panel", index), id: tab_content_id) do
83
+ if hashtaggable
84
+ hashtaggable_text_field(type, name, locale, options.merge(label: false))
85
+ elsif type.to_sym == :editor
86
+ send(type, name_with_locale(name, locale), options.merge(label: false, hashtaggable: hashtaggable))
87
+ else
88
+ send(type, name_with_locale(name, locale), options.merge(label: false))
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ safe_join [label_tabs, tabs_content]
95
+ end
96
+
97
+ def translated_one_locale(type, name, locale, options = {})
98
+ return hashtaggable_text_field(type, name, locale, options) if options.delete(:hashtaggable)
99
+
100
+ send(
101
+ type,
102
+ "#{name}_#{locale.to_s.gsub("-", "__")}",
103
+ options.merge(label: options[:label] || label_for(name))
104
+ )
105
+ end
106
+
107
+ # Public: Generates a field for hashtaggable type.
108
+ # type - The form field's type, like `text_area` or `text_input`
109
+ # name - The name of the field
110
+ # handlers - The social handlers to be created
111
+ # options - The set of options to send to the field
112
+ #
113
+ # Renders form fields for each locale.
114
+ def hashtaggable_text_field(type, name, locale, options = {})
115
+ options[:hashtaggable] = true if type.to_sym == :editor
116
+
117
+ content_tag(:div, class: "hashtags__container") do
118
+ if options[:value]
119
+ send(type, name_with_locale(name, locale), options.merge(label: options[:label], value: options[:value][locale]))
120
+ else
121
+ send(type, name_with_locale(name, locale), options.merge(label: options[:label]))
122
+ end
123
+ end
124
+ end
125
+
126
+ # Public: Generates an form field for each social.
127
+ #
128
+ # type - The form field's type, like `text_area` or `text_input`
129
+ # name - The name of the field
130
+ # handlers - The social handlers to be created
131
+ # options - The set of options to send to the field
132
+ #
133
+ # Renders form fields for each locale.
134
+ def social_field(type, name, handlers, options = {})
135
+ tabs_id = sanitize_tabs_selector(options[:tabs_id] || "#{object_name}-#{name}-tabs")
136
+
137
+ label_tabs = content_tag(:div, class: "label--tabs") do
138
+ field_label = label_i18n(name, options[:label] || label_for(name))
139
+
140
+ tabs_panels = "".html_safe
141
+ if options[:label] != false
142
+ tabs_panels = content_tag(:ul, class: "tabs tabs--lang", id: tabs_id, data: { tabs: true }) do
143
+ handlers.each_with_index.inject("".html_safe) do |string, (handler, index)|
144
+ string + content_tag(:li, class: tab_element_class_for("title", index)) do
145
+ title = I18n.t(".#{handler}", scope: "activemodel.attributes.#{object_name}")
146
+ tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
147
+ content_tag(:a, title, href: "##{tab_content_id}")
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ safe_join [field_label, tabs_panels]
154
+ end
155
+
156
+ tabs_content = content_tag(:div, class: "tabs-content", data: { tabs_content: tabs_id }) do
157
+ handlers.each_with_index.inject("".html_safe) do |string, (handler, index)|
158
+ tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
159
+ string + content_tag(:div, class: tab_element_class_for("panel", index), id: tab_content_id) do
160
+ send(type, "#{handler}_handler", options.merge(label: false))
161
+ end
162
+ end
163
+ end
164
+
165
+ safe_join [label_tabs, tabs_content]
166
+ end
167
+
168
+ # Public: generates a hidden field and a container for WYSIWYG editor
169
+ #
170
+ # name - The name of the field
171
+ # options - The set of options to send to the field
172
+ # :label - The Boolean value to create or not the input label (optional) (default: true)
173
+ # :toolbar - The String value to configure WYSIWYG toolbar. It should be 'basic' or
174
+ # or 'full' (optional) (default: 'basic')
175
+ # :lines - The Integer to indicate how many lines should editor have (optional) (default: 10)
176
+ # :disabled - Whether the editor should be disabled
177
+ # :editor_images - Allow attached images (optional) (default: false)
178
+ #
179
+ # Renders a container with both hidden field and editor container
180
+ def editor(name, options = {})
181
+ options[:disabled] ||= false
182
+ toolbar = options.delete(:toolbar) || "basic"
183
+ lines = options.delete(:lines) || 10
184
+ label_text = options[:label].to_s
185
+ label_text = label_for(name) if label_text.blank?
186
+ options.delete(:required)
187
+ hashtaggable = options.delete(:hashtaggable)
188
+ hidden_options = extract_validations(name, options).merge(options)
189
+
190
+ content_tag(:div, class: "editor #{"hashtags__container" if hashtaggable}") do
191
+ template = ""
192
+ template += label(name, label_text + required_for_attribute(name)) if options.fetch(:label, true)
193
+ template += hidden_field(name, hidden_options)
194
+ template += content_tag(:div, nil, class: "editor-container #{"js-hashtags" if hashtaggable}", data: {
195
+ toolbar: toolbar,
196
+ disabled: options[:disabled]
197
+ }.merge(editor_images_options(options)), style: "height: #{lines}rem")
198
+ template += error_for(name, options) if error?(name)
199
+ template.html_safe
200
+ end
201
+ end
202
+
203
+ # Public: Generates a select field with the categories. Only leaf categories can be set as selected.
204
+ #
205
+ # name - The name of the field (usually category_id)
206
+ # collection - A collection of categories.
207
+ # options - An optional Hash with options:
208
+ # - prompt - An optional String with the text to display as prompt.
209
+ # - disable_parents - A Boolean to disable parent categories. Defaults to `true`.
210
+ # html_options - HTML options for the select
211
+ #
212
+ # Returns a String.
213
+ def categories_select(name, collection, options = {}, html_options = {})
214
+ options = {
215
+ disable_parents: true
216
+ }.merge(options)
217
+
218
+ disable_parents = options[:disable_parents]
219
+
220
+ selected = object.send(name)
221
+ selected = selected.first if selected.is_a?(Array) && selected.length > 1
222
+ categories = categories_for_select(collection)
223
+ disabled = if disable_parents
224
+ disabled_categories_for(collection)
225
+ else
226
+ []
227
+ end
228
+
229
+ select(name, @template.options_for_select(categories, selected: selected, disabled: disabled), options, html_options)
230
+ end
231
+
232
+ # Public: Generates a select field for areas.
233
+ #
234
+ # name - The name of the field (usually area_id)
235
+ # collection - A collection of areas or area_types.
236
+ # If it's areas, we sort the selectable options alphabetically.
237
+ #
238
+ # Returns a String.
239
+ def areas_select(name, collection, options = {}, html_options = {})
240
+ selectables = if collection.first.is_a?(Decidim::Area)
241
+ assemblies = collection
242
+ .map { |a| [a.name[I18n.locale.to_s], a.id] }
243
+ .sort_by { |arr| arr[0] }
244
+
245
+ @template.options_for_select(
246
+ assemblies,
247
+ selected: options[:selected]
248
+ )
249
+ else
250
+ @template.option_groups_from_collection_for_select(
251
+ collection,
252
+ :areas,
253
+ :translated_name,
254
+ :id,
255
+ :translated_name,
256
+ selected: options[:selected]
257
+ )
258
+ end
259
+
260
+ select(name, selectables, options, html_options)
261
+ end
262
+
263
+ # Public: Generates a select field for resource types.
264
+ #
265
+ # name - The name of the field (usually resource_type)
266
+ # collection - A collection of resource types.
267
+ # The options are sorted alphabetically.
268
+ #
269
+ # Returns a String.
270
+ def resources_select(name, collection, options = {})
271
+ resources =
272
+ collection
273
+ .map { |r| [I18n.t(r.split("::").last.underscore, scope: "decidim.components.component_order_selector.order"), r] }
274
+ .sort
275
+
276
+ select(name, @template.options_for_select(resources, selected: options[:selected]), options)
277
+ end
278
+
279
+ # Public: Generates a picker field for scope selection.
280
+ #
281
+ # attribute - The name of the field (usually scope_id)
282
+ # options - An optional Hash with options:
283
+ # - multiple - Multiple mode, to allow multiple scopes selection.
284
+ # - label - Show label?
285
+ # - checkboxes_on_top - Show checked picker values on top (default) or below the picker prompt (only for multiple pickers)
286
+ # - namespace - prepend a custom name to the html element's DOM id.
287
+ #
288
+ # Also it should receive a block that returns a Hash with :url and :text for each selected scope (and for null scope for prompt)
289
+ #
290
+ # Returns a String.
291
+ def scopes_picker(attribute, options = {})
292
+ id = if self.options.has_key?(:namespace)
293
+ "#{self.options[:namespace]}_#{sanitize_for_dom_selector(@object_name)}"
294
+ else
295
+ "#{sanitize_for_dom_selector(@object_name)}_#{attribute}"
296
+ end
297
+
298
+ picker_options = {
299
+ id: id,
300
+ class: "picker-#{options[:multiple] ? "multiple" : "single"}",
301
+ name: "#{@object_name}[#{attribute}]"
302
+ }
303
+
304
+ picker_options[:class] += " is-invalid-input" if error?(attribute)
305
+
306
+ prompt_params = yield(nil)
307
+ scopes = selected_scopes(attribute).map { |scope| [scope, yield(scope)] }
308
+ template = ""
309
+ template += "<label>#{label_for(attribute) + required_for_attribute(attribute)}</label>" unless options[:label] == false
310
+ template += @template.render("decidim/scopes/scopes_picker_input",
311
+ picker_options: picker_options,
312
+ prompt_params: prompt_params,
313
+ scopes: scopes,
314
+ values_on_top: !options[:multiple] || options[:checkboxes_on_top])
315
+ template += error_and_help_text(attribute, options)
316
+ template.html_safe
317
+ end
318
+
319
+ # Public: Generates a picker field for selection (either simple or multiselect).
320
+ #
321
+ # attribute - The name of the object's attribute.
322
+ # options - A Hash with options:
323
+ # - multiple: Multiple mode, to allow selection of multiple items.
324
+ # - label: Show label?
325
+ # - name: (optional) The name attribute of the input elements.
326
+ # prompt_params - Hash with options:
327
+ # - url: The url where the ajax endpoint that will fill the content of the selector popup (the prompt).
328
+ # - text: Text in the button to open the Data Picker selector.
329
+ #
330
+ # Also it should receive a block that returns a Hash with :url and :text for each selected scope
331
+ #
332
+ # Returns an html String.
333
+ def data_picker(attribute, options = {}, prompt_params = {})
334
+ picker_options = {
335
+ id: "#{@object_name}_#{attribute}",
336
+ class: "picker-#{options[:multiple] ? "multiple" : "single"}",
337
+ name: options[:name] || "#{@object_name}[#{attribute}]"
338
+ }
339
+ picker_options[:class] += " is-invalid-input" if error?(attribute)
340
+ picker_options[:class] += " picker-autosort" if options[:autosort]
341
+
342
+ items = object.send(attribute).collect { |item| [item, yield(item)] }
343
+
344
+ template = ""
345
+ template += label(attribute, label_for(attribute) + required_for_attribute(attribute)) unless options[:label] == false
346
+ template += @template.render("decidim/widgets/data_picker", picker_options: picker_options, prompt_params: prompt_params, items: items)
347
+ template += error_and_help_text(attribute, options)
348
+ template.html_safe
349
+ end
350
+
351
+ # Public: Override so checkboxes are rendered before the label.
352
+ def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
353
+ custom_label(attribute, options[:label], options[:label_options], field_before_label: true) do
354
+ options.delete(:label)
355
+ options.delete(:label_options)
356
+ @template.check_box(@object_name, attribute, objectify_options(options), checked_value, unchecked_value)
357
+ end + error_and_help_text(attribute, options)
358
+ end
359
+
360
+ # Public: Override so the date fields are rendered using foundation
361
+ # datepicker library
362
+ def date_field(attribute, options = {})
363
+ value = object.send(attribute)
364
+ data = { datepicker: "" }
365
+ data[:startdate] = I18n.l(value, format: :decidim_short) if value.present? && value.is_a?(Date)
366
+ datepicker_format = ruby_format_to_datepicker(I18n.t("date.formats.decidim_short"))
367
+ data[:"date-format"] = datepicker_format
368
+
369
+ template = text_field(
370
+ attribute,
371
+ options.merge(data: data)
372
+ )
373
+ help_text = I18n.t("decidim.datepicker.help_text", datepicker_format: datepicker_format)
374
+ template += error_and_help_text(attribute, options.merge(help_text: help_text))
375
+ template.html_safe
376
+ end
377
+
378
+ # Public: Generates a timepicker field using foundation
379
+ # datepicker library
380
+ def datetime_field(attribute, options = {})
381
+ value = object.send(attribute)
382
+ data = { datepicker: "", timepicker: "" }
383
+ data[:startdate] = I18n.l(value, format: :decidim_short) if value.present? && value.is_a?(ActiveSupport::TimeWithZone)
384
+ datepicker_format = ruby_format_to_datepicker(I18n.t("time.formats.decidim_short"))
385
+ data[:"date-format"] = datepicker_format
386
+
387
+ template = text_field(
388
+ attribute,
389
+ options.merge(data: data)
390
+ )
391
+ help_text = I18n.t("decidim.datepicker.help_text", datepicker_format: datepicker_format)
392
+ template += content_tag(:span, help_text, class: "help-text")
393
+ template.html_safe
394
+ end
395
+
396
+ # Public: Generates a file upload field and sets the form as multipart.
397
+ # If the file is an image it displays the default image if present or the current one.
398
+ # By default it also generates a checkbox to delete the file. This checkbox can
399
+ # be hidden if `options[:optional]` is passed as `false`.
400
+ #
401
+ # attribute - The String name of the attribute to buidl the field.
402
+ # options - A Hash with options to build the field.
403
+ # * optional: Whether the file can be optional or not.
404
+ # rubocop:disable Metrics/CyclomaticComplexity
405
+ # rubocop:disable Metrics/PerceivedComplexity
406
+ def upload(attribute, options = {})
407
+ self.multipart = true
408
+ options[:optional] = options[:optional].nil? ? true : options[:optional]
409
+ label_text = options[:label] || label_for(attribute)
410
+ alt_text = label_text
411
+
412
+ file = object.send attribute
413
+ template = ""
414
+ template += label(attribute, label_text + required_for_attribute(attribute))
415
+ template += upload_help(attribute, options)
416
+ template += @template.file_field @object_name, attribute
417
+
418
+ template += extension_allowlist_help(options[:extension_allowlist]) if options[:extension_allowlist].present?
419
+ template += image_dimensions_help(options[:dimensions_info]) if options[:dimensions_info].present?
420
+
421
+ default_image_path = uploader_default_image_path(attribute)
422
+ file_path = file_attachment_path(file)
423
+
424
+ if file_path.present? && file.attachment.image? || default_image_path.present?
425
+ if file_path.present?
426
+ template += @template.content_tag :label, I18n.t("current_image", scope: "decidim.forms")
427
+ template += @template.link_to @template.image_tag(file_path, alt: alt_text), file_path, target: "_blank", rel: "noopener"
428
+ else
429
+ template += @template.content_tag :label, I18n.t("default_image", scope: "decidim.forms")
430
+ template += @template.link_to @template.image_tag(default_image_path, alt: alt_text), default_image_path, target: "_blank", rel: "noopener"
431
+ end
432
+ elsif file_path.present?
433
+ template += @template.label_tag I18n.t("current_file", scope: "decidim.forms")
434
+ template += @template.link_to file.filename, file_path, target: "_blank", rel: "noopener"
435
+ end
436
+
437
+ if file_path.present? && options[:optional]
438
+ template += content_tag :div, class: "field" do
439
+ safe_join([
440
+ @template.check_box(@object_name, "remove_#{attribute}"),
441
+ label("remove_#{attribute}", I18n.t("remove_this_file", scope: "decidim.forms"))
442
+ ])
443
+ end
444
+ end
445
+
446
+ if object.errors[attribute].any?
447
+ template += content_tag :p, class: "is-invalid-label" do
448
+ safe_join object.errors[attribute], "<br/>".html_safe
449
+ end
450
+ end
451
+
452
+ template.html_safe
453
+ end
454
+ # rubocop:enable Metrics/CyclomaticComplexity
455
+ # rubocop:enable Metrics/PerceivedComplexity
456
+
457
+ def upload_help(attribute, options = {})
458
+ humanizer = FileValidatorHumanizer.new(object, attribute)
459
+
460
+ help_scope = begin
461
+ if options[:help_i18n_scope].present?
462
+ options[:help_i18n_scope]
463
+ elsif humanizer.uploader.is_a?(Decidim::ImageUploader)
464
+ "decidim.forms.file_help.image"
465
+ else
466
+ "decidim.forms.file_help.file"
467
+ end
468
+ end
469
+
470
+ help_messages = begin
471
+ if options[:help_i18n_messages].present?
472
+ Array(options[:help_i18n_messages])
473
+ else
474
+ %w(message_1 message_2)
475
+ end
476
+ end
477
+
478
+ content_tag(:div, class: "help-text") do
479
+ inner = "<p>#{I18n.t("explanation", scope: help_scope)}</p>".html_safe
480
+ inner + content_tag(:ul) do
481
+ messages = help_messages.each.map { |msg| I18n.t(msg, scope: help_scope) }
482
+ messages += humanizer.messages
483
+
484
+ messages.map { |msg| content_tag(:li, msg) }.join("\n").html_safe
485
+ end.html_safe
486
+ end
487
+ end
488
+
489
+ # Public: Returns the translated name for the given attribute.
490
+ #
491
+ # attribute - The String name of the attribute to return the name.
492
+ def label_for(attribute)
493
+ if object.class.respond_to?(:human_attribute_name)
494
+ object.class.human_attribute_name(attribute)
495
+ else
496
+ attribute.to_s.humanize
497
+ end
498
+ end
499
+
500
+ def form_field_for(attribute, options = {})
501
+ if attribute == :body
502
+ text_area(attribute, options.merge(rows: 10))
503
+ else
504
+ text_field(attribute, options)
505
+ end
506
+ end
507
+
508
+ # Discard the pattern attribute which is not allowed for textarea elements.
509
+ def text_area(attribute, options = {})
510
+ field(attribute, options) do |opts|
511
+ opts.delete(:pattern)
512
+ @template.send(
513
+ :text_area,
514
+ @object_name,
515
+ attribute,
516
+ objectify_options(opts)
517
+ )
518
+ end
519
+ end
520
+
521
+ private
522
+
523
+ # Private: Override from FoundationRailsHelper in order to render
524
+ # inputs inside the label and to automatically inject validations
525
+ # from the object.
526
+ #
527
+ # attribute - The String name of the attribute to buidl the field.
528
+ # options - A Hash with options to build the field.
529
+ # html_options - An optional Hash with options to pass to the html element.
530
+ #
531
+ # Returns a String
532
+ def field(attribute, options, html_options = nil, &block)
533
+ label = options.delete(:label)
534
+ label_options = options.delete(:label_options)
535
+ custom_label(attribute, label, label_options) do
536
+ field_with_validations(attribute, options, html_options, &block)
537
+ end
538
+ end
539
+
540
+ # Private: Builds a form field and detects validations from
541
+ # the form object.
542
+ #
543
+ # attribute - The String name of the attribute to build the field.
544
+ # options - A Hash with options to build the field.
545
+ # html_options - An optional Hash with options to pass to the html element.
546
+ #
547
+ # Returns a String.
548
+ def field_with_validations(attribute, options, html_options)
549
+ class_options = html_options || options
550
+
551
+ if error?(attribute)
552
+ class_options[:class] = class_options[:class].to_s
553
+ class_options[:class] += " is-invalid-input"
554
+ end
555
+
556
+ help_text = options.delete(:help_text)
557
+ prefix = options.delete(:prefix)
558
+ postfix = options.delete(:postfix)
559
+
560
+ class_options = extract_validations(attribute, options).merge(class_options)
561
+
562
+ content = yield(class_options)
563
+ content += abide_error_element(attribute) if class_options[:pattern] || class_options[:required]
564
+ content = content.html_safe
565
+
566
+ html = wrap_prefix_and_postfix(content, prefix, postfix)
567
+ html + error_and_help_text(attribute, options.merge(help_text: help_text))
568
+ end
569
+
570
+ # rubocop: disable Metrics/CyclomaticComplexity
571
+ # rubocop: disable Metrics/PerceivedComplexity
572
+
573
+ # Private: Builds a Hash of options to be injected at the HTML output as
574
+ # HTML5 validations.
575
+ #
576
+ # attribute - The String name of the attribute to extract the validations.
577
+ # options - A Hash of options to extract validations.
578
+ #
579
+ # Returns a Hash.
580
+ def extract_validations(attribute, options)
581
+ min_length = options.delete(:minlength) || length_for_attribute(attribute, :minimum) || 0
582
+ max_length = options.delete(:maxlength) || length_for_attribute(attribute, :maximum)
583
+
584
+ validation_options = {}
585
+ validation_options[:pattern] = "^(.|[\n\r]){#{min_length},#{max_length}}$" if min_length.to_i.positive? || max_length.to_i.positive?
586
+ validation_options[:required] = options[:required] || attribute_required?(attribute)
587
+ validation_options[:minlength] ||= min_length if min_length.to_i.positive?
588
+ validation_options[:maxlength] ||= max_length if max_length.to_i.positive?
589
+ validation_options
590
+ end
591
+ # rubocop: enable Metrics/CyclomaticComplexity
592
+ # rubocop: enable Metrics/PerceivedComplexity
593
+
594
+ # Private: Tries to find if an attribute is required in the form object.
595
+ #
596
+ # Returns Boolean.
597
+ def attribute_required?(attribute)
598
+ validator = find_validator(attribute, ActiveModel::Validations::PresenceValidator) ||
599
+ find_validator(attribute, TranslatablePresenceValidator)
600
+
601
+ return unless validator
602
+
603
+ # Check if the if condition is present and it evaluates to true
604
+ if_condition = validator.options[:if]
605
+ validator_if_condition = if_condition.nil? ||
606
+ (string_or_symbol?(if_condition) ? object.send(if_condition) : if_condition.call(object))
607
+
608
+ # Check if the unless condition is present and it evaluates to false
609
+ unless_condition = validator.options[:unless]
610
+ validator_unless_condition = unless_condition.nil? ||
611
+ (string_or_symbol?(unless_condition) ? !object.send(unless_condition) : !unless_condition.call(object))
612
+
613
+ validator_if_condition && validator_unless_condition
614
+ end
615
+
616
+ def string_or_symbol?(obj)
617
+ obj.is_a?(String) || obj.is_a?(Symbol)
618
+ end
619
+
620
+ # Private: Tries to find a length validator in the form object.
621
+ #
622
+ # attribute - The attribute to look for the validations.
623
+ # type - A Symbol for the type of length to fetch. Currently only :minimum & :maximum are supported.
624
+ #
625
+ # Returns an Integer or Nil.
626
+ def length_for_attribute(attribute, type)
627
+ length_validator = find_validator(attribute, ActiveModel::Validations::LengthValidator)
628
+ return length_validator.options[type] if length_validator
629
+
630
+ length_validator = find_validator(attribute, ProposalLengthValidator) if Object.const_defined?("ProposalLengthValidator")
631
+ if length_validator
632
+ length = length_validator.options[type]
633
+ return length.call(object) if length.respond_to?(:call)
634
+
635
+ return length
636
+ end
637
+
638
+ nil
639
+ end
640
+
641
+ # Private: Finds a validator.
642
+ #
643
+ # attribute - The attribute to validate.
644
+ # klass - The Class of the validator to find.
645
+ #
646
+ # Returns a klass object.
647
+ def find_validator(attribute, klass)
648
+ return unless object.respond_to?(:_validators)
649
+
650
+ object._validators[attribute.to_sym].find { |validator| validator.class == klass }
651
+ end
652
+
653
+ # Private: Override method from FoundationRailsHelper to render the text of the
654
+ # label before the input, instead of after.
655
+ #
656
+ # attribute - The String name of the attribute we're build the label.
657
+ # text - The String text to use as label.
658
+ # options - A Hash to build the label.
659
+ #
660
+ # Returns a String.
661
+ # rubocop:disable Metrics/CyclomaticComplexity
662
+ # rubocop:disable Metrics/PerceivedComplexity
663
+ def custom_label(attribute, text, options, field_before_label: false, show_required: true)
664
+ return block_given? ? yield.html_safe : "".html_safe if text == false
665
+
666
+ text = default_label_text(object, attribute) if text.nil? || text == true
667
+ text += required_for_attribute(attribute) if show_required
668
+
669
+ text = if field_before_label && block_given?
670
+ safe_join([yield, text.html_safe])
671
+ elsif block_given?
672
+ safe_join([text.html_safe, yield])
673
+ end
674
+
675
+ label(attribute, text, options || {})
676
+ end
677
+ # rubocop:enable Metrics/PerceivedComplexity
678
+ # rubocop:enable Metrics/CyclomaticComplexity
679
+
680
+ # Private: Builds a span to be shown when there's a validation error in a field.
681
+ # It looks for the text that will be the content in a similar way `human_attribute_name`
682
+ # does it.
683
+ #
684
+ # attribute - The name of the attribute of the field.
685
+ #
686
+ # Returns a String.
687
+ def abide_error_element(attribute)
688
+ defaults = []
689
+ defaults << :"decidim.forms.errors.#{object.class.model_name.i18n_key}.#{attribute}"
690
+ defaults << :"decidim.forms.errors.#{attribute}"
691
+ defaults << :"forms.errors.#{attribute}"
692
+ defaults << :"decidim.forms.errors.error"
693
+
694
+ options = { count: 1, default: defaults }
695
+
696
+ text = I18n.t(defaults.shift, **options)
697
+ content_tag(:span, text, class: "form-error")
698
+ end
699
+
700
+ def categories_for_select(scope)
701
+ sorted_main_categories = scope.first_class.includes(:subcategories).sort_by do |category|
702
+ [category.weight, translated_attribute(category.name, category.participatory_space.organization)]
703
+ end
704
+
705
+ sorted_main_categories.flat_map do |category|
706
+ parent = [[translated_attribute(category.name, category.participatory_space.organization), category.id]]
707
+
708
+ sorted_subcategories = category.subcategories.sort_by do |subcategory|
709
+ [subcategory.weight, translated_attribute(subcategory.name, subcategory.participatory_space.organization)]
710
+ end
711
+
712
+ sorted_subcategories.each do |subcategory|
713
+ parent << ["- #{translated_attribute(subcategory.name, subcategory.participatory_space.organization)}", subcategory.id]
714
+ end
715
+
716
+ parent
717
+ end
718
+ end
719
+
720
+ def disabled_categories_for(scope)
721
+ scope.first_class.joins(:subcategories).pluck(:id)
722
+ end
723
+
724
+ def tab_element_class_for(type, index)
725
+ element_class = "tabs-#{type}"
726
+ element_class += " is-active" if index.zero?
727
+ element_class
728
+ end
729
+
730
+ def locales
731
+ @locales ||= if @template.respond_to?(:available_locales)
732
+ Set.new([@template.current_locale] + @template.available_locales).to_a
733
+ else
734
+ Decidim.available_locales
735
+ end
736
+ end
737
+
738
+ def name_with_locale(name, locale)
739
+ "#{name}_#{locale.to_s.gsub("-", "__")}"
740
+ end
741
+
742
+ def label_i18n(attribute, text = nil, options = {})
743
+ errored = error?(attribute) || locales.any? { |locale| error?(name_with_locale(attribute, locale)) }
744
+
745
+ if errored
746
+ options[:class] ||= ""
747
+ options[:class] += " is-invalid-label"
748
+ end
749
+ text += required_for_attribute(attribute)
750
+
751
+ label(attribute, (text || "").html_safe, options)
752
+ end
753
+
754
+ # Private: Returns default url for attribute when uploader is an
755
+ # image and has defined a default url
756
+ def uploader_default_image_path(attribute)
757
+ uploader = FileValidatorHumanizer.new(object, attribute).uploader
758
+ return if uploader.blank?
759
+ return unless uploader.is_a?(Decidim::ImageUploader)
760
+
761
+ uploader.try(:default_url)
762
+ end
763
+
764
+ # Private: Returns blob path when file is attached
765
+ def file_attachment_path(file)
766
+ return unless file && file.try(:attached?)
767
+
768
+ Rails.application.routes.url_helpers.rails_blob_url(file.blob, only_path: true)
769
+ end
770
+
771
+ def required_for_attribute(attribute)
772
+ if attribute_required?(attribute) || (attribute == :password && object.class.name == "Decidim::RegistrationForm")
773
+ visible_title = content_tag(:span, "*", "aria-hidden": true)
774
+ screenreader_title = content_tag(
775
+ :span,
776
+ I18n.t("required", scope: "forms"),
777
+ class: "show-for-sr"
778
+ )
779
+ return content_tag(
780
+ :span,
781
+ visible_title + screenreader_title,
782
+ title: I18n.t("required", scope: "forms"),
783
+ data: { tooltip: true, disable_hover: false, keep_on_hover: true },
784
+ class: "label-required"
785
+ ).html_safe
786
+ end
787
+ "".html_safe
788
+ end
789
+
790
+ # Private: Returns an array of scopes related to object attribute
791
+ def selected_scopes(attribute)
792
+ selected = object.send(attribute) || []
793
+ selected = selected.values if selected.is_a?(Hash)
794
+ selected = [selected] unless selected.is_a?(Array)
795
+ selected = Decidim::Scope.where(id: selected.map(&:to_i)) unless selected.first.is_a?(Decidim::Scope)
796
+ selected
797
+ end
798
+
799
+ # Private: Returns the help text and error tags at the end of the field.
800
+ # Modified to change the tag to a valid HTML tag inside the <label> element.
801
+ def error_and_help_text(attribute, options = {})
802
+ html = ""
803
+ html += content_tag(:span, options[:help_text], class: "help-text") if options[:help_text]
804
+ html += error_for(attribute, options) || ""
805
+ html.html_safe
806
+ end
807
+
808
+ def ruby_format_to_datepicker(ruby_date_format)
809
+ ruby_date_format.gsub("%d", "dd").gsub("%m", "mm").gsub("%Y", "yyyy").gsub("%H", "hh").gsub("%M", "ii")
810
+ end
811
+
812
+ def sanitize_tabs_selector(id)
813
+ id.tr("[", "-").tr("]", "-")
814
+ end
815
+
816
+ def sanitize_for_dom_selector(name)
817
+ name.to_s.parameterize.underscore
818
+ end
819
+
820
+ def extension_allowlist_help(extension_allowlist)
821
+ content_tag :p, class: "extensions-help help-text" do
822
+ safe_join([
823
+ content_tag(:span, I18n.t("extension_allowlist", scope: "decidim.forms.files")),
824
+ " ",
825
+ safe_join(extension_allowlist.map { |ext| content_tag(:b, ext) }, ", ")
826
+ ])
827
+ end
828
+ end
829
+
830
+ def image_dimensions_help(dimensions_info)
831
+ content_tag :p, class: "image-dimensions-help help-text" do
832
+ safe_join([
833
+ content_tag(:span, I18n.t("dimensions_info", scope: "decidim.forms.images")),
834
+ " ",
835
+ content_tag(:span) do
836
+ safe_join(dimensions_info.map do |_version, info|
837
+ processor = @template.content_tag(:span, I18n.t("processors.#{info[:processor]}", scope: "decidim.forms.images"))
838
+ dimensions = @template.content_tag(:b, I18n.t("dimensions", scope: "decidim.forms.images", width: info[:dimensions].first, height: info[:dimensions].last))
839
+ safe_join([processor, " ", dimensions, ". ".html_safe])
840
+ end)
841
+ end
842
+ ])
843
+ end
844
+ end
845
+
846
+ # Private: Creates a tag from the given options for the field prefix and
847
+ # suffix. Overridden from Foundation Rails helper to make the generated HTML
848
+ # valid since these elements are printed within <label> elements and <div>'s
849
+ # are not allowed there.
850
+ def tag_from_options(name, options)
851
+ return "".html_safe unless options && options[:value].present?
852
+
853
+ content_tag(:span,
854
+ content_tag(:span, options[:value], class: name),
855
+ class: column_classes(options).to_s)
856
+ end
857
+
858
+ # Private: Wraps the prefix and postfix for the field. Overridden from
859
+ # Foundation Rails helper to make the generated HTML valid since these
860
+ # elements are printed within <label> elements and <div>'s are not allowed
861
+ # there.
862
+ def wrap_prefix_and_postfix(block, prefix_options, postfix_options)
863
+ prefix = tag_from_options("prefix", prefix_options)
864
+ postfix = tag_from_options("postfix", postfix_options)
865
+
866
+ input_size = calculate_input_size(prefix_options, postfix_options)
867
+ klass = column_classes(input_size.marshal_dump).to_s
868
+ input = content_tag(:span, block, class: klass)
869
+
870
+ return block unless input_size.changed?
871
+
872
+ content_tag(:span, prefix + input + postfix, class: "row collapse")
873
+ end
874
+
875
+ def language_selector_select(locales, tabs_id, name)
876
+ content_tag(:div) do
877
+ content_tag(:select, id: tabs_id, class: "language-change") do
878
+ locales.each_with_index.inject("".html_safe) do |string, (locale, index)|
879
+ title = if error?(name_with_locale(name, locale))
880
+ I18n.with_locale(locale) { I18n.t("name_with_error", scope: "locale") }
881
+ else
882
+ I18n.with_locale(locale) { I18n.t("name", scope: "locale") }
883
+ end
884
+ tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
885
+ string + content_tag(:option, title, value: "##{tab_content_id}")
886
+ end
887
+ end
888
+ end
889
+ end
890
+
891
+ def language_tabs(locales, tabs_id, name)
892
+ content_tag(:ul, class: "tabs tabs--lang", id: tabs_id, data: { tabs: true }) do
893
+ locales.each_with_index.inject("".html_safe) do |string, (locale, index)|
894
+ string + content_tag(:li, class: tab_element_class_for("title", index)) do
895
+ title = I18n.with_locale(locale) { I18n.t("name", scope: "locale") }
896
+ element_class = nil
897
+ element_class = "is-tab-error" if error?(name_with_locale(name, locale))
898
+ tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
899
+ content_tag(:a, title, href: "##{tab_content_id}", class: element_class)
900
+ end
901
+ end
902
+ end
903
+ end
904
+
905
+ def editor_images_options(options)
906
+ return {} unless options[:editor_images]
907
+
908
+ {
909
+ editor_images: true,
910
+ upload_images_path: Decidim::Core::Engine.routes.url_helpers.editor_images_path,
911
+ drag_and_drop_help_text: I18n.t("drag_and_drop_help", scope: "decidim.editor_images")
912
+ }
913
+ end
914
+ end
915
+ end