/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import type {MenuRenderFn, MenuResolution} from './shared/LexicalMenu'; import type {JSX} from 'react'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import { $getNodeByKey, COMMAND_PRIORITY_LOW, CommandListenerPriority, NodeKey, TextNode, } from 'lexical'; import * as React from 'react'; import {useCallback, useEffect, useState} from 'react'; import {LexicalMenu, MenuOption, useMenuAnchorRef} from './shared/LexicalMenu'; import {startTransition} from './shared/reactPatches'; export type NodeMenuPluginProps = { onSelectOption: ( option: TOption, textNodeContainingQuery: TextNode | null, closeMenu: () => void, matchingString: string, ) => void; options: Array; nodeKey: NodeKey | null; menuRenderFn?: MenuRenderFn; onClose?: () => void; onOpen?: (resolution: MenuResolution) => void; anchorClassName?: string; commandPriority?: CommandListenerPriority; parent?: HTMLElement; }; export function LexicalNodeMenuPlugin({ options, nodeKey, onClose, onOpen, onSelectOption, menuRenderFn, anchorClassName, commandPriority = COMMAND_PRIORITY_LOW, parent, }: NodeMenuPluginProps): JSX.Element | null { const [editor] = useLexicalComposerContext(); const [resolution, setResolution] = useState(null); const anchorElementRef = useMenuAnchorRef( resolution, setResolution, anchorClassName, parent, ); const closeNodeMenu = useCallback(() => { setResolution(null); if (onClose != null && resolution !== null) { onClose(); } }, [onClose, resolution]); const openNodeMenu = useCallback( (res: MenuResolution) => { setResolution(res); if (onOpen != null && resolution === null) { onOpen(res); } }, [onOpen, resolution], ); const positionOrCloseMenu = useCallback(() => { if (nodeKey) { editor.update(() => { const node = $getNodeByKey(nodeKey); const domElement = editor.getElementByKey(nodeKey); if (node != null && domElement != null) { if (resolution == null) { startTransition(() => openNodeMenu({ getRect: () => domElement.getBoundingClientRect(), }), ); } } }); } else if (nodeKey == null && resolution != null) { closeNodeMenu(); } }, [closeNodeMenu, editor, nodeKey, openNodeMenu, resolution]); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect positionOrCloseMenu(); }, [positionOrCloseMenu, nodeKey]); useEffect(() => { if (nodeKey != null) { return editor.registerUpdateListener(({dirtyElements}) => { if (dirtyElements.get(nodeKey)) { positionOrCloseMenu(); } }); } }, [editor, positionOrCloseMenu, nodeKey]); return anchorElementRef.current === null || resolution === null || editor === null ? null : ( ); } export {MenuOption, MenuRenderFn, MenuResolution};