ruby_ui 1.1.0 → 1.2.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/lib/generators/ruby_ui/component_generator.rb +5 -1
- data/lib/generators/ruby_ui/dependencies.yml +10 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +4 -0
- data/lib/ruby_ui/combobox/combobox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_badge.rb +17 -0
- data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +243 -53
- data/lib/ruby_ui/combobox/combobox_docs.rb +199 -64
- data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
- data/lib/ruby_ui/combobox/combobox_item.rb +5 -7
- data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
- data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
- data/lib/ruby_ui/combobox/combobox_popover.rb +0 -5
- data/lib/ruby_ui/combobox/combobox_radio.rb +1 -8
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
- data/lib/ruby_ui/data_table/data_table.rb +29 -0
- data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
- data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
- data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
- data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
- data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
- data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
- data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
- data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
- data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
- data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
- data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
- data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
- data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
- data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
- data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
- data/lib/ruby_ui/masked_input/masked_input.rb +11 -1
- data/lib/ruby_ui/masked_input/masked_input_controller.js +13 -0
- data/lib/ruby_ui/native_select/native_select.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
- data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
- data/lib/ruby_ui.rb +1 -1
- metadata +43 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 488fdc5b97a189ce4916f776af376a06760255437f681b3e3ef7e9aa0998e2b3
|
|
4
|
+
data.tar.gz: ff44a35b46e45d1cf1b510716fdc174fc50e229ef7a82d81c8318d51911beb3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f7fe5941b4a9e4375d280efd31ab1e35dca88b699cf4580f6722294c4b8fe161240bbe7bce09bf5f75d5a41a9c2d62e6c24ab9a3b4d4f32ef8062fa4e81d5b3
|
|
7
|
+
data.tar.gz: 0b3e785f7380c7ba5abc0e721d5d17c3a5708a554b13bb2cd946e8019241642ebd7dfb1b3cfc46b759d14e18e37d82cbf0f874824a0489ddf8d09003bda1d9d1
|
|
@@ -9,6 +9,7 @@ module RubyUI
|
|
|
9
9
|
source_root File.expand_path("../../ruby_ui", __dir__)
|
|
10
10
|
argument :component_name, type: :string, required: true
|
|
11
11
|
class_option :force, type: :boolean, default: false
|
|
12
|
+
class_option :with_docs, type: :boolean, default: false
|
|
12
13
|
|
|
13
14
|
def generate_component
|
|
14
15
|
if component_not_found?
|
|
@@ -63,7 +64,10 @@ module RubyUI
|
|
|
63
64
|
|
|
64
65
|
def component_folder_path = File.join(self.class.source_root, component_folder_name)
|
|
65
66
|
|
|
66
|
-
def components_file_paths
|
|
67
|
+
def components_file_paths
|
|
68
|
+
files = Dir.glob(File.join(component_folder_path, "*.rb"))
|
|
69
|
+
options["with_docs"] ? files : files.reject { |f| f.end_with?("_docs.rb") }
|
|
70
|
+
end
|
|
67
71
|
|
|
68
72
|
def js_controller_file_paths = Dir.glob(File.join(component_folder_path, "*.js"))
|
|
69
73
|
|
|
@@ -4,6 +4,8 @@ module RubyUI
|
|
|
4
4
|
def install_js_package(package)
|
|
5
5
|
if using_importmap?
|
|
6
6
|
pin_with_importmap(package)
|
|
7
|
+
elsif using_bun?
|
|
8
|
+
run "bun add #{package}"
|
|
7
9
|
elsif using_yarn?
|
|
8
10
|
run "yarn add #{package}"
|
|
9
11
|
elsif using_npm?
|
|
@@ -30,6 +32,8 @@ module RubyUI
|
|
|
30
32
|
File.exist?(Rails.root.join("config/importmap.rb")) && File.exist?(Rails.root.join("bin/importmap"))
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
def using_bun? = File.exist?(Rails.root.join("bun.lock"))
|
|
36
|
+
|
|
33
37
|
def using_npm? = File.exist?(Rails.root.join("package-lock.json"))
|
|
34
38
|
|
|
35
39
|
def using_pnpm? = File.exist?(Rails.root.join("pnpm-lock.yaml"))
|
|
@@ -19,7 +19,13 @@ module RubyUI
|
|
|
19
19
|
data: {
|
|
20
20
|
controller: "ruby-ui--combobox",
|
|
21
21
|
ruby_ui__combobox_term_value: @term,
|
|
22
|
-
action:
|
|
22
|
+
action: %w[
|
|
23
|
+
turbo:morph@window->ruby-ui--combobox#updateTriggerContent
|
|
24
|
+
keydown.down->ruby-ui--combobox#keyDownPressed
|
|
25
|
+
keydown.up->ruby-ui--combobox#keyUpPressed
|
|
26
|
+
keydown.enter->ruby-ui--combobox#keyEnterPressed
|
|
27
|
+
keydown.esc->ruby-ui--combobox#closePopover:prevent
|
|
28
|
+
]
|
|
23
29
|
}
|
|
24
30
|
}
|
|
25
31
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ComboboxBadge < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
span(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
class: "inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-xs font-medium text-secondary-foreground"
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ComboboxBadgeTrigger < Base
|
|
5
|
+
def initialize(placeholder: "", clear_button: false, **)
|
|
6
|
+
@placeholder = placeholder
|
|
7
|
+
@clear_button = clear_button
|
|
8
|
+
super(**)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def view_template(&)
|
|
12
|
+
div(**attrs) do
|
|
13
|
+
div(data: {ruby_ui__combobox_target: "badgeContainer"}, class: "hidden")
|
|
14
|
+
input(
|
|
15
|
+
type: "text",
|
|
16
|
+
class: "flex-1 min-w-8 bg-transparent border-0 px-0 outline-none focus:ring-0 placeholder:text-muted-foreground text-sm",
|
|
17
|
+
autocomplete: "off",
|
|
18
|
+
autocorrect: "off",
|
|
19
|
+
spellcheck: "false",
|
|
20
|
+
placeholder: @placeholder,
|
|
21
|
+
data: {
|
|
22
|
+
ruby_ui__combobox_target: "badgeInput",
|
|
23
|
+
action: "keyup->ruby-ui--combobox#filterItems input->ruby-ui--combobox#filterItems keydown.backspace->ruby-ui--combobox#handleBadgeInputBackspace"
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
render ComboboxClearButton.new if @clear_button
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# JS-toggled classes (referenced here so Tailwind compiles them): h-auto min-h-9 pt-1.5
|
|
33
|
+
def default_attrs
|
|
34
|
+
{
|
|
35
|
+
class: "flex h-9 w-full flex-wrap items-center gap-1 rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 cursor-text",
|
|
36
|
+
data: {
|
|
37
|
+
ruby_ui__combobox_target: "trigger",
|
|
38
|
+
action: "click->ruby-ui--combobox#openPopover focusin->ruby-ui--combobox#openPopover"
|
|
39
|
+
},
|
|
40
|
+
aria: {
|
|
41
|
+
haspopup: "listbox",
|
|
42
|
+
expanded: "false"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -10,13 +10,7 @@ module RubyUI
|
|
|
10
10
|
|
|
11
11
|
def default_attrs
|
|
12
12
|
{
|
|
13
|
-
class:
|
|
14
|
-
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
|
|
15
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
-
"checked:bg-primary checked:text-primary-foreground",
|
|
17
|
-
"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none",
|
|
18
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
19
|
-
],
|
|
13
|
+
class: "peer sr-only",
|
|
20
14
|
data: {
|
|
21
15
|
ruby_ui__combobox_target: "input",
|
|
22
16
|
action: "ruby-ui--combobox#inputChanged"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ComboboxClearButton < Base
|
|
5
|
+
def view_template
|
|
6
|
+
button(**attrs) do
|
|
7
|
+
svg(
|
|
8
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
9
|
+
width: "24",
|
|
10
|
+
height: "24",
|
|
11
|
+
viewbox: "0 0 24 24",
|
|
12
|
+
fill: "none",
|
|
13
|
+
stroke: "currentColor",
|
|
14
|
+
stroke_width: "2",
|
|
15
|
+
stroke_linecap: "round",
|
|
16
|
+
stroke_linejoin: "round",
|
|
17
|
+
class: "size-3.5"
|
|
18
|
+
) do |s|
|
|
19
|
+
s.path(d: "M18 6 6 18")
|
|
20
|
+
s.path(d: "m6 6 12 12")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def default_attrs
|
|
28
|
+
{
|
|
29
|
+
type: "button",
|
|
30
|
+
class: "ml-auto shrink-0 rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hidden",
|
|
31
|
+
aria: {label: "Clear selection"},
|
|
32
|
+
data: {
|
|
33
|
+
ruby_ui__combobox_target: "clearButton",
|
|
34
|
+
# JS implementation in combobox_controller.js
|
|
35
|
+
action: "ruby-ui--combobox#clearAll"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -15,90 +15,276 @@ export default class extends Controller {
|
|
|
15
15
|
"emptyState",
|
|
16
16
|
"searchInput",
|
|
17
17
|
"trigger",
|
|
18
|
-
"triggerContent"
|
|
18
|
+
"triggerContent",
|
|
19
|
+
"badgeContainer",
|
|
20
|
+
"clearButton",
|
|
21
|
+
"badgeInput",
|
|
22
|
+
"inputTrigger"
|
|
19
23
|
]
|
|
20
24
|
|
|
21
25
|
selectedItemIndex = null
|
|
22
26
|
|
|
23
27
|
connect() {
|
|
24
28
|
this.updateTriggerContent()
|
|
29
|
+
this.updateBadges()
|
|
30
|
+
this.updateClearButton()
|
|
31
|
+
this.updateInputTrigger()
|
|
32
|
+
|
|
33
|
+
// Track mouse state to distinguish click-focus from tab-focus
|
|
34
|
+
this._mouseDown = false
|
|
35
|
+
this.element.addEventListener("mousedown", () => { this._mouseDown = true })
|
|
36
|
+
this.element.addEventListener("mouseup", () => { setTimeout(() => { this._mouseDown = false }, 0) })
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
disconnect() {
|
|
28
40
|
if (this.cleanup) { this.cleanup() }
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
// Popover
|
|
44
|
+
|
|
45
|
+
togglePopover(event) {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
|
|
48
|
+
if (this.triggerTarget.ariaExpanded === "true") {
|
|
49
|
+
this.closePopover()
|
|
50
|
+
} else {
|
|
51
|
+
this.openPopover(event)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
openPopover(event) {
|
|
56
|
+
if (event && event.type !== "focusin" && event.type !== "focus") event.preventDefault()
|
|
57
|
+
|
|
58
|
+
// focusin/focus: only open on keyboard focus (tab), not mouse click
|
|
59
|
+
if (event && (event.type === "focusin" || event.type === "focus")) {
|
|
60
|
+
if (this._mouseDown || this.triggerTarget.ariaExpanded === "true" || this._closingPopover) return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.updatePopoverPosition()
|
|
64
|
+
this.updatePopoverWidth()
|
|
65
|
+
this.triggerTarget.ariaExpanded = "true"
|
|
66
|
+
this.selectedItemIndex = null
|
|
67
|
+
this.itemTargets.forEach(item => item.ariaCurrent = "false")
|
|
68
|
+
this.popoverTarget.showPopover()
|
|
69
|
+
|
|
70
|
+
// Always show all items on open; filter only on user typing
|
|
71
|
+
this.applyFilter("")
|
|
72
|
+
|
|
73
|
+
if (this.hasBadgeInputTarget) {
|
|
74
|
+
this.badgeInputTarget.value = ""
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
closePopover() {
|
|
79
|
+
this._closingPopover = true
|
|
80
|
+
this.triggerTarget.ariaExpanded = "false"
|
|
81
|
+
this.popoverTarget.hidePopover()
|
|
82
|
+
setTimeout(() => this._closingPopover = false, 200)
|
|
83
|
+
}
|
|
84
|
+
|
|
31
85
|
handlePopoverToggle(event) {
|
|
32
86
|
// Keep ariaExpanded in sync with the actual popover state
|
|
33
87
|
this.triggerTarget.ariaExpanded = event.newState === 'open' ? 'true' : 'false'
|
|
34
88
|
}
|
|
35
89
|
|
|
90
|
+
updatePopoverPosition() {
|
|
91
|
+
this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {
|
|
92
|
+
computePosition(this.triggerTarget, this.popoverTarget, {
|
|
93
|
+
placement: 'bottom-start',
|
|
94
|
+
middleware: [offset(4), flip()],
|
|
95
|
+
}).then(({ x, y }) => {
|
|
96
|
+
Object.assign(this.popoverTarget.style, {
|
|
97
|
+
left: `${x}px`,
|
|
98
|
+
top: `${y}px`,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
updatePopoverWidth() {
|
|
105
|
+
this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Selection
|
|
109
|
+
|
|
36
110
|
inputChanged(e) {
|
|
37
111
|
this.updateTriggerContent()
|
|
38
112
|
|
|
39
113
|
if (e.target.type == "radio") {
|
|
40
114
|
this.closePopover()
|
|
115
|
+
this.updateInputTrigger()
|
|
41
116
|
}
|
|
42
117
|
|
|
43
118
|
if (this.hasToggleAllTarget && !e.target.checked) {
|
|
44
119
|
this.toggleAllTarget.checked = false
|
|
45
120
|
}
|
|
46
|
-
}
|
|
47
121
|
|
|
48
|
-
|
|
49
|
-
|
|
122
|
+
this.updateBadges()
|
|
123
|
+
this.updateClearButton()
|
|
50
124
|
}
|
|
51
125
|
|
|
52
126
|
toggleAllItems() {
|
|
53
127
|
const isChecked = this.toggleAllTarget.checked
|
|
54
128
|
this.inputTargets.forEach(input => input.checked = isChecked)
|
|
55
129
|
this.updateTriggerContent()
|
|
130
|
+
this.updateBadges()
|
|
131
|
+
this.updateClearButton()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clearAll(event) {
|
|
135
|
+
if (event) event.preventDefault()
|
|
136
|
+
|
|
137
|
+
this.inputTargets.forEach(input => input.checked = false)
|
|
138
|
+
this.updateBadges()
|
|
139
|
+
this.updateClearButton()
|
|
140
|
+
this.updateTriggerContent()
|
|
141
|
+
this.updateInputTrigger()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
removeBadge(event) {
|
|
145
|
+
event.preventDefault()
|
|
146
|
+
event.stopPropagation()
|
|
147
|
+
|
|
148
|
+
const value = event.currentTarget.closest('[data-value]').dataset.value
|
|
149
|
+
const input = this.inputTargets.find(input => input.value === value)
|
|
150
|
+
|
|
151
|
+
if (input) {
|
|
152
|
+
input.checked = false
|
|
153
|
+
input.dispatchEvent(new Event("change", { bubbles: true }))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Display
|
|
158
|
+
|
|
159
|
+
inputContent(input) {
|
|
160
|
+
return input.dataset.text || input.parentElement.textContent
|
|
56
161
|
}
|
|
57
162
|
|
|
58
163
|
updateTriggerContent() {
|
|
164
|
+
if (!this.hasTriggerContentTarget) return
|
|
165
|
+
|
|
59
166
|
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
60
167
|
|
|
61
168
|
if (checkedInputs.length === 0) {
|
|
62
169
|
this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
|
|
170
|
+
this.triggerContentTarget.classList.add("text-muted-foreground")
|
|
63
171
|
} else if (this.termValue && checkedInputs.length > 1) {
|
|
64
172
|
this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
|
|
173
|
+
this.triggerContentTarget.classList.remove("text-muted-foreground")
|
|
65
174
|
} else {
|
|
66
175
|
this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(", ")
|
|
176
|
+
this.triggerContentTarget.classList.remove("text-muted-foreground")
|
|
67
177
|
}
|
|
68
178
|
}
|
|
69
179
|
|
|
70
|
-
|
|
71
|
-
|
|
180
|
+
updateInputTrigger() {
|
|
181
|
+
if (!this.hasInputTriggerTarget) return
|
|
182
|
+
const checked = this.inputTargets.find(i => i.checked)
|
|
183
|
+
this.inputTriggerTarget.value = checked ? this.inputContent(checked) : ""
|
|
184
|
+
}
|
|
72
185
|
|
|
73
|
-
|
|
74
|
-
|
|
186
|
+
// NOTE: badge classes mirror ComboboxBadge Ruby component. Update both if styles change.
|
|
187
|
+
updateBadges() {
|
|
188
|
+
if (!this.hasBadgeContainerTarget) return
|
|
189
|
+
|
|
190
|
+
// Remove existing badges
|
|
191
|
+
this.triggerTarget.querySelectorAll("[data-combobox-badge]").forEach(el => el.remove())
|
|
192
|
+
|
|
193
|
+
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
194
|
+
|
|
195
|
+
// Toggle trigger height: h-9 when empty, h-auto min-h-9 when badges exist
|
|
196
|
+
if (checkedInputs.length > 0) {
|
|
197
|
+
this.triggerTarget.classList.remove("h-9")
|
|
198
|
+
this.triggerTarget.classList.add("h-auto", "min-h-9")
|
|
75
199
|
} else {
|
|
76
|
-
this.
|
|
200
|
+
this.triggerTarget.classList.remove("h-auto", "min-h-9", "pt-1.5")
|
|
201
|
+
this.triggerTarget.classList.add("h-9")
|
|
77
202
|
}
|
|
78
|
-
}
|
|
79
203
|
|
|
80
|
-
|
|
81
|
-
|
|
204
|
+
checkedInputs.forEach(input => {
|
|
205
|
+
const badge = document.createElement("span")
|
|
206
|
+
badge.setAttribute("data-combobox-badge", "")
|
|
207
|
+
badge.className = "inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-xs font-medium text-secondary-foreground"
|
|
208
|
+
badge.dataset.value = input.value
|
|
209
|
+
|
|
210
|
+
badge.appendChild(document.createTextNode(this.inputContent(input).trim()))
|
|
211
|
+
|
|
212
|
+
const btn = document.createElement("button")
|
|
213
|
+
btn.type = "button"
|
|
214
|
+
btn.setAttribute("aria-label", "Remove")
|
|
215
|
+
btn.className = "rounded-sm opacity-50 hover:opacity-100 focus-visible:outline-none"
|
|
216
|
+
|
|
217
|
+
btn.addEventListener("click", (e) => {
|
|
218
|
+
e.preventDefault()
|
|
219
|
+
e.stopPropagation()
|
|
220
|
+
e.stopImmediatePropagation()
|
|
221
|
+
const target = this.inputTargets.find(i => i.value === input.value)
|
|
222
|
+
if (target) {
|
|
223
|
+
target.checked = false
|
|
224
|
+
this.updateBadges()
|
|
225
|
+
this.updateClearButton()
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
230
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
|
|
231
|
+
svg.setAttribute("width", "12")
|
|
232
|
+
svg.setAttribute("height", "12")
|
|
233
|
+
svg.setAttribute("viewBox", "0 0 24 24")
|
|
234
|
+
svg.setAttribute("fill", "none")
|
|
235
|
+
svg.setAttribute("stroke", "currentColor")
|
|
236
|
+
svg.setAttribute("stroke-width", "2")
|
|
237
|
+
svg.setAttribute("stroke-linecap", "round")
|
|
238
|
+
svg.setAttribute("stroke-linejoin", "round")
|
|
239
|
+
svg.classList.add("pointer-events-none")
|
|
240
|
+
|
|
241
|
+
const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path")
|
|
242
|
+
path1.setAttribute("d", "M18 6 6 18")
|
|
243
|
+
const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path")
|
|
244
|
+
path2.setAttribute("d", "m6 6 12 12")
|
|
245
|
+
|
|
246
|
+
svg.appendChild(path1)
|
|
247
|
+
svg.appendChild(path2)
|
|
248
|
+
btn.appendChild(svg)
|
|
249
|
+
badge.appendChild(btn)
|
|
250
|
+
|
|
251
|
+
// Insert badge directly in trigger, before the text input
|
|
252
|
+
this.badgeInputTarget.insertAdjacentElement("beforebegin", badge)
|
|
253
|
+
})
|
|
82
254
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.triggerTarget.
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
255
|
+
// Add top padding only when badges wrap to multiple lines
|
|
256
|
+
// Class "pt-1.5" is referenced in ComboboxBadgeTrigger for Tailwind to compile it
|
|
257
|
+
const badges = this.triggerTarget.querySelectorAll("[data-combobox-badge]")
|
|
258
|
+
if (badges.length > 0 && this.badgeInputTarget.offsetTop > badges[0].offsetTop) {
|
|
259
|
+
this.triggerTarget.classList.add("pt-1.5")
|
|
260
|
+
} else {
|
|
261
|
+
this.triggerTarget.classList.remove("pt-1.5")
|
|
262
|
+
}
|
|
89
263
|
}
|
|
90
264
|
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
265
|
+
updateClearButton() {
|
|
266
|
+
if (!this.hasClearButtonTarget) return
|
|
267
|
+
|
|
268
|
+
const hasChecked = this.inputTargets.some(input => input.checked)
|
|
269
|
+
this.clearButtonTarget.classList.toggle("hidden", !hasChecked)
|
|
94
270
|
}
|
|
95
271
|
|
|
272
|
+
// Filter
|
|
273
|
+
|
|
96
274
|
filterItems(e) {
|
|
97
|
-
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key))
|
|
98
|
-
|
|
99
|
-
|
|
275
|
+
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) return
|
|
276
|
+
|
|
277
|
+
const term = this.hasBadgeInputTarget
|
|
278
|
+
? this.badgeInputTarget.value
|
|
279
|
+
: this.hasInputTriggerTarget
|
|
280
|
+
? this.inputTriggerTarget.value
|
|
281
|
+
: this.searchInputTarget.value
|
|
282
|
+
|
|
283
|
+
this.applyFilter(term)
|
|
284
|
+
}
|
|
100
285
|
|
|
101
|
-
|
|
286
|
+
applyFilter(term) {
|
|
287
|
+
const filterTerm = term.toLowerCase()
|
|
102
288
|
|
|
103
289
|
if (this.hasToggleAllTarget) {
|
|
104
290
|
if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden")
|
|
@@ -106,12 +292,10 @@ export default class extends Controller {
|
|
|
106
292
|
}
|
|
107
293
|
|
|
108
294
|
let resultCount = 0
|
|
109
|
-
|
|
110
295
|
this.selectedItemIndex = null
|
|
111
296
|
|
|
112
297
|
this.inputTargets.forEach((input) => {
|
|
113
298
|
const text = this.inputContent(input).toLowerCase()
|
|
114
|
-
|
|
115
299
|
if (text.indexOf(filterTerm) > -1) {
|
|
116
300
|
input.parentElement.classList.remove("hidden")
|
|
117
301
|
resultCount++
|
|
@@ -121,9 +305,20 @@ export default class extends Controller {
|
|
|
121
305
|
})
|
|
122
306
|
|
|
123
307
|
this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0)
|
|
308
|
+
|
|
309
|
+
// Auto-highlight first visible result (without scrolling to avoid page jump)
|
|
310
|
+
this.itemTargets.forEach(item => item.ariaCurrent = "false")
|
|
311
|
+
const firstVisible = this.inputTargets.find(i => !i.parentElement.classList.contains("hidden"))
|
|
312
|
+
if (firstVisible) {
|
|
313
|
+
this.selectedItemIndex = 0
|
|
314
|
+
firstVisible.parentElement.ariaCurrent = "true"
|
|
315
|
+
}
|
|
124
316
|
}
|
|
125
317
|
|
|
126
|
-
|
|
318
|
+
// Keyboard
|
|
319
|
+
|
|
320
|
+
keyDownPressed(event) {
|
|
321
|
+
event.preventDefault()
|
|
127
322
|
if (this.selectedItemIndex !== null) {
|
|
128
323
|
this.selectedItemIndex++
|
|
129
324
|
} else {
|
|
@@ -133,7 +328,8 @@ export default class extends Controller {
|
|
|
133
328
|
this.focusSelectedInput()
|
|
134
329
|
}
|
|
135
330
|
|
|
136
|
-
keyUpPressed() {
|
|
331
|
+
keyUpPressed(event) {
|
|
332
|
+
event.preventDefault()
|
|
137
333
|
if (this.selectedItemIndex !== null) {
|
|
138
334
|
this.selectedItemIndex--
|
|
139
335
|
} else {
|
|
@@ -143,6 +339,15 @@ export default class extends Controller {
|
|
|
143
339
|
this.focusSelectedInput()
|
|
144
340
|
}
|
|
145
341
|
|
|
342
|
+
keyEnterPressed(event) {
|
|
343
|
+
event.preventDefault()
|
|
344
|
+
const option = this.itemTargets.find(item => item.ariaCurrent === "true")
|
|
345
|
+
|
|
346
|
+
if (option) {
|
|
347
|
+
option.click()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
146
351
|
focusSelectedInput() {
|
|
147
352
|
const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden"))
|
|
148
353
|
|
|
@@ -158,34 +363,19 @@ export default class extends Controller {
|
|
|
158
363
|
})
|
|
159
364
|
}
|
|
160
365
|
|
|
161
|
-
keyEnterPressed(event) {
|
|
162
|
-
event.preventDefault()
|
|
163
|
-
const option = this.itemTargets.find(item => item.ariaCurrent === "true")
|
|
164
|
-
|
|
165
|
-
if (option) {
|
|
166
|
-
option.click()
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
366
|
wrapSelectedInputIndex(length) {
|
|
171
367
|
this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length
|
|
172
368
|
}
|
|
173
369
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
computePosition(this.triggerTarget, this.popoverTarget, {
|
|
177
|
-
placement: 'bottom-start',
|
|
178
|
-
middleware: [offset(4), flip()],
|
|
179
|
-
}).then(({ x, y }) => {
|
|
180
|
-
Object.assign(this.popoverTarget.style, {
|
|
181
|
-
left: `${x}px`,
|
|
182
|
-
top: `${y}px`,
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
}
|
|
370
|
+
handleBadgeInputBackspace(event) {
|
|
371
|
+
if (this.badgeInputTarget.value !== "") return
|
|
187
372
|
|
|
188
|
-
|
|
189
|
-
|
|
373
|
+
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
374
|
+
const lastChecked = checkedInputs[checkedInputs.length - 1]
|
|
375
|
+
|
|
376
|
+
if (lastChecked) {
|
|
377
|
+
lastChecked.checked = false
|
|
378
|
+
lastChecked.dispatchEvent(new Event("change", { bubbles: true }))
|
|
379
|
+
}
|
|
190
380
|
}
|
|
191
381
|
}
|