stimulus_plumbers 0.2.7 → 0.2.9
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/CHANGELOG.md +59 -0
- data/README.md +60 -41
- data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +760 -237
- data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
- data/lib/stimulus_plumbers/components/action_list/item.rb +27 -0
- data/lib/stimulus_plumbers/components/action_list/section.rb +21 -0
- data/lib/stimulus_plumbers/components/action_list.rb +23 -0
- data/lib/stimulus_plumbers/components/avatar.rb +72 -0
- data/lib/stimulus_plumbers/components/button/group.rb +17 -0
- data/lib/stimulus_plumbers/components/button.rb +27 -0
- data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +2 -2
- data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +2 -2
- data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +55 -0
- data/lib/stimulus_plumbers/components/calendar.rb +33 -0
- data/lib/stimulus_plumbers/components/card/section.rb +25 -0
- data/lib/stimulus_plumbers/components/card.rb +27 -0
- data/lib/stimulus_plumbers/components/combobox/autocomplete.rb +53 -0
- data/lib/stimulus_plumbers/components/combobox/date.rb +50 -0
- data/lib/stimulus_plumbers/components/combobox/dropdown.rb +38 -0
- data/lib/stimulus_plumbers/components/combobox/options/option.rb +34 -0
- data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +29 -0
- data/lib/stimulus_plumbers/components/combobox/options.rb +59 -0
- data/lib/stimulus_plumbers/components/combobox/popover.rb +20 -0
- data/lib/stimulus_plumbers/components/combobox/time/drum.rb +37 -0
- data/lib/stimulus_plumbers/components/combobox/time.rb +120 -0
- data/lib/stimulus_plumbers/components/combobox/trigger.rb +38 -0
- data/lib/stimulus_plumbers/components/combobox.rb +59 -0
- data/lib/stimulus_plumbers/components/date_picker/navigation.rb +1 -1
- data/lib/stimulus_plumbers/components/date_picker/navigator.rb +1 -1
- data/lib/stimulus_plumbers/components/icon.rb +49 -0
- data/lib/stimulus_plumbers/components/popover/builder.rb +25 -0
- data/lib/stimulus_plumbers/components/popover.rb +26 -0
- data/lib/stimulus_plumbers/form/builder.rb +64 -17
- data/lib/stimulus_plumbers/form/{field_component.rb → field.rb} +13 -11
- data/lib/stimulus_plumbers/form/fields/combobox.rb +41 -0
- data/lib/stimulus_plumbers/form/fields/error.rb +14 -0
- data/lib/stimulus_plumbers/form/fields/group.rb +14 -0
- data/lib/stimulus_plumbers/form/fields/hint.rb +14 -0
- data/lib/stimulus_plumbers/form/fields/label.rb +21 -0
- data/lib/stimulus_plumbers/form/fields/password.rb +55 -0
- data/lib/stimulus_plumbers/form/fields/renderer.rb +16 -21
- data/lib/stimulus_plumbers/form/fields/search.rb +54 -0
- data/lib/stimulus_plumbers/form/fields/select.rb +8 -2
- data/lib/stimulus_plumbers/form/fields/submit.rb +23 -0
- data/lib/stimulus_plumbers/form/fields/text.rb +12 -4
- data/lib/stimulus_plumbers/helpers/action_list_helper.rb +2 -2
- data/lib/stimulus_plumbers/helpers/avatar_helper.rb +2 -2
- data/lib/stimulus_plumbers/helpers/button_helper.rb +2 -2
- data/lib/stimulus_plumbers/helpers/calendar_helper.rb +1 -1
- data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +1 -1
- data/lib/stimulus_plumbers/helpers/card_helper.rb +2 -2
- data/lib/stimulus_plumbers/helpers/combobox_helper.rb +74 -0
- data/lib/stimulus_plumbers/helpers/popover_helper.rb +2 -2
- data/lib/stimulus_plumbers/helpers.rb +2 -2
- data/lib/stimulus_plumbers/plumber/base.rb +20 -0
- data/lib/stimulus_plumbers/plumber/dispatcher.rb +111 -0
- data/lib/stimulus_plumbers/plumber/html_options.rb +51 -0
- data/lib/stimulus_plumbers/plumber/renderer.rb +89 -0
- data/lib/stimulus_plumbers/themes/base.rb +9 -15
- data/lib/stimulus_plumbers/themes/schema/ranges.rb +5 -5
- data/lib/stimulus_plumbers/themes/schema.rb +97 -0
- data/lib/stimulus_plumbers/themes/tailwind/calendar.rb +48 -2
- data/lib/stimulus_plumbers/themes/tailwind/combobox.rb +75 -0
- data/lib/stimulus_plumbers/themes/tailwind/form.rb +10 -6
- data/lib/stimulus_plumbers/themes/tailwind_theme.rb +2 -0
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/lib/stimulus_plumbers.rb +41 -14
- metadata +42 -23
- data/lib/stimulus_plumbers/components/action_list/renderer.rb +0 -47
- data/lib/stimulus_plumbers/components/avatar/renderer.rb +0 -74
- data/lib/stimulus_plumbers/components/button/renderer.rb +0 -33
- data/lib/stimulus_plumbers/components/calendar/month/turbo/renderer.rb +0 -57
- data/lib/stimulus_plumbers/components/calendar/renderer.rb +0 -35
- data/lib/stimulus_plumbers/components/card/renderer.rb +0 -41
- data/lib/stimulus_plumbers/components/date_picker/renderer.rb +0 -82
- data/lib/stimulus_plumbers/components/icon/renderer.rb +0 -51
- data/lib/stimulus_plumbers/components/plumber/base.rb +0 -22
- data/lib/stimulus_plumbers/components/plumber/dispatcher.rb +0 -113
- data/lib/stimulus_plumbers/components/plumber/html_options.rb +0 -34
- data/lib/stimulus_plumbers/components/plumber/renderer.rb +0 -91
- data/lib/stimulus_plumbers/components/popover/renderer.rb +0 -46
- data/lib/stimulus_plumbers/helpers/date_picker_helper.rb +0 -17
- data/lib/stimulus_plumbers/themes/action_list.rb +0 -14
- data/lib/stimulus_plumbers/themes/avatar.rb +0 -14
- data/lib/stimulus_plumbers/themes/button.rb +0 -18
- data/lib/stimulus_plumbers/themes/calendar.rb +0 -15
- data/lib/stimulus_plumbers/themes/card.rb +0 -12
- data/lib/stimulus_plumbers/themes/form.rb +0 -30
- data/lib/stimulus_plumbers/themes/layout.rb +0 -12
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Dropdown < Plumber::Base
|
|
7
|
+
STIMULUS_CONTROLLER = "combobox-dropdown"
|
|
8
|
+
STIMULUS_ACTION = [
|
|
9
|
+
"click->#{STIMULUS_CONTROLLER}#select",
|
|
10
|
+
"keydown->#{STIMULUS_CONTROLLER}#onNavigate",
|
|
11
|
+
"#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
|
|
12
|
+
].join(" ").freeze
|
|
13
|
+
|
|
14
|
+
def self.default_opts
|
|
15
|
+
{
|
|
16
|
+
popover: {
|
|
17
|
+
tag: :div,
|
|
18
|
+
haspopup: "listbox",
|
|
19
|
+
data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render(options: [], value: nil, label: nil, **_kwargs)
|
|
25
|
+
listbox_attrs = merge_html_options(
|
|
26
|
+
{ classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
|
|
27
|
+
{ role: "listbox", data: { "#{STIMULUS_CONTROLLER}_target": "listbox" } }
|
|
28
|
+
)
|
|
29
|
+
listbox_attrs[:aria] = { label: label } if label
|
|
30
|
+
|
|
31
|
+
template.content_tag(:ul, **listbox_attrs) do
|
|
32
|
+
Options.new(template).render(options, value: value)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Options
|
|
7
|
+
class Option < Plumber::Base
|
|
8
|
+
def render(label:, value:, description: nil, disabled: false, selected: false)
|
|
9
|
+
aria = { selected: selected ? "true" : "false" }
|
|
10
|
+
aria[:disabled] = "true" if disabled
|
|
11
|
+
|
|
12
|
+
attrs = merge_html_options(
|
|
13
|
+
{ classes: theme.resolve(:combobox_option, selected: selected, disabled: disabled).fetch(:classes, "") },
|
|
14
|
+
{ role: "option", aria: aria, data: { value: value } }
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
template.content_tag(:li, **attrs) do
|
|
18
|
+
if description
|
|
19
|
+
template.safe_join(
|
|
20
|
+
[
|
|
21
|
+
template.content_tag(:span, label),
|
|
22
|
+
template.content_tag(:span, description)
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
else
|
|
26
|
+
label
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Options
|
|
7
|
+
class OptionGroup < Plumber::Base
|
|
8
|
+
def render(label:, options:, value: nil)
|
|
9
|
+
attrs = merge_html_options(
|
|
10
|
+
{ classes: theme.resolve(:combobox_option_group).fetch(:classes, "") },
|
|
11
|
+
{ role: "group", aria: { label: label } }
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
template.content_tag(:li, **attrs) do
|
|
15
|
+
template.safe_join(
|
|
16
|
+
[
|
|
17
|
+
template.content_tag(:span, label, aria: { hidden: "true" }),
|
|
18
|
+
template.content_tag(:ul) do
|
|
19
|
+
Options.new(template).render(options, value: value)
|
|
20
|
+
end
|
|
21
|
+
]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Options < Plumber::Base
|
|
7
|
+
def render(items, value: nil, &block)
|
|
8
|
+
@selected_value = value.to_s
|
|
9
|
+
template.safe_join(items.filter_map { |item| render_item(item, &block) })
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def render_item(item, &block)
|
|
15
|
+
attrs = normalize_item(item)
|
|
16
|
+
return nil if attrs.nil?
|
|
17
|
+
|
|
18
|
+
if attrs.key?(:optgroup)
|
|
19
|
+
block ? block.call(attrs) : OptionGroup.new(template).render(**attrs[:optgroup], value: @selected_value)
|
|
20
|
+
else
|
|
21
|
+
block ? block.call(attrs) : Option.new(template).render(**attrs)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def normalize_item(item)
|
|
26
|
+
case item
|
|
27
|
+
when Hash then normalize_hash(item)
|
|
28
|
+
when Array then normalize_array(item)
|
|
29
|
+
else
|
|
30
|
+
StimulusPlumbers::Logger.warn("Options#normalize_item: unrecognized item type #{item.class}, skipping")
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def normalize_hash(item)
|
|
36
|
+
if item.key?(:options)
|
|
37
|
+
{ optgroup: { label: item[:label], options: item[:options] } }
|
|
38
|
+
else
|
|
39
|
+
normalize_option(item[:label], item[:value].to_s, item)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_array(item)
|
|
44
|
+
normalize_option(item[0], item[1].to_s, item[2] || {})
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def normalize_option(label, value, attrs)
|
|
48
|
+
{
|
|
49
|
+
label: label,
|
|
50
|
+
value: value,
|
|
51
|
+
selected: @selected_value == value,
|
|
52
|
+
disabled: attrs[:disabled] || false,
|
|
53
|
+
description: attrs[:description]
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Popover < Plumber::Base
|
|
7
|
+
def render(stimulus_controller:, id:, tag: :div, role: nil, label: nil, content: nil, data: {}, **_rest, &block)
|
|
8
|
+
base_data = { "#{stimulus_controller}_target": "popover" }
|
|
9
|
+
|
|
10
|
+
attrs = { id: id, hidden: "", data: merge_data_options(base_data, data.symbolize_keys) }
|
|
11
|
+
attrs[:role] = role if role
|
|
12
|
+
attrs[:aria] = { label: label } if label
|
|
13
|
+
|
|
14
|
+
html_content = block_given? ? template.capture(&block) : content
|
|
15
|
+
template.content_tag(tag, **attrs) { html_content }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Time
|
|
7
|
+
class Drum < Plumber::Base
|
|
8
|
+
def render(stimulus_controller:, target:, label:, items:, selected: nil)
|
|
9
|
+
template.content_tag(
|
|
10
|
+
:ul,
|
|
11
|
+
**merge_html_options(
|
|
12
|
+
{ classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
|
|
13
|
+
{
|
|
14
|
+
role: "listbox",
|
|
15
|
+
tabindex: "0",
|
|
16
|
+
aria: { label: label },
|
|
17
|
+
data: { "#{stimulus_controller}_target": target }
|
|
18
|
+
},
|
|
19
|
+
{ data: { action: "click->#{stimulus_controller}#select keydown->#{stimulus_controller}#onNavigate" } }
|
|
20
|
+
)
|
|
21
|
+
) do
|
|
22
|
+
template.safe_join(
|
|
23
|
+
items.map do |text, value|
|
|
24
|
+
Options::Option.new(template).render(
|
|
25
|
+
label: text,
|
|
26
|
+
value: value,
|
|
27
|
+
selected: value.to_s == selected.to_s
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Time < Plumber::Base
|
|
7
|
+
STIMULUS_CONTROLLER = "combobox-time"
|
|
8
|
+
|
|
9
|
+
def self.default_opts
|
|
10
|
+
{
|
|
11
|
+
popover: { label: "Picker", role: "dialog", tag: :div }
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(format: :h12, step: 1, value: nil, **_kwargs)
|
|
16
|
+
@format = format
|
|
17
|
+
@step = [1, step.to_i].max
|
|
18
|
+
@time = parse_time(value)
|
|
19
|
+
|
|
20
|
+
template.content_tag(
|
|
21
|
+
:div,
|
|
22
|
+
**merge_html_options(
|
|
23
|
+
{ classes: theme.resolve(:combobox_time).fetch(:classes, "") },
|
|
24
|
+
{ data: { controller: STIMULUS_CONTROLLER,
|
|
25
|
+
action: "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
) do
|
|
30
|
+
template.safe_join(drums)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def drums
|
|
37
|
+
cols = [hour_drum, minute_drum]
|
|
38
|
+
cols << period_drum if @format == :h12
|
|
39
|
+
cols
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def hour_drum
|
|
43
|
+
drum.render(
|
|
44
|
+
stimulus_controller: STIMULUS_CONTROLLER,
|
|
45
|
+
target: "hour",
|
|
46
|
+
label: "Hour",
|
|
47
|
+
items: hour_items,
|
|
48
|
+
selected: current_hour
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def minute_drum
|
|
53
|
+
items = (0...60).step(@step).map do |m|
|
|
54
|
+
s = m.to_s.rjust(2, "0")
|
|
55
|
+
[s, s]
|
|
56
|
+
end
|
|
57
|
+
selected = @time ? snap_minute(@time.min).to_s.rjust(2, "0") : nil
|
|
58
|
+
drum.render(
|
|
59
|
+
stimulus_controller: STIMULUS_CONTROLLER,
|
|
60
|
+
target: "minute",
|
|
61
|
+
label: "Minute",
|
|
62
|
+
items: items,
|
|
63
|
+
selected: selected
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def period_drum
|
|
68
|
+
selected = @time && (@time.hour < 12 ? "AM" : "PM")
|
|
69
|
+
drum.render(
|
|
70
|
+
stimulus_controller: STIMULUS_CONTROLLER,
|
|
71
|
+
target: "period",
|
|
72
|
+
label: "Period",
|
|
73
|
+
items: [%w[AM AM], %w[PM PM]],
|
|
74
|
+
selected: selected
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def hour_items
|
|
79
|
+
if @format == :h12
|
|
80
|
+
(1..12).map { |h| [h.to_s, h.to_s] }
|
|
81
|
+
else
|
|
82
|
+
(0..23).map do |h|
|
|
83
|
+
s = h.to_s.rjust(2, "0")
|
|
84
|
+
[s, s]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def current_hour
|
|
90
|
+
return nil unless @time
|
|
91
|
+
|
|
92
|
+
if @format == :h12
|
|
93
|
+
h = @time.hour % 12
|
|
94
|
+
(h.zero? ? 12 : h).to_s
|
|
95
|
+
else
|
|
96
|
+
@time.hour.to_s.rjust(2, "0")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def snap_minute(minute)
|
|
101
|
+
return minute if @step == 1
|
|
102
|
+
|
|
103
|
+
((minute.to_f / @step).round * @step) % 60
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def drum
|
|
107
|
+
@drum ||= Time::Drum.new(template)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_time(value)
|
|
111
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
112
|
+
|
|
113
|
+
::Time.parse(value.to_s)
|
|
114
|
+
rescue ArgumentError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox
|
|
6
|
+
class Trigger < Plumber::Base
|
|
7
|
+
def render(
|
|
8
|
+
stimulus_controller:,
|
|
9
|
+
popover_id:,
|
|
10
|
+
haspopup:,
|
|
11
|
+
readonly: true,
|
|
12
|
+
aria_autocomplete: nil,
|
|
13
|
+
aria_label: nil,
|
|
14
|
+
data: {},
|
|
15
|
+
**_rest
|
|
16
|
+
)
|
|
17
|
+
base_data = {
|
|
18
|
+
"#{stimulus_controller}_target": "trigger",
|
|
19
|
+
input_format_target: "input",
|
|
20
|
+
action: "focus->#{stimulus_controller}#open keydown.esc->#{stimulus_controller}#close"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
aria = { haspopup: haspopup, expanded: "false", controls: popover_id }
|
|
24
|
+
aria[:autocomplete] = aria_autocomplete if aria_autocomplete
|
|
25
|
+
aria[:label] = aria_label if aria_label
|
|
26
|
+
|
|
27
|
+
template.tag.input(
|
|
28
|
+
type: "text",
|
|
29
|
+
readonly: (readonly ? true : nil),
|
|
30
|
+
role: "combobox",
|
|
31
|
+
aria: aria,
|
|
32
|
+
data: merge_data_options(base_data, data.symbolize_keys)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Combobox < Plumber::Base
|
|
6
|
+
STIMULUS_CONTROLLER = "input-combobox"
|
|
7
|
+
FORMAT_CONTROLLER = "input-format"
|
|
8
|
+
FORMAT_ACTION = "input-combobox:changed->input-format#format"
|
|
9
|
+
|
|
10
|
+
def render(base_id:, options: {}, **kwargs)
|
|
11
|
+
popover_id = "#{base_id}_popover"
|
|
12
|
+
initial_value = options.dig(:input, :value)
|
|
13
|
+
|
|
14
|
+
base_data = {
|
|
15
|
+
controller: "#{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
|
|
16
|
+
action: FORMAT_ACTION
|
|
17
|
+
}
|
|
18
|
+
base_data[:input_combobox_value_value] = initial_value if initial_value.present?
|
|
19
|
+
|
|
20
|
+
html_options = merge_html_options({ data: base_data }, kwargs)
|
|
21
|
+
|
|
22
|
+
template.content_tag(:div, **html_options) do
|
|
23
|
+
template.safe_join(
|
|
24
|
+
[
|
|
25
|
+
trigger(popover_id, options),
|
|
26
|
+
hidden_input(options.fetch(:input, {})),
|
|
27
|
+
popover(popover_id, options)
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def trigger(popover_id, options)
|
|
34
|
+
haspopup = options.dig(:popover, :haspopup) || options.dig(:popover, :role) || "dialog"
|
|
35
|
+
Combobox::Trigger.new(template).render(
|
|
36
|
+
stimulus_controller: STIMULUS_CONTROLLER,
|
|
37
|
+
popover_id: popover_id,
|
|
38
|
+
haspopup: haspopup,
|
|
39
|
+
**options.fetch(:trigger, {})
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def popover(popover_id, options)
|
|
44
|
+
Combobox::Popover.new(template).render(
|
|
45
|
+
stimulus_controller: STIMULUS_CONTROLLER,
|
|
46
|
+
id: popover_id,
|
|
47
|
+
**options.fetch(:popover, {})
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def hidden_input(opts)
|
|
54
|
+
data = { "#{STIMULUS_CONTROLLER}_target": "value" }.merge(opts.fetch(:data, {}))
|
|
55
|
+
template.tag.input(type: "hidden", name: opts[:name], value: opts[:value], data: data)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -4,7 +4,7 @@ module StimulusPlumbers
|
|
|
4
4
|
module Components
|
|
5
5
|
module DatePicker
|
|
6
6
|
class Navigation < Plumber::Base
|
|
7
|
-
def render(
|
|
7
|
+
def render(step:, stimulus_controller:, **kwargs)
|
|
8
8
|
html_options = merge_html_options(
|
|
9
9
|
{ classes: theme.resolve(:calendar_navigation).fetch(:classes, ""), aria: { label: "DatePicker Navigation" } },
|
|
10
10
|
kwargs
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Icon < Plumber::Base
|
|
6
|
+
ICONS = {
|
|
7
|
+
"arrow-left" => "M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18",
|
|
8
|
+
"arrow-right" => "M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3",
|
|
9
|
+
"arrow-up" => "M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18",
|
|
10
|
+
"arrow-down" => "M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def render(name:, **kwargs)
|
|
14
|
+
html_options = merge_html_options(
|
|
15
|
+
{ classes: theme.resolve(:icon).fetch(:classes, "") },
|
|
16
|
+
kwargs
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if ICONS[name]
|
|
20
|
+
svg_icon(ICONS[name], html_options)
|
|
21
|
+
else
|
|
22
|
+
template.content_tag(:span, nil, **html_options)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def svg_icon(path, html_options)
|
|
29
|
+
template.content_tag(
|
|
30
|
+
:svg,
|
|
31
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
32
|
+
fill: "none",
|
|
33
|
+
viewBox: "0 0 24 24",
|
|
34
|
+
width: "24",
|
|
35
|
+
height: "24",
|
|
36
|
+
"stroke-width": "1.5",
|
|
37
|
+
stroke: "currentColor",
|
|
38
|
+
**html_options
|
|
39
|
+
) do
|
|
40
|
+
template.tag.path(
|
|
41
|
+
"stroke-linecap": "round",
|
|
42
|
+
"stroke-linejoin": "round",
|
|
43
|
+
d: path
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Popover
|
|
6
|
+
class Builder
|
|
7
|
+
attr_reader :activator_html, :content_html
|
|
8
|
+
|
|
9
|
+
def initialize(template)
|
|
10
|
+
@template = template
|
|
11
|
+
@activator_html = "".html_safe
|
|
12
|
+
@content_html = "".html_safe
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def activator(&block)
|
|
16
|
+
@activator_html = @template.capture(&block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def content(&block)
|
|
20
|
+
@content_html = @template.capture(&block)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class Popover < Plumber::Base
|
|
6
|
+
def render(interactive: true, **kwargs, &block)
|
|
7
|
+
html_options = merge_html_options(
|
|
8
|
+
{ classes: theme.resolve(:popover).fetch(:classes, "") },
|
|
9
|
+
kwargs
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
builder = Popover::Builder.new(template)
|
|
13
|
+
template.capture(builder, &block)
|
|
14
|
+
|
|
15
|
+
template.content_tag(:div, **html_options) do
|
|
16
|
+
wrapped_content = if interactive
|
|
17
|
+
template.content_tag(:template, builder.content_html)
|
|
18
|
+
else
|
|
19
|
+
builder.content_html
|
|
20
|
+
end
|
|
21
|
+
template.safe_join([builder.activator_html, wrapped_content])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -2,29 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
require "action_view/version"
|
|
4
4
|
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "field"
|
|
6
|
+
require_relative "fields/choice"
|
|
7
|
+
require_relative "fields/combobox"
|
|
8
|
+
require_relative "fields/file"
|
|
9
|
+
require_relative "fields/password"
|
|
6
10
|
require_relative "fields/renderer"
|
|
11
|
+
require_relative "fields/search"
|
|
12
|
+
require_relative "fields/select"
|
|
7
13
|
require_relative "fields/text"
|
|
8
14
|
require_relative "fields/text_area"
|
|
9
|
-
require_relative "fields/
|
|
10
|
-
require_relative "
|
|
11
|
-
require_relative "fields/choice"
|
|
12
|
-
require_relative "../components/plumber/html_options"
|
|
15
|
+
require_relative "fields/submit"
|
|
16
|
+
require_relative "../plumber/html_options"
|
|
13
17
|
|
|
14
18
|
module StimulusPlumbers
|
|
15
19
|
module Form
|
|
16
20
|
class Builder < ActionView::Helpers::FormBuilder
|
|
17
|
-
include
|
|
18
|
-
include Fields::
|
|
19
|
-
include Fields::
|
|
21
|
+
include Plumber::HtmlOptions
|
|
22
|
+
include Fields::Choice
|
|
23
|
+
include Fields::Combobox
|
|
20
24
|
include Fields::File
|
|
25
|
+
include Fields::Password
|
|
26
|
+
include Fields::Search
|
|
21
27
|
include Fields::Select
|
|
22
|
-
include Fields::
|
|
28
|
+
include Fields::Submit
|
|
29
|
+
include Fields::Text
|
|
30
|
+
include Fields::TextArea
|
|
23
31
|
|
|
24
32
|
private
|
|
25
33
|
|
|
26
34
|
def build_field(attribute, form_field_opts, input_id: field_id(attribute))
|
|
27
|
-
|
|
35
|
+
Field.new(
|
|
28
36
|
object: object,
|
|
29
37
|
attribute: attribute,
|
|
30
38
|
input_id: input_id,
|
|
@@ -41,8 +49,17 @@ module StimulusPlumbers
|
|
|
41
49
|
Fields::Renderer.new(@template, theme, field).call(input_html)
|
|
42
50
|
end
|
|
43
51
|
|
|
52
|
+
def build_input_group(input_tag, field, trailing:, **wrapper_opts)
|
|
53
|
+
@template.content_tag(
|
|
54
|
+
:div,
|
|
55
|
+
input_tag.html_safe + trailing,
|
|
56
|
+
class: field_theme(:form_input_group, error: field.error?)[:class],
|
|
57
|
+
**wrapper_opts
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
44
61
|
def extract_options(options)
|
|
45
|
-
[options.except(*
|
|
62
|
+
[options.except(*Field::OPTIONS), options.slice(*Field::OPTIONS)]
|
|
46
63
|
end
|
|
47
64
|
|
|
48
65
|
def field_theme(key, **variants)
|
|
@@ -53,15 +70,45 @@ module StimulusPlumbers
|
|
|
53
70
|
StimulusPlumbers.config.theme
|
|
54
71
|
end
|
|
55
72
|
|
|
56
|
-
#
|
|
73
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
57
74
|
if ActionView.version < "7.0"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
# field_id was added in Rails 7.0, backports it to Rails 6.1.
|
|
76
|
+
# https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_helper.rb#L1777
|
|
77
|
+
# https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_tag_helper.rb#L101
|
|
78
|
+
def field_id(method_name, *suffixes, namespace: @options[:namespace], index: @options[:index])
|
|
79
|
+
object_name = @object_name.respond_to?(:model_name) ? @object_name.model_name.singular : @object_name
|
|
80
|
+
|
|
81
|
+
sanitized_object_name = object_name.to_s.gsub(%r{\]\[|[^-a-zA-Z0-9:.]}, "_").delete_suffix("_")
|
|
82
|
+
sanitized_method_name = method_name.to_s.delete_suffix("?")
|
|
83
|
+
|
|
84
|
+
[
|
|
85
|
+
namespace,
|
|
86
|
+
sanitized_object_name.presence,
|
|
87
|
+
(index unless sanitized_object_name.empty?),
|
|
88
|
+
sanitized_method_name,
|
|
89
|
+
*suffixes
|
|
90
|
+
].tap(&:compact!).join("_")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# field_name was added in Rails 7.0, backports it to Rails 6.1.
|
|
94
|
+
# https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_helper.rb#L1797
|
|
95
|
+
# https://github.com/rails/rails/blob/2d670320f7b02ae879545d5202f0633841b8f196/actionview/lib/action_view/helpers/form_tag_helper.rb#L131
|
|
96
|
+
def field_name(method_name, *method_names, multiple: false, index: @options[:index])
|
|
97
|
+
object_name = @options.fetch(:as) { @object_name }
|
|
98
|
+
|
|
99
|
+
names = method_names.map! { |name| "[#{name}]" }.join
|
|
100
|
+
|
|
101
|
+
# a little duplication to construct fewer strings
|
|
102
|
+
if object_name.blank?
|
|
103
|
+
"#{method_name}#{names}#{"[]" if multiple}"
|
|
104
|
+
elsif index
|
|
105
|
+
"#{object_name}[#{index}][#{method_name}]#{names}#{"[]" if multiple}"
|
|
106
|
+
else
|
|
107
|
+
"#{object_name}[#{method_name}]#{names}#{"[]" if multiple}"
|
|
108
|
+
end
|
|
63
109
|
end
|
|
64
110
|
end
|
|
111
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
65
112
|
end
|
|
66
113
|
end
|
|
67
114
|
end
|