import { useEffect, useRef, useState } from 'react' import { searchDbWords, type DbWordSearchResult } from '../api' interface WordAutocompleteProps { value: string onChange: (v: string) => void onSubmit: () => void onPick?: (word: DbWordSearchResult) => void token: string | null placeholder?: string className?: string inputStyle?: React.CSSProperties minChars?: number debounceMs?: number excludeTitles?: string[] } export default function WordAutocomplete({ value, onChange, onSubmit, onPick, token, placeholder, className, inputStyle, minChars = 1, debounceMs = 180, excludeTitles = [], }: WordAutocompleteProps) { const [results, setResults] = useState([]) const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) const [highlight, setHighlight] = useState(0) const wrapRef = useRef(null) const reqIdRef = useRef(0) const excludeSet = new Set(excludeTitles.map(s => s.trim().toLowerCase())) // Debounced search useEffect(() => { if (!token) return const trimmed = value.trim() if (trimmed.length < minChars) { setResults([]) setOpen(false) return } const myId = ++reqIdRef.current setLoading(true) const t = setTimeout(() => { searchDbWords(trimmed, token, 10) .then(r => { if (myId !== reqIdRef.current) return const filtered = r.filter(x => !excludeSet.has(x.titel_de.trim().toLowerCase())) setResults(filtered) setOpen(filtered.length > 0) setHighlight(0) }) .finally(() => { if (myId === reqIdRef.current) setLoading(false) }) }, debounceMs) return () => clearTimeout(t) }, [value, token, minChars, debounceMs]) // Close on outside click useEffect(() => { const onClick = (e: MouseEvent) => { if (!wrapRef.current) return if (!wrapRef.current.contains(e.target as Node)) setOpen(false) } document.addEventListener('mousedown', onClick) return () => document.removeEventListener('mousedown', onClick) }, []) const pick = (r: DbWordSearchResult) => { onChange(r.titel_de) onPick?.(r) setOpen(false) } const onKeyDown = (e: React.KeyboardEvent) => { if (open && results.length > 0) { if (e.key === 'ArrowDown') { e.preventDefault() setHighlight(h => (h + 1) % results.length) return } if (e.key === 'ArrowUp') { e.preventDefault() setHighlight(h => (h - 1 + results.length) % results.length) return } if (e.key === 'Escape') { e.preventDefault() setOpen(false) return } if (e.key === 'Tab') { e.preventDefault() pick(results[highlight]) return } if (e.key === 'Enter') { // If user has typed something matching a suggestion exactly, prefer submit; else pick. const exact = results.find(r => r.titel_de.trim().toLowerCase() === value.trim().toLowerCase()) if (exact) { setOpen(false) onSubmit() } else { e.preventDefault() pick(results[highlight]) } return } } if (e.key === 'Enter') { onSubmit() } } return (
onChange(e.target.value)} onKeyDown={onKeyDown} onFocus={() => { if (results.length > 0) setOpen(true) }} placeholder={placeholder} autoComplete="off" spellCheck={false} style={inputStyle} /> {open && (
{loading && results.length === 0 && (
Suche…
)} {results.map((r, i) => ( ))}
)}
) }