@lexical/react
Version:
This package provides Lexical components and hooks for React applications.
256 lines (250 loc) • 7.16 kB
JavaScript
/**
* 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.
*
*/
;
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;