@lexical/react 0.38.3-nightly.20251119.0 → 0.38.3-nightly.20251121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { CommandListenerPriority, LexicalNode } from 'lexical';
9
9
  import type { JSX } from 'react';
10
- import { MenuOption } from '@lexical/react/LexicalNodeMenuPlugin';
10
+ import { MenuOption, MenuRenderFn } from '@lexical/react/LexicalNodeMenuPlugin';
11
11
  import { LexicalCommand, LexicalEditor } from 'lexical';
12
12
  export type EmbedMatchResult<TEmbedMatchResult = unknown> = {
13
13
  url: string;
@@ -32,7 +32,8 @@ type LexicalAutoEmbedPluginProps<TEmbedConfig extends EmbedConfig> = {
32
32
  embedConfigs: Array<TEmbedConfig>;
33
33
  onOpenEmbedModalForConfig: (embedConfig: TEmbedConfig) => void;
34
34
  getMenuOptions: (activeEmbedConfig: TEmbedConfig, embedFn: () => void, dismissFn: () => void) => Array<AutoEmbedOption>;
35
+ menuRenderFn: MenuRenderFn<AutoEmbedOption>;
35
36
  menuCommandPriority?: CommandListenerPriority;
36
37
  };
37
- export declare function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({ embedConfigs, onOpenEmbedModalForConfig, getMenuOptions, menuCommandPriority, }: LexicalAutoEmbedPluginProps<TEmbedConfig>): JSX.Element | null;
38
+ export declare function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({ embedConfigs, onOpenEmbedModalForConfig, getMenuOptions, menuRenderFn, menuCommandPriority, }: LexicalAutoEmbedPluginProps<TEmbedConfig>): JSX.Element | null;
38
39
  export {};
@@ -39,6 +39,7 @@ function LexicalAutoEmbedPlugin({
39
39
  embedConfigs,
40
40
  onOpenEmbedModalForConfig,
41
41
  getMenuOptions,
42
+ menuRenderFn,
42
43
  menuCommandPriority = lexical.COMMAND_PRIORITY_LOW
43
44
  }) {
44
45
  const [editor] = LexicalComposerContext.useLexicalComposerContext();
@@ -134,6 +135,7 @@ function LexicalAutoEmbedPlugin({
134
135
  onClose: reset,
135
136
  onSelectOption: onSelectOption,
136
137
  options: options,
138
+ menuRenderFn: menuRenderFn,
137
139
  commandPriority: menuCommandPriority
138
140
  }) : null;
139
141
  }
@@ -37,6 +37,7 @@ function LexicalAutoEmbedPlugin({
37
37
  embedConfigs,
38
38
  onOpenEmbedModalForConfig,
39
39
  getMenuOptions,
40
+ menuRenderFn,
40
41
  menuCommandPriority = COMMAND_PRIORITY_LOW
41
42
  }) {
42
43
  const [editor] = useLexicalComposerContext();
@@ -132,6 +133,7 @@ function LexicalAutoEmbedPlugin({
132
133
  onClose: reset,
133
134
  onSelectOption: onSelectOption,
134
135
  options: options,
136
+ menuRenderFn: menuRenderFn,
135
137
  commandPriority: menuCommandPriority
136
138
  }) : null;
137
139
  }
@@ -13,6 +13,7 @@ import {MenuOption} from '@lexical/react/LexicalTypeaheadMenuPlugin';
13
13
  import type {LexicalCommand, LexicalEditor, NodeKey, TextNode} from 'lexical';
14
14
  import * as React from 'react';
15
15
  import {createCommand} from 'lexical';
16
+ import type {MenuRenderFn} from './LexicalTypeaheadMenuPlugin';
16
17
 
17
18
  export type EmbedMatchResult = {
18
19
  url: string,
@@ -42,6 +43,7 @@ type LexicalAutoEmbedPluginProps<TEmbedConfig> = {
42
43
  embedFn: () => void,
43
44
  dismissFn: () => void,
44
45
  ) => Array<AutoEmbedOption>,
46
+ menuRenderFn: MenuRenderFn<AutoEmbedOption>,
45
47
  };
46
48
 
47
49
  declare export class AutoEmbedOption extends MenuOption {
@@ -6,4 +6,4 @@
6
6
  *
7
7
  */
8
8
 
9
- "use strict";var e=require("@lexical/link"),t=require("@lexical/react/LexicalComposerContext"),n=require("@lexical/react/LexicalNodeMenuPlugin"),o=require("@lexical/utils"),i=require("lexical"),l=require("react"),r=require("react/jsx-runtime");const s=i.createCommand("INSERT_EMBED_COMMAND");class a extends n.MenuOption{title;onSelect;constructor(e,t){super(e),this.title=e,this.onSelect=t.onSelect.bind(this)}}exports.AutoEmbedOption=a,exports.INSERT_EMBED_COMMAND=s,exports.LexicalAutoEmbedPlugin=function({embedConfigs:a,onOpenEmbedModalForConfig:u,getMenuOptions:c,menuCommandPriority:d=i.COMMAND_PRIORITY_LOW}){const[m]=t.useLexicalComposerContext(),[p,x]=l.useState(null),[C,f]=l.useState(null),M=l.useCallback(()=>{x(null),f(null)},[]),g=l.useCallback(async t=>{const n=m.getEditorState().read(function(){const n=i.$getNodeByKey(t);if(e.$isLinkNode(n))return n.getURL()});if(void 0!==n)for(const e of a){null!=await Promise.resolve(e.parseUrl(n))&&(f(e),x(t))}},[m,a]);l.useEffect(()=>o.mergeRegister(...[e.LinkNode,e.AutoLinkNode].map(e=>m.registerMutationListener(e,(...e)=>((e,{updateTags:t,dirtyLeaves:n})=>{for(const[o,l]of e)"created"===l&&t.has(i.PASTE_TAG)&&n.size<=3?g(o):o===p&&M()})(...e),{skipInitialization:!0}))),[g,m,a,p,M]),l.useEffect(()=>m.registerCommand(s,e=>{const t=a.find(({type:t})=>t===e);return!!t&&(u(t),!0)},i.COMMAND_PRIORITY_EDITOR),[m,a,u]);const E=l.useCallback(async function(){if(null!=C&&null!=p){const t=m.getEditorState().read(()=>{const t=i.$getNodeByKey(p);return e.$isLinkNode(t)?t:null});if(e.$isLinkNode(t)){const e=await Promise.resolve(C.parseUrl(t.__url));null!=e&&m.update(()=>{i.$getSelection()||t.selectEnd(),C.insertNode(m,e),t.isAttached()&&t.remove()})}}},[C,m,p]),N=l.useMemo(()=>null!=C&&null!=p?c(C,E,M):[],[C,E,c,p,M]),L=l.useCallback((e,t,n)=>{m.update(()=>{e.onSelect(t),n()})},[m]);return null!=p?r.jsx(n.LexicalNodeMenuPlugin,{nodeKey:p,onClose:M,onSelectOption:L,options:N,commandPriority:d}):null},exports.URL_MATCHER=/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
9
+ "use strict";var e=require("@lexical/link"),t=require("@lexical/react/LexicalComposerContext"),n=require("@lexical/react/LexicalNodeMenuPlugin"),o=require("@lexical/utils"),i=require("lexical"),l=require("react"),r=require("react/jsx-runtime");const s=i.createCommand("INSERT_EMBED_COMMAND");class u extends n.MenuOption{title;onSelect;constructor(e,t){super(e),this.title=e,this.onSelect=t.onSelect.bind(this)}}exports.AutoEmbedOption=u,exports.INSERT_EMBED_COMMAND=s,exports.LexicalAutoEmbedPlugin=function({embedConfigs:u,onOpenEmbedModalForConfig:a,getMenuOptions:c,menuRenderFn:d,menuCommandPriority:m=i.COMMAND_PRIORITY_LOW}){const[p]=t.useLexicalComposerContext(),[x,C]=l.useState(null),[f,M]=l.useState(null),g=l.useCallback(()=>{C(null),M(null)},[]),E=l.useCallback(async t=>{const n=p.getEditorState().read(function(){const n=i.$getNodeByKey(t);if(e.$isLinkNode(n))return n.getURL()});if(void 0!==n)for(const e of u){null!=await Promise.resolve(e.parseUrl(n))&&(M(e),C(t))}},[p,u]);l.useEffect(()=>o.mergeRegister(...[e.LinkNode,e.AutoLinkNode].map(e=>p.registerMutationListener(e,(...e)=>((e,{updateTags:t,dirtyLeaves:n})=>{for(const[o,l]of e)"created"===l&&t.has(i.PASTE_TAG)&&n.size<=3?E(o):o===x&&g()})(...e),{skipInitialization:!0}))),[E,p,u,x,g]),l.useEffect(()=>p.registerCommand(s,e=>{const t=u.find(({type:t})=>t===e);return!!t&&(a(t),!0)},i.COMMAND_PRIORITY_EDITOR),[p,u,a]);const N=l.useCallback(async function(){if(null!=f&&null!=x){const t=p.getEditorState().read(()=>{const t=i.$getNodeByKey(x);return e.$isLinkNode(t)?t:null});if(e.$isLinkNode(t)){const e=await Promise.resolve(f.parseUrl(t.__url));null!=e&&p.update(()=>{i.$getSelection()||t.selectEnd(),f.insertNode(p,e),t.isAttached()&&t.remove()})}}},[f,p,x]),L=l.useMemo(()=>null!=f&&null!=x?c(f,N,g):[],[f,N,c,x,g]),A=l.useCallback((e,t,n)=>{p.update(()=>{e.onSelect(t),n()})},[p]);return null!=x?r.jsx(n.LexicalNodeMenuPlugin,{nodeKey:x,onClose:g,onSelectOption:A,options:L,menuRenderFn:d,commandPriority:m}):null},exports.URL_MATCHER=/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
@@ -6,4 +6,4 @@
6
6
  *
7
7
  */
8
8
 
9
- import{$isLinkNode as t,LinkNode as e,AutoLinkNode as o}from"@lexical/link";import{useLexicalComposerContext as n}from"@lexical/react/LexicalComposerContext";import{MenuOption as i,LexicalNodeMenuPlugin as r}from"@lexical/react/LexicalNodeMenuPlugin";import{mergeRegister as l}from"@lexical/utils";import{createCommand as s,$getNodeByKey as a,COMMAND_PRIORITY_EDITOR as c,$getSelection as u,COMMAND_PRIORITY_LOW as m,PASTE_TAG as d}from"lexical";import{useState as p,useCallback as f,useEffect as x,useMemo as g}from"react";import{jsx as w}from"react/jsx-runtime";const C=/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/,S=s("INSERT_EMBED_COMMAND");class y extends i{title;onSelect;constructor(t,e){super(t),this.title=t,this.onSelect=e.onSelect.bind(this)}}function E({embedConfigs:i,onOpenEmbedModalForConfig:s,getMenuOptions:C,menuCommandPriority:y=m}){const[E]=n(),[M,h]=p(null),[_,v]=p(null),z=f(()=>{h(null),v(null)},[]),A=f(async e=>{const o=E.getEditorState().read(function(){const o=a(e);if(t(o))return o.getURL()});if(void 0!==o)for(const t of i){null!=await Promise.resolve(t.parseUrl(o))&&(v(t),h(e))}},[E,i]);x(()=>l(...[e,o].map(t=>E.registerMutationListener(t,(...t)=>((t,{updateTags:e,dirtyLeaves:o})=>{for(const[n,i]of t)"created"===i&&e.has(d)&&o.size<=3?A(n):n===M&&z()})(...t),{skipInitialization:!0}))),[A,E,i,M,z]),x(()=>E.registerCommand(S,t=>{const e=i.find(({type:e})=>e===t);return!!e&&(s(e),!0)},c),[E,i,s]);const L=f(async function(){if(null!=_&&null!=M){const e=E.getEditorState().read(()=>{const e=a(M);return t(e)?e:null});if(t(e)){const t=await Promise.resolve(_.parseUrl(e.__url));null!=t&&E.update(()=>{u()||e.selectEnd(),_.insertNode(E,t),e.isAttached()&&e.remove()})}}},[_,E,M]),P=g(()=>null!=_&&null!=M?C(_,L,z):[],[_,L,C,M,z]),b=f((t,e,o)=>{E.update(()=>{t.onSelect(e),o()})},[E]);return null!=M?w(r,{nodeKey:M,onClose:z,onSelectOption:b,options:P,commandPriority:y}):null}export{y as AutoEmbedOption,S as INSERT_EMBED_COMMAND,E as LexicalAutoEmbedPlugin,C as URL_MATCHER};
9
+ import{$isLinkNode as e,LinkNode as t,AutoLinkNode as n}from"@lexical/link";import{useLexicalComposerContext as o}from"@lexical/react/LexicalComposerContext";import{MenuOption as r,LexicalNodeMenuPlugin as i}from"@lexical/react/LexicalNodeMenuPlugin";import{mergeRegister as l}from"@lexical/utils";import{createCommand as s,$getNodeByKey as a,COMMAND_PRIORITY_EDITOR as c,$getSelection as u,COMMAND_PRIORITY_LOW as m,PASTE_TAG as d}from"lexical";import{useState as p,useCallback as f,useEffect as x,useMemo as g}from"react";import{jsx as w}from"react/jsx-runtime";const C=/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/,S=s("INSERT_EMBED_COMMAND");class y extends r{title;onSelect;constructor(e,t){super(e),this.title=e,this.onSelect=t.onSelect.bind(this)}}function E({embedConfigs:r,onOpenEmbedModalForConfig:s,getMenuOptions:C,menuRenderFn:y,menuCommandPriority:E=m}){const[M]=o(),[h,_]=p(null),[v,z]=p(null),A=f(()=>{_(null),z(null)},[]),L=f(async t=>{const n=M.getEditorState().read(function(){const n=a(t);if(e(n))return n.getURL()});if(void 0!==n)for(const e of r){null!=await Promise.resolve(e.parseUrl(n))&&(z(e),_(t))}},[M,r]);x(()=>l(...[t,n].map(e=>M.registerMutationListener(e,(...e)=>((e,{updateTags:t,dirtyLeaves:n})=>{for(const[o,r]of e)"created"===r&&t.has(d)&&n.size<=3?L(o):o===h&&A()})(...e),{skipInitialization:!0}))),[L,M,r,h,A]),x(()=>M.registerCommand(S,e=>{const t=r.find(({type:t})=>t===e);return!!t&&(s(t),!0)},c),[M,r,s]);const P=f(async function(){if(null!=v&&null!=h){const t=M.getEditorState().read(()=>{const t=a(h);return e(t)?t:null});if(e(t)){const e=await Promise.resolve(v.parseUrl(t.__url));null!=e&&M.update(()=>{u()||t.selectEnd(),v.insertNode(M,e),t.isAttached()&&t.remove()})}}},[v,M,h]),b=g(()=>null!=v&&null!=h?C(v,P,A):[],[v,P,C,h,A]),N=f((e,t,n)=>{M.update(()=>{e.onSelect(t),n()})},[M]);return null!=h?w(i,{nodeKey:h,onClose:A,onSelectOption:N,options:b,menuRenderFn:y,commandPriority:E}):null}export{y as AutoEmbedOption,S as INSERT_EMBED_COMMAND,E as LexicalAutoEmbedPlugin,C as URL_MATCHER};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import type { MenuRenderFn, MenuResolution } from './shared/LexicalMenu';
9
+ import type { JSX } from 'react';
10
+ import { CommandListenerPriority, LexicalNode } from 'lexical';
11
+ import { ReactPortal, RefObject } from 'react';
12
+ import { MenuOption } from './shared/LexicalMenu';
13
+ export type ContextMenuRenderFn<TOption extends MenuOption> = (anchorElementRef: RefObject<HTMLElement | null>, itemProps: {
14
+ selectedIndex: number | null;
15
+ selectOptionAndCleanUp: (option: TOption) => void;
16
+ setHighlightedIndex: (index: number) => void;
17
+ options: Array<TOption>;
18
+ }, menuProps: {
19
+ setMenuRef: (element: HTMLElement | null) => void;
20
+ }) => ReactPortal | JSX.Element | null;
21
+ export type LexicalContextMenuPluginProps<TOption extends MenuOption> = {
22
+ onSelectOption: (option: TOption, textNodeContainingQuery: LexicalNode | null, closeMenu: () => void, matchingString: string) => void;
23
+ options: Array<TOption>;
24
+ onClose?: () => void;
25
+ onWillOpen?: (event: MouseEvent) => void;
26
+ onOpen?: (resolution: MenuResolution) => void;
27
+ menuRenderFn: ContextMenuRenderFn<TOption>;
28
+ anchorClassName?: string;
29
+ commandPriority?: CommandListenerPriority;
30
+ parent?: HTMLElement;
31
+ };
32
+ /**
33
+ * @deprecated Use LexicalNodeContextMenuPlugin instead.
34
+ */
35
+ export declare function LexicalContextMenuPlugin<TOption extends MenuOption>({ options, onWillOpen, onClose, onOpen, onSelectOption, menuRenderFn: contextMenuRenderFn, anchorClassName, commandPriority, parent, }: LexicalContextMenuPluginProps<TOption>): JSX.Element | null;
36
+ export { MenuOption, MenuRenderFn, MenuResolution };
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ var LexicalComposerContext = require('@lexical/react/LexicalComposerContext');
12
+ var utils = require('@lexical/utils');
13
+ var lexical = require('lexical');
14
+ var React = require('react');
15
+ var jsxRuntime = require('react/jsx-runtime');
16
+
17
+ function _interopNamespaceDefault(e) {
18
+ var n = Object.create(null);
19
+ if (e) {
20
+ for (var k in e) {
21
+ n[k] = e[k];
22
+ }
23
+ }
24
+ n.default = e;
25
+ return n;
26
+ }
27
+
28
+ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
29
+
30
+ /**
31
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
32
+ *
33
+ * This source code is licensed under the MIT license found in the
34
+ * LICENSE file in the root directory of this source tree.
35
+ *
36
+ */
37
+
38
+ const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
39
+
40
+ /**
41
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
42
+ *
43
+ * This source code is licensed under the MIT license found in the
44
+ * LICENSE file in the root directory of this source tree.
45
+ *
46
+ */
47
+
48
+
49
+ // This workaround is no longer necessary in React 19,
50
+ // but we currently support React >=17.x
51
+ // https://github.com/facebook/react/pull/26395
52
+ const useLayoutEffectImpl = CAN_USE_DOM ? React.useLayoutEffect : React.useEffect;
53
+
54
+ /**
55
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
56
+ *
57
+ * This source code is licensed under the MIT license found in the
58
+ * LICENSE file in the root directory of this source tree.
59
+ *
60
+ */
61
+
62
+ class MenuOption {
63
+ key;
64
+ ref;
65
+ constructor(key) {
66
+ this.key = key;
67
+ this.ref = {
68
+ current: null
69
+ };
70
+ this.setRefElement = this.setRefElement.bind(this);
71
+ }
72
+ setRefElement(element) {
73
+ this.ref = {
74
+ current: element
75
+ };
76
+ }
77
+ }
78
+ const scrollIntoViewIfNeeded = target => {
79
+ const typeaheadContainerNode = document.getElementById('typeahead-menu');
80
+ if (!typeaheadContainerNode) {
81
+ return;
82
+ }
83
+ const typeaheadRect = typeaheadContainerNode.getBoundingClientRect();
84
+ if (typeaheadRect.top + typeaheadRect.height > window.innerHeight) {
85
+ typeaheadContainerNode.scrollIntoView({
86
+ block: 'center'
87
+ });
88
+ }
89
+ if (typeaheadRect.top < 0) {
90
+ typeaheadContainerNode.scrollIntoView({
91
+ block: 'center'
92
+ });
93
+ }
94
+ target.scrollIntoView({
95
+ block: 'nearest'
96
+ });
97
+ };
98
+
99
+ /**
100
+ * Walk backwards along user input and forward through entity title to try
101
+ * and replace more of the user's text with entity.
102
+ */
103
+ function getFullMatchOffset(documentText, entryText, offset) {
104
+ let triggerOffset = offset;
105
+ for (let i = triggerOffset; i <= entryText.length; i++) {
106
+ if (documentText.slice(-i) === entryText.substring(0, i)) {
107
+ triggerOffset = i;
108
+ }
109
+ }
110
+ return triggerOffset;
111
+ }
112
+
113
+ /**
114
+ * Split Lexical TextNode and return a new TextNode only containing matched text.
115
+ * Common use cases include: removing the node, replacing with a new node.
116
+ */
117
+ function $splitNodeContainingQuery(match) {
118
+ const selection = lexical.$getSelection();
119
+ if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
120
+ return null;
121
+ }
122
+ const anchor = selection.anchor;
123
+ if (anchor.type !== 'text') {
124
+ return null;
125
+ }
126
+ const anchorNode = anchor.getNode();
127
+ if (!anchorNode.isSimpleText()) {
128
+ return null;
129
+ }
130
+ const selectionOffset = anchor.offset;
131
+ const textContent = anchorNode.getTextContent().slice(0, selectionOffset);
132
+ const characterOffset = match.replaceableString.length;
133
+ const queryOffset = getFullMatchOffset(textContent, match.matchingString, characterOffset);
134
+ const startOffset = selectionOffset - queryOffset;
135
+ if (startOffset < 0) {
136
+ return null;
137
+ }
138
+ let newNode;
139
+ if (startOffset === 0) {
140
+ [newNode] = anchorNode.splitText(selectionOffset);
141
+ } else {
142
+ [, newNode] = anchorNode.splitText(startOffset, selectionOffset);
143
+ }
144
+ return newNode;
145
+ }
146
+
147
+ // Got from https://stackoverflow.com/a/42543908/2013580
148
+ function getScrollParent(element, includeHidden) {
149
+ let style = getComputedStyle(element);
150
+ const excludeStaticParent = style.position === 'absolute';
151
+ const overflowRegex = /(auto|scroll)/;
152
+ if (style.position === 'fixed') {
153
+ return document.body;
154
+ }
155
+ for (let parent = element; parent = parent.parentElement;) {
156
+ style = getComputedStyle(parent);
157
+ if (excludeStaticParent && style.position === 'static') {
158
+ continue;
159
+ }
160
+ if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
161
+ return parent;
162
+ }
163
+ }
164
+ return document.body;
165
+ }
166
+ function isTriggerVisibleInNearestScrollContainer(targetElement, containerElement) {
167
+ const tRect = targetElement.getBoundingClientRect();
168
+ const cRect = containerElement.getBoundingClientRect();
169
+ const VISIBILITY_MARGIN_PX = 6;
170
+ return tRect.top >= cRect.top - VISIBILITY_MARGIN_PX && tRect.top <= cRect.bottom + VISIBILITY_MARGIN_PX;
171
+ }
172
+
173
+ // Reposition the menu on scroll, window resize, and element resize.
174
+ function useDynamicPositioning(resolution, targetElement, onReposition, onVisibilityChange) {
175
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
176
+ React.useEffect(() => {
177
+ if (targetElement != null && resolution != null) {
178
+ const rootElement = editor.getRootElement();
179
+ const rootScrollParent = rootElement != null ? getScrollParent(rootElement) : document.body;
180
+ let ticking = false;
181
+ let previousIsInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
182
+ const handleScroll = function () {
183
+ if (!ticking) {
184
+ window.requestAnimationFrame(function () {
185
+ onReposition();
186
+ ticking = false;
187
+ });
188
+ ticking = true;
189
+ }
190
+ const isInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
191
+ if (isInView !== previousIsInView) {
192
+ previousIsInView = isInView;
193
+ if (onVisibilityChange != null) {
194
+ onVisibilityChange(isInView);
195
+ }
196
+ }
197
+ };
198
+ const resizeObserver = new ResizeObserver(onReposition);
199
+ window.addEventListener('resize', onReposition);
200
+ document.addEventListener('scroll', handleScroll, {
201
+ capture: true,
202
+ passive: true
203
+ });
204
+ resizeObserver.observe(targetElement);
205
+ return () => {
206
+ resizeObserver.unobserve(targetElement);
207
+ window.removeEventListener('resize', onReposition);
208
+ document.removeEventListener('scroll', handleScroll, true);
209
+ };
210
+ }
211
+ }, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
212
+ }
213
+ const SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND = lexical.createCommand('SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND');
214
+ function LexicalMenu({
215
+ close,
216
+ editor,
217
+ anchorElementRef,
218
+ resolution,
219
+ options,
220
+ menuRenderFn,
221
+ onSelectOption,
222
+ shouldSplitNodeWithQuery = false,
223
+ commandPriority = lexical.COMMAND_PRIORITY_LOW,
224
+ preselectFirstItem = true
225
+ }) {
226
+ const [rawSelectedIndex, setHighlightedIndex] = React.useState(null);
227
+ // Clamp highlighted index if options list shrinks
228
+ const selectedIndex = rawSelectedIndex !== null ? Math.min(options.length - 1, rawSelectedIndex) : null;
229
+ const matchingString = resolution.match && resolution.match.matchingString;
230
+ React.useEffect(() => {
231
+ if (preselectFirstItem) {
232
+ setHighlightedIndex(0);
233
+ }
234
+ }, [matchingString, preselectFirstItem]);
235
+ const selectOptionAndCleanUp = React.useCallback(selectedEntry => {
236
+ editor.update(() => {
237
+ const textNodeContainingQuery = resolution.match != null && shouldSplitNodeWithQuery ? $splitNodeContainingQuery(resolution.match) : null;
238
+ onSelectOption(selectedEntry, textNodeContainingQuery, close, resolution.match ? resolution.match.matchingString : '');
239
+ });
240
+ }, [editor, shouldSplitNodeWithQuery, resolution.match, onSelectOption, close]);
241
+ const updateSelectedIndex = React.useCallback(index => {
242
+ const rootElem = editor.getRootElement();
243
+ if (rootElem !== null) {
244
+ rootElem.setAttribute('aria-activedescendant', 'typeahead-item-' + index);
245
+ setHighlightedIndex(index);
246
+ }
247
+ }, [editor]);
248
+ React.useEffect(() => {
249
+ return () => {
250
+ const rootElem = editor.getRootElement();
251
+ if (rootElem !== null) {
252
+ rootElem.removeAttribute('aria-activedescendant');
253
+ }
254
+ };
255
+ }, [editor]);
256
+ useLayoutEffectImpl(() => {
257
+ if (options === null) {
258
+ setHighlightedIndex(null);
259
+ } else if (selectedIndex === null && preselectFirstItem) {
260
+ updateSelectedIndex(0);
261
+ }
262
+ }, [options, selectedIndex, updateSelectedIndex, preselectFirstItem]);
263
+ React.useEffect(() => {
264
+ return utils.mergeRegister(editor.registerCommand(SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND, ({
265
+ option
266
+ }) => {
267
+ if (option.ref && option.ref.current != null) {
268
+ scrollIntoViewIfNeeded(option.ref.current);
269
+ return true;
270
+ }
271
+ return false;
272
+ }, commandPriority));
273
+ }, [editor, updateSelectedIndex, commandPriority]);
274
+ React.useEffect(() => {
275
+ return utils.mergeRegister(editor.registerCommand(lexical.KEY_ARROW_DOWN_COMMAND, payload => {
276
+ const event = payload;
277
+ if (options !== null && options.length) {
278
+ const newSelectedIndex = selectedIndex === null ? 0 : selectedIndex !== options.length - 1 ? selectedIndex + 1 : 0;
279
+ updateSelectedIndex(newSelectedIndex);
280
+ const option = options[newSelectedIndex];
281
+ if (!option) {
282
+ updateSelectedIndex(-1);
283
+ event.preventDefault();
284
+ event.stopImmediatePropagation();
285
+ return true;
286
+ }
287
+ if (option.ref && option.ref.current) {
288
+ editor.dispatchCommand(SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND, {
289
+ index: newSelectedIndex,
290
+ option
291
+ });
292
+ }
293
+ event.preventDefault();
294
+ event.stopImmediatePropagation();
295
+ }
296
+ return true;
297
+ }, commandPriority), editor.registerCommand(lexical.KEY_ARROW_UP_COMMAND, payload => {
298
+ const event = payload;
299
+ if (options !== null && options.length) {
300
+ const newSelectedIndex = selectedIndex === null ? options.length - 1 : selectedIndex !== 0 ? selectedIndex - 1 : options.length - 1;
301
+ updateSelectedIndex(newSelectedIndex);
302
+ const option = options[newSelectedIndex];
303
+ if (!option) {
304
+ updateSelectedIndex(-1);
305
+ event.preventDefault();
306
+ event.stopImmediatePropagation();
307
+ return true;
308
+ }
309
+ if (option.ref && option.ref.current) {
310
+ scrollIntoViewIfNeeded(option.ref.current);
311
+ }
312
+ event.preventDefault();
313
+ event.stopImmediatePropagation();
314
+ }
315
+ return true;
316
+ }, commandPriority), editor.registerCommand(lexical.KEY_ESCAPE_COMMAND, payload => {
317
+ const event = payload;
318
+ event.preventDefault();
319
+ event.stopImmediatePropagation();
320
+ close();
321
+ return true;
322
+ }, commandPriority), editor.registerCommand(lexical.KEY_TAB_COMMAND, payload => {
323
+ const event = payload;
324
+ if (options === null || selectedIndex === null || options[selectedIndex] == null) {
325
+ return false;
326
+ }
327
+ event.preventDefault();
328
+ event.stopImmediatePropagation();
329
+ selectOptionAndCleanUp(options[selectedIndex]);
330
+ return true;
331
+ }, commandPriority), editor.registerCommand(lexical.KEY_ENTER_COMMAND, event => {
332
+ if (options === null || selectedIndex === null || options[selectedIndex] == null) {
333
+ return false;
334
+ }
335
+ if (event !== null) {
336
+ event.preventDefault();
337
+ event.stopImmediatePropagation();
338
+ }
339
+ selectOptionAndCleanUp(options[selectedIndex]);
340
+ return true;
341
+ }, commandPriority));
342
+ }, [selectOptionAndCleanUp, close, editor, options, selectedIndex, updateSelectedIndex, commandPriority]);
343
+ const listItemProps = React.useMemo(() => ({
344
+ options,
345
+ selectOptionAndCleanUp,
346
+ selectedIndex,
347
+ setHighlightedIndex
348
+ }), [selectOptionAndCleanUp, selectedIndex, options]);
349
+ return menuRenderFn(anchorElementRef, listItemProps, resolution.match ? resolution.match.matchingString : '');
350
+ }
351
+ function setContainerDivAttributes(containerDiv, className) {
352
+ if (className != null) {
353
+ containerDiv.className = className;
354
+ }
355
+ containerDiv.setAttribute('aria-label', 'Typeahead menu');
356
+ containerDiv.setAttribute('role', 'listbox');
357
+ containerDiv.style.display = 'block';
358
+ containerDiv.style.position = 'absolute';
359
+ }
360
+ function useMenuAnchorRef(resolution, setResolution, className, parent = CAN_USE_DOM ? document.body : undefined, shouldIncludePageYOffset__EXPERIMENTAL = true) {
361
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
362
+ const initialAnchorElement = CAN_USE_DOM ? document.createElement('div') : null;
363
+ const anchorElementRef = React.useRef(initialAnchorElement);
364
+ const positionMenu = React.useCallback(() => {
365
+ if (anchorElementRef.current === null || parent === undefined) {
366
+ return;
367
+ }
368
+ anchorElementRef.current.style.top = anchorElementRef.current.style.bottom;
369
+ const rootElement = editor.getRootElement();
370
+ const containerDiv = anchorElementRef.current;
371
+ const menuEle = containerDiv.firstChild;
372
+ if (rootElement !== null && resolution !== null) {
373
+ const {
374
+ left,
375
+ top,
376
+ width,
377
+ height
378
+ } = resolution.getRect();
379
+ const anchorHeight = anchorElementRef.current.offsetHeight; // use to position under anchor
380
+ containerDiv.style.top = `${top + anchorHeight + 3 + (shouldIncludePageYOffset__EXPERIMENTAL ? window.pageYOffset : 0)}px`;
381
+ containerDiv.style.left = `${left + window.pageXOffset}px`;
382
+ containerDiv.style.height = `${height}px`;
383
+ containerDiv.style.width = `${width}px`;
384
+ if (menuEle !== null) {
385
+ menuEle.style.top = `${top}`;
386
+ const menuRect = menuEle.getBoundingClientRect();
387
+ const menuHeight = menuRect.height;
388
+ const menuWidth = menuRect.width;
389
+ const rootElementRect = rootElement.getBoundingClientRect();
390
+ if (left + menuWidth > rootElementRect.right) {
391
+ containerDiv.style.left = `${rootElementRect.right - menuWidth + window.pageXOffset}px`;
392
+ }
393
+ if ((top + menuHeight > window.innerHeight || top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight + height) {
394
+ containerDiv.style.top = `${top - menuHeight - height + (shouldIncludePageYOffset__EXPERIMENTAL ? window.pageYOffset : 0)}px`;
395
+ }
396
+ }
397
+ if (!containerDiv.isConnected) {
398
+ setContainerDivAttributes(containerDiv, className);
399
+ parent.append(containerDiv);
400
+ }
401
+ containerDiv.setAttribute('id', 'typeahead-menu');
402
+ rootElement.setAttribute('aria-controls', 'typeahead-menu');
403
+ }
404
+ }, [editor, resolution, shouldIncludePageYOffset__EXPERIMENTAL, className, parent]);
405
+ React.useEffect(() => {
406
+ const rootElement = editor.getRootElement();
407
+ if (resolution !== null) {
408
+ positionMenu();
409
+ }
410
+ return () => {
411
+ if (rootElement !== null) {
412
+ rootElement.removeAttribute('aria-controls');
413
+ }
414
+ // eslint-disable-next-line react-hooks/exhaustive-deps
415
+ const containerDiv = anchorElementRef.current;
416
+ if (containerDiv !== null && containerDiv.isConnected) {
417
+ containerDiv.remove();
418
+ containerDiv.removeAttribute('id');
419
+ }
420
+ };
421
+ }, [editor, positionMenu, resolution]);
422
+ const onVisibilityChange = React.useCallback(isInView => {
423
+ if (resolution !== null) {
424
+ if (!isInView) {
425
+ setResolution(null);
426
+ }
427
+ }
428
+ }, [resolution, setResolution]);
429
+ useDynamicPositioning(resolution, anchorElementRef.current, positionMenu, onVisibilityChange);
430
+
431
+ // Append the context for the menu immediately
432
+ if (initialAnchorElement != null && initialAnchorElement === anchorElementRef.current) {
433
+ setContainerDivAttributes(initialAnchorElement, className);
434
+ if (parent != null) {
435
+ parent.append(initialAnchorElement);
436
+ }
437
+ }
438
+ return anchorElementRef;
439
+ }
440
+
441
+ /**
442
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
443
+ *
444
+ * This source code is licensed under the MIT license found in the
445
+ * LICENSE file in the root directory of this source tree.
446
+ *
447
+ */
448
+
449
+ const PRE_PORTAL_DIV_SIZE = 1;
450
+
451
+ /**
452
+ * @deprecated Use LexicalNodeContextMenuPlugin instead.
453
+ */
454
+ function LexicalContextMenuPlugin({
455
+ options,
456
+ onWillOpen,
457
+ onClose,
458
+ onOpen,
459
+ onSelectOption,
460
+ menuRenderFn: contextMenuRenderFn,
461
+ anchorClassName,
462
+ commandPriority = lexical.COMMAND_PRIORITY_LOW,
463
+ parent
464
+ }) {
465
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
466
+ const [resolution, setResolution] = React.useState(null);
467
+ const menuRef = React__namespace.useRef(null);
468
+ const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName, parent);
469
+ const closeNodeMenu = React.useCallback(() => {
470
+ setResolution(null);
471
+ if (onClose != null && resolution !== null) {
472
+ onClose();
473
+ }
474
+ }, [onClose, resolution]);
475
+ const openNodeMenu = React.useCallback(res => {
476
+ setResolution(res);
477
+ if (onOpen != null && resolution === null) {
478
+ onOpen(res);
479
+ }
480
+ }, [onOpen, resolution]);
481
+ const handleContextMenu = React.useCallback(event => {
482
+ event.preventDefault();
483
+ if (onWillOpen != null) {
484
+ onWillOpen(event);
485
+ }
486
+ const zoom = utils.calculateZoomLevel(event.target);
487
+ openNodeMenu({
488
+ getRect: () => new DOMRect(event.clientX / zoom, event.clientY / zoom, PRE_PORTAL_DIV_SIZE, PRE_PORTAL_DIV_SIZE)
489
+ });
490
+ }, [openNodeMenu, onWillOpen]);
491
+ const handleClick = React.useCallback(event => {
492
+ if (resolution !== null && menuRef.current != null && event.target != null && lexical.isDOMNode(event.target) && !menuRef.current.contains(event.target)) {
493
+ closeNodeMenu();
494
+ }
495
+ }, [closeNodeMenu, resolution]);
496
+ React.useEffect(() => {
497
+ const editorElement = editor.getRootElement();
498
+ if (editorElement) {
499
+ editorElement.addEventListener('contextmenu', handleContextMenu);
500
+ return () => editorElement.removeEventListener('contextmenu', handleContextMenu);
501
+ }
502
+ }, [editor, handleContextMenu]);
503
+ React.useEffect(() => {
504
+ document.addEventListener('click', handleClick);
505
+ return () => document.removeEventListener('click', handleClick);
506
+ }, [editor, handleClick]);
507
+ return anchorElementRef.current === null || resolution === null || editor === null ? null : /*#__PURE__*/jsxRuntime.jsx(LexicalMenu, {
508
+ close: closeNodeMenu,
509
+ resolution: resolution,
510
+ editor: editor,
511
+ anchorElementRef: anchorElementRef,
512
+ options: options,
513
+ menuRenderFn: (anchorRef, itemProps) => contextMenuRenderFn(anchorRef, itemProps, {
514
+ setMenuRef: ref => {
515
+ menuRef.current = ref;
516
+ }
517
+ }),
518
+ onSelectOption: onSelectOption,
519
+ commandPriority: commandPriority
520
+ });
521
+ }
522
+
523
+ exports.LexicalContextMenuPlugin = LexicalContextMenuPlugin;
524
+ exports.MenuOption = MenuOption;