@lexical/react 0.52.1-nightly.20261006.0 → 0.52.1-nightly.20261007.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.
- package/dist/LexicalAutoEmbedPlugin.dev.js +57 -34
- package/dist/LexicalAutoEmbedPlugin.prod.js +1 -1
- package/dist/LexicalTypeaheadMenuPlugin.dev.js +32 -8
- package/dist/LexicalTypeaheadMenuPlugin.prod.js +1 -1
- package/package.json +20 -20
- package/src/LexicalAutoEmbedPlugin.tsx +69 -41
- package/src/LexicalTypeaheadMenuPlugin.tsx +41 -14
|
@@ -13,7 +13,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|
|
13
13
|
import { LexicalNodeMenuPlugin } from '@lexical/react/LexicalNodeMenuPlugin';
|
|
14
14
|
import { objectKlassEquals } from '@lexical/utils';
|
|
15
15
|
import { $getNodeByKey, mergeRegister, PASTE_COMMAND, $onUpdate, COMMAND_PRIORITY_BEFORE_EDITOR, COMMAND_PRIORITY_EDITOR, $getSelection, COMMAND_PRIORITY_LOW, PASTE_TAG } from 'lexical';
|
|
16
|
-
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
16
|
+
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
17
17
|
import { jsx } from 'react/jsx-runtime';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -24,6 +24,11 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
24
24
|
*
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
function $getCurrentLink(key, url) {
|
|
28
|
+
const linkNode = $getNodeByKey(key);
|
|
29
|
+
return $isLinkNode(linkNode) && linkNode.isAttached() && linkNode.getURL() === url ? linkNode : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Watches for pasted AutoLink nodes that match any of the provided embed configurations (e.g., YouTube, Twitter URLs).
|
|
29
34
|
* When a match is found, it shows a menu offering to replace the link with an embedded node.
|
|
@@ -60,13 +65,17 @@ function LexicalAutoEmbedPlugin({
|
|
|
60
65
|
menuCommandPriority = COMMAND_PRIORITY_LOW
|
|
61
66
|
}) {
|
|
62
67
|
const [editor] = useLexicalComposerContext();
|
|
63
|
-
const [
|
|
64
|
-
const
|
|
68
|
+
const [embedMatch, setEmbedMatch] = useState(null);
|
|
69
|
+
const nodeKey = embedMatch === null ? null : embedMatch.key;
|
|
70
|
+
const activeEmbedConfig = embedMatch === null ? null : embedMatch.embedConfig;
|
|
71
|
+
// Only the most recent paste may open the menu, even when an older
|
|
72
|
+
// asynchronous parseUrl result resolves after a newer one.
|
|
73
|
+
const latestCheckRef = useRef(0);
|
|
65
74
|
const reset = useCallback(() => {
|
|
66
|
-
|
|
67
|
-
setActiveEmbedConfig(null);
|
|
75
|
+
setEmbedMatch(null);
|
|
68
76
|
}, []);
|
|
69
77
|
const checkIfLinkNodeIsEmbeddable = useCallback(async key => {
|
|
78
|
+
const check = ++latestCheckRef.current;
|
|
70
79
|
const url = editor.read('latest', function () {
|
|
71
80
|
const linkNode = $getNodeByKey(key);
|
|
72
81
|
if ($isLinkNode(linkNode)) {
|
|
@@ -76,11 +85,26 @@ function LexicalAutoEmbedPlugin({
|
|
|
76
85
|
if (url === undefined) {
|
|
77
86
|
return;
|
|
78
87
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// When several configs match, the last one wins, so check them from
|
|
89
|
+
// the end and stop at the first match to call parseUrl no more often
|
|
90
|
+
// than needed.
|
|
91
|
+
for (let i = embedConfigs.length - 1; i >= 0; i--) {
|
|
92
|
+
const embedConfig = embedConfigs[i];
|
|
93
|
+
const result = await Promise.resolve(embedConfig.parseUrl(url));
|
|
94
|
+
if (check !== latestCheckRef.current) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (result != null) {
|
|
98
|
+
// Edits made while parseUrl was pending may not be committed yet
|
|
99
|
+
if (editor.read('pending', () => $getCurrentLink(key, url) !== null)) {
|
|
100
|
+
setEmbedMatch({
|
|
101
|
+
embedConfig,
|
|
102
|
+
key,
|
|
103
|
+
result,
|
|
104
|
+
url
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
110
|
}, [editor, embedConfigs]);
|
|
@@ -122,31 +146,30 @@ function LexicalAutoEmbedPlugin({
|
|
|
122
146
|
return false;
|
|
123
147
|
}, COMMAND_PRIORITY_EDITOR);
|
|
124
148
|
}, [editor, embedConfigs, onOpenEmbedModalForConfig]);
|
|
125
|
-
const embedLinkViaActiveEmbedConfig = useCallback(
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
const node = $getNodeByKey(nodeKey);
|
|
129
|
-
if ($isLinkNode(node)) {
|
|
130
|
-
return node;
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
});
|
|
134
|
-
if ($isLinkNode(linkNode)) {
|
|
135
|
-
const result = await Promise.resolve(activeEmbedConfig.parseUrl(linkNode.__url));
|
|
136
|
-
if (result != null) {
|
|
137
|
-
editor.update(() => {
|
|
138
|
-
if (!$getSelection()) {
|
|
139
|
-
linkNode.selectEnd();
|
|
140
|
-
}
|
|
141
|
-
activeEmbedConfig.insertNode(editor, result);
|
|
142
|
-
if (linkNode.isAttached()) {
|
|
143
|
-
linkNode.remove();
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
149
|
+
const embedLinkViaActiveEmbedConfig = useCallback(() => {
|
|
150
|
+
if (embedMatch == null) {
|
|
151
|
+
return;
|
|
148
152
|
}
|
|
149
|
-
|
|
153
|
+
const {
|
|
154
|
+
embedConfig,
|
|
155
|
+
key,
|
|
156
|
+
result,
|
|
157
|
+
url
|
|
158
|
+
} = embedMatch;
|
|
159
|
+
editor.update(() => {
|
|
160
|
+
const linkNode = $getCurrentLink(key, url);
|
|
161
|
+
if (linkNode === null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!$getSelection()) {
|
|
165
|
+
linkNode.selectEnd();
|
|
166
|
+
}
|
|
167
|
+
embedConfig.insertNode(editor, result);
|
|
168
|
+
if (linkNode.isAttached()) {
|
|
169
|
+
linkNode.remove();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}, [editor, embedMatch]);
|
|
150
173
|
const options = useMemo(() => {
|
|
151
174
|
return activeEmbedConfig != null && nodeKey != null ? getMenuOptions(activeEmbedConfig, embedLinkViaActiveEmbedConfig, reset) : [];
|
|
152
175
|
}, [activeEmbedConfig, embedLinkViaActiveEmbedConfig, getMenuOptions, nodeKey, reset]);
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import{$isLinkNode as e,LinkNode as t,AutoLinkNode as n}from"@lexical/link";import{INSERT_EMBED_COMMAND as
|
|
9
|
+
import{$isLinkNode as e,LinkNode as t,AutoLinkNode as n}from"@lexical/link";import{INSERT_EMBED_COMMAND as r}from"@lexical/react/LexicalAutoEmbedPluginUtils";export{AutoEmbedOption,INSERT_EMBED_COMMAND,URL_MATCHER}from"@lexical/react/LexicalAutoEmbedPluginUtils";import{useLexicalComposerContext as o}from"@lexical/react/LexicalComposerContext";import{LexicalNodeMenuPlugin as l}from"@lexical/react/LexicalNodeMenuPlugin";import{objectKlassEquals as i}from"@lexical/utils";import{$getNodeByKey as a,mergeRegister as u,PASTE_COMMAND as c,$onUpdate as m,COMMAND_PRIORITY_BEFORE_EDITOR as s,COMMAND_PRIORITY_EDITOR as d,$getSelection as p,COMMAND_PRIORITY_LOW as f,PASTE_TAG as g}from"lexical";import{useState as x,useRef as C,useCallback as b,useEffect as E,useMemo as y}from"react";import{jsx as L}from"react/jsx-runtime";function M(t,n){const r=a(t);return e(r)&&r.isAttached()&&r.getURL()===n?r:null}function A({embedConfigs:A,onOpenEmbedModalForConfig:R,getMenuOptions:P,menuRenderFn:U,menuCommandPriority:k=f}){const[v]=o(),[D,O]=x(null),h=null===D?null:D.key,N=null===D?null:D.embedConfig,S=C(0),F=b(()=>{O(null)},[]),T=b(async t=>{const n=++S.current,r=v.read("latest",function(){const n=a(t);if(e(n))return n.getURL()});if(void 0!==r)for(let e=A.length-1;e>=0;e--){const o=A[e],l=await Promise.resolve(o.parseUrl(r));if(n!==S.current)return;if(null!=l)return void(v.read("pending",()=>null!==M(t,r))&&O({embedConfig:o,key:t,result:l,url:r}))}},[v,A]);E(()=>{let e=!1;const r=(t,{updateTags:n})=>{for(const[r,o]of t)"created"===o&&n.has(g)&&e?T(r):r===h&&F()};return u(v.registerCommand(c,t=>(e=i(t,ClipboardEvent)&&null!==t.clipboardData&&/^\S+$/.test(t.clipboardData.getData("text/plain")),e&&m(()=>{e=!1}),!1),s),...[t,n].map(e=>v.registerMutationListener(e,r,{skipInitialization:!0})))},[T,v,h,F]),E(()=>{if(R)return v.registerCommand(r,e=>{const t=A.find(({type:t})=>t===e);return!!t&&(R(t),!0)},d)},[v,A,R]);const _=b(()=>{if(null==D)return;const{embedConfig:e,key:t,result:n,url:r}=D;v.update(()=>{const o=M(t,r);null!==o&&(p()||o.selectEnd(),e.insertNode(v,n),o.isAttached()&&o.remove())})},[v,D]),I=y(()=>null!=N&&null!=h?P(N,_,F):[],[N,_,P,h,F]),j=b((e,t,n)=>{v.update(()=>{e.onSelect(t),n()})},[v]);return null!=h?/*#__PURE__*/L(l,{nodeKey:h,onClose:F,onSelectOption:j,options:I,menuRenderFn:U,commandPriority:k}):null}export{A as LexicalAutoEmbedPlugin};
|
|
@@ -662,13 +662,21 @@ function LexicalTypeaheadMenuPlugin({
|
|
|
662
662
|
}) {
|
|
663
663
|
const [editor] = useLexicalComposerContext();
|
|
664
664
|
const [resolution, setResolution] = useState(null);
|
|
665
|
-
const
|
|
665
|
+
const openGenerationRef = useRef(0);
|
|
666
|
+
// Whether the menu is open as far as onOpen/onClose are concerned. Unlike
|
|
667
|
+
// `resolution`, this sees an open whose transition has not rendered yet and
|
|
668
|
+
// a close whose onClose promise is still pending.
|
|
669
|
+
const isOpenRef = useRef(false);
|
|
666
670
|
const closeTypeahead = useCallback(() => {
|
|
667
|
-
if (
|
|
671
|
+
if (!isOpenRef.current) {
|
|
668
672
|
return;
|
|
669
673
|
}
|
|
674
|
+
isOpenRef.current = false;
|
|
675
|
+
const openGeneration = openGenerationRef.current;
|
|
670
676
|
const finish = () => {
|
|
671
|
-
|
|
677
|
+
if (openGenerationRef.current === openGeneration) {
|
|
678
|
+
setResolution(null);
|
|
679
|
+
}
|
|
672
680
|
};
|
|
673
681
|
let result;
|
|
674
682
|
try {
|
|
@@ -680,13 +688,29 @@ function LexicalTypeaheadMenuPlugin({
|
|
|
680
688
|
finish();
|
|
681
689
|
}
|
|
682
690
|
}
|
|
683
|
-
}, [onClose
|
|
691
|
+
}, [onClose]);
|
|
684
692
|
const openTypeahead = useCallback(res => {
|
|
693
|
+
// Track pending opens before their transition has rendered.
|
|
694
|
+
openGenerationRef.current++;
|
|
685
695
|
setResolution(res);
|
|
686
|
-
if (
|
|
687
|
-
|
|
696
|
+
if (!isOpenRef.current) {
|
|
697
|
+
isOpenRef.current = true;
|
|
698
|
+
if (onOpen != null) {
|
|
699
|
+
onOpen(res);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}, [onOpen]);
|
|
703
|
+
|
|
704
|
+
// LexicalMenu hides the menu when its anchor scrolls out of view. Route
|
|
705
|
+
// that through closeTypeahead so onClose still pairs with onOpen.
|
|
706
|
+
const setMenuResolution = useCallback(res => {
|
|
707
|
+
if (res === null) {
|
|
708
|
+
closeTypeahead();
|
|
709
|
+
} else {
|
|
710
|
+
setResolution(res);
|
|
688
711
|
}
|
|
689
|
-
}, [
|
|
712
|
+
}, [closeTypeahead]);
|
|
713
|
+
const anchorElementRef = useMenuAnchorRef(resolution, setMenuResolution, anchorClassName, parent);
|
|
690
714
|
useEffect(() => {
|
|
691
715
|
const updateListener = () => {
|
|
692
716
|
editor.read('latest', () => {
|
|
@@ -712,7 +736,7 @@ function LexicalTypeaheadMenuPlugin({
|
|
|
712
736
|
onQueryChange(match ? match.matchingString : null);
|
|
713
737
|
if (match !== null && (ignoreEntityBoundary || !isSelectionOnEntityBoundary(editor, match.leadOffset))) {
|
|
714
738
|
const isRangePositioned = tryToPositionRange(match.leadOffset, range, editorWindow, editor.getRootElement());
|
|
715
|
-
if (isRangePositioned
|
|
739
|
+
if (isRangePositioned) {
|
|
716
740
|
startTransition(() => openTypeahead({
|
|
717
741
|
getRect: () => range.getBoundingClientRect(),
|
|
718
742
|
match
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import{useLexicalComposerContext as t}from"@lexical/react/LexicalComposerContext";import{CAN_USE_DOM as e,getDOMShadowRoots as n,mergeRegister as
|
|
9
|
+
import{useLexicalComposerContext as t}from"@lexical/react/LexicalComposerContext";import{CAN_USE_DOM as e,getDOMShadowRoots as n,mergeRegister as o,registerEventListener as l,getRootOwnerDocument as r,COMMAND_PRIORITY_LOW as i,KEY_ARROW_DOWN_COMMAND as c,KEY_ARROW_UP_COMMAND as u,KEY_ESCAPE_COMMAND as s,KEY_TAB_COMMAND as a,KEY_ENTER_COMMAND as d,isDOMShadowRoot as p,isHTMLElement as m,getParentElement as f,$getSelection as g,$isRangeSelection as h,$isTextNode as y,getDOMSelection as x,getDOMSelectionPoints as C}from"lexical";import{useLayoutEffect as b,useEffect as v,useRef as w,useCallback as R,useState as E,useMemo as I,startTransition as S}from"react";import{SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND as O}from"@lexical/react/LexicalTypeaheadMenuPluginUtils";export{PUNCTUATION,SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND,getScrollParent,useBasicTypeaheadTriggerMatch}from"@lexical/react/LexicalTypeaheadMenuPluginUtils";import{getScrollParent as T}from"@lexical/utils";import P from"react-dom";import{jsx as N,jsxs as A}from"react/jsx-runtime";export{MenuOption}from"@lexical/react/LexicalMenuOption";const k=e?b:v,D=t=>{const e=t.closest("#typeahead-menu");if(!e)return;const n=e.getBoundingClientRect();n.top+n.height>window.innerHeight&&e.scrollIntoView({block:"center"}),n.top<0&&e.scrollIntoView({block:"center"}),t.scrollIntoView({block:"nearest"})};function M(t,e){const n=t.getBoundingClientRect(),o=e.getBoundingClientRect();return n.top>=o.top-6&&n.top<=o.bottom+6}function L(e,r,i,c){const[u]=t();v(()=>{if(null!=r&&null!=e){const t=u.getRootElement(),e=null!=t?T(t,!1):document.body;let s=!1,a=M(r,e);const d=function(){s||(window.requestAnimationFrame(function(){i(),s=!1}),s=!0);const t=M(r,e);t!==a&&(a=t,null!=c&&c(t))},p=new ResizeObserver(i),m=n(t??r);return p.observe(r),o(l(window,"resize",i),l(document,"scroll",d,{capture:!0,passive:!0}),...m.map(t=>l(t,"scroll",d,{capture:!0,passive:!0})),()=>p.unobserve(r))}},[r,u,c,i,e])}function B({index:t,isSelected:e,onClick:n,onMouseEnter:o,option:l}){let r="item";return e&&(r+=" selected"),/*#__PURE__*/A("li",{tabIndex:-1,className:r,ref:l.setRefElement,role:"option","aria-selected":e,id:"typeahead-item-"+t,onMouseEnter:o,onClick:n,children:[l.icon,/*#__PURE__*/N("span",{className:"text",children:l.title})]},l.key)}function F({close:t,editor:e,anchorElementRef:n,resolution:l,options:r,menuRenderFn:p,onSelectOption:m,shouldSplitNodeWithQuery:f=!1,commandPriority:y=i,preselectFirstItem:x=!0}){const[C,b]=E(null),w=null!==C?Math.min(r.length-1,C):null,S=l.match&&l.match.matchingString;v(()=>{x&&b(0)},[S,x]);const T=R(n=>{e.update(()=>{const e=null!=l.match&&f?function(t){const e=g();if(!h(e)||!e.isCollapsed())return null;const n=e.anchor;if("text"!==n.type)return null;const o=n.getNode();if(!o.isSimpleText())return null;const l=n.offset,r=o.getTextContent().slice(0,l),i=t.replaceableString.length,c=l-function(t,e,n){let o=n;for(let n=o;n<=e.length;n++)t.slice(-n)===e.substring(0,n)&&(o=n);return o}(r,t.matchingString,i);if(c<0)return null;let u;return 0===c?[u]=o.splitText(l):[,u]=o.splitText(c,l),u}(l.match):null;m(n,e,t,l.match?l.match.matchingString:"")})},[e,f,l.match,m,t]),A=R(t=>{const n=e.getRootElement();null!==n&&(n.setAttribute("aria-activedescendant","typeahead-item-"+t),b(t))},[e]),M=R(()=>n.current&&r.length?/*#__PURE__*/P.createPortal(/*#__PURE__*/N("div",{className:"typeahead-popover mentions-menu",children:/*#__PURE__*/N("ul",{children:r.map((t,e)=>/*#__PURE__*/N(B,{index:e,isSelected:w===e,onClick:()=>{b(e),T(t)},onMouseEnter:()=>{b(e)},option:t},t.key))})}),n.current):null,[n,r,w,T,b]);v(()=>()=>{const t=e.getRootElement();null!==t&&t.removeAttribute("aria-activedescendant")},[e]),k(()=>{null===r?b(null):null===w&&x&&A(0)},[r,w,A,x]);const L=null==p&&(null===r||!r.length);v(()=>o(e.registerCommand(O,({option:t})=>!(!t.ref||null==t.ref.current||(D(t.ref.current),0)),y)),[e,A,y]),v(()=>o(e.registerCommand(c,t=>{const n=t;if(L)return!1;const o=null===w?0:w!==r.length-1?w+1:0;A(o);const l=r[o];return l?(l.ref&&l.ref.current&&e.dispatchCommand(O,{index:o,option:l}),n.preventDefault(),n.stopImmediatePropagation(),!0):(A(-1),n.preventDefault(),n.stopImmediatePropagation(),!0)},y),e.registerCommand(u,t=>{const e=t;if(L)return!1;const n=null===w?r.length-1:0!==w?w-1:r.length-1;A(n);const o=r[n];return o?(o.ref&&o.ref.current&&D(o.ref.current),e.preventDefault(),e.stopImmediatePropagation(),!0):(A(-1),e.preventDefault(),e.stopImmediatePropagation(),!0)},y),e.registerCommand(s,e=>{const n=e;return!L&&(n.preventDefault(),n.stopImmediatePropagation(),t(),!0)},y),e.registerCommand(a,t=>{const e=t;return null!==r&&null!==w&&null!=r[w]&&(e.preventDefault(),e.stopImmediatePropagation(),T(r[w]),!0)},y),e.registerCommand(d,t=>!(null===r||null===w||null==r[w]||t&&t.shiftKey||(null!==t&&(t.preventDefault(),t.stopImmediatePropagation()),T(r[w]),0)),y)),[T,t,e,r,L,w,A,y]);const F=I(()=>({options:r,selectOptionAndCleanUp:T,selectedIndex:w,setHighlightedIndex:b}),[T,w,r]);return null!=p?p(n,F,l.match?l.match.matchingString:""):M()}function $(t,e){null!=e&&(t.className=e),t.setAttribute("aria-label","Typeahead menu"),t.setAttribute("role","listbox"),t.style.display="block",t.style.position="absolute"}function U(t){if("static"!==t.position)return!0;const e=t.willChange;return"none"!==t.transform||"none"!==t.perspective||"none"!==t.filter||"none"!==t.backdropFilter||t.contain.includes("paint")||t.contain.includes("layout")||t.contain.includes("strict")||t.contain.includes("content")||e.includes("transform")||e.includes("perspective")||e.includes("filter")||e.includes("contain")}function _({options:n,onQueryChange:o,onSelectOption:l,onOpen:c,onClose:u,menuRenderFn:s,triggerFn:a,anchorClassName:d,commandPriority:b=i,parent:I,preselectFirstItem:O=!0,ignoreEntityBoundary:T=!1}){const[P]=t(),[A,k]=E(null),D=w(0),M=w(!1),B=R(()=>{if(!M.current)return;M.current=!1;const t=D.current,e=()=>{D.current===t&&k(null)};let n;try{n=u&&u()}finally{n?n.then(e,e):e()}},[u]),_=R(t=>{D.current++,k(t),M.current||(M.current=!0,null!=c&&c(t))},[c]),H=function(n,o,l,i,c=!0){const[u]=t(),s=i??function(t){if(!e)return;const n=t.getRootElement();if(null!==n){const t=n.getRootNode();return p(t)?t:n.ownerDocument.body}return document.body}(u),a=e?r(u.getRootElement()).createElement("div"):null,d=w(a),g=R(()=>{if(null===d.current||void 0===s)return;d.current.style.top=d.current.style.bottom;const t=u.getRootElement(),e=d.current,o=e.firstChild;if(null!==t&&null!==n){const{left:r,top:i,width:u,height:a}=n.getRect(),g=d.current.offsetHeight,h=function(t){const e=p(t)?t.host:t;for(let t=m(e)?e:null;null!==t;t=f(t)){const e=t.ownerDocument.defaultView;if(null===e)break;if(U(e.getComputedStyle(t))){const e=t.getBoundingClientRect();return{left:e.left+t.clientLeft-t.scrollLeft,top:e.top+t.clientTop-t.scrollTop}}}return null}(s),y=t=>null!==h?t-h.left:t+window.pageXOffset,x=t=>null!==h?t-h.top:t+(c?window.pageYOffset:0);if(e.style.top=`${x(i+g+3)}px`,e.style.left=`${y(r)}px`,e.style.height=`${a}px`,e.style.width=`${u}px`,null!==o){o.style.top=`${i}`;const n=o.getBoundingClientRect(),l=n.height,c=n.width,u=t.getBoundingClientRect();r+c>u.right&&(e.style.left=`${y(u.right-c)}px`),(i+l>window.innerHeight||i+l>u.bottom)&&i-u.top>l+a&&(e.style.top=`${x(i-l-a)}px`)}e.isConnected||($(e,l),s.append(e)),e.setAttribute("id","typeahead-menu"),t.setAttribute("aria-controls","typeahead-menu")}},[u,n,c,l,s]);v(()=>{const t=u.getRootElement();return null!==n&&g(),()=>{null!==t&&t.removeAttribute("aria-controls");const e=d.current;null!==e&&e.isConnected&&(e.remove(),e.removeAttribute("id"))}},[u,g,n]);const h=R(t=>{null!==n&&(t||o(null))},[n,o]);return L(n,d.current,g,h),null!=a&&a===d.current&&($(a,l),null!=s&&s.append(a)),d}(A,R(t=>{null===t?B():k(t)},[B]),d,I);return v(()=>{const t=P.registerUpdateListener(()=>{P.read("latest",()=>{if(!P.isEditable())return void B();const t=()=>{P.isComposing()||B()},e=P._window||window,n=e.document.createRange(),l=g(),r=function(t){let e=null;return t.read("latest",()=>{const t=g();h(t)&&(e=function(t){const e=t.anchor;if("text"!==e.type)return null;const n=e.getNode();if(!n.isSimpleText())return null;const o=e.offset;return n.getTextContent().slice(0,o)}(t))}),e}(P);if(!h(l)||!l.isCollapsed()||null===r||null===n)return void t();const i=a(r,P);if(o(i?i.matchingString:null),null!==i&&(T||!function(t,e){return 0===e&&t.read("latest",()=>{const t=g();if(h(t)){const e=t.anchor.getNode().getPreviousSibling();return y(e)&&e.isTextEntity()}return!1})}(P,i.leadOffset))){const t=function(t,e,n,o){const l=x(n);if(null===l||!l.isCollapsed)return!1;const r=C(l,o),i=r.anchorNode,c=t,u=r.anchorOffset;if(null==i||null==u)return!1;try{e.setStart(i,c),e.setEnd(i,u)}catch(t){return!1}return!0}(i.leadOffset,n,e,P.getRootElement());if(t)return void S(()=>_({getRect:()=>n.getBoundingClientRect(),match:i}))}t()})});return()=>{t()}},[P,a,o,A,B,_,T]),v(()=>P.registerEditableListener(t=>{t||B()}),[P,B]),null===A||null===P||null===H.current?null:/*#__PURE__*/N(F,{close:B,resolution:A,editor:P,anchorElementRef:H,options:n,menuRenderFn:s,shouldSplitNodeWithQuery:!0,onSelectOption:l,commandPriority:b,preselectFirstItem:O})}export{_ as LexicalTypeaheadMenuPlugin,L as useDynamicPositioning};
|
package/package.json
CHANGED
|
@@ -8,28 +8,28 @@
|
|
|
8
8
|
"rich-text"
|
|
9
9
|
],
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"version": "0.52.1-nightly.
|
|
11
|
+
"version": "0.52.1-nightly.20261007.0",
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@floating-ui/react": "^0.27.19",
|
|
14
|
-
"@lexical/a11y": "0.52.1-nightly.
|
|
15
|
-
"@lexical/devtools-core": "0.52.1-nightly.
|
|
16
|
-
"@lexical/dragon": "0.52.1-nightly.
|
|
17
|
-
"@lexical/extension": "0.52.1-nightly.
|
|
18
|
-
"@lexical/hashtag": "0.52.1-nightly.
|
|
19
|
-
"@lexical/history": "0.52.1-nightly.
|
|
20
|
-
"@lexical/internal": "0.52.1-nightly.
|
|
21
|
-
"@lexical/link": "0.52.1-nightly.
|
|
22
|
-
"@lexical/list": "0.52.1-nightly.
|
|
23
|
-
"@lexical/mark": "0.52.1-nightly.
|
|
24
|
-
"@lexical/markdown": "0.52.1-nightly.
|
|
25
|
-
"@lexical/overflow": "0.52.1-nightly.
|
|
26
|
-
"@lexical/plain-text": "0.52.1-nightly.
|
|
27
|
-
"@lexical/rich-text": "0.52.1-nightly.
|
|
28
|
-
"@lexical/table": "0.52.1-nightly.
|
|
29
|
-
"@lexical/text": "0.52.1-nightly.
|
|
30
|
-
"@lexical/utils": "0.52.1-nightly.
|
|
31
|
-
"@lexical/yjs": "0.52.1-nightly.
|
|
32
|
-
"lexical": "0.52.1-nightly.
|
|
14
|
+
"@lexical/a11y": "0.52.1-nightly.20261007.0",
|
|
15
|
+
"@lexical/devtools-core": "0.52.1-nightly.20261007.0",
|
|
16
|
+
"@lexical/dragon": "0.52.1-nightly.20261007.0",
|
|
17
|
+
"@lexical/extension": "0.52.1-nightly.20261007.0",
|
|
18
|
+
"@lexical/hashtag": "0.52.1-nightly.20261007.0",
|
|
19
|
+
"@lexical/history": "0.52.1-nightly.20261007.0",
|
|
20
|
+
"@lexical/internal": "0.52.1-nightly.20261007.0",
|
|
21
|
+
"@lexical/link": "0.52.1-nightly.20261007.0",
|
|
22
|
+
"@lexical/list": "0.52.1-nightly.20261007.0",
|
|
23
|
+
"@lexical/mark": "0.52.1-nightly.20261007.0",
|
|
24
|
+
"@lexical/markdown": "0.52.1-nightly.20261007.0",
|
|
25
|
+
"@lexical/overflow": "0.52.1-nightly.20261007.0",
|
|
26
|
+
"@lexical/plain-text": "0.52.1-nightly.20261007.0",
|
|
27
|
+
"@lexical/rich-text": "0.52.1-nightly.20261007.0",
|
|
28
|
+
"@lexical/table": "0.52.1-nightly.20261007.0",
|
|
29
|
+
"@lexical/text": "0.52.1-nightly.20261007.0",
|
|
30
|
+
"@lexical/utils": "0.52.1-nightly.20261007.0",
|
|
31
|
+
"@lexical/yjs": "0.52.1-nightly.20261007.0",
|
|
32
|
+
"lexical": "0.52.1-nightly.20261007.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": ">=18.x",
|
|
@@ -33,7 +33,14 @@ import {
|
|
|
33
33
|
PASTE_TAG,
|
|
34
34
|
type TextNode,
|
|
35
35
|
} from 'lexical';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
type JSX,
|
|
38
|
+
useCallback,
|
|
39
|
+
useEffect,
|
|
40
|
+
useMemo,
|
|
41
|
+
useRef,
|
|
42
|
+
useState,
|
|
43
|
+
} from 'react';
|
|
37
44
|
|
|
38
45
|
export {
|
|
39
46
|
AutoEmbedOption,
|
|
@@ -84,6 +91,22 @@ type LexicalAutoEmbedPluginProps<TEmbedConfig extends EmbedConfig> = {
|
|
|
84
91
|
menuCommandPriority?: CommandListenerPriority;
|
|
85
92
|
};
|
|
86
93
|
|
|
94
|
+
type EmbedMatch<TEmbedConfig extends EmbedConfig> = {
|
|
95
|
+
embedConfig: TEmbedConfig;
|
|
96
|
+
key: NodeKey;
|
|
97
|
+
result: Parameters<TEmbedConfig['insertNode']>[1];
|
|
98
|
+
url: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function $getCurrentLink(key: NodeKey, url: string): LinkNode | null {
|
|
102
|
+
const linkNode = $getNodeByKey(key);
|
|
103
|
+
return $isLinkNode(linkNode) &&
|
|
104
|
+
linkNode.isAttached() &&
|
|
105
|
+
linkNode.getURL() === url
|
|
106
|
+
? linkNode
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
/**
|
|
88
111
|
* Watches for pasted AutoLink nodes that match any of the provided embed configurations (e.g., YouTube, Twitter URLs).
|
|
89
112
|
* When a match is found, it shows a menu offering to replace the link with an embedded node.
|
|
@@ -121,17 +144,22 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({
|
|
|
121
144
|
}: LexicalAutoEmbedPluginProps<TEmbedConfig>): JSX.Element | null {
|
|
122
145
|
const [editor] = useLexicalComposerContext();
|
|
123
146
|
|
|
124
|
-
const [
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
const [embedMatch, setEmbedMatch] = useState<EmbedMatch<TEmbedConfig> | null>(
|
|
148
|
+
null,
|
|
149
|
+
);
|
|
150
|
+
const nodeKey = embedMatch === null ? null : embedMatch.key;
|
|
151
|
+
const activeEmbedConfig = embedMatch === null ? null : embedMatch.embedConfig;
|
|
152
|
+
// Only the most recent paste may open the menu, even when an older
|
|
153
|
+
// asynchronous parseUrl result resolves after a newer one.
|
|
154
|
+
const latestCheckRef = useRef(0);
|
|
127
155
|
|
|
128
156
|
const reset = useCallback(() => {
|
|
129
|
-
|
|
130
|
-
setActiveEmbedConfig(null);
|
|
157
|
+
setEmbedMatch(null);
|
|
131
158
|
}, []);
|
|
132
159
|
|
|
133
160
|
const checkIfLinkNodeIsEmbeddable = useCallback(
|
|
134
161
|
async (key: NodeKey) => {
|
|
162
|
+
const check = ++latestCheckRef.current;
|
|
135
163
|
const url = editor.read('latest', function () {
|
|
136
164
|
const linkNode = $getNodeByKey(key);
|
|
137
165
|
if ($isLinkNode(linkNode)) {
|
|
@@ -141,11 +169,23 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({
|
|
|
141
169
|
if (url === undefined) {
|
|
142
170
|
return;
|
|
143
171
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
// When several configs match, the last one wins, so check them from
|
|
173
|
+
// the end and stop at the first match to call parseUrl no more often
|
|
174
|
+
// than needed.
|
|
175
|
+
for (let i = embedConfigs.length - 1; i >= 0; i--) {
|
|
176
|
+
const embedConfig = embedConfigs[i];
|
|
177
|
+
const result = await Promise.resolve(embedConfig.parseUrl(url));
|
|
178
|
+
if (check !== latestCheckRef.current) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (result != null) {
|
|
182
|
+
// Edits made while parseUrl was pending may not be committed yet
|
|
183
|
+
if (
|
|
184
|
+
editor.read('pending', () => $getCurrentLink(key, url) !== null)
|
|
185
|
+
) {
|
|
186
|
+
setEmbedMatch({embedConfig, key, result, url});
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
149
189
|
}
|
|
150
190
|
}
|
|
151
191
|
},
|
|
@@ -211,37 +251,25 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({
|
|
|
211
251
|
);
|
|
212
252
|
}, [editor, embedConfigs, onOpenEmbedModalForConfig]);
|
|
213
253
|
|
|
214
|
-
const embedLinkViaActiveEmbedConfig = useCallback(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
if ($isLinkNode(linkNode)) {
|
|
226
|
-
const result = await Promise.resolve(
|
|
227
|
-
activeEmbedConfig.parseUrl(linkNode.__url),
|
|
228
|
-
);
|
|
229
|
-
if (result != null) {
|
|
230
|
-
editor.update(() => {
|
|
231
|
-
if (!$getSelection()) {
|
|
232
|
-
linkNode.selectEnd();
|
|
233
|
-
}
|
|
234
|
-
activeEmbedConfig.insertNode(editor, result);
|
|
235
|
-
if (linkNode.isAttached()) {
|
|
236
|
-
linkNode.remove();
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
254
|
+
const embedLinkViaActiveEmbedConfig = useCallback(() => {
|
|
255
|
+
if (embedMatch == null) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const {embedConfig, key, result, url} = embedMatch;
|
|
259
|
+
editor.update(() => {
|
|
260
|
+
const linkNode = $getCurrentLink(key, url);
|
|
261
|
+
if (linkNode === null) {
|
|
262
|
+
return;
|
|
241
263
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
264
|
+
if (!$getSelection()) {
|
|
265
|
+
linkNode.selectEnd();
|
|
266
|
+
}
|
|
267
|
+
embedConfig.insertNode(editor, result);
|
|
268
|
+
if (linkNode.isAttached()) {
|
|
269
|
+
linkNode.remove();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}, [editor, embedMatch]);
|
|
245
273
|
|
|
246
274
|
const options = useMemo(() => {
|
|
247
275
|
return activeEmbedConfig != null && nodeKey != null
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
startTransition,
|
|
25
25
|
useCallback,
|
|
26
26
|
useEffect,
|
|
27
|
+
useRef,
|
|
27
28
|
useState,
|
|
28
29
|
} from 'react';
|
|
29
30
|
|
|
@@ -166,19 +167,21 @@ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>({
|
|
|
166
167
|
}: TypeaheadMenuPluginProps<TOption>): JSX.Element | null {
|
|
167
168
|
const [editor] = useLexicalComposerContext();
|
|
168
169
|
const [resolution, setResolution] = useState<MenuResolution | null>(null);
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
175
|
-
|
|
170
|
+
const openGenerationRef = useRef(0);
|
|
171
|
+
// Whether the menu is open as far as onOpen/onClose are concerned. Unlike
|
|
172
|
+
// `resolution`, this sees an open whose transition has not rendered yet and
|
|
173
|
+
// a close whose onClose promise is still pending.
|
|
174
|
+
const isOpenRef = useRef(false);
|
|
176
175
|
const closeTypeahead = useCallback(() => {
|
|
177
|
-
if (
|
|
176
|
+
if (!isOpenRef.current) {
|
|
178
177
|
return;
|
|
179
178
|
}
|
|
179
|
+
isOpenRef.current = false;
|
|
180
|
+
const openGeneration = openGenerationRef.current;
|
|
180
181
|
const finish = () => {
|
|
181
|
-
|
|
182
|
+
if (openGenerationRef.current === openGeneration) {
|
|
183
|
+
setResolution(null);
|
|
184
|
+
}
|
|
182
185
|
};
|
|
183
186
|
let result;
|
|
184
187
|
try {
|
|
@@ -190,16 +193,40 @@ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>({
|
|
|
190
193
|
finish();
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
|
-
}, [onClose
|
|
196
|
+
}, [onClose]);
|
|
194
197
|
|
|
195
198
|
const openTypeahead = useCallback(
|
|
196
199
|
(res: MenuResolution) => {
|
|
200
|
+
// Track pending opens before their transition has rendered.
|
|
201
|
+
openGenerationRef.current++;
|
|
197
202
|
setResolution(res);
|
|
198
|
-
if (
|
|
199
|
-
|
|
203
|
+
if (!isOpenRef.current) {
|
|
204
|
+
isOpenRef.current = true;
|
|
205
|
+
if (onOpen != null) {
|
|
206
|
+
onOpen(res);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
[onOpen],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// LexicalMenu hides the menu when its anchor scrolls out of view. Route
|
|
214
|
+
// that through closeTypeahead so onClose still pairs with onOpen.
|
|
215
|
+
const setMenuResolution = useCallback(
|
|
216
|
+
(res: MenuResolution | null) => {
|
|
217
|
+
if (res === null) {
|
|
218
|
+
closeTypeahead();
|
|
219
|
+
} else {
|
|
220
|
+
setResolution(res);
|
|
200
221
|
}
|
|
201
222
|
},
|
|
202
|
-
[
|
|
223
|
+
[closeTypeahead],
|
|
224
|
+
);
|
|
225
|
+
const anchorElementRef = useMenuAnchorRef(
|
|
226
|
+
resolution,
|
|
227
|
+
setMenuResolution,
|
|
228
|
+
anchorClassName,
|
|
229
|
+
parent,
|
|
203
230
|
);
|
|
204
231
|
|
|
205
232
|
useEffect(() => {
|
|
@@ -246,7 +273,7 @@ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>({
|
|
|
246
273
|
editorWindow,
|
|
247
274
|
editor.getRootElement(),
|
|
248
275
|
);
|
|
249
|
-
if (isRangePositioned
|
|
276
|
+
if (isRangePositioned) {
|
|
250
277
|
startTransition(() =>
|
|
251
278
|
openTypeahead({
|
|
252
279
|
getRect: () => range.getBoundingClientRect(),
|