@1771technologies/lytenyte-pro
Version:
Blazingly fast headless React data grid with 100s of features.
159 lines (158 loc) • 7.51 kB
JavaScript
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 }) })] }));
}