stimulus_plumbers 0.2.8 → 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 +21 -0
- data/README.md +3 -0
- data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +339 -302
- 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 +30 -34
- data/lib/stimulus_plumbers/components/combobox/date.rb +16 -18
- data/lib/stimulus_plumbers/components/combobox/dropdown.rb +13 -16
- 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 +32 -15
- 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/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 +7 -5
- data/lib/stimulus_plumbers/form/{field_component.rb → field.rb} +1 -1
- data/lib/stimulus_plumbers/form/fields/combobox.rb +1 -1
- 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/renderer.rb +16 -20
- data/lib/stimulus_plumbers/form/fields/search.rb +23 -9
- data/lib/stimulus_plumbers/form/fields/submit.rb +23 -0
- 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 +5 -5
- data/lib/stimulus_plumbers/helpers/popover_helper.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_theme.rb +2 -0
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/lib/stimulus_plumbers.rb +29 -19
- metadata +33 -25
- 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/combobox/option.rb +0 -27
- data/lib/stimulus_plumbers/components/combobox/option_group.rb +0 -52
- data/lib/stimulus_plumbers/components/combobox/renderer.rb +0 -78
- 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 -53
- data/lib/stimulus_plumbers/components/plumber/renderer.rb +0 -91
- data/lib/stimulus_plumbers/components/popover/renderer.rb +0 -46
- data/lib/stimulus_plumbers/components/time_picker/renderer.rb +0 -38
- data/lib/stimulus_plumbers/themes/base/action_list.rb +0 -14
- data/lib/stimulus_plumbers/themes/base/avatar.rb +0 -14
- data/lib/stimulus_plumbers/themes/base/button.rb +0 -18
- data/lib/stimulus_plumbers/themes/base/calendar.rb +0 -15
- data/lib/stimulus_plumbers/themes/base/card.rb +0 -12
- data/lib/stimulus_plumbers/themes/base/form.rb +0 -34
- data/lib/stimulus_plumbers/themes/base/layout.rb +0 -12
|
@@ -2,54 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
module StimulusPlumbers
|
|
4
4
|
module Components
|
|
5
|
-
|
|
6
|
-
# Renders a listbox popover body for autocomplete.
|
|
7
|
-
# Re-uses combobox-dropdown for selection/filtering; input-combobox relays
|
|
8
|
-
# the trigger input event via the combobox-dropdown outlet.
|
|
5
|
+
class Combobox
|
|
9
6
|
class Autocomplete < Plumber::Base
|
|
10
|
-
include OptionGroup
|
|
11
|
-
|
|
12
|
-
DROPDOWN_CONTROLLER = "combobox-dropdown"
|
|
13
|
-
DROPDOWN_ACTION = [
|
|
14
|
-
"click->combobox-dropdown#select",
|
|
15
|
-
"keydown->combobox-dropdown#navigate",
|
|
16
|
-
"combobox-dropdown:selected->input-combobox#onSelected"
|
|
17
|
-
].join(" ").freeze
|
|
18
|
-
|
|
19
7
|
def self.default_opts
|
|
20
|
-
|
|
21
|
-
popover: {
|
|
22
|
-
tag: :div,
|
|
23
|
-
haspopup: "listbox",
|
|
24
|
-
data: { controller: DROPDOWN_CONTROLLER, action: DROPDOWN_ACTION }
|
|
25
|
-
},
|
|
8
|
+
Dropdown.default_opts.deep_merge(
|
|
26
9
|
trigger: { aria_autocomplete: "list", readonly: false }
|
|
27
|
-
|
|
10
|
+
)
|
|
28
11
|
end
|
|
29
12
|
|
|
30
13
|
def render(options: [], value: nil, label: nil, **_kwargs)
|
|
31
|
-
|
|
32
|
-
|
|
14
|
+
template.safe_join([listbox(options, value, label), loading, empty])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
33
18
|
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
def listbox(options, value, label)
|
|
20
|
+
attrs = merge_html_options(
|
|
21
|
+
{ classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
|
|
22
|
+
{ role: "listbox", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "listbox" } }
|
|
23
|
+
)
|
|
24
|
+
attrs[:aria] = { label: label } if label
|
|
25
|
+
|
|
26
|
+
template.content_tag(:ul, **attrs) do
|
|
27
|
+
Options.new(template).render(options, value: value)
|
|
36
28
|
end
|
|
29
|
+
end
|
|
37
30
|
|
|
38
|
-
|
|
31
|
+
def loading
|
|
32
|
+
template.content_tag(
|
|
39
33
|
:div,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
**merge_html_options(
|
|
35
|
+
{ classes: theme.resolve(:combobox_autocomplete_loading).fetch(:classes, "") },
|
|
36
|
+
{ hidden: "", aria: { live: "polite" }, data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "loading" } }
|
|
37
|
+
)
|
|
43
38
|
) { "" }
|
|
39
|
+
end
|
|
44
40
|
|
|
45
|
-
|
|
41
|
+
def empty
|
|
42
|
+
template.content_tag(
|
|
46
43
|
:div,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
**merge_html_options(
|
|
45
|
+
{ classes: theme.resolve(:combobox_autocomplete_empty).fetch(:classes, "") },
|
|
46
|
+
{ hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "empty" } }
|
|
47
|
+
)
|
|
50
48
|
) { "No results" }
|
|
51
|
-
|
|
52
|
-
template.safe_join([listbox, loading, empty])
|
|
53
49
|
end
|
|
54
50
|
end
|
|
55
51
|
end
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module StimulusPlumbers
|
|
4
4
|
module Components
|
|
5
|
-
|
|
6
|
-
# Renders the date picker popover body: navigation + calendar grid.
|
|
7
|
-
# Wires ComboboxDateController; dispatches combobox-date:selected → InputComboboxController.
|
|
5
|
+
class Combobox
|
|
8
6
|
class Date < Plumber::Base
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
STIMULUS_CONTROLLER = "combobox-date"
|
|
8
|
+
CALENDAR_OUTLET = "#{STIMULUS_CONTROLLER}-calendar-month-outlet".freeze
|
|
11
9
|
|
|
12
10
|
def self.default_opts
|
|
13
11
|
{
|
|
@@ -19,17 +17,17 @@ module StimulusPlumbers
|
|
|
19
17
|
def render(value: nil, **_kwargs)
|
|
20
18
|
calendar_id = "combobox_date_#{SecureRandom.hex(8)}_calendar"
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
) do
|
|
20
|
+
data = {
|
|
21
|
+
controller: STIMULUS_CONTROLLER,
|
|
22
|
+
CALENDAR_OUTLET => "##{calendar_id}",
|
|
23
|
+
action: [
|
|
24
|
+
"calendar-month-observer:selected->#{STIMULUS_CONTROLLER}#onSelect",
|
|
25
|
+
"#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
|
|
26
|
+
].join(" "),
|
|
27
|
+
"#{STIMULUS_CONTROLLER}-date-value" => value
|
|
28
|
+
}.compact
|
|
29
|
+
|
|
30
|
+
template.content_tag(:div, data: data) do
|
|
33
31
|
template.safe_join([navigation, calendar_month(id: calendar_id)])
|
|
34
32
|
end
|
|
35
33
|
end
|
|
@@ -39,12 +37,12 @@ module StimulusPlumbers
|
|
|
39
37
|
def navigation
|
|
40
38
|
DatePicker::Navigation.new(template).render(
|
|
41
39
|
step: "month",
|
|
42
|
-
stimulus_controller:
|
|
40
|
+
stimulus_controller: STIMULUS_CONTROLLER
|
|
43
41
|
)
|
|
44
42
|
end
|
|
45
43
|
|
|
46
44
|
def calendar_month(**kwargs)
|
|
47
|
-
Calendar
|
|
45
|
+
Calendar.new(template).month(**kwargs)
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
end
|
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module StimulusPlumbers
|
|
4
4
|
module Components
|
|
5
|
-
|
|
6
|
-
# Renders a static listbox popover body with combobox-dropdown controller.
|
|
7
|
-
# Supports flat options, descriptions, and option groups.
|
|
5
|
+
class Combobox
|
|
8
6
|
class Dropdown < Plumber::Base
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"keydown->combobox-dropdown#navigate",
|
|
15
|
-
"combobox-dropdown:selected->input-combobox#onSelected"
|
|
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"
|
|
16
12
|
].join(" ").freeze
|
|
17
13
|
|
|
18
14
|
def self.default_opts
|
|
@@ -20,20 +16,21 @@ module StimulusPlumbers
|
|
|
20
16
|
popover: {
|
|
21
17
|
tag: :div,
|
|
22
18
|
haspopup: "listbox",
|
|
23
|
-
data: { controller:
|
|
19
|
+
data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION }
|
|
24
20
|
}
|
|
25
21
|
}
|
|
26
22
|
end
|
|
27
23
|
|
|
28
24
|
def render(options: [], value: nil, label: nil, **_kwargs)
|
|
29
|
-
listbox_attrs =
|
|
25
|
+
listbox_attrs = merge_html_options(
|
|
26
|
+
{ classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
|
|
27
|
+
{ role: "listbox", data: { "#{STIMULUS_CONTROLLER}_target": "listbox" } }
|
|
28
|
+
)
|
|
30
29
|
listbox_attrs[:aria] = { label: label } if label
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
template.content_tag(:ul, **listbox_attrs) do
|
|
32
|
+
Options.new(template).render(options, value: value)
|
|
34
33
|
end
|
|
35
|
-
|
|
36
|
-
template.safe_join([listbox])
|
|
37
34
|
end
|
|
38
35
|
end
|
|
39
36
|
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
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module StimulusPlumbers
|
|
4
4
|
module Components
|
|
5
|
-
|
|
6
|
-
# Renders an iOS-style drum/wheel time picker as the popover body.
|
|
7
|
-
# Wires ComboboxTimeController; dispatches combobox-time:selected → InputComboboxController.
|
|
5
|
+
class Combobox
|
|
8
6
|
class Time < Plumber::Base
|
|
9
|
-
|
|
7
|
+
STIMULUS_CONTROLLER = "combobox-time"
|
|
10
8
|
|
|
11
9
|
def self.default_opts
|
|
12
10
|
{
|
|
@@ -21,10 +19,13 @@ module StimulusPlumbers
|
|
|
21
19
|
|
|
22
20
|
template.content_tag(
|
|
23
21
|
:div,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
)
|
|
28
29
|
) do
|
|
29
30
|
template.safe_join(drums)
|
|
30
31
|
end
|
|
@@ -39,7 +40,13 @@ module StimulusPlumbers
|
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def hour_drum
|
|
42
|
-
drum.render(
|
|
43
|
+
drum.render(
|
|
44
|
+
stimulus_controller: STIMULUS_CONTROLLER,
|
|
45
|
+
target: "hour",
|
|
46
|
+
label: "Hour",
|
|
47
|
+
items: hour_items,
|
|
48
|
+
selected: current_hour
|
|
49
|
+
)
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
def minute_drum
|
|
@@ -48,14 +55,24 @@ module StimulusPlumbers
|
|
|
48
55
|
[s, s]
|
|
49
56
|
end
|
|
50
57
|
selected = @time ? snap_minute(@time.min).to_s.rjust(2, "0") : nil
|
|
51
|
-
drum.render(
|
|
58
|
+
drum.render(
|
|
59
|
+
stimulus_controller: STIMULUS_CONTROLLER,
|
|
60
|
+
target: "minute",
|
|
61
|
+
label: "Minute",
|
|
62
|
+
items: items,
|
|
63
|
+
selected: selected
|
|
64
|
+
)
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
def period_drum
|
|
55
|
-
selected =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
)
|
|
59
76
|
end
|
|
60
77
|
|
|
61
78
|
def hour_items
|
|
@@ -87,7 +104,7 @@ module StimulusPlumbers
|
|
|
87
104
|
end
|
|
88
105
|
|
|
89
106
|
def drum
|
|
90
|
-
|
|
107
|
+
@drum ||= Time::Drum.new(template)
|
|
91
108
|
end
|
|
92
109
|
|
|
93
110
|
def parse_time(value)
|
|
@@ -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
|
|
@@ -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
|