import {type Placement, autoUpdate, computePosition, flip, offset, shift, size} from '@floating-ui/dom' import {controllerFactory} from '@utils/createController' import {useClickOutside, useMutation} from 'stimulus-use' export default class ComboboxController extends controllerFactory()({ targets: { anchor: null, options: null, popover: null, searchInput: HTMLInputElement, }, values: { clamped: Boolean, placement: String, }, }) { private changedIds = new Set() private clickHandlers: Array<() => void> = [] labels: Array<{el: HTMLLabelElement; searchString: string}> unsubAutoUpdate: (() => void) | undefined private setupClickHandlers() { const cb = () => this.toggle() for (const fn of this.clickHandlers) { fn() } this.clickHandlers = [] for (const el of this.anchorTarget.querySelectorAll('button, [tabindex]:not([tabindex="-1"])')) { el.addEventListener('click', cb) this.clickHandlers.push(() => el.removeEventListener('click', cb)) } } checkboxClicked(e: Event) { const target = e.target as HTMLInputElement const value = target.value if (this.changedIds.has(value)) { this.changedIds.delete(value) } else { this.changedIds.add(value) } this.dispatch('clicked', {detail: value}) } clickOutside() { this.element.open = false this.setupAutoUpdate() this.close() } close() { if (this.hasSearchInputTarget) this.searchInputTarget.value = '' if (this.changedIds.size > 0) { this.dispatch('changed') this.changedIds.clear() } } connect() { useClickOutside(this) useMutation(this, {childList: true, subtree: true}) this.setupAutoUpdate() this.setupClickHandlers() } disconnect() { this.unsubAutoUpdate?.() } setupAutoUpdate(): void { if (!this.element.open) { this.unsubAutoUpdate?.() return } const updatePopoverPosition = (): void => { const options = this.optionsTarget const searchInput = this.hasSearchInputTarget ? this.searchInputTarget : null const shouldClamp = this.clampedValue void computePosition(this.anchorTarget, this.popoverTarget, { middleware: [ offset(6), flip(), shift({padding: 6}), size({ apply({availableHeight}) { const inputHeight = searchInput ? searchInput.getBoundingClientRect().height : 0 let maxHeight = availableHeight - inputHeight - 6 if (shouldClamp && maxHeight > 400) maxHeight = 400 Object.assign(options.style, { maxHeight: `${maxHeight}px`, }) }, }), ], placement: this.placementValue as Placement, strategy: 'fixed', }).then(({x, y}) => { Object.assign(this.popoverTarget.style, { left: `${x}px`, top: `${y}px`, }) }) } updatePopoverPosition() this.unsubAutoUpdate = autoUpdate(this.anchorTarget, this.popoverTarget, updatePopoverPosition) } toggle(): void { this.element.open = !this.element.open this.setupAutoUpdate() } }