UNPKG

@lexical/react

Version:

This package provides Lexical components and hooks for React applications.

256 lines (250 loc) 7.16 kB
/** * 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. * */ 'use strict'; var react$1 = require('@floating-ui/react'); var LexicalComposerContext = require('@lexical/react/LexicalComposerContext'); var lexical = require('lexical'); var react = require('react'); var jsxRuntime = require('react/jsx-runtime'); /** * 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. * */ class MenuOption { key; ref; constructor(key) { this.key = key; this.ref = { current: null }; this.setRefElement = this.setRefElement.bind(this); } setRefElement(element) { this.ref = { current: element }; } } class NodeContextMenuOption extends MenuOption { type; title; icon; disabled; $onSelect; $showOn; constructor(title, options) { super(title); this.type = 'item'; this.title = title; this.disabled = options.disabled ?? false; this.icon = options.icon ?? null; this.$onSelect = options.$onSelect; if (options.$showOn) { this.$showOn = options.$showOn; } } } class NodeContextMenuSeparator extends MenuOption { type; $showOn; constructor(options) { super('_separator'); this.type = 'separator'; if (options && options.$showOn) { this.$showOn = options.$showOn; } } } const ContextMenuSeparatorItem = /*#__PURE__*/react.forwardRef(({ className, disabled, ...props }, ref) => { return /*#__PURE__*/jsxRuntime.jsx("hr", { className: className }); }); const ContextMenuItem = /*#__PURE__*/react.forwardRef(({ className, label, disabled, icon, ...props }, ref) => { return /*#__PURE__*/jsxRuntime.jsxs("button", { ...props, className: className, ref: ref, role: "menuitem", disabled: disabled, children: [icon, label] }); }); const NodeContextMenuPlugin = /*#__PURE__*/react.forwardRef(({ items, className, itemClassName, separatorClassName }, forwardedRef) => { const [editor] = LexicalComposerContext.useLexicalComposerContext(); const [activeIndex, setActiveIndex] = react.useState(null); const [isOpen, setIsOpen] = react.useState(false); const listItemsRef = react.useRef([]); const listContentRef = react.useRef([]); const { refs, floatingStyles, context } = react$1.useFloating({ middleware: [react$1.offset({ alignmentAxis: 4, mainAxis: 5 }), react$1.flip({ fallbackPlacements: ['left-start'] }), react$1.shift({ padding: 10 })], onOpenChange: setIsOpen, open: isOpen, placement: 'right-start', strategy: 'fixed', whileElementsMounted: react$1.autoUpdate }); const role = react$1.useRole(context, { role: 'menu' }); const dismiss = react$1.useDismiss(context); const listNavigation = react$1.useListNavigation(context, { activeIndex, listRef: listItemsRef, onNavigate: setActiveIndex }); const typeahead = react$1.useTypeahead(context, { activeIndex, enabled: isOpen, listRef: listContentRef, onMatch: setActiveIndex }); const { getFloatingProps, getItemProps } = react$1.useInteractions([role, dismiss, listNavigation, typeahead]); const [renderItems, setRenderItems] = react.useState([]); react.useEffect(() => { function onContextMenu(e) { e.preventDefault(); refs.setPositionReference({ getBoundingClientRect() { return { bottom: e.clientY, height: 0, left: e.clientX, right: e.clientX, top: e.clientY, width: 0, x: e.clientX, y: e.clientY }; } }); let visibleItems = []; if (items) { editor.read(() => { const node = lexical.$getNearestNodeFromDOMNode(e.target); if (node) { visibleItems = items.filter(option => option.$showOn ? option.$showOn(node) : true); } }); } const renderableItems = visibleItems.map((option, index) => { if (option.type === 'separator') { return { className: separatorClassName, key: option.key + '-' + index, type: option.type }; } else { return { className: itemClassName, disabled: option.disabled, icon: option.icon, key: option.key, label: option.title, onClick: () => editor.update(() => option.$onSelect()), title: option.title, type: option.type }; } }); listContentRef.current = renderableItems.map(item => item.key); setRenderItems(renderableItems); setIsOpen(true); } return editor.registerRootListener(rootElement => { if (rootElement !== null) { rootElement.addEventListener('contextmenu', onContextMenu); return () => rootElement.removeEventListener('contextmenu', onContextMenu); } }); }, [items, itemClassName, separatorClassName, refs, editor]); return /*#__PURE__*/jsxRuntime.jsx(react$1.FloatingPortal, { children: isOpen && /*#__PURE__*/jsxRuntime.jsx(react$1.FloatingOverlay, { lockScroll: true, children: /*#__PURE__*/jsxRuntime.jsx(react$1.FloatingFocusManager, { context: context, initialFocus: refs.floating, children: /*#__PURE__*/jsxRuntime.jsx("div", { className: className, ref: refs.setFloating, style: floatingStyles, ...getFloatingProps(), children: renderItems.map((item, index) => { if (item.type === 'item') { return /*#__PURE__*/react.createElement(ContextMenuItem, { ...getItemProps({ ...item, onClick() { item.onClick(); setIsOpen(false); }, onMouseUp() { item.onClick(); setIsOpen(false); }, ref(node) { listItemsRef.current[index] = node; }, tabIndex: activeIndex === index ? 0 : -1 }), key: item.key }); } else if (item.type === 'separator') { return /*#__PURE__*/react.createElement(ContextMenuSeparatorItem, { ...getItemProps({ ...item, ref(node) { listItemsRef.current[index] = node; }, tabIndex: activeIndex === index ? 0 : -1 }), key: item.key }); } }) }) }) }) }); }); exports.NodeContextMenuOption = NodeContextMenuOption; exports.NodeContextMenuPlugin = NodeContextMenuPlugin; exports.NodeContextMenuSeparator = NodeContextMenuSeparator;