UNPKG

@1771technologies/lytenyte-pro

Version:

Blazingly fast headless React data grid with 100s of features.

159 lines (158 loc) 7.51 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useRef, useCallback, useMemo, Fragment } from "react"; import { useCompletions } from "./intellisence/use-completions.js"; import { useCompletionTrigger } from "./intellisence/use-complete-trigger.js"; import { useKeyboardNavigation } from "./use-keyboard-navigation.js"; import { useCursorPosition } from "./cursor-position/use-cursor-position.js"; import { createKeyDownHandler } from "./create-key-down-handler.js"; import { getWordAtCursor } from "./intellisence/get-word-at-cursor.js"; import { DefaultTokenHighlighter } from "./token-highlighter-default.js"; import { CompletionListProvider, CompletionPopoverProvider } from "./intellisence/completion-context.js"; const sharedFontStyle = { fontFamily: "inherit", fontSize: "inherit", fontWeight: "inherit", fontStyle: "inherit", letterSpacing: "inherit", lineHeight: "inherit", padding: "0", }; const triggerCharacters = [".", " "]; export function ExpressionEditor({ value, onChange: onValueChange, tokenize, highlight: Highlighter = DefaultTokenHighlighter, completionProvider, placeholder, disabled, readOnly, className, style, children, }) { // TODO @Lee: we should eventually allow multiline expressions but for now there isn't really a valid // use case that requires multiline expressions without also allow variable support. const multiline = false; const textareaRef = useRef(null); const depsRef = useRef(null); const tokens = useMemo(() => tokenize(value), [value, tokenize]); const completions = useCompletions(completionProvider); const cursorPosition = useCursorPosition(textareaRef); const trigger = useCompletionTrigger({ triggerCharacters, onTrigger: (toks, pos, word) => { cursorPosition.update(); completions.fetchCompletions(toks, pos, word); }, tokenize, }); const navigation = useKeyboardNavigation({ onValueChange, onAccepted: (value, cursorPosition) => { if (completionProvider) { trigger.triggerManually(value, cursorPosition + 1); } else { completions.dismiss(); } }, textareaRef, }); depsRef.current = { isOpen: completions.isOpen, selectedItem: completions.selectedItem, navigate: completions.navigate, dismiss: completions.dismiss, acceptCompletion: navigation.acceptCompletion, triggerManually: trigger.triggerManually, onValueChange, multiline, updateFilter: completions.updateFilter, handleInputChange: trigger.handleInputChange, cancel: trigger.cancel, }; const handleKeyDown = useCallback((e) => { const deps = depsRef.current; const textarea = e.currentTarget; const wordAtCursor = getWordAtCursor(textarea.value, textarea.selectionStart); const handler = createKeyDownHandler({ isPopoverOpen: deps.isOpen, multiline: deps.multiline, selectedItem: deps.selectedItem, wordAtCursor, onAcceptCompletion: deps.acceptCompletion, onNavigate: deps.navigate, onDismiss: deps.dismiss, onCancelTrigger: deps.cancel, onManualTrigger: () => { deps.triggerManually(textarea.value, textarea.selectionStart); }, onValueChange: deps.onValueChange, onExternalKeyDown: deps.onExternalKeyDown, }); handler(e); }, []); const handleChange = useCallback((e) => { const { value: newValue, selectionStart } = e.currentTarget; const deps = depsRef.current; deps.onValueChange(newValue); const word = getWordAtCursor(newValue, selectionStart); if (deps.isOpen) { const charBefore = newValue[selectionStart - 1] || ""; if (word.word.length > 0) { deps.updateFilter(word.word); } else if (charBefore === '"' || charBefore === "'") { // String delimiter typed — close the popover rather than re-triggering. deps.dismiss(); deps.cancel(); } else if (!triggerCharacters.includes(charBefore)) { // Non-word, non-trigger char (e.g. an operator) - re-fetch so the list // reflects the new syntactic context rather than keeping stale results. deps.triggerManually(newValue, selectionStart); } } deps.handleInputChange(newValue, selectionStart); }, []); const handleBlur = useCallback(() => { const deps = depsRef.current; deps.dismiss(); deps.cancel(); }, []); const handleItemSelect = useCallback((item) => { const textarea = textareaRef.current; if (!textarea) return; const wordAtCursor = getWordAtCursor(textarea.value, textarea.selectionStart); depsRef.current.acceptCompletion(item, wordAtCursor); }, []); const whiteSpace = multiline ? "pre-wrap" : "pre"; const wordBreak = multiline ? "break-word" : undefined; const gridCell = { gridArea: "1 / 1", ...sharedFontStyle, whiteSpace, wordBreak, overflowWrap: multiline ? "break-word" : undefined, boxSizing: "border-box", }; const popoverValue = useMemo(() => { return { isOpen: completions.isOpen, referenceElement: cursorPosition.virtualElement }; }, [completions.isOpen, cursorPosition.virtualElement]); const listValue = useMemo(() => { return { items: completions.filteredItems, loading: completions.isLoading, selectedIndex: completions.selectedIndex, onSelect: handleItemSelect, }; }, [completions.filteredItems, completions.isLoading, completions.selectedIndex, handleItemSelect]); return (_jsxs("div", { className: className, style: { display: "grid", width: "100%", ...style, }, "data-ln-expression-editor": true, children: [_jsx("textarea", { ref: textareaRef, value: value, onChange: handleChange, onKeyDown: handleKeyDown, onBlur: handleBlur, disabled: disabled, readOnly: readOnly, placeholder: placeholder, rows: multiline ? undefined : 1, "data-ln-expression-input": true, style: { ...gridCell, display: "block", width: "100%", resize: multiline ? "vertical" : "none", background: "transparent", color: "transparent", WebkitTextFillColor: "transparent", outline: "none", overflow: multiline ? "auto" : "hidden", margin: 0, border: 0, zIndex: 1, }, autoCapitalize: "off", autoComplete: "off", autoCorrect: "off", spellCheck: false }), _jsx("div", { "aria-hidden": "true", style: { ...gridCell, pointerEvents: "none", overflow: "hidden", zIndex: 0 }, "data-ln-expression--tokens": true, children: tokens.map((token) => (_jsx(Fragment, { children: _jsx(Highlighter, { token: token }) }, token.start))) }), _jsx(CompletionPopoverProvider, { value: popoverValue, children: _jsx(CompletionListProvider, { value: listValue, children: children }) })] })); }