shadcn_phlexcomponents 0.1.11 → 0.1.16
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/app/javascript/controllers/accordion_controller.js +107 -0
- data/app/javascript/controllers/alert_dialog_controller.js +7 -0
- data/app/javascript/controllers/avatar_controller.js +14 -0
- data/app/javascript/controllers/checkbox_controller.js +29 -0
- data/app/javascript/controllers/collapsible_controller.js +39 -0
- data/app/javascript/controllers/combobox_controller.js +278 -0
- data/app/javascript/controllers/command_controller.js +207 -0
- data/app/javascript/controllers/date_picker_controller.js +258 -0
- data/app/javascript/controllers/date_range_picker_controller.js +200 -0
- data/app/javascript/controllers/dialog_controller.js +83 -0
- data/app/javascript/controllers/dropdown_menu_controller.js +238 -0
- data/app/javascript/controllers/dropdown_menu_sub_controller.js +118 -0
- data/app/javascript/controllers/form_field_controller.js +20 -0
- data/app/javascript/controllers/hover_card_controller.js +73 -0
- data/app/javascript/controllers/loading_button_controller.js +14 -0
- data/app/javascript/controllers/popover_controller.js +90 -0
- data/app/javascript/controllers/progress_controller.js +14 -0
- data/app/javascript/controllers/radio_group_controller.js +80 -0
- data/app/javascript/controllers/select_controller.js +265 -0
- data/app/javascript/controllers/sidebar_controller.js +29 -0
- data/app/javascript/controllers/sidebar_trigger_controller.js +15 -0
- data/app/javascript/controllers/slider_controller.js +82 -0
- data/app/javascript/controllers/switch_controller.js +26 -0
- data/app/javascript/controllers/tabs_controller.js +66 -0
- data/app/javascript/controllers/theme_switcher_controller.js +32 -0
- data/app/javascript/controllers/toast_container_controller.js +48 -0
- data/app/javascript/controllers/toast_controller.js +22 -0
- data/app/javascript/controllers/toggle_controller.js +20 -0
- data/app/javascript/controllers/toggle_group_controller.js +20 -0
- data/app/javascript/controllers/tooltip_controller.js +79 -0
- data/app/javascript/shadcn_phlexcomponents.js +60 -0
- data/app/javascript/utils/command.js +448 -0
- data/app/javascript/utils/floating_ui.js +160 -0
- data/app/javascript/utils/index.js +288 -0
- data/app/stylesheets/date_picker.css +118 -0
- data/app/typescript/controllers/accordion_controller.ts +136 -0
- data/app/typescript/controllers/alert_dialog_controller.ts +12 -0
- data/app/{javascript → typescript}/controllers/avatar_controller.ts +7 -2
- data/app/{javascript → typescript}/controllers/checkbox_controller.ts +11 -4
- data/app/{javascript → typescript}/controllers/collapsible_controller.ts +12 -5
- data/app/typescript/controllers/combobox_controller.ts +376 -0
- data/app/typescript/controllers/command_controller.ts +301 -0
- data/app/{javascript → typescript}/controllers/date_picker_controller.ts +185 -125
- data/app/{javascript → typescript}/controllers/date_range_picker_controller.ts +89 -79
- data/app/{javascript → typescript}/controllers/dialog_controller.ts +59 -57
- data/app/typescript/controllers/dropdown_menu_controller.ts +309 -0
- data/app/{javascript → typescript}/controllers/dropdown_menu_sub_controller.ts +31 -29
- data/app/{javascript → typescript}/controllers/form_field_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/hover_card_controller.ts +36 -26
- data/app/{javascript → typescript}/controllers/loading_button_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/popover_controller.ts +42 -65
- data/app/{javascript → typescript}/controllers/progress_controller.ts +9 -3
- data/app/{javascript → typescript}/controllers/radio_group_controller.ts +16 -9
- data/app/typescript/controllers/select_controller.ts +341 -0
- data/app/{javascript → typescript}/controllers/slider_controller.ts +23 -16
- data/app/{javascript → typescript}/controllers/switch_controller.ts +11 -4
- data/app/{javascript → typescript}/controllers/tabs_controller.ts +26 -18
- data/app/{javascript → typescript}/controllers/theme_switcher_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/toast_container_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/toast_controller.ts +7 -1
- data/app/typescript/controllers/toggle_controller.ts +28 -0
- data/app/typescript/controllers/toggle_group_controller.ts +28 -0
- data/app/{javascript → typescript}/controllers/tooltip_controller.ts +43 -31
- data/app/typescript/shadcn_phlexcomponents.ts +61 -0
- data/app/typescript/utils/command.ts +544 -0
- data/app/typescript/utils/floating_ui.ts +196 -0
- data/app/typescript/utils/index.ts +424 -0
- data/lib/install/install_shadcn_phlexcomponents.rb +10 -3
- data/lib/shadcn_phlexcomponents/alias.rb +3 -0
- data/lib/shadcn_phlexcomponents/components/accordion.rb +2 -1
- data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +18 -15
- data/lib/shadcn_phlexcomponents/components/base.rb +14 -0
- data/lib/shadcn_phlexcomponents/components/collapsible.rb +1 -2
- data/lib/shadcn_phlexcomponents/components/combobox.rb +87 -57
- data/lib/shadcn_phlexcomponents/components/command.rb +77 -47
- data/lib/shadcn_phlexcomponents/components/date_picker.rb +25 -81
- data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +21 -4
- data/lib/shadcn_phlexcomponents/components/dialog.rb +14 -12
- data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +5 -4
- data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +2 -1
- data/lib/shadcn_phlexcomponents/components/form/form_combobox.rb +64 -0
- data/lib/shadcn_phlexcomponents/components/form.rb +14 -0
- data/lib/shadcn_phlexcomponents/components/hover_card.rb +3 -2
- data/lib/shadcn_phlexcomponents/components/popover.rb +3 -3
- data/lib/shadcn_phlexcomponents/components/select.rb +10 -25
- data/lib/shadcn_phlexcomponents/components/sheet.rb +15 -11
- data/lib/shadcn_phlexcomponents/components/table.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/tabs.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/toast_container.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/toggle.rb +54 -0
- data/lib/shadcn_phlexcomponents/components/tooltip.rb +3 -2
- data/lib/shadcn_phlexcomponents/engine.rb +1 -5
- data/lib/shadcn_phlexcomponents/version.rb +1 -1
- metadata +71 -32
- data/app/javascript/controllers/accordion_controller.ts +0 -133
- data/app/javascript/controllers/combobox_controller.ts +0 -145
- data/app/javascript/controllers/command_controller.ts +0 -129
- data/app/javascript/controllers/command_root_controller.ts +0 -355
- data/app/javascript/controllers/dropdown_menu_controller.ts +0 -133
- data/app/javascript/controllers/dropdown_menu_root_controller.ts +0 -234
- data/app/javascript/controllers/select_controller.ts +0 -200
- data/app/javascript/shadcn_phlexcomponents.ts +0 -57
- data/app/javascript/utils.ts +0 -437
- /data/app/{javascript → typescript}/controllers/sidebar_controller.ts +0 -0
- /data/app/{javascript → typescript}/controllers/sidebar_trigger_controller.ts +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
2
2
|
|
3
|
-
|
3
|
+
const LoadingButtonController = class extends Controller<HTMLButtonElement> {
|
4
4
|
connect() {
|
5
5
|
const el = this.element
|
6
6
|
const form = el.closest('form')
|
@@ -13,3 +13,8 @@ export default class extends Controller<HTMLButtonElement> {
|
|
13
13
|
}
|
14
14
|
}
|
15
15
|
}
|
16
|
+
|
17
|
+
type LoadingButton = InstanceType<typeof LoadingButtonController>
|
18
|
+
|
19
|
+
export { LoadingButtonController }
|
20
|
+
export type { LoadingButton }
|
@@ -1,22 +1,28 @@
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
2
2
|
import { useClickOutside } from 'stimulus-use'
|
3
|
+
import { initFloatingUi } from '../utils/floating_ui'
|
3
4
|
import {
|
4
|
-
initFloatingUi,
|
5
5
|
focusTrigger,
|
6
|
-
ON_OPEN_FOCUS_DELAY,
|
7
6
|
getFocusableElements,
|
8
7
|
showContent,
|
9
8
|
hideContent,
|
9
|
+
onClickOutside,
|
10
|
+
handleTabNavigation,
|
11
|
+
focusElement,
|
10
12
|
} from '../utils'
|
11
13
|
|
12
|
-
|
14
|
+
const PopoverController = class extends Controller<HTMLElement> {
|
15
|
+
// targets
|
13
16
|
static targets = ['trigger', 'contentContainer', 'content']
|
14
|
-
static values = { isOpen: Boolean }
|
15
|
-
|
16
|
-
declare isOpenValue: boolean
|
17
17
|
declare readonly triggerTarget: HTMLElement
|
18
18
|
declare readonly contentContainerTarget: HTMLElement
|
19
19
|
declare readonly contentTarget: HTMLElement
|
20
|
+
|
21
|
+
// values
|
22
|
+
static values = { isOpen: Boolean }
|
23
|
+
declare isOpenValue: boolean
|
24
|
+
|
25
|
+
// custom properties
|
20
26
|
declare DOMKeydownListener: (event: KeyboardEvent) => void
|
21
27
|
declare cleanup: () => void
|
22
28
|
|
@@ -35,67 +41,18 @@ export default class extends Controller<HTMLElement> {
|
|
35
41
|
|
36
42
|
open() {
|
37
43
|
this.isOpenValue = true
|
38
|
-
this.onOpen()
|
39
|
-
|
40
|
-
setTimeout(() => {
|
41
|
-
this.onOpenFocusedElement().focus()
|
42
|
-
}, ON_OPEN_FOCUS_DELAY)
|
43
44
|
}
|
44
45
|
|
45
46
|
close() {
|
46
47
|
this.isOpenValue = false
|
47
|
-
this.onClose()
|
48
|
-
}
|
49
|
-
|
50
|
-
onDOMKeydown(event: KeyboardEvent) {
|
51
|
-
if (!this.isOpenValue) return
|
52
|
-
|
53
|
-
const key = event.key
|
54
|
-
|
55
|
-
if (key === 'Escape') {
|
56
|
-
this.close()
|
57
|
-
} else if (key === 'Tab') {
|
58
|
-
const focusableElements = getFocusableElements(this.contentTarget)
|
59
|
-
|
60
|
-
const firstElement = focusableElements[0]
|
61
|
-
const lastElement = focusableElements[focusableElements.length - 1]
|
62
|
-
|
63
|
-
// If Shift + Tab pressed on first element, go to last element
|
64
|
-
if (event.shiftKey && document.activeElement === firstElement) {
|
65
|
-
event.preventDefault()
|
66
|
-
lastElement.focus()
|
67
|
-
}
|
68
|
-
// If Tab pressed on last element, go to first element
|
69
|
-
else if (!event.shiftKey && document.activeElement === lastElement) {
|
70
|
-
event.preventDefault()
|
71
|
-
firstElement.focus()
|
72
|
-
}
|
73
|
-
}
|
74
48
|
}
|
75
49
|
|
76
50
|
clickOutside(event: MouseEvent) {
|
77
|
-
|
78
|
-
// Let #toggle to handle state when clicked on trigger
|
79
|
-
if (target === this.triggerTarget) return
|
80
|
-
|
81
|
-
this.close()
|
82
|
-
}
|
83
|
-
|
84
|
-
onOpen() {}
|
85
|
-
|
86
|
-
onOpenFocusedElement() {
|
87
|
-
const focusableElements = getFocusableElements(this.contentTarget)
|
88
|
-
return focusableElements[0]
|
89
|
-
}
|
90
|
-
|
91
|
-
onClose() {}
|
92
|
-
|
93
|
-
referenceElement() {
|
94
|
-
return this.triggerTarget
|
51
|
+
onClickOutside(this, event)
|
95
52
|
}
|
96
53
|
|
97
54
|
isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
|
98
|
-
if (isOpen
|
55
|
+
if (isOpen) {
|
99
56
|
showContent({
|
100
57
|
trigger: this.triggerTarget,
|
101
58
|
content: this.contentTarget,
|
@@ -103,12 +60,16 @@ export default class extends Controller<HTMLElement> {
|
|
103
60
|
})
|
104
61
|
|
105
62
|
this.cleanup = initFloatingUi({
|
106
|
-
referenceElement: this.
|
63
|
+
referenceElement: this.triggerTarget,
|
107
64
|
floatingElement: this.contentContainerTarget,
|
108
65
|
side: this.contentTarget.dataset.side,
|
109
66
|
align: this.contentTarget.dataset.align,
|
110
67
|
sideOffset: 4,
|
111
68
|
})
|
69
|
+
|
70
|
+
const focusableElements = getFocusableElements(this.contentTarget)
|
71
|
+
focusElement(focusableElements[0])
|
72
|
+
|
112
73
|
this.setupEventListeners()
|
113
74
|
} else {
|
114
75
|
hideContent({
|
@@ -117,25 +78,41 @@ export default class extends Controller<HTMLElement> {
|
|
117
78
|
contentContainer: this.contentContainerTarget,
|
118
79
|
})
|
119
80
|
|
120
|
-
this.cleanupEventListeners()
|
121
|
-
|
122
|
-
// Only focus trigger when is previously opened
|
123
81
|
if (previousIsOpen) {
|
124
82
|
focusTrigger(this.triggerTarget)
|
125
83
|
}
|
84
|
+
|
85
|
+
this.cleanupEventListeners()
|
126
86
|
}
|
127
87
|
}
|
128
88
|
|
129
|
-
|
89
|
+
disconnect() {
|
90
|
+
this.cleanupEventListeners()
|
91
|
+
}
|
92
|
+
|
93
|
+
protected setupEventListeners() {
|
130
94
|
document.addEventListener('keydown', this.DOMKeydownListener)
|
131
95
|
}
|
132
96
|
|
133
|
-
cleanupEventListeners() {
|
97
|
+
protected cleanupEventListeners() {
|
134
98
|
if (this.cleanup) this.cleanup()
|
135
99
|
document.removeEventListener('keydown', this.DOMKeydownListener)
|
136
100
|
}
|
137
101
|
|
138
|
-
|
139
|
-
this.
|
102
|
+
protected onDOMKeydown(event: KeyboardEvent) {
|
103
|
+
if (!this.isOpenValue) return
|
104
|
+
|
105
|
+
const key = event.key
|
106
|
+
|
107
|
+
if (key === 'Escape') {
|
108
|
+
this.close()
|
109
|
+
} else if (key === 'Tab') {
|
110
|
+
handleTabNavigation(this.contentTarget, event)
|
111
|
+
}
|
140
112
|
}
|
141
113
|
}
|
114
|
+
|
115
|
+
type Popover = InstanceType<typeof PopoverController>
|
116
|
+
|
117
|
+
export { PopoverController }
|
118
|
+
export type { Popover }
|
@@ -1,13 +1,14 @@
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
2
2
|
|
3
|
-
|
3
|
+
const ProgressController = class extends Controller {
|
4
|
+
// targets
|
4
5
|
static targets = ['indicator']
|
6
|
+
declare readonly indicatorTarget: HTMLElement
|
5
7
|
|
8
|
+
// values
|
6
9
|
static values = {
|
7
10
|
percent: Number,
|
8
11
|
}
|
9
|
-
|
10
|
-
declare readonly indicatorTarget: HTMLElement
|
11
12
|
declare percentValue: number
|
12
13
|
|
13
14
|
percentValueChanged(value: number) {
|
@@ -15,3 +16,8 @@ export default class extends Controller {
|
|
15
16
|
this.indicatorTarget.style.transform = `translateX(-${100 - value}%)`
|
16
17
|
}
|
17
18
|
}
|
19
|
+
|
20
|
+
type Progress = InstanceType<typeof ProgressController>
|
21
|
+
|
22
|
+
export { ProgressController }
|
23
|
+
export type { Progress }
|
@@ -1,14 +1,16 @@
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
2
2
|
|
3
|
-
|
3
|
+
const RadioGroupController = class extends Controller<HTMLElement> {
|
4
|
+
// targets
|
4
5
|
static targets = ['item', 'input', 'indicator']
|
5
|
-
static values = {
|
6
|
-
selected: String,
|
7
|
-
}
|
8
|
-
|
9
6
|
declare readonly itemTargets: HTMLInputElement[]
|
10
7
|
declare readonly inputTargets: HTMLInputElement[]
|
11
8
|
declare readonly indicatorTargets: HTMLInputElement[]
|
9
|
+
|
10
|
+
// values
|
11
|
+
static values = {
|
12
|
+
selected: String,
|
13
|
+
}
|
12
14
|
declare selectedValue: string
|
13
15
|
|
14
16
|
connect() {
|
@@ -22,10 +24,6 @@ export default class extends Controller<HTMLElement> {
|
|
22
24
|
this.selectedValue = item.dataset.value as string
|
23
25
|
}
|
24
26
|
|
25
|
-
preventDefault(event: KeyboardEvent) {
|
26
|
-
event.preventDefault()
|
27
|
-
}
|
28
|
-
|
29
27
|
selectItem(event: KeyboardEvent) {
|
30
28
|
const focusableItems = this.itemTargets.filter(
|
31
29
|
(t) => !t.disabled,
|
@@ -53,6 +51,10 @@ export default class extends Controller<HTMLElement> {
|
|
53
51
|
this.selectedValue = focusableItems[newIndex].dataset.value as string
|
54
52
|
}
|
55
53
|
|
54
|
+
preventDefault(event: KeyboardEvent) {
|
55
|
+
event.preventDefault()
|
56
|
+
}
|
57
|
+
|
56
58
|
focusItem() {
|
57
59
|
const item = this.itemTargets.find(
|
58
60
|
(i) => i.dataset.value === this.selectedValue,
|
@@ -104,3 +106,8 @@ export default class extends Controller<HTMLElement> {
|
|
104
106
|
this.focusItem()
|
105
107
|
}
|
106
108
|
}
|
109
|
+
|
110
|
+
type RadioGroup = InstanceType<typeof RadioGroupController>
|
111
|
+
|
112
|
+
export { RadioGroupController }
|
113
|
+
export type { RadioGroup }
|
@@ -0,0 +1,341 @@
|
|
1
|
+
import { useClickOutside } from 'stimulus-use'
|
2
|
+
import { onKeydown, focusItemByIndex } from './dropdown_menu_controller'
|
3
|
+
import { initFloatingUi } from '../utils/floating_ui'
|
4
|
+
import {
|
5
|
+
getSameLevelItems,
|
6
|
+
focusTrigger,
|
7
|
+
hideContent,
|
8
|
+
showContent,
|
9
|
+
lockScroll,
|
10
|
+
unlockScroll,
|
11
|
+
onClickOutside,
|
12
|
+
setGroupLabelsId,
|
13
|
+
getNextEnabledIndex,
|
14
|
+
getPreviousEnabledIndex,
|
15
|
+
focusElement,
|
16
|
+
} from '../utils'
|
17
|
+
import { Controller } from '@hotwired/stimulus'
|
18
|
+
|
19
|
+
const SelectController = class extends Controller<HTMLElement> {
|
20
|
+
// targets
|
21
|
+
static targets = [
|
22
|
+
'trigger',
|
23
|
+
'contentContainer',
|
24
|
+
'content',
|
25
|
+
'item',
|
26
|
+
'triggerText',
|
27
|
+
'group',
|
28
|
+
'select',
|
29
|
+
]
|
30
|
+
declare readonly triggerTarget: HTMLElement
|
31
|
+
declare readonly contentContainerTarget: HTMLElement
|
32
|
+
declare readonly contentTarget: HTMLElement
|
33
|
+
declare readonly itemTargets: HTMLElement[]
|
34
|
+
declare triggerTextTarget: HTMLElement
|
35
|
+
declare groupTargets: HTMLElement[]
|
36
|
+
declare selectTarget: HTMLSelectElement
|
37
|
+
|
38
|
+
// values
|
39
|
+
static values = {
|
40
|
+
isOpen: Boolean,
|
41
|
+
selected: String,
|
42
|
+
}
|
43
|
+
declare isOpenValue: boolean
|
44
|
+
declare selectedValue: string
|
45
|
+
|
46
|
+
// custom properties
|
47
|
+
declare searchString: string
|
48
|
+
declare searchTimeout: number
|
49
|
+
declare itemsInnerText: string[]
|
50
|
+
declare items: HTMLElement[]
|
51
|
+
declare DOMKeydownListener: (event: KeyboardEvent) => void
|
52
|
+
declare cleanup: () => void
|
53
|
+
|
54
|
+
connect() {
|
55
|
+
this.items = getSameLevelItems({
|
56
|
+
content: this.contentTarget,
|
57
|
+
items: this.itemTargets,
|
58
|
+
closestContentSelector: '[data-select-target="content"]',
|
59
|
+
})
|
60
|
+
this.itemsInnerText = this.items.map((i) => i.innerText.trim())
|
61
|
+
this.searchString = ''
|
62
|
+
useClickOutside(this, { element: this.contentTarget, dispatchEvent: false })
|
63
|
+
this.DOMKeydownListener = this.onDOMKeydown.bind(this)
|
64
|
+
setGroupLabelsId(this)
|
65
|
+
}
|
66
|
+
|
67
|
+
toggle(event: MouseEvent) {
|
68
|
+
if (this.isOpenValue) {
|
69
|
+
this.close()
|
70
|
+
} else {
|
71
|
+
this.open(event)
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
open(event: MouseEvent | KeyboardEvent) {
|
76
|
+
this.isOpenValue = true
|
77
|
+
|
78
|
+
let elementToFocus = null as HTMLElement | null
|
79
|
+
|
80
|
+
if (this.selectedValue) {
|
81
|
+
const item = this.itemTargets.find(
|
82
|
+
(i) => i.dataset.value === this.selectedValue,
|
83
|
+
)
|
84
|
+
|
85
|
+
if (item && !item.dataset.disabled) {
|
86
|
+
elementToFocus = item
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
if (!elementToFocus) {
|
91
|
+
if (event instanceof KeyboardEvent) {
|
92
|
+
const key = event.key
|
93
|
+
|
94
|
+
if (['ArrowDown', 'Enter', ' '].includes(key)) {
|
95
|
+
elementToFocus = this.items[0]
|
96
|
+
}
|
97
|
+
} else {
|
98
|
+
elementToFocus = this.contentTarget
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
focusElement(elementToFocus)
|
103
|
+
}
|
104
|
+
|
105
|
+
close() {
|
106
|
+
this.isOpenValue = false
|
107
|
+
}
|
108
|
+
|
109
|
+
onItemFocus(event: FocusEvent) {
|
110
|
+
const item = event.currentTarget as HTMLElement
|
111
|
+
item.tabIndex = 0
|
112
|
+
}
|
113
|
+
|
114
|
+
onItemBlur(event: FocusEvent) {
|
115
|
+
const item = event.currentTarget as HTMLElement
|
116
|
+
item.tabIndex = -1
|
117
|
+
}
|
118
|
+
|
119
|
+
focusItemByIndex(
|
120
|
+
event: KeyboardEvent | null = null,
|
121
|
+
index: number | null = null,
|
122
|
+
) {
|
123
|
+
focusItemByIndex(this, event, index)
|
124
|
+
}
|
125
|
+
|
126
|
+
focusItem(event: MouseEvent | KeyboardEvent) {
|
127
|
+
const item = event.currentTarget as HTMLElement
|
128
|
+
const index = this.items.indexOf(item)
|
129
|
+
|
130
|
+
if (event instanceof KeyboardEvent) {
|
131
|
+
const key = event.key
|
132
|
+
let newIndex = 0
|
133
|
+
|
134
|
+
if (key === 'ArrowUp') {
|
135
|
+
newIndex = getPreviousEnabledIndex({
|
136
|
+
items: this.items,
|
137
|
+
currentIndex: index,
|
138
|
+
wrapAround: false,
|
139
|
+
})
|
140
|
+
} else {
|
141
|
+
newIndex = getNextEnabledIndex({
|
142
|
+
items: this.items,
|
143
|
+
currentIndex: index,
|
144
|
+
wrapAround: false,
|
145
|
+
})
|
146
|
+
}
|
147
|
+
|
148
|
+
this.items[newIndex].focus()
|
149
|
+
} else {
|
150
|
+
// item mouseover event
|
151
|
+
this.items[index].focus()
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
focusContent() {
|
156
|
+
this.contentTarget.focus()
|
157
|
+
}
|
158
|
+
|
159
|
+
select(event: MouseEvent | KeyboardEvent) {
|
160
|
+
const item = event.currentTarget as HTMLElement
|
161
|
+
const value = item.dataset.value as string
|
162
|
+
this.selectedValue = value
|
163
|
+
this.close()
|
164
|
+
}
|
165
|
+
|
166
|
+
clickOutside(event: MouseEvent) {
|
167
|
+
onClickOutside(this, event)
|
168
|
+
}
|
169
|
+
|
170
|
+
isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
|
171
|
+
if (isOpen) {
|
172
|
+
lockScroll(this.contentTarget.id)
|
173
|
+
|
174
|
+
showContent({
|
175
|
+
trigger: this.triggerTarget,
|
176
|
+
content: this.contentTarget,
|
177
|
+
contentContainer: this.contentContainerTarget,
|
178
|
+
setEqualWidth: true,
|
179
|
+
})
|
180
|
+
|
181
|
+
this.cleanup = initFloatingUi({
|
182
|
+
referenceElement: this.triggerTarget,
|
183
|
+
floatingElement: this.contentContainerTarget,
|
184
|
+
side: this.contentTarget.dataset.side,
|
185
|
+
align: this.contentTarget.dataset.align,
|
186
|
+
sideOffset: 4,
|
187
|
+
})
|
188
|
+
|
189
|
+
this.setupEventListeners()
|
190
|
+
} else {
|
191
|
+
unlockScroll(this.contentTarget.id)
|
192
|
+
|
193
|
+
hideContent({
|
194
|
+
trigger: this.triggerTarget,
|
195
|
+
content: this.contentTarget,
|
196
|
+
contentContainer: this.contentContainerTarget,
|
197
|
+
})
|
198
|
+
|
199
|
+
if (previousIsOpen) {
|
200
|
+
focusTrigger(this.triggerTarget)
|
201
|
+
}
|
202
|
+
|
203
|
+
this.cleanupEventListeners()
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
selectedValueChanged(value: string) {
|
208
|
+
const item = this.itemTargets.find((i) => i.dataset.value === value)
|
209
|
+
|
210
|
+
if (item) {
|
211
|
+
this.triggerTextTarget.textContent = item.textContent
|
212
|
+
|
213
|
+
this.itemTargets.forEach((i) => {
|
214
|
+
if (i.dataset.value === value) {
|
215
|
+
i.setAttribute('aria-selected', 'true')
|
216
|
+
} else {
|
217
|
+
i.setAttribute('aria-selected', 'false')
|
218
|
+
}
|
219
|
+
})
|
220
|
+
|
221
|
+
this.selectTarget.value = value
|
222
|
+
}
|
223
|
+
|
224
|
+
this.triggerTarget.dataset.hasValue = `${!!value && value.length > 0}`
|
225
|
+
|
226
|
+
const placeholder = this.triggerTarget.dataset.placeholder
|
227
|
+
|
228
|
+
if (placeholder && this.triggerTarget.dataset.hasValue === 'false') {
|
229
|
+
this.triggerTextTarget.textContent = placeholder
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
disconnect() {
|
234
|
+
this.cleanupEventListeners()
|
235
|
+
}
|
236
|
+
|
237
|
+
protected onDOMKeydown(event: KeyboardEvent) {
|
238
|
+
if (!this.isOpenValue) return
|
239
|
+
|
240
|
+
onKeydown(this, event)
|
241
|
+
|
242
|
+
const { key, altKey, ctrlKey, metaKey } = event
|
243
|
+
|
244
|
+
if (
|
245
|
+
key === 'Backspace' ||
|
246
|
+
key === 'Clear' ||
|
247
|
+
(key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey)
|
248
|
+
) {
|
249
|
+
this.handleSearch(key)
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
253
|
+
protected setupEventListeners() {
|
254
|
+
document.addEventListener('keydown', this.DOMKeydownListener)
|
255
|
+
}
|
256
|
+
|
257
|
+
protected cleanupEventListeners() {
|
258
|
+
if (this.cleanup) this.cleanup()
|
259
|
+
document.removeEventListener('keydown', this.DOMKeydownListener)
|
260
|
+
}
|
261
|
+
|
262
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
|
263
|
+
protected handleSearch(char: string) {
|
264
|
+
const searchString = this.getSearchString(char)
|
265
|
+
const focusedItem = this.items.find(
|
266
|
+
(item) => document.activeElement === item,
|
267
|
+
)
|
268
|
+
const focusedIndex = focusedItem ? this.items.indexOf(focusedItem) : 0
|
269
|
+
const searchIndex = this.getIndexByLetter(searchString, focusedIndex + 1)
|
270
|
+
|
271
|
+
// if a match was found, go to it
|
272
|
+
if (searchIndex >= 0) {
|
273
|
+
this.focusItemByIndex(null, searchIndex)
|
274
|
+
}
|
275
|
+
// if no matches, clear the timeout and search string
|
276
|
+
else {
|
277
|
+
window.clearTimeout(this.searchTimeout)
|
278
|
+
this.searchString = ''
|
279
|
+
}
|
280
|
+
}
|
281
|
+
|
282
|
+
protected filterItemsInnerText(items: string[], filter: string) {
|
283
|
+
return items.filter((item) => {
|
284
|
+
const matches = item.toLowerCase().indexOf(filter.toLowerCase()) === 0
|
285
|
+
return matches
|
286
|
+
})
|
287
|
+
}
|
288
|
+
|
289
|
+
protected getSearchString(char: string) {
|
290
|
+
// reset typing timeout and start new timeout
|
291
|
+
// this allows us to make multiple-letter matches, like a native select
|
292
|
+
if (typeof this.searchTimeout === 'number') {
|
293
|
+
window.clearTimeout(this.searchTimeout)
|
294
|
+
}
|
295
|
+
|
296
|
+
this.searchTimeout = window.setTimeout(() => {
|
297
|
+
this.searchString = ''
|
298
|
+
}, 500)
|
299
|
+
|
300
|
+
// add most recent letter to saved search string
|
301
|
+
this.searchString += char
|
302
|
+
return this.searchString
|
303
|
+
}
|
304
|
+
|
305
|
+
// return the index of an option from an array of options, based on a search string
|
306
|
+
// if the filter is multiple iterations of the same letter (e.g "aaa"), then cycle through first-letter matches
|
307
|
+
protected getIndexByLetter(filter: string, startIndex: number) {
|
308
|
+
const orderedItems = [
|
309
|
+
...this.itemsInnerText.slice(startIndex),
|
310
|
+
...this.itemsInnerText.slice(0, startIndex),
|
311
|
+
]
|
312
|
+
|
313
|
+
const firstMatch = this.filterItemsInnerText(orderedItems, filter)[0]
|
314
|
+
|
315
|
+
const allSameLetter = (array: string[]) =>
|
316
|
+
array.every((letter) => letter === array[0])
|
317
|
+
|
318
|
+
// first check if there is an exact match for the typed string
|
319
|
+
if (firstMatch) {
|
320
|
+
const index = this.itemsInnerText.indexOf(firstMatch)
|
321
|
+
return index
|
322
|
+
}
|
323
|
+
|
324
|
+
// if the same letter is being repeated, cycle through first-letter matches
|
325
|
+
else if (allSameLetter(filter.split(''))) {
|
326
|
+
const matches = this.filterItemsInnerText(orderedItems, filter[0])
|
327
|
+
const index = this.itemsInnerText.indexOf(matches[0])
|
328
|
+
return index
|
329
|
+
}
|
330
|
+
|
331
|
+
// if no matches, return -1
|
332
|
+
else {
|
333
|
+
return -1
|
334
|
+
}
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
type Select = InstanceType<typeof SelectController>
|
339
|
+
|
340
|
+
export { SelectController }
|
341
|
+
export type { Select }
|
@@ -1,17 +1,19 @@
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
2
2
|
import noUiSlider, { API, Options } from 'nouislider'
|
3
3
|
|
4
|
-
|
4
|
+
const SliderController = class extends Controller<HTMLElement> {
|
5
|
+
// targets
|
5
6
|
static targets = ['slider', 'hiddenInput', 'endHiddenInput']
|
6
|
-
|
7
7
|
declare readonly sliderTarget: HTMLInputElement
|
8
8
|
declare readonly hiddenInputTarget: HTMLInputElement
|
9
9
|
declare readonly endHiddenInputTarget: HTMLInputElement
|
10
10
|
declare readonly hasEndHiddenInputTarget: boolean
|
11
|
-
|
12
|
-
|
11
|
+
|
12
|
+
// custom properties
|
13
13
|
declare range: boolean
|
14
14
|
declare slider: API
|
15
|
+
declare onUpdateValuesListener: (values: (string | number)[]) => void
|
16
|
+
declare DOMClickListener: (event: MouseEvent) => void
|
15
17
|
|
16
18
|
connect() {
|
17
19
|
this.range = this.element.dataset.range === 'true'
|
@@ -35,15 +37,11 @@ export default class extends Controller<HTMLElement> {
|
|
35
37
|
document.addEventListener('click', this.DOMClickListener)
|
36
38
|
}
|
37
39
|
|
38
|
-
|
39
|
-
this.
|
40
|
-
|
41
|
-
if (this.range && this.hasEndHiddenInputTarget) {
|
42
|
-
this.endHiddenInputTarget.value = `${values[1]}`
|
43
|
-
}
|
40
|
+
disconnect() {
|
41
|
+
document.removeEventListener('click', this.DOMClickListener)
|
44
42
|
}
|
45
43
|
|
46
|
-
getOptions() {
|
44
|
+
protected getOptions() {
|
47
45
|
const defaultOptions = {
|
48
46
|
connect: this.range ? true : 'lower',
|
49
47
|
tooltips: true,
|
@@ -84,7 +82,15 @@ export default class extends Controller<HTMLElement> {
|
|
84
82
|
}
|
85
83
|
}
|
86
84
|
|
87
|
-
|
85
|
+
protected onUpdateValues(values: (string | number)[]) {
|
86
|
+
this.hiddenInputTarget.value = `${values[0]}`
|
87
|
+
|
88
|
+
if (this.range && this.hasEndHiddenInputTarget) {
|
89
|
+
this.endHiddenInputTarget.value = `${values[1]}`
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
protected onDOMClick(event: MouseEvent) {
|
88
94
|
const target = event.target
|
89
95
|
|
90
96
|
// Focus handle of slider when label is clicked.
|
@@ -100,8 +106,9 @@ export default class extends Controller<HTMLElement> {
|
|
100
106
|
}
|
101
107
|
}
|
102
108
|
}
|
103
|
-
|
104
|
-
disconnect() {
|
105
|
-
document.removeEventListener('click', this.DOMClickListener)
|
106
|
-
}
|
107
109
|
}
|
110
|
+
|
111
|
+
type Slider = InstanceType<typeof SliderController>
|
112
|
+
|
113
|
+
export { SliderController }
|
114
|
+
export type { Slider }
|
@@ -1,13 +1,15 @@
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
2
2
|
|
3
|
-
|
3
|
+
const SwitchController = class extends Controller<HTMLElement> {
|
4
|
+
// targets
|
4
5
|
static targets = ['input', 'thumb']
|
6
|
+
declare readonly inputTarget: HTMLInputElement
|
7
|
+
declare readonly thumbTarget: HTMLElement
|
8
|
+
|
9
|
+
// values
|
5
10
|
static values = {
|
6
11
|
isChecked: Boolean,
|
7
12
|
}
|
8
|
-
|
9
|
-
declare readonly inputTarget: HTMLInputElement
|
10
|
-
declare readonly thumbTarget: HTMLElement
|
11
13
|
declare isCheckedValue: boolean
|
12
14
|
|
13
15
|
toggle() {
|
@@ -28,3 +30,8 @@ export default class extends Controller<HTMLElement> {
|
|
28
30
|
}
|
29
31
|
}
|
30
32
|
}
|
33
|
+
|
34
|
+
type Switch = InstanceType<typeof SwitchController>
|
35
|
+
|
36
|
+
export { SwitchController }
|
37
|
+
export type { Switch }
|