diff --git a/packages/ui/components/editor/Editor.tsx b/packages/ui/components/editor/Editor.tsx index a159634c06..a5abba0e3c 100644 --- a/packages/ui/components/editor/Editor.tsx +++ b/packages/ui/components/editor/Editor.tsx @@ -17,6 +17,8 @@ import type { Dispatch, SetStateAction } from "react"; import { classNames } from "@calcom/lib"; import ExampleTheme from "./ExampleTheme"; +import { VariableNode } from "./nodes/VariableNode"; +import AddVariablesPlugin from "./plugins/AddVariablesPlugin"; import AutoLinkPlugin from "./plugins/AutoLinkPlugin"; import ToolbarPlugin from "./plugins/ToolbarPlugin"; import "./stylesEditor.css"; @@ -60,6 +62,7 @@ const editorConfig = { TableRowNode, AutoLinkNode, LinkNode, + VariableNode, ], }; @@ -100,6 +103,7 @@ export const Editor = (props: TextEditorProps) => { + {props?.variables ? : null} } + let match; + + // Iterate over the text content + for (let i = 0; i < text.length; i++) { + regex.lastIndex = i; // Set the regex search position to the current index + match = regex.exec(text); + + if (match !== null) { + const matchedText = match[0]; // The entire matched text {VARIABLE_NAME } + const matchIndex = match.index; // Start index of the matched text + + // Ensure that we move the loop index past the current match + i = matchIndex + matchedText.length - 1; + + let targetNode; + if (matchIndex === 0) { + [targetNode] = node.splitText(matchIndex + matchedText.length); + } else { + [, targetNode] = node.splitText(matchIndex, matchIndex + matchedText.length); + } + const variableNode = $createVariableNode(matchedText); + targetNode.replace(variableNode); + return variableNode; + } + } + + return null; +} + +function useVariablesTransform(editor: LexicalEditor): void { + useEffect(() => { + if (!editor.hasNodes([VariableNode])) { + console.error("VariableNode is not registered in the editor"); + return; + } + return editor.registerNodeTransform(TextNode, (node) => { + let targetNode: TextNode | null = node; + + while (targetNode !== null) { + if (!targetNode.isSimpleText()) { + return; + } + targetNode = $findAndTransformVariable(targetNode); + } + }); + }, [editor]); +} + +class VariableTypeaheadOption extends TypeaheadOption { + name: string; + constructor(name: string) { + super(name); + this.name = name; + } +} + +interface AddVariablesPluginProps { + variables: string[]; +} + +export default function AddVariablesPlugin({ variables }: AddVariablesPluginProps): JSX.Element | null { + const { t } = useLocale(); + const [editor] = useLexicalComposerContext(); + useVariablesTransform(editor); + + const [queryString, setQueryString] = useState(null); + + const checkForTriggerMatch = useBasicTypeaheadTriggerMatch("{", { + minLength: 0, + }); + + const options = useMemo(() => { + return variables + .filter((variable) => variable.toLowerCase().includes(queryString?.toLowerCase() ?? "")) + .map((result) => new VariableTypeaheadOption(result)); + }, [variables, queryString]); + + const onSelectOption = useCallback( + (selectedOption: VariableTypeaheadOption, nodeToReplace: TextNode | null, closeMenu: () => void) => { + editor.update(() => { + const variableNode = $createVariableNode( + `{${t(`${selectedOption.name}_variable`).toUpperCase().replace(/ /g, "_")}}` + ); + if (nodeToReplace) { + nodeToReplace.replace(variableNode); + } + variableNode.select(); + closeMenu(); + }); + }, + [editor, t] + ); + + return ( + + onQueryChange={setQueryString} + onSelectOption={onSelectOption} + triggerFn={checkForTriggerMatch} + options={options} + menuRenderFn={(anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => + anchorElementRef.current && options.length + ? createPortal( + + + {options.map((option, index) => ( + { + setHighlightedIndex(index); + selectOptionAndCleanUp(option); + }} + onMouseEnter={() => { + setHighlightedIndex(index); + }}> + + {`{${t(`${option.name}_variable`).toUpperCase().replace(/ /g, "_")}}`} + + {t(`${option.name}_info`)} + + ))} + + , + anchorElementRef.current + ) + : null + } + /> + ); +}
+ {`{${t(`${option.name}_variable`).toUpperCase().replace(/ /g, "_")}}`} +