import { useEffect, useRef, type ReactNode, type KeyboardEvent, useState } from "react"; interface InputProps { placeholder: string; text: string; suggestion: string; onTextChange: (text: string) => void; focus: boolean; setFocus: (focus: boolean) => void; onEnter: () => void; onCancel: () => void; extraHandlers: {[key:string]: (e: KeyboardEvent) => void} children?: ReactNode | ReactNode[]; } const autoInputWidth = (ref: React.RefObject, text, emptyWidth=150) => { if (ref.current) { ref.current.style.width = `${text.length === 0 ? emptyWidth : (text.length*8.0)}px`; } } function getCaretPosition(ref: React.RefObject): number { return ref.current?.selectionStart || 0; } // Move caret all the way to the right in the currently focused element function setRightMostCaretPosition(elem) { const range = document.createRange(); range.selectNode(elem); if (elem.lastChild) { // if no text is entered, there is no lastChild range.setStart(elem.lastChild, elem.textContent.length); range.setEnd(elem.lastChild, elem.textContent.length); const selection = window.getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } } function focusNextElement() { const editable = Array.from(document.querySelectorAll('input.editable')); const index = editable.indexOf(document.activeElement); const nextElem = editable[index+1]; if (nextElem) { nextElem.focus(); } } function focusPrevElement() { const editable = Array.from(document.querySelectorAll('input.editable')); const index = editable.indexOf(document.activeElement); const prevElem = editable[index-1] if (prevElem) { prevElem.focus(); setRightMostCaretPosition(prevElem); } } export function Input({placeholder, text, suggestion, onTextChange, focus, setFocus, onEnter, onCancel, extraHandlers, children}: InputProps) { const ref = useRef(null); const [hideChildren, setHideChildren] = useState(false); useEffect(() => { if (focus) { ref.current?.focus(); setHideChildren(false); } }, [focus]); useEffect(() => autoInputWidth(ref, (text+(focus?suggestion:'')) || placeholder), [ref, text, suggestion, focus]); const onKeyDown = (e: KeyboardEvent) => { setHideChildren(false); const keys = { // auto-complete Tab: () => { if (e.shiftKey) { focusPrevElement(); e.preventDefault(); } else { // not shift key if (suggestion.length > 0) { // complete with greyed out text const newText = text + suggestion; onTextChange(newText); setRightMostCaretPosition(ref.current); e.preventDefault(); } else { onEnter(); e.preventDefault(); } } }, Enter: () => { onEnter(); e.preventDefault(); }, // cancel Backspace: () => { if (text.length === 0) { onCancel(); e.preventDefault(); } }, // navigate with arrows ArrowLeft: () => { if (getCaretPosition(ref) <= 0) { focusPrevElement(); e.preventDefault(); } }, ArrowRight: () => { if (getCaretPosition(ref) === text.length) { focusNextElement(); e.preventDefault(); } }, Escape: () => { if (!hideChildren) { setHideChildren(true); e.preventDefault(); } }, ...extraHandlers, }; const handler = keys[e.key]; if (handler) { handler(e); } }; return {focus && !hideChildren && children} {text}{focus && suggestion} // @ts-ignore onTextChange(e.target.value)} onKeyDown={onKeyDown} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} spellCheck={false} /> ; }