import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { LexicalTypeaheadMenuPlugin, TypeaheadOption, useBasicTypeaheadTriggerMatch, } from "@lexical/react/LexicalTypeaheadMenuPlugin"; import type { LexicalEditor } from "lexical"; import { TextNode } from "lexical"; import { useCallback, useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { VariableNode, $createVariableNode } from "../nodes/VariableNode"; function $findAndTransformVariable(node: TextNode): null | TextNode { const text = node.getTextContent(); const regex = /\{[^}]+\}/g; // Regular expression to match {VARIABLE_NAME } 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 } /> ); }