import { useEffect, useRef, useState } from "../deps.ts"; import { FilterOption } from "../../common/types.ts"; import fuzzysort from "https://esm.sh/fuzzysort@2.0.1"; import { FunctionalComponent } from "https://esm.sh/v99/preact@10.11.3/src/index"; import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types"; function magicSorter(a: FilterOption, b: FilterOption): number { if (a.orderId && b.orderId) { return a.orderId < b.orderId ? -1 : 1; } if (a.orderId) { return -1; } if (b.orderId) { return 1; } return 0; } type FilterResult = FilterOption & { result?: any; }; function simpleFilter( pattern: string, options: FilterOption[], ): FilterOption[] { const lowerPattern = pattern.toLowerCase(); return options.filter((option) => { return option.name.toLowerCase().includes(lowerPattern); }); } function escapeHtml(unsafe: string): string { return unsafe .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function fuzzySorter(pattern: string, options: FilterOption[]): FilterResult[] { return fuzzysort .go(pattern, options, { all: true, key: "name", }) .map((result: any) => ({ ...result.obj, result: result })) .sort(magicSorter); } export function FilterList({ placeholder, options, label, onSelect, onKeyPress, allowNew = false, helpText = "", completePrefix, icon: Icon, newHint, }: { placeholder: string; options: FilterOption[]; label: string; onKeyPress?: (key: string, currentText: string) => void; onSelect: (option: FilterOption | undefined) => void; allowNew?: boolean; completePrefix?: string; helpText: string; newHint?: string; icon?: FunctionalComponent; }) { const searchBoxRef = useRef(null); const [text, setText] = useState(""); const [matchingOptions, setMatchingOptions] = useState( fuzzySorter("", options), ); const [selectedOption, setSelectionOption] = useState(0); const selectedElementRef = useRef(null); function updateFilter(originalPhrase: string) { const foundExactMatch = false; const results = fuzzySorter(originalPhrase, options); if (allowNew && !foundExactMatch && originalPhrase) { results.splice(1, 0, { name: originalPhrase, hint: newHint, }); } setMatchingOptions(results); setText(originalPhrase); setSelectionOption(0); } useEffect(() => { updateFilter(text); }, [options]); useEffect(() => { searchBoxRef.current!.focus(); }, []); useEffect(() => { function closer() { onSelect(undefined); } document.addEventListener("click", closer); return () => { document.removeEventListener("click", closer); }; }, []); let exiting = false; const returnEl = (
{ if (!exiting && searchBoxRef.current) { searchBoxRef.current.focus(); } }} onKeyUp={(e) => { if (onKeyPress) { onKeyPress(e.key, text); } switch (e.key) { case "ArrowUp": setSelectionOption(Math.max(0, selectedOption - 1)); break; case "ArrowDown": setSelectionOption( Math.min(matchingOptions.length - 1, selectedOption + 1), ); break; case "Enter": exiting = true; onSelect(matchingOptions[selectedOption]); e.preventDefault(); break; case "PageUp": setSelectionOption(Math.max(0, selectedOption - 5)); break; case "PageDown": setSelectionOption(Math.max(0, selectedOption + 5)); break; case "Home": setSelectionOption(0); break; case "End": setSelectionOption(matchingOptions.length - 1); break; case "Escape": exiting = true; onSelect(undefined); e.preventDefault(); break; case " ": if (completePrefix && !text) { updateFilter(completePrefix); e.preventDefault(); } break; default: updateFilter((e.target as any).value); } e.stopPropagation(); }} onKeyDown={(e) => { e.stopPropagation(); }} onClick={(e) => e.stopPropagation()} />
{matchingOptions && matchingOptions.length > 0 ? matchingOptions.map((option, idx) => (
{ setSelectionOption(idx); }} onClick={(e) => { e.preventDefault(); exiting = true; onSelect(option); }} > {Icon && ( )} ", "")! : escapeHtml(option.name), }} > {option.hint && {option.hint}}
)) : null}
); useEffect(() => { selectedElementRef.current?.scrollIntoView({ block: "nearest", }); }); return returnEl; }