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, isDisabled, onClick, onMouseEnter, text, id}) { const className = classNames('relative select-none py-2 px-3', { 'cursor-pointer': !isDisabled, 'text-gray-300 dark:text-gray-600': isDisabled, 'text-gray-900 dark:text-gray-300': !isDisabled && !isHighlighted, 'bg-indigo-600 text-white': !isDisabled && 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), []) 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 (isDisabled(option)) return 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) if (!searchBoxHidden) { 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 matchesFound = !loading && visibleOptions.length > 0 const noMatchesFound = !loading && visibleOptions.length === 0 const searchBoxHidden = !!props.singleOption && props.values.length === 1 const searchBoxClass = classNames('border-none py-1 px-1 p-0 w-full inline-block rounded-md focus:outline-none focus:ring-0 text-sm', { 'hidden': searchBoxHidden }) const containerClass = classNames('relative w-full', { [props.className]: !!props.className, 'opacity-20 cursor-default pointer-events-none': props.isDisabled }) return (
    { props.values.map((value) => { return (
    {value.label} removeOption(value, e)} className="cursor-pointer font-bold ml-1">×
    ) }) }
    {!loading && } {loading && }
    ); } function Spinner() { return ( ) }