import React, { Fragment, useState, useCallback, useEffect, useRef } from 'react'
import { Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import debounce from 'debounce-promise'
import classNames from 'classnames'
function Option({isHighlighted, onClick, onMouseEnter, text, id}) {
const className = classNames('relative cursor-pointer select-none py-2 px-3', {
'text-gray-900 dark:text-gray-300': !isHighlighted,
'bg-indigo-600 text-white': isHighlighted,
})
return (
{text}
)
}
function scrollTo(wrapper, id) {
if (wrapper) {
const el = wrapper.querySelector('#' + id)
if (el) {
el.scrollIntoView({block: 'center'})
}
}
}
function optionId(index) {
return `plausible-combobox-option-${index}`
}
export default function PlausibleCombobox(props) {
const [options, setOptions] = useState([])
const [loading, setLoading] = useState(false)
const [isOpen, setOpen] = useState(false)
const [input, setInput] = useState('')
const [highlightedIndex, setHighlightedIndex] = useState(0)
const searchRef = useRef(null)
const containerRef = useRef(null)
const listRef = useRef(null)
const visibleOptions = [...options]
if (props.freeChoice && input.length > 0 && options.every(option => option.value !== input)) {
visibleOptions.push({value: input, label: input, freeChoice: true})
}
function highLight(index) {
let newIndex = index
if (index < 0) {
newIndex = visibleOptions.length - 1
} else if (index >= visibleOptions.length) {
newIndex = 0
}
setHighlightedIndex(newIndex)
scrollTo(listRef.current, optionId(newIndex))
}
function onKeyDown(e) {
if (e.key === 'Enter') {
if (!isOpen || loading || visibleOptions.length === 0) return null
selectOption(visibleOptions[highlightedIndex])
e.preventDefault()
}
if (e.key === 'Escape') {
if (!isOpen || loading) return null
setOpen(false)
searchRef.current?.focus()
e.preventDefault()
}
if (e.key === 'ArrowDown') {
if (isOpen) {
highLight(highlightedIndex + 1)
} else {
setOpen(true)
}
}
if (e.key === 'ArrowUp') {
if (isOpen) {
highLight(highlightedIndex - 1)
} else {
setOpen(true)
}
}
}
function isDisabled(option) {
return props.values.some((val) => val.value === option.value)
}
function fetchOptions(query) {
setLoading(true)
setOpen(true)
return props.fetchOptions(query).then((loadedOptions) => {
setLoading(false)
setHighlightedIndex(0)
setOptions(loadedOptions)
})
}
const debouncedFetchOptions = useCallback(debounce(fetchOptions, 200), [fetchOptions])
function onInput(e) {
const newInput = e.target.value
setInput(newInput)
debouncedFetchOptions(newInput)
}
function toggleOpen() {
if (!isOpen) {
fetchOptions(input)
searchRef.current.focus()
} else {
setInput('')
setOpen(false)
}
}
function selectOption(option) {
if (props.singleOption) {
props.onSelect([option])
} else {
searchRef.current.focus()
props.onSelect([...props.values, option])
}
setOpen(false)
setInput('')
}
function removeOption(option, e) {
e.stopPropagation()
const newValues = props.values.filter((val) => val.value !== option.value)
props.onSelect(newValues)
searchRef.current.focus()
setOpen(false)
}
const handleClick = useCallback((e) => {
if (containerRef.current && containerRef.current.contains(e.target)) { return }
setInput('')
setOpen(false)
})
useEffect(() => {
document.addEventListener("mousedown", handleClick, false)
return () => { document.removeEventListener("mousedown", handleClick, false) }
}, [])
useEffect(() => {
if (props.singleOption && props.values.length === 0) {
searchRef.current.focus()
}
}, [props.values.length === 0])
const searchBoxClass = 'border-none py-1 px-0 w-full inline-block rounded-md focus:outline-none focus:ring-0 text-sm'
const containerClass = classNames('relative w-full', {
[props.className]: !!props.className,
'opacity-30 cursor-default pointer-events-none': props.isDisabled
})
function renderSingleOptionContent() {
const itemSelected = props.values.length === 1
const placeholder = itemSelected ? '' : props.placeholder
return (
{ itemSelected && renderSingleSelectedItem() }
)
}
function renderSingleSelectedItem() {
if (input === '') {
return (
{props.values[0].label}
)
}
}
function renderMultiOptionContent() {
return (
<>
{ props.values.map((value) => {
return (
{value.label}
removeOption(value, e)} className="cursor-pointer font-bold ml-1">×
)
})
}
>
)
}
function renderDropDownContent() {
const matchesFound = visibleOptions.length > 0 && visibleOptions.some(option => !isDisabled(option))
if (loading) {
return Loading options...
}
if (matchesFound) {
return visibleOptions
.filter(option => !isDisabled(option))
.map((option, i) => {
const text = option.freeChoice ? `Filter by '${option.label}'` : option.label
return (