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 (