mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-03-11 12:45:18 +09:00
209 lines
6.1 KiB
JavaScript
209 lines
6.1 KiB
JavaScript
import { getAbsolutePositionForOffsets, getElementMargin, getViewportRect, synchronize, transitionElementWithClass } from "lib/autocomplete/helpers"
|
|
|
|
export default class SuggestionResultsController {
|
|
constructor(options = {}) {
|
|
this.revealOption = this.revealOption.bind(this)
|
|
this.didMouseDown = this.didMouseDown.bind(this)
|
|
this.flashSelection = synchronize(this.flashSelection)
|
|
this.id = options.id || `suggestion_results_${generateUUID()}`
|
|
|
|
this.#createSelectElement()
|
|
}
|
|
|
|
destroy() {
|
|
this.hide()
|
|
this.#removeSelectElement()
|
|
}
|
|
|
|
displayAtOffsets(offsets, {placement} = {}) {
|
|
let availableMaxHeight, elementHeight, height, left, maxHeight, top
|
|
this.show()
|
|
|
|
this.selectElement.style.height = ""
|
|
const style = getComputedStyle(this.selectElement)
|
|
const margin = getElementMargin(this.selectElement)
|
|
const position = getAbsolutePositionForOffsets(offsets)
|
|
|
|
const elementRect = this.selectElement.getBoundingClientRect()
|
|
const viewportRect = getViewportRect()
|
|
|
|
const availableHeightAbove = position.top - viewportRect.top - margin.top
|
|
const availableHeightBelow = viewportRect.bottom - position.bottom - margin.bottom
|
|
const thresholdHeight = this.getOptionHeight() * 3
|
|
|
|
if (availableHeightAbove > thresholdHeight && thresholdHeight > availableHeightBelow) {
|
|
if (placement == null) { placement = "above" }
|
|
} else {
|
|
if (placement == null) { placement = "below" }
|
|
}
|
|
|
|
if (placement === "above") {
|
|
availableMaxHeight = availableHeightAbove
|
|
} else {
|
|
availableMaxHeight = availableHeightBelow
|
|
}
|
|
|
|
if (style.maxHeight === "none") {
|
|
maxHeight = availableMaxHeight
|
|
} else {
|
|
const requestedMaxHeight = parseInt(style.maxHeight, 10)
|
|
maxHeight = Math.min(availableMaxHeight, requestedMaxHeight)
|
|
}
|
|
|
|
if (elementRect.height > maxHeight) {
|
|
elementHeight = (height = maxHeight)
|
|
} else {
|
|
elementHeight = elementRect.height
|
|
}
|
|
|
|
if (placement === "above") {
|
|
top = position.top - elementHeight - margin.top
|
|
} else {
|
|
top = position.bottom - margin.top
|
|
}
|
|
|
|
const elementRight = position.left + elementRect.width + margin.right
|
|
|
|
if (elementRight > viewportRect.right) {
|
|
left = position.left - (elementRight - viewportRect.right) - margin.right
|
|
} else {
|
|
left = position.left - margin.right
|
|
}
|
|
|
|
this.selectElement.style.top = `${top}px`
|
|
this.selectElement.style.left = `${left}px`
|
|
this.selectElement.style.height = height ? `${height}px` : "auto"
|
|
}
|
|
|
|
show() {
|
|
if (!this.visible) {
|
|
this.visible = true
|
|
this.selectElement.setAttribute("aria-hidden", "false")
|
|
this.selectElement.style.visibility = ""
|
|
return this.delegate.didShowResults(this.selectElement)
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
if (this.visible) {
|
|
this.visible = false
|
|
this.selectElement.style.visibility = "hidden"
|
|
this.selectElement.setAttribute("aria-hidden", "true")
|
|
return this.delegate.didHideResults(this.selectElement)
|
|
}
|
|
}
|
|
|
|
hideWithAnimation = synchronize((callback) => {
|
|
if (this.visible) {
|
|
return transitionElementWithClass(this.selectElement, "hiding", () => {
|
|
this.hide()
|
|
return callback()
|
|
})
|
|
} else {
|
|
return callback()
|
|
}
|
|
})
|
|
|
|
selectUp() {
|
|
this.selectElement.selectedIndex--
|
|
return this.revealOption()
|
|
}
|
|
|
|
selectDown() {
|
|
this.selectElement.selectedIndex++
|
|
return this.revealOption()
|
|
}
|
|
|
|
revealOption() {
|
|
const {
|
|
selectedOption
|
|
} = this.selectElement
|
|
if (selectedOption) {
|
|
const {scrollTop} = this.selectElement
|
|
const selectHeight = this.selectElement.clientHeight
|
|
const scrollBottom = scrollTop + selectHeight
|
|
|
|
const optionTop = selectedOption.offsetTop
|
|
const optionHeight = selectedOption.offsetHeight
|
|
const optionBottom = optionTop + optionHeight
|
|
|
|
if (optionTop < scrollTop) {
|
|
this.selectElement.scrollTop = optionTop
|
|
} else if (optionBottom > scrollBottom) {
|
|
this.selectElement.scrollTop = scrollTop + (optionBottom - scrollBottom)
|
|
}
|
|
}
|
|
}
|
|
|
|
flashSelection(callback) {
|
|
const { selectedOption } = this.selectElement
|
|
if (selectedOption) {
|
|
this.selectElement.classList.add("flashing")
|
|
return selectedOption.flash(() => {
|
|
this.selectElement.classList.remove("flashing")
|
|
return callback()
|
|
})
|
|
} else {
|
|
return callback()
|
|
}
|
|
}
|
|
|
|
updateResults(results) {
|
|
this.selectElement.innerHTML = results.toString().trim()
|
|
if (this.selectElement.selectedIndex != null) {
|
|
return requestAnimationFrame(this.revealOption)
|
|
} else {
|
|
this.selectElement.selectedIndex = 0
|
|
}
|
|
}
|
|
|
|
hasResults() {
|
|
return this.selectElement.innerHTML.length > 0
|
|
}
|
|
|
|
getSelectedValue() {
|
|
return this.selectElement.value
|
|
}
|
|
|
|
getOptionHeight() {
|
|
return this.selectElement.optionElements[0]?.offsetHeight != null ? this.selectElement.optionElements[0]?.offsetHeight : 0
|
|
}
|
|
|
|
didMouseDown(event) {
|
|
const url = event.target.getAttribute("href")
|
|
const option = event.target.closest("suggestion-option")
|
|
|
|
if (url) {
|
|
Turbo.visit(url)
|
|
} else if (option) {
|
|
option.selectElement.selectedOption = option
|
|
this.delegate.didClickOption(option)
|
|
this.#cancelEvent(event)
|
|
}
|
|
}
|
|
|
|
#createSelectElement() {
|
|
this.selectElement = document.createElement("suggestion-select")
|
|
this.selectElement.setAttribute("class", "autocomplete__list shadow margin-none unpad")
|
|
this.selectElement.addEventListener("mousedown", this.didMouseDown, true)
|
|
this.selectElement.addEventListener("click", this.#cancelEvent)
|
|
this.selectElement.setAttribute("id", this.id)
|
|
this.selectElement.setAttribute("data-behavior", "scrollable_menu")
|
|
this.selectElement.setAttribute("aria-live", "assertive")
|
|
|
|
document.body.appendChild(this.selectElement)
|
|
}
|
|
|
|
#removeSelectElement() {
|
|
this.selectElement.removeEventListener("mousedown", this.didMouseDown, true)
|
|
this.selectElement.removeEventListener("click", this.#cancelEvent)
|
|
|
|
return this.selectElement.remove()
|
|
}
|
|
|
|
#cancelEvent(event) {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
}
|
|
}
|