import { Double, getType, Int, newDynamic, trie } from "dope2"; import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery"; import { parseDouble, parseInt } from "./util/parse"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { Type } from "./Type"; import "./InputBlock.css"; import type { Dynamic, State2Props } from "./util/extra"; import { EnvContext } from "./EnvContext"; export interface InputBlockState { kind: "input"; text: string; resolved: undefined | Dynamic; focus: boolean } interface InputBlockProps extends State2Props { filter: (suggestion: [string, Dynamic]) => boolean; onCancel: () => void; } const computeSuggestions = (text, env, filter) => { const asDouble = parseDouble(text); const asInt = parseInt(text); const ls = [ ... (asDouble ? [[asDouble.toString(), newDynamic(asDouble)(Double)]] : []), ... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []), ... trie.suggest(env.name2dyn)(text)(Infinity), ] return ls; return [ ...ls.filter(filter), // ones that match filter come first ...ls.filter(s => !filter(s)), ] // .slice(0,30); } export function InputBlock({ state, setState, filter, onCancel }: InputBlockProps) { const {text, resolved, focus} = state; const env = useContext(EnvContext); const inputRef = useRef(null); const [i, setI] = useState(0); // selected suggestion idx const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not const singleSuggestion = trie.growPrefix(env.name2dyn)(text); const suggestions = useMemo(() => computeSuggestions(text, env, filter), [text]); useEffect(() => autoInputWidth(inputRef, text), [inputRef, text]); useEffect(() => { if (focus) { inputRef.current?.focus(); } }, [focus]); useEffect(() => { if (suggestions.length >= i) { setI(0); } }, [suggestions.length]); const getCaretPosition = () => { return inputRef.current?.selectionStart || -1; } const onTextChange = newText => { const found = trie.get(env.name2dyn)(newText); setState(state => ({...state, text: newText, resolved: found})); } // fired before onInput const onKeyDown = (e: React.KeyboardEvent) => { const fns = { Tab: () => { if (e.shiftKey) { focusPrevElement(); e.preventDefault(); } else { // not shift key if (singleSuggestion.length > 0) { const newText = text + singleSuggestion; onTextChange(newText); setRightMostCaretPosition(inputRef.current); e.preventDefault(); } else { onSelectSuggestion(suggestions[i]); e.preventDefault(); } } }, ArrowDown: () => { setI((i + 1) % suggestions.length); e.preventDefault(); }, ArrowUp: () => { setI((suggestions.length + i - 1) % suggestions.length); e.preventDefault(); }, ArrowLeft: () => { if (getCaretPosition() <= 0) { focusPrevElement(); e.preventDefault(); } }, ArrowRight: () => { if (getCaretPosition() === text.length) { focusNextElement(); e.preventDefault(); } }, Enter: () => { onSelectSuggestion(suggestions[i]); e.preventDefault(); }, Backspace: () => { if (text.length === 0) { onCancel(); e.preventDefault(); } } }; fns[e.key]?.(); }; const onInput = e => { onTextChange(e.target.value); }; const onSelectSuggestion = ([name, dynamic]) => { setState(state => ({...state, text: name, resolved: dynamic})); }; return {/* Dropdown suggestions */} {haveFocus && } {/* Input box */} setHaveFocus(true)} onBlur={() => setHaveFocus(false)} spellCheck={false}/> {/* Single 'grey' suggestion */} {singleSuggestion} { resolved && <>☑} ; } function Suggestions({ suggestions, onSelect, i, setI }) { const onMouseEnter = j => () => { setI(j); }; const onMouseDown = j => () => { setI(j); onSelect(suggestions[i]); }; return <>{(suggestions.length > 0) &&
{suggestions.map(([name, dynamic], j) =>
{name} ::
)}
}; }