+ "content": "import { Controller } from \"@hotwired/stimulus\";\nimport { computePosition, autoUpdate, offset, flip } from \"@floating-ui/dom\";\n\n// Connects to data-controller=\"ruby-ui--combobox\"\nexport default class extends Controller {\n static values = {\n term: String,\n minPopoverWidth: { type: Number, default: 240 },\n placement: { type: String, default: \"bottom-start\" }\n }\n\n static targets = [\n \"input\",\n \"toggleAll\",\n \"popover\",\n \"item\",\n \"emptyState\",\n \"searchInput\",\n \"trigger\",\n \"triggerContent\"\n ]\n\n selectedItemIndex = null\n\n connect() {\n this.updateTriggerContent()\n }\n\n disconnect() {\n if (this.cleanup) { this.cleanup() }\n }\n\n handlePopoverToggle(event) {\n // Keep ariaExpanded in sync with the actual popover state\n this.triggerTarget.ariaExpanded = event.newState === 'open' ? 'true' : 'false'\n }\n\n inputChanged(e) {\n this.updateTriggerContent()\n\n if (e.target.type == \"radio\") {\n this.closePopover()\n }\n\n if (this.hasToggleAllTarget && !e.target.checked) {\n this.toggleAllTarget.checked = false\n }\n }\n\n inputContent(input) {\n return input.dataset.text || input.parentElement.textContent\n }\n\n toggleAllItems() {\n const isChecked = this.toggleAllTarget.checked\n this.inputTargets.forEach(input => input.checked = isChecked)\n this.updateTriggerContent()\n }\n\n updateTriggerContent() {\n const checkedInputs = this.inputTargets.filter(input => input.checked)\n\n if (checkedInputs.length === 0) {\n this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder\n } else if (this.termValue && checkedInputs.length > 1) {\n this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`\n } else {\n this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(\", \")\n }\n }\n\n togglePopover(event) {\n event.preventDefault()\n\n if (this.triggerTarget.ariaExpanded === \"true\") {\n this.closePopover()\n } else {\n this.openPopover(event)\n }\n }\n\n openPopover(event) {\n if (event) event.preventDefault()\n\n this.updatePopoverPosition()\n this.updatePopoverWidth()\n this.triggerTarget.ariaExpanded = \"true\"\n this.selectedItemIndex = null\n this.itemTargets.forEach(item => item.ariaCurrent = \"false\")\n this.popoverTarget.showPopover()\n }\n\n closePopover() {\n this.triggerTarget.ariaExpanded = \"false\"\n this.popoverTarget.hidePopover()\n }\n\n filterItems(e) {\n if ([\"ArrowDown\", \"ArrowUp\", \"Tab\", \"Enter\"].includes(e.key)) {\n return\n }\n\n const filterTerm = this.searchInputTarget.value.toLowerCase()\n\n if (this.hasToggleAllTarget) {\n if (filterTerm) this.toggleAllTarget.parentElement.classList.add(\"hidden\")\n else this.toggleAllTarget.parentElement.classList.remove(\"hidden\")\n }\n\n let resultCount = 0\n\n this.selectedItemIndex = null\n\n this.inputTargets.forEach((input) => {\n const text = this.inputContent(input).toLowerCase()\n\n if (text.indexOf(filterTerm) > -1) {\n input.parentElement.classList.remove(\"hidden\")\n resultCount++\n } else {\n input.parentElement.classList.add(\"hidden\")\n }\n })\n\n this.emptyStateTarget.classList.toggle(\"hidden\", resultCount !== 0)\n }\n\n keyDownPressed() {\n if (this.selectedItemIndex !== null) {\n this.selectedItemIndex++\n } else {\n this.selectedItemIndex = 0\n }\n\n this.focusSelectedInput()\n }\n\n keyUpPressed() {\n if (this.selectedItemIndex !== null) {\n this.selectedItemIndex--\n } else {\n this.selectedItemIndex = -1\n }\n\n this.focusSelectedInput()\n }\n\n focusSelectedInput() {\n const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains(\"hidden\"))\n\n this.wrapSelectedInputIndex(visibleInputs.length)\n\n visibleInputs.forEach((input, index) => {\n if (index == this.selectedItemIndex) {\n input.parentElement.ariaCurrent = \"true\"\n input.parentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })\n } else {\n input.parentElement.ariaCurrent = \"false\"\n }\n })\n }\n\n keyEnterPressed(event) {\n event.preventDefault()\n const option = this.itemTargets.find(item => item.ariaCurrent === \"true\")\n\n if (option) {\n option.click()\n }\n }\n\n wrapSelectedInputIndex(length) {\n this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length\n }\n\n updatePopoverPosition() {\n this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {\n computePosition(this.triggerTarget, this.popoverTarget, {\n placement: this.placementValue,\n middleware: [offset(4), flip()],\n }).then(({ x, y }) => {\n Object.assign(this.popoverTarget.style, {\n left: `${x}px`,\n top: `${y}px`,\n });\n });\n });\n }\n\n updatePopoverWidth() {\n const width = Math.max(this.triggerTarget.offsetWidth, this.minPopoverWidthValue)\n this.popoverTarget.style.width = `${width}px`\n }\n}\n"
0 commit comments