mirror of
https://github.com/czhu12/canine.git
synced 2025-12-19 09:49:58 -06:00
added floating ui for search dropdowns
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import { computePosition, autoUpdate, flip, shift, offset, size } from "@floating-ui/dom"
|
||||||
import { debounce } from "../../utils"
|
import { debounce } from "../../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,9 +27,9 @@ export default class extends Controller {
|
|||||||
// Disable browser autocomplete
|
// Disable browser autocomplete
|
||||||
this.input.setAttribute('autocomplete', 'off')
|
this.input.setAttribute('autocomplete', 'off')
|
||||||
|
|
||||||
// Create dropdown
|
// Create dropdown and append to the appropriate container
|
||||||
this.dropdown = this.createDropdown()
|
this.dropdown = this.createDropdown()
|
||||||
this.element.appendChild(this.dropdown)
|
this.getDropdownContainer().appendChild(this.dropdown)
|
||||||
|
|
||||||
// Bind search handler with debounce
|
// Bind search handler with debounce
|
||||||
this.searchHandler = debounce(this.performSearch.bind(this), this.getDebounceDelay())
|
this.searchHandler = debounce(this.performSearch.bind(this), this.getDebounceDelay())
|
||||||
@@ -37,6 +38,8 @@ export default class extends Controller {
|
|||||||
// Handle click outside to close dropdown
|
// Handle click outside to close dropdown
|
||||||
this.clickOutsideHandler = this.handleClickOutside.bind(this)
|
this.clickOutsideHandler = this.handleClickOutside.bind(this)
|
||||||
document.addEventListener('click', this.clickOutsideHandler)
|
document.addEventListener('click', this.clickOutsideHandler)
|
||||||
|
|
||||||
|
this.cleanupAutoUpdate = null
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
@@ -44,14 +47,52 @@ export default class extends Controller {
|
|||||||
this.input.removeEventListener('input', this.searchHandler)
|
this.input.removeEventListener('input', this.searchHandler)
|
||||||
}
|
}
|
||||||
document.removeEventListener('click', this.clickOutsideHandler)
|
document.removeEventListener('click', this.clickOutsideHandler)
|
||||||
|
if (this.cleanupAutoUpdate) {
|
||||||
|
this.cleanupAutoUpdate()
|
||||||
|
}
|
||||||
|
if (this.dropdown && this.dropdown.parentNode) {
|
||||||
|
this.dropdown.parentNode.removeChild(this.dropdown)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createDropdown() {
|
createDropdown() {
|
||||||
const dropdown = document.createElement('ul')
|
const dropdown = document.createElement('ul')
|
||||||
dropdown.className = 'hidden absolute z-10 w-full mt-1 menu bg-neutral block rounded-box shadow-lg max-h-[300px] overflow-y-auto'
|
dropdown.className = 'hidden z-50 menu bg-neutral rounded-box shadow-lg max-h-[300px] overflow-y-auto'
|
||||||
|
dropdown.style.position = 'absolute'
|
||||||
|
dropdown.style.top = '0'
|
||||||
|
dropdown.style.left = '0'
|
||||||
return dropdown
|
return dropdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDropdownContainer() {
|
||||||
|
// Check if we're inside a modal and append there to maintain stacking context
|
||||||
|
const modal = this.element.closest('.modal, [role="dialog"], dialog')
|
||||||
|
return modal || document.body
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition() {
|
||||||
|
computePosition(this.input, this.dropdown, {
|
||||||
|
placement: 'bottom-start',
|
||||||
|
middleware: [
|
||||||
|
offset(4),
|
||||||
|
flip({ fallbackPlacements: ['top-start'] }),
|
||||||
|
shift({ padding: 8 }),
|
||||||
|
size({
|
||||||
|
apply({ rects, elements }) {
|
||||||
|
Object.assign(elements.floating.style, {
|
||||||
|
minWidth: `${rects.reference.width}px`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}).then(({ x, y }) => {
|
||||||
|
Object.assign(this.dropdown.style, {
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getInputElement() {
|
getInputElement() {
|
||||||
return this.element.querySelector('input')
|
return this.element.querySelector('input')
|
||||||
}
|
}
|
||||||
@@ -121,11 +162,26 @@ export default class extends Controller {
|
|||||||
|
|
||||||
showDropdown() {
|
showDropdown() {
|
||||||
this.dropdown.classList.remove('hidden')
|
this.dropdown.classList.remove('hidden')
|
||||||
|
this.updatePosition()
|
||||||
|
|
||||||
|
// Set up auto-update to keep position in sync during scroll/resize
|
||||||
|
if (this.cleanupAutoUpdate) {
|
||||||
|
this.cleanupAutoUpdate()
|
||||||
|
}
|
||||||
|
this.cleanupAutoUpdate = autoUpdate(this.input, this.dropdown, () => {
|
||||||
|
this.updatePosition()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hideDropdown() {
|
hideDropdown() {
|
||||||
this.dropdown.classList.add('hidden')
|
this.dropdown.classList.add('hidden')
|
||||||
this.dropdown.innerHTML = ''
|
this.dropdown.innerHTML = ''
|
||||||
|
|
||||||
|
// Clean up auto-update listener
|
||||||
|
if (this.cleanupAutoUpdate) {
|
||||||
|
this.cleanupAutoUpdate()
|
||||||
|
this.cleanupAutoUpdate = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoading() {
|
showLoading() {
|
||||||
@@ -157,7 +213,7 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickOutside(event) {
|
handleClickOutside(event) {
|
||||||
if (!this.element.contains(event.target)) {
|
if (!this.element.contains(event.target) && !this.dropdown.contains(event.target)) {
|
||||||
this.hideDropdown()
|
this.hideDropdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@codemirror/view": "^6.38.1",
|
"@codemirror/view": "^6.38.1",
|
||||||
|
"@floating-ui/dom": "^1.7.4",
|
||||||
"@gorails/ninja-keys": "^1.2.1",
|
"@gorails/ninja-keys": "^1.2.1",
|
||||||
"@hotwired/stimulus": "^3.2.2",
|
"@hotwired/stimulus": "^3.2.2",
|
||||||
"@hotwired/turbo-rails": "^8.0.20",
|
"@hotwired/turbo-rails": "^8.0.20",
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@@ -217,6 +217,26 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244"
|
||||||
integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==
|
integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==
|
||||||
|
|
||||||
|
"@floating-ui/core@^1.7.3":
|
||||||
|
version "1.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7"
|
||||||
|
integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/utils" "^0.2.10"
|
||||||
|
|
||||||
|
"@floating-ui/dom@^1.7.4":
|
||||||
|
version "1.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77"
|
||||||
|
integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core" "^1.7.3"
|
||||||
|
"@floating-ui/utils" "^0.2.10"
|
||||||
|
|
||||||
|
"@floating-ui/utils@^0.2.10":
|
||||||
|
version "0.2.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||||
|
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||||
|
|
||||||
"@gorails/ninja-keys@^1.2.1":
|
"@gorails/ninja-keys@^1.2.1":
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.npmjs.org/@gorails/ninja-keys/-/ninja-keys-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/@gorails/ninja-keys/-/ninja-keys-1.2.1.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user