katalyst-govuk-formbuilder 1.30.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +141 -15
- data/app/assets/builds/katalyst/govuk/formbuilder.css +77 -0
- data/app/assets/builds/katalyst/govuk/formbuilder.js +573 -10
- data/app/assets/builds/katalyst/govuk/formbuilder.min.js +8 -8
- data/app/assets/builds/katalyst/govuk/formbuilder.min.js.map +1 -1
- data/app/assets/stylesheets/katalyst/govuk/components/_index.scss +1 -0
- data/app/assets/stylesheets/katalyst/govuk/components/attachment/_index.scss +3 -0
- data/app/assets/stylesheets/katalyst/govuk/components/attachment/_mixin.scss +89 -0
- data/app/helpers/katalyst/govuk/form_builder/builder.rb +504 -0
- data/app/helpers/katalyst/govuk/form_builder/containers/fieldset_context.rb +19 -0
- data/app/helpers/katalyst/govuk/form_builder/elements/attachment.rb +80 -0
- data/app/helpers/katalyst/govuk/form_builder/elements/combobox.rb +85 -0
- data/app/helpers/katalyst/govuk/form_builder/elements/document.rb +68 -0
- data/app/helpers/katalyst/govuk/form_builder/elements/image.rb +65 -0
- data/app/helpers/katalyst/govuk/form_builder/elements/label.rb +38 -0
- data/app/helpers/katalyst/govuk/form_builder/elements/legend.rb +32 -0
- data/app/helpers/katalyst/govuk/form_builder/elements/rich_textarea.rb +70 -0
- data/app/helpers/katalyst/govuk/form_builder/frontend.rb +34 -0
- data/app/helpers/katalyst/govuk/form_builder/traits/attachment.rb +215 -0
- data/app/helpers/katalyst/govuk/form_builder/traits/file.rb +104 -0
- data/app/helpers/katalyst/govuk/form_builder/traits/label.rb +33 -0
- data/config/importmap.rb +1 -0
- data/config/locales/en.yml +16 -0
- data/lib/katalyst/govuk/form_builder/config.rb +20 -0
- data/lib/katalyst/govuk/form_builder/engine.rb +1 -1
- metadata +31 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Katalyst
|
|
4
|
+
module GOVUK
|
|
5
|
+
module FormBuilder
|
|
6
|
+
module Elements
|
|
7
|
+
class Attachment < GOVUKDesignSystemFormBuilder::Elements::File
|
|
8
|
+
include FormBuilder::Traits::Attachment
|
|
9
|
+
|
|
10
|
+
def initialize(builder, object_name, attribute_name, direct_upload_url:,
|
|
11
|
+
upload_succeeded_text: nil, upload_failed_text: nil,
|
|
12
|
+
retry_button_text: nil, file_removed_text: nil,
|
|
13
|
+
remove_button_text: nil, remove_button_content_text: nil, **, &)
|
|
14
|
+
super(builder, object_name, attribute_name, javascript: true, **, &)
|
|
15
|
+
|
|
16
|
+
@direct_upload_url = direct_upload_url
|
|
17
|
+
@upload_succeeded_text = upload_succeeded_text
|
|
18
|
+
@upload_failed_text = upload_failed_text
|
|
19
|
+
@retry_button_text = retry_button_text
|
|
20
|
+
@file_removed_text = file_removed_text
|
|
21
|
+
@remove_button_text = remove_button_text
|
|
22
|
+
@remove_button_content_text = remove_button_content_text
|
|
23
|
+
|
|
24
|
+
raise ArgumentError, "Unsupported attribute type #{value.class} for #{attribute_name}" unless attachment?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def options
|
|
28
|
+
super.merge(
|
|
29
|
+
"data-direct-upload-url" => @direct_upload_url,
|
|
30
|
+
include_hidden: false, # we always render remove_field
|
|
31
|
+
multiple: many?,
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Extends the file element's data-i18n.* attributes with the
|
|
38
|
+
# attachment strings: an explicit option wins, else the current
|
|
39
|
+
# locale's translation. When neither adds anything beyond the
|
|
40
|
+
# gem's en defaults the attribute is omitted and the JS falls
|
|
41
|
+
# back to its bundled mirror of the same table.
|
|
42
|
+
def i18n_data
|
|
43
|
+
super.merge({
|
|
44
|
+
"data-i18n.upload-succeeded" => attachment_text(@upload_succeeded_text, :upload_succeeded),
|
|
45
|
+
"data-i18n.upload-failed" => attachment_text(@upload_failed_text, :upload_failed),
|
|
46
|
+
"data-i18n.retry-button" => attachment_text(@retry_button_text, :retry_button),
|
|
47
|
+
"data-i18n.file-removed" => attachment_text(@file_removed_text, :file_removed),
|
|
48
|
+
"data-i18n.remove-button" => attachment_text(@remove_button_text, :remove_button),
|
|
49
|
+
"data-i18n.remove-button-content" => attachment_text(@remove_button_content_text, :remove_button_content),
|
|
50
|
+
}.compact)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def attachment_text(option, key)
|
|
54
|
+
return option if option
|
|
55
|
+
|
|
56
|
+
text = attachment_translation(key)
|
|
57
|
+
text unless text == Traits::Attachment::BUNDLED_DEFAULTS[key.to_s]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def file
|
|
61
|
+
safe_join([remove_field, attachment, @builder.file_field(@attribute_name, attributes(@html_attributes))])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def file_with_javascript_markup
|
|
65
|
+
tag.div(class: "#{brand}-file-upload-wrapper", data: { controller: "govuk-file-upload" }, **i18n_data) do
|
|
66
|
+
file
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A removed figure takes its select with it and an empty file input contributes
|
|
71
|
+
# no value. This input ensures that there's always a value to process so that
|
|
72
|
+
# attachments can be removed (both one? and many? cases).
|
|
73
|
+
def remove_field
|
|
74
|
+
tag.input(name: @builder.field_name(@attribute_name, multiple: many?), type: "hidden", value: "")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "govuk_design_system_formbuilder"
|
|
4
|
+
|
|
5
|
+
module Katalyst
|
|
6
|
+
module GOVUK
|
|
7
|
+
module FormBuilder
|
|
8
|
+
module Elements
|
|
9
|
+
class Combobox < GOVUKDesignSystemFormBuilder::Base
|
|
10
|
+
include GOVUKDesignSystemFormBuilder::Traits::Error
|
|
11
|
+
include GOVUKDesignSystemFormBuilder::Traits::Label
|
|
12
|
+
include GOVUKDesignSystemFormBuilder::Traits::Hint
|
|
13
|
+
include GOVUKDesignSystemFormBuilder::Traits::HTMLAttributes
|
|
14
|
+
include GOVUKDesignSystemFormBuilder::Traits::ContentBeforeAndAfter
|
|
15
|
+
|
|
16
|
+
def initialize(builder,
|
|
17
|
+
object_name,
|
|
18
|
+
attribute_name,
|
|
19
|
+
options_or_src,
|
|
20
|
+
options:,
|
|
21
|
+
form_group:,
|
|
22
|
+
label:,
|
|
23
|
+
hint:,
|
|
24
|
+
caption:,
|
|
25
|
+
before_input:,
|
|
26
|
+
after_input:,
|
|
27
|
+
**kwargs,
|
|
28
|
+
&block)
|
|
29
|
+
# assign the block to a variable rather than passing to super so
|
|
30
|
+
# we can send it through to #combobox
|
|
31
|
+
super(builder, object_name, attribute_name)
|
|
32
|
+
@block = block
|
|
33
|
+
|
|
34
|
+
@form_group = form_group
|
|
35
|
+
@hint = hint
|
|
36
|
+
@label = label
|
|
37
|
+
@caption = caption
|
|
38
|
+
@options_or_src = options_or_src
|
|
39
|
+
@options = options
|
|
40
|
+
@html_attributes = kwargs
|
|
41
|
+
@before_input = before_input
|
|
42
|
+
@after_input = after_input
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def html
|
|
46
|
+
GOVUKDesignSystemFormBuilder::Containers::FormGroup.new(*bound, **@form_group).html do
|
|
47
|
+
safe_join([
|
|
48
|
+
label_element,
|
|
49
|
+
hint_element,
|
|
50
|
+
error_element,
|
|
51
|
+
before_input_content,
|
|
52
|
+
combobox,
|
|
53
|
+
after_input_content,
|
|
54
|
+
])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def combobox
|
|
61
|
+
attrs = attributes(@html_attributes)
|
|
62
|
+
attrs[:class] = attrs.delete(:class).join(" ") # hotwire_combobox does not support flattening classes
|
|
63
|
+
@builder.combobox(@attribute_name, @options_or_src, **@options, **attrs, &@block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def options
|
|
67
|
+
{
|
|
68
|
+
id: field_id(link_errors: true),
|
|
69
|
+
class: classes,
|
|
70
|
+
aria: { describedby: combine_references(hint_id, error_id) },
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def classes
|
|
75
|
+
combine_references(%(#{brand}-combobox), error_class)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def error_class
|
|
79
|
+
%(#{brand}-combobox--error) if has_errors?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "govuk_design_system_formbuilder"
|
|
4
|
+
|
|
5
|
+
module Katalyst
|
|
6
|
+
module GOVUK
|
|
7
|
+
module FormBuilder
|
|
8
|
+
module Elements
|
|
9
|
+
class Document < GOVUKDesignSystemFormBuilder::Base
|
|
10
|
+
include ActionDispatch::Routing::RouteSet::MountedHelpers
|
|
11
|
+
|
|
12
|
+
include GOVUKDesignSystemFormBuilder::Traits::Error
|
|
13
|
+
include GOVUKDesignSystemFormBuilder::Traits::Hint
|
|
14
|
+
include GOVUKDesignSystemFormBuilder::Traits::Label
|
|
15
|
+
include GOVUKDesignSystemFormBuilder::Traits::Supplemental
|
|
16
|
+
include GOVUKDesignSystemFormBuilder::Traits::HTMLAttributes
|
|
17
|
+
include GOVUKDesignSystemFormBuilder::Traits::HTMLClasses
|
|
18
|
+
|
|
19
|
+
include FormBuilder::Traits::File
|
|
20
|
+
|
|
21
|
+
def initialize(builder, object_name, attribute_name, hint:, label:, caption:, form_group:, mime_types:,
|
|
22
|
+
**kwargs, &)
|
|
23
|
+
super(builder, object_name, attribute_name, &)
|
|
24
|
+
|
|
25
|
+
@mime_types = mime_types
|
|
26
|
+
@label = label
|
|
27
|
+
@caption = caption
|
|
28
|
+
@hint = hint
|
|
29
|
+
@html_attributes = kwargs.merge(file_input_options)
|
|
30
|
+
@form_group = form_group
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def html
|
|
34
|
+
GOVUKDesignSystemFormBuilder::Containers::FormGroup.new(
|
|
35
|
+
*bound,
|
|
36
|
+
**default_form_group_options(**@form_group),
|
|
37
|
+
).html do
|
|
38
|
+
safe_join([label_element, preview, hint_element, error_element, file, destroy_element,
|
|
39
|
+
supplemental_content])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def preview
|
|
44
|
+
options = {}
|
|
45
|
+
add_option(options, :data, "#{stimulus_controller}_target", "preview")
|
|
46
|
+
add_option(options, :class, "preview-file")
|
|
47
|
+
options[:hidden] = "" unless preview?
|
|
48
|
+
|
|
49
|
+
tag.div(**options) do
|
|
50
|
+
filename = @builder.object.send(@attribute_name).filename.to_s
|
|
51
|
+
tag.p(filename, class: "preview-filename") + destroy_element_trigger
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def stimulus_controller
|
|
58
|
+
"govuk-document-field"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def form_group_class
|
|
62
|
+
"govuk-document-field"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Katalyst
|
|
4
|
+
module GOVUK
|
|
5
|
+
module FormBuilder
|
|
6
|
+
module Elements
|
|
7
|
+
class Image < GOVUKDesignSystemFormBuilder::Base
|
|
8
|
+
include ActionDispatch::Routing::RouteSet::MountedHelpers
|
|
9
|
+
|
|
10
|
+
include GOVUKDesignSystemFormBuilder::Traits::Error
|
|
11
|
+
include GOVUKDesignSystemFormBuilder::Traits::Hint
|
|
12
|
+
include GOVUKDesignSystemFormBuilder::Traits::Label
|
|
13
|
+
include GOVUKDesignSystemFormBuilder::Traits::Supplemental
|
|
14
|
+
include GOVUKDesignSystemFormBuilder::Traits::HTMLAttributes
|
|
15
|
+
include GOVUKDesignSystemFormBuilder::Traits::HTMLClasses
|
|
16
|
+
|
|
17
|
+
include FormBuilder::Traits::File
|
|
18
|
+
|
|
19
|
+
def initialize(builder, object_name, attribute_name, hint:, label:, caption:, form_group:, mime_types:,
|
|
20
|
+
**kwargs, &)
|
|
21
|
+
super(builder, object_name, attribute_name, &)
|
|
22
|
+
|
|
23
|
+
@mime_types = mime_types
|
|
24
|
+
@label = label
|
|
25
|
+
@caption = caption
|
|
26
|
+
@hint = hint
|
|
27
|
+
@html_attributes = kwargs.merge(file_input_options)
|
|
28
|
+
@form_group = form_group
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def html
|
|
32
|
+
GOVUKDesignSystemFormBuilder::Containers::FormGroup.new(
|
|
33
|
+
*bound,
|
|
34
|
+
**default_form_group_options(**@form_group),
|
|
35
|
+
).html do
|
|
36
|
+
safe_join([label_element, preview, hint_element, error_element, file, destroy_element,
|
|
37
|
+
supplemental_content])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def preview
|
|
42
|
+
options = {}
|
|
43
|
+
add_option(options, :data, "#{stimulus_controller}_target", "preview")
|
|
44
|
+
add_option(options, :class, "preview-image")
|
|
45
|
+
options[:hidden] = "" unless preview?
|
|
46
|
+
|
|
47
|
+
tag.div(**options) do
|
|
48
|
+
tag.img(src: preview_url, class: "image-thumbnail") + destroy_element_trigger
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def stimulus_controller
|
|
55
|
+
"govuk-image-field"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def form_group_class
|
|
59
|
+
"govuk-image-field"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "govuk_design_system_formbuilder"
|
|
4
|
+
|
|
5
|
+
module Katalyst
|
|
6
|
+
module GOVUK
|
|
7
|
+
module FormBuilder
|
|
8
|
+
module Elements
|
|
9
|
+
# Extend Elements::Label to add support for human_attribute_name as a fallback
|
|
10
|
+
module Label
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
def retrieve_text(option_text, hidden)
|
|
15
|
+
text = option_text.presence ||
|
|
16
|
+
localised_text(:label).presence ||
|
|
17
|
+
human_attribute_name.presence ||
|
|
18
|
+
@attribute_name.to_s.humanize.capitalize.presence
|
|
19
|
+
|
|
20
|
+
if hidden
|
|
21
|
+
tag.span(text, class: %(#{brand}-visually-hidden))
|
|
22
|
+
else
|
|
23
|
+
text
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def human_attribute_name
|
|
29
|
+
return unless @object_name.present? && @attribute_name.present?
|
|
30
|
+
return unless @builder.object&.class.respond_to?(:human_attribute_name)
|
|
31
|
+
|
|
32
|
+
@builder.object.class.human_attribute_name(@attribute_name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "govuk_design_system_formbuilder"
|
|
4
|
+
|
|
5
|
+
module Katalyst
|
|
6
|
+
module GOVUK
|
|
7
|
+
module FormBuilder
|
|
8
|
+
module Elements
|
|
9
|
+
# Extend Elements::Legend to add support for human_attribute_name as a fallback
|
|
10
|
+
module Legend
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
def retrieve_text(supplied_text)
|
|
15
|
+
supplied_text.presence ||
|
|
16
|
+
localised_text(:legend).presence ||
|
|
17
|
+
human_attribute_name.presence ||
|
|
18
|
+
@attribute_name.to_s.humanize.capitalize.presence
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def human_attribute_name
|
|
23
|
+
return unless @object_name.present? && @attribute_name.present?
|
|
24
|
+
return unless @builder.object&.class.respond_to?(:human_attribute_name)
|
|
25
|
+
|
|
26
|
+
@builder.object.class.human_attribute_name(@attribute_name)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "govuk_design_system_formbuilder"
|
|
4
|
+
|
|
5
|
+
module Katalyst
|
|
6
|
+
module GOVUK
|
|
7
|
+
module FormBuilder
|
|
8
|
+
module Elements
|
|
9
|
+
class RichTextarea < GOVUKDesignSystemFormBuilder::Base
|
|
10
|
+
using GOVUKDesignSystemFormBuilder::PrefixableArray
|
|
11
|
+
|
|
12
|
+
include GOVUKDesignSystemFormBuilder::Traits::Error
|
|
13
|
+
include GOVUKDesignSystemFormBuilder::Traits::Hint
|
|
14
|
+
include GOVUKDesignSystemFormBuilder::Traits::Label
|
|
15
|
+
include GOVUKDesignSystemFormBuilder::Traits::Supplemental
|
|
16
|
+
include GOVUKDesignSystemFormBuilder::Traits::HTMLAttributes
|
|
17
|
+
include GOVUKDesignSystemFormBuilder::Traits::HTMLClasses
|
|
18
|
+
|
|
19
|
+
def initialize(
|
|
20
|
+
builder,
|
|
21
|
+
object_name,
|
|
22
|
+
attribute_name,
|
|
23
|
+
hint:,
|
|
24
|
+
label:,
|
|
25
|
+
caption:,
|
|
26
|
+
form_group:,
|
|
27
|
+
**kwargs,
|
|
28
|
+
&
|
|
29
|
+
)
|
|
30
|
+
super(builder, object_name, attribute_name, &)
|
|
31
|
+
|
|
32
|
+
@label = label
|
|
33
|
+
@caption = caption
|
|
34
|
+
@hint = hint
|
|
35
|
+
@form_group = form_group
|
|
36
|
+
@html_attributes = kwargs
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def html
|
|
40
|
+
GOVUKDesignSystemFormBuilder::Containers::FormGroup.new(*bound, **@form_group).html do
|
|
41
|
+
safe_join([label_element, supplemental_content, hint_element, error_element, rich_textarea])
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def rich_textarea
|
|
48
|
+
@builder.rich_textarea(@attribute_name, **attributes(@html_attributes))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def classes
|
|
52
|
+
build_classes(%(rich-textarea), %(rich-textarea--error) => has_errors?).prefix(brand)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def options
|
|
56
|
+
{
|
|
57
|
+
id: field_id,
|
|
58
|
+
class: classes,
|
|
59
|
+
aria: { describedby: combine_references(hint_id, error_id) },
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def label_content
|
|
64
|
+
super.merge(link_errors: false)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Katalyst
|
|
4
|
+
module GOVUK
|
|
5
|
+
module FormBuilder
|
|
6
|
+
module Frontend
|
|
7
|
+
# Marks the page as JS-capable and enhances govuk-frontend components,
|
|
8
|
+
# on load and as the DOM changes (Turbo morphs, lazily-loaded frames,
|
|
9
|
+
# stream inserts). Render at the end of <body>; on a Turbo replace
|
|
10
|
+
# render the snippet re-executes with the new body, and everything it
|
|
11
|
+
# sets up is scoped to the body element it ran against.
|
|
12
|
+
# rubocop:disable Rails/OutputSafety
|
|
13
|
+
def govuk_formbuilder_init
|
|
14
|
+
tag.script type: "module", nonce: request.content_security_policy_nonce do
|
|
15
|
+
<<~JS.html_safe
|
|
16
|
+
import {initAll} from "@katalyst/govuk-formbuilder";
|
|
17
|
+
initAll(#{govuk_formbuilder_init_options});
|
|
18
|
+
JS
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
# rubocop:enable Rails/OutputSafety
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# The bundle's own default is govuk; only a non-default brand renders.
|
|
26
|
+
def govuk_formbuilder_init_options
|
|
27
|
+
brand = GOVUKDesignSystemFormBuilder.brand
|
|
28
|
+
|
|
29
|
+
brand.to_s == "govuk" ? "" : "{brand: #{brand.to_json}}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "govuk_design_system_formbuilder"
|
|
4
|
+
|
|
5
|
+
module Katalyst
|
|
6
|
+
module GOVUK
|
|
7
|
+
module FormBuilder
|
|
8
|
+
module Traits
|
|
9
|
+
# Generates attachment inputs and previews for ActiveStorage associations.
|
|
10
|
+
module Attachment
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
include ActionView::Helpers::NumberHelper
|
|
14
|
+
|
|
15
|
+
# The attachment strings' canonical home (config/locales).
|
|
16
|
+
I18N_SCOPE = %i[katalyst govuk attachment].freeze
|
|
17
|
+
|
|
18
|
+
# The gem's own en strings, read straight from its locale file:
|
|
19
|
+
# resolving them through I18n would absorb a host app's en
|
|
20
|
+
# overrides, and this table is the baseline those overrides are
|
|
21
|
+
# detected against.
|
|
22
|
+
BUNDLED_DEFAULTS = YAML.load_file(
|
|
23
|
+
Engine.root.join("config/locales/en.yml"),
|
|
24
|
+
).dig("en", *I18N_SCOPE.map(&:to_s)).freeze
|
|
25
|
+
|
|
26
|
+
def attachment?
|
|
27
|
+
value.is_a?(ActiveStorage::Attached)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def one?
|
|
31
|
+
value.is_a?(ActiveStorage::Attached::One)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def many?
|
|
35
|
+
value.is_a?(ActiveStorage::Attached::Many)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def value
|
|
39
|
+
@builder.object.send(@attribute_name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
delegate :attached?, to: :value
|
|
43
|
+
|
|
44
|
+
# @return [ActiveSupport::SafeBuffer,nil]
|
|
45
|
+
def attachment
|
|
46
|
+
return unless attached?
|
|
47
|
+
|
|
48
|
+
# Preserve unsaved multi-part form uploads before rendering
|
|
49
|
+
# mimics direct-upload for non-js consumers.
|
|
50
|
+
persist_pending_blobs
|
|
51
|
+
|
|
52
|
+
blobs = (one? ? [value.blob] : value.blobs).select(&:persisted?)
|
|
53
|
+
safe_join(blobs.map { |blob| attachment_for(blob) })
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param [ActiveStorage::Blob] blob
|
|
57
|
+
# @return [ActiveSupport::SafeBuffer,nil]
|
|
58
|
+
def attachment_for(blob)
|
|
59
|
+
tag.figure(class: "#{brand}-attachment",
|
|
60
|
+
aria: { labelledby: attachment_id_for(blob, :filename) },
|
|
61
|
+
data: { controller: "govuk-attachment" }) do
|
|
62
|
+
safe_join([
|
|
63
|
+
attachment_preview_for(blob),
|
|
64
|
+
attachment_caption_for(blob),
|
|
65
|
+
attachment_actions_for(blob),
|
|
66
|
+
])
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A <div> with form elements for managing the attachment.
|
|
71
|
+
# @param [ActiveStorage::Blob] blob
|
|
72
|
+
# @return [ActiveSupport::SafeBuffer|nil]
|
|
73
|
+
def attachment_actions_for(blob)
|
|
74
|
+
return if blob.nil?
|
|
75
|
+
|
|
76
|
+
tag.div(class: "actions") do
|
|
77
|
+
safe_join([
|
|
78
|
+
attachment_input_for(blob),
|
|
79
|
+
attachment_remove_for(blob),
|
|
80
|
+
])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A <select> with options to keep or remove the attachment that can be used without JavaScript.
|
|
85
|
+
# @param [ActiveStorage::Blob] blob
|
|
86
|
+
# @return [ActiveSupport::SafeBuffer,nil]
|
|
87
|
+
def attachment_input_for(blob)
|
|
88
|
+
@builder.select(
|
|
89
|
+
@attribute_name,
|
|
90
|
+
[[blob.filename.to_s, blob.signed_id],
|
|
91
|
+
[remove_button_label(blob), ""]],
|
|
92
|
+
{ selected: blob.signed_id },
|
|
93
|
+
id: attachment_id_for(blob, :input),
|
|
94
|
+
name: @builder.field_name(@attribute_name, multiple: many?),
|
|
95
|
+
aria: { labelledby: attachment_id_for(blob, :filename) },
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A <button> that will remove the attachment when clicked (requires javascript).
|
|
100
|
+
# @param [ActiveStorage::Blob] blob
|
|
101
|
+
# @return [ActiveSupport::SafeBuffer,nil]
|
|
102
|
+
def attachment_remove_for(blob)
|
|
103
|
+
tag.button(remove_button_content,
|
|
104
|
+
type: "button",
|
|
105
|
+
class: "#{brand}-button #{brand}-button--secondary #{brand}-attachment__remove",
|
|
106
|
+
aria: { label: remove_button_label(blob) },
|
|
107
|
+
data: { action: "govuk-attachment#destroy", module: "govuk-button" })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The remove strings render here and in the JS figure template, so
|
|
111
|
+
# both draw from the same options (or matching defaults) — the two
|
|
112
|
+
# figure sources must stay string-identical.
|
|
113
|
+
# @param [ActiveStorage::Blob] blob
|
|
114
|
+
# @return [String]
|
|
115
|
+
def remove_button_label(blob)
|
|
116
|
+
if @remove_button_text
|
|
117
|
+
# %{filename} is the option placeholder (govuk-frontend's i18n
|
|
118
|
+
# convention), substituted directly — not a Ruby format token.
|
|
119
|
+
# rubocop:disable Style/FormatStringToken
|
|
120
|
+
@remove_button_text.gsub("%{filename}", blob.filename.to_s)
|
|
121
|
+
# rubocop:enable Style/FormatStringToken
|
|
122
|
+
else
|
|
123
|
+
attachment_translation(:remove_button, filename: blob.filename.to_s)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [String]
|
|
128
|
+
def remove_button_content
|
|
129
|
+
@remove_button_content_text || attachment_translation(:remove_button_content)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The current locale's translation, falling back to the gem's en
|
|
133
|
+
# defaults rather than a "translation missing" marker.
|
|
134
|
+
# @return [String]
|
|
135
|
+
def attachment_translation(key, **)
|
|
136
|
+
I18n.t(key, scope: I18N_SCOPE, default: nil, **) ||
|
|
137
|
+
I18n.t(key, scope: I18N_SCOPE, locale: :en, **)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The representation is rendered lazily: the variant is processed
|
|
141
|
+
# when the browser requests it, never during the form render. If the
|
|
142
|
+
# blob's bytes turn out to be missing or unprocessable the preview
|
|
143
|
+
# simply fails to load — validating attachment content is the
|
|
144
|
+
# model's responsibility, not the form's.
|
|
145
|
+
# @param [ActiveStorage::Blob] blob
|
|
146
|
+
# @return [ActiveSupport::SafeBuffer,nil]
|
|
147
|
+
def attachment_preview_for(blob)
|
|
148
|
+
return unless blob.representable?
|
|
149
|
+
|
|
150
|
+
url = @builder.attachment_preview_url(
|
|
151
|
+
blob.representation(config.attachment_preview_representation),
|
|
152
|
+
)
|
|
153
|
+
return if url.nil?
|
|
154
|
+
|
|
155
|
+
# Setting alt to "" as the details already describe the attachment, equivalent to role="presentation"
|
|
156
|
+
@builder.image_tag(url, alt: "", class: "preview")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The caption is a polite atomic live region: JS writes upload status
|
|
160
|
+
# into the status span, and the announcement reads the whole caption
|
|
161
|
+
# so the user hears which file the status belongs to.
|
|
162
|
+
# @param [ActiveStorage::Blob] blob
|
|
163
|
+
# @return [ActiveSupport::SafeBuffer,nil]
|
|
164
|
+
def attachment_caption_for(blob)
|
|
165
|
+
tag.figcaption(class: "caption", aria: { atomic: true, live: "polite" }) do
|
|
166
|
+
safe_join([
|
|
167
|
+
tag.span(blob.filename, id: attachment_id_for(blob, :filename), class: "filename"),
|
|
168
|
+
" ",
|
|
169
|
+
tag.span(number_to_human_size(blob.byte_size), class: "size"),
|
|
170
|
+
" ",
|
|
171
|
+
tag.span(class: "status"),
|
|
172
|
+
])
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# @param [ActiveStorage::Blob] blob
|
|
177
|
+
# @return [String,nil]
|
|
178
|
+
def attachment_id_for(blob, *suffixes)
|
|
179
|
+
return nil if @html_attributes.fetch(:skip_default_ids, false)
|
|
180
|
+
|
|
181
|
+
@builder.field_id(@attribute_name, :attachment, blob.id, *suffixes)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def persist_pending_blobs
|
|
187
|
+
case (change = @builder.object.attachment_changes[@attribute_name.to_s])
|
|
188
|
+
when ActiveStorage::Attached::Changes::CreateOne
|
|
189
|
+
persist_pending_change(change)
|
|
190
|
+
when ActiveStorage::Attached::Changes::CreateMany
|
|
191
|
+
change.pending_uploads.each do |subchange|
|
|
192
|
+
persist_pending_change(subchange)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def persist_pending_change(change)
|
|
198
|
+
change.upload
|
|
199
|
+
change.blob.save!
|
|
200
|
+
rescue ActiveStorage::Error, Errno::ENOENT => e
|
|
201
|
+
# no recovery available (ENOENT: tempfile vanished before render)
|
|
202
|
+
log_dropped_upload(e)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def log_dropped_upload(error)
|
|
206
|
+
Rails.logger.warn(
|
|
207
|
+
"Dropped pending attachment for " \
|
|
208
|
+
"#{@builder.object.class}##{@attribute_name}: #{error.class}: #{error.message}",
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|