1||c.getRowSpan()>1){Ae(c)||ke(176);const e=Oe(c.__headerState);null!==t?t.insertAfter(e):s(l,e)}}}for(const e of i)e.setColSpan(1),e.setRowSpan(1)}})}function bn(e,t=!0){const n=new Map,o=(o,r,l)=>{const s=Ot(o,l),i=Mt(o,s,e,t);n.set(r,[i,s])},r=e.registerMutationListener(hn,t=>{e.getEditorState().read(()=>{for(const[e,r]of t){const t=n.get(e);if("created"===r||"updated"===r){const{tableNode:r,tableElement:l}=xt(e);void 0===t?o(r,e,l):l!==t[1]&&(t[0].removeListeners(),n.delete(e),o(r,e,l))}else"destroyed"===r&&void 0!==t&&(t[0].removeListeners(),n.delete(e))}},{editor:e})},{skipInitialization:!1});return()=>{r();for(const[,[e]]of n)e.removeListeners()}}function yn(e,t){e.hasNodes([hn])||ke(255);const{hasNestedTables:n=Se(!1)}=t??{};return i(e.registerCommand(Ke,e=>function({rows:e,columns:t,includeHeaders:n},o){const r=S()||Z();if(!r||!w(r))return!1;if(!o&&Qt(r.anchor.getNode()))return!1;const l=Be(Number(e),Number(t),n);c(l);const s=l.getFirstDescendant();return f(s)&&s.select(),!0}(e,n.peek()),me),e.registerCommand(Q,({nodes:t,selection:o},r)=>{if(n.peek()||e!==r||!w(o))return!1;return null!==Qt(o.anchor.getNode())&&t.some(mn)},me),e.registerCommand(pe,Sn,me),e.registerNodeTransform(hn,_n),e.registerNodeTransform(Ee,Cn),e.registerNodeTransform(Te,pn))}const Nn=Ce({build:(e,t,n)=>be(t),config:_e({hasCellBackgroundColor:!0,hasCellMerge:!0,hasHorizontalScroll:!0,hasNestedTables:!1,hasTabHandler:!0}),name:"@lexical/table/Table",nodes:()=>[hn,Ee,Te],register(e,t,n){const o=n.getOutput(),{hasNestedTables:r}=o;return i(we(()=>{const t=o.hasHorizontalScroll.value;an(e)!==t&&(un(e,t),e.registerNodeTransform(hn,()=>{})())}),yn(e,{hasNestedTables:r}),we(()=>bn(e,o.hasTabHandler.value)),we(()=>o.hasCellMerge.value?void 0:wn(e)),we(()=>o.hasCellBackgroundColor.value?void 0:e.registerNodeTransform(Te,e=>{null!==e.getBackgroundColor()&&e.setBackgroundColor(null)})))}});export{gt as $computeTableMap,mt as $computeTableMapSkipCellCheck,Oe as $createTableCellNode,gn as $createTableNode,Be as $createTableNodeWithDimensions,$e as $createTableRowNode,yt as $createTableSelection,Nt as $createTableSelectionFrom,ot as $deleteTableColumn,st as $deleteTableColumnAtSelection,it as $deleteTableColumn__EXPERIMENTAL,rt as $deleteTableRowAtSelection,lt as $deleteTableRow__EXPERIMENTAL,Gt as $findCellNode,Qt as $findTableNode,dn as $getElementForTableNode,pt as $getNodeTriplet,xt as $getTableAndElementByKey,Pe as $getTableCellNodeFromLexicalNode,_t as $getTableCellNodeRect,Je as $getTableColumnIndexFromTableCellNode,Ie as $getTableNodeFromLexicalNodeOrThrow,Ue as $getTableRowIndexFromTableCellNode,De as $getTableRowNodeFromTableCellNodeOrThrow,Ze as $insertTableColumn,et as $insertTableColumnAtSelection,tt as $insertTableColumn__EXPERIMENTAL,qe as $insertTableRow,Ve as $insertTableRowAtSelection,Ge as $insertTableRow__EXPERIMENTAL,an as $isScrollableTablesActive,Ae as $isTableCellNode,mn as $isTableNode,We as $isTableRowNode,bt as $isTableSelection,ut as $mergeCells,Xe as $removeTableRowAtIndex,dt as $unmergeCell,Ke as INSERT_TABLE_COMMAND,xe as TableCellHeaderStates,Te as TableCellNode,Nn as TableExtension,hn as TableNode,Tt as TableObserver,Ee as TableRowNode,Mt as applyTableHandlers,Wt as getDOMCellFromTarget,Ot as getTableElement,$t as getTableObserverFromTableElement,wn as registerTableCellUnmergeTransform,yn as registerTablePlugin,bn as registerTableSelectionObserver,un as setScrollableTablesActive};
+diff --git a/node_modules/@lexical/table/LexicalTableExtension.d.ts b/node_modules/@lexical/table/LexicalTableExtension.d.ts
+index ef9c6a4..fde457d 100644
+--- a/node_modules/@lexical/table/LexicalTableExtension.d.ts
++++ b/node_modules/@lexical/table/LexicalTableExtension.d.ts
+@@ -23,6 +23,12 @@ export interface TableConfig {
+ * When `true` (default `true`), tables will be wrapped in a `` to enable horizontal scrolling
+ */
+ hasHorizontalScroll: boolean;
++ /**
++ * When `true` (default `false`), nested tables will be allowed.
++ *
++ * @experimental Nested tables are not officially supported.
++ */
++ hasNestedTables: boolean;
+ }
+ /**
+ * Configures {@link TableNode}, {@link TableRowNode}, {@link TableCellNode} and
+diff --git a/node_modules/@lexical/table/LexicalTablePluginHelpers.d.ts b/node_modules/@lexical/table/LexicalTablePluginHelpers.d.ts
+index f3a54fd..8b2b305 100644
+--- a/node_modules/@lexical/table/LexicalTablePluginHelpers.d.ts
++++ b/node_modules/@lexical/table/LexicalTablePluginHelpers.d.ts
+@@ -5,6 +5,7 @@
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
++import { Signal } from '@lexical/extension';
+ import { LexicalEditor } from 'lexical';
+ /**
+ * Register a transform to ensure that all TableCellNode have a colSpan and rowSpan of 1.
+@@ -23,4 +24,6 @@ export declare function registerTableSelectionObserver(editor: LexicalEditor, ha
+ * @param editor The editor
+ * @returns An unregister callback
+ */
+-export declare function registerTablePlugin(editor: LexicalEditor): () => void;
++export declare function registerTablePlugin(editor: LexicalEditor, options?: {
++ hasNestedTables?: Signal;
++}): () => void;
diff --git a/patches/@lexical+utils+0.38.2.patch b/patches/@lexical+utils+0.38.2.patch
new file mode 100644
index 00000000..f025314d
--- /dev/null
+++ b/patches/@lexical+utils+0.38.2.patch
@@ -0,0 +1,292 @@
+diff --git a/node_modules/@lexical/utils/LexicalUtils.dev.js b/node_modules/@lexical/utils/LexicalUtils.dev.js
+index ddd4507..aafc2b0 100644
+--- a/node_modules/@lexical/utils/LexicalUtils.dev.js
++++ b/node_modules/@lexical/utils/LexicalUtils.dev.js
+@@ -393,9 +393,39 @@ function markSelection(editor, onReposition) {
+ function selectionAlwaysOnDisplay(editor) {
+ let removeSelectionMark = null;
+ const onSelectionChange = () => {
+- const domSelection = getSelection();
+- const domAnchorNode = domSelection && domSelection.anchorNode;
+ const editorRootElement = editor.getRootElement();
++ if (!editorRootElement) {
++ return;
++ }
++
++ // Get selection from the proper context (shadow DOM or document)
++ let domSelection = null;
++ let current = editorRootElement;
++ while (current) {
++ if (lexical.isDocumentFragment(current.nodeType)) {
++ const shadowRoot = current;
++
++ // Try modern getComposedRanges API first
++ if ('getComposedRanges' in Selection.prototype) {
++ const globalSelection = window.getSelection();
++ if (globalSelection) {
++ const ranges = globalSelection.getComposedRanges({
++ shadowRoots: [shadowRoot]
++ });
++ if (ranges.length > 0) {
++ // Use the global selection with composed ranges context
++ domSelection = globalSelection;
++ }
++ }
++ }
++ break;
++ }
++ current = current.parentNode;
++ }
++ if (!domSelection) {
++ domSelection = getSelection();
++ }
++ const domAnchorNode = domSelection && domSelection.anchorNode;
+ const isSelectionInsideEditor = domAnchorNode !== null && editorRootElement !== null && editorRootElement.contains(domAnchorNode);
+ if (isSelectionInsideEditor) {
+ if (removeSelectionMark !== null) {
+@@ -408,12 +438,27 @@ function selectionAlwaysOnDisplay(editor) {
+ }
+ }
+ };
+- document.addEventListener('selectionchange', onSelectionChange);
++
++ // Get the proper document context for event listeners
++ const editorRootElement = editor.getRootElement();
++ let targetDocument = document;
++ if (editorRootElement) {
++ let current = editorRootElement;
++ while (current) {
++ if (lexical.isDocumentFragment(current.nodeType)) {
++ targetDocument = current.ownerDocument || document;
++ break;
++ }
++ current = current.parentNode;
++ }
++ targetDocument = editorRootElement.ownerDocument || document;
++ }
++ targetDocument.addEventListener('selectionchange', onSelectionChange);
+ return () => {
+ if (removeSelectionMark !== null) {
+ removeSelectionMark();
+ }
+- document.removeEventListener('selectionchange', onSelectionChange);
++ targetDocument.removeEventListener('selectionchange', onSelectionChange);
+ };
+ }
+
+@@ -525,8 +570,10 @@ function mediaFileReader(files, acceptableMimeTypes) {
+ * before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a
+ * branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.
+ * It will then return all the nodes found in the search in an array of objects.
+- * @param startNode - The node to start the search, if omitted, it will start at the root node.
+- * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode.
++ * Preorder traversal is used, meaning that nodes are listed in the order of when they are FIRST encountered.
++ * @param startNode - The node to start the search (inclusive), if omitted, it will start at the root node.
++ * @param endNode - The node to end the search (inclusive), if omitted, it will find all descendants of the startingNode. If endNode
++ * is an ElementNode, it will stop before visiting any of its children.
+ * @returns An array of objects of all the nodes found by the search, including their depth into the tree.
+ * \\{depth: number, node: LexicalNode\\} It will always return at least 1 node (the start node).
+ */
+@@ -556,8 +603,10 @@ function $reverseDfs(startNode, endNode) {
+
+ /**
+ * $dfs iterator (left to right). Tree traversal is done on the fly as new values are requested with O(1) memory.
+- * @param startNode - The node to start the search, if omitted, it will start at the root node.
+- * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode.
++ * Preorder traversal is used, meaning that nodes are iterated over in the order of when they are FIRST encountered.
++ * @param startNode - The node to start the search (inclusive), if omitted, it will start at the root node.
++ * @param endNode - The node to end the search (inclusive), if omitted, it will find all descendants of the startingNode.
++ * If endNode is an ElementNode, the iterator will end as soon as it reaches the endNode (no children will be visited).
+ * @returns An iterator, each yielded value is a DFSNode. It will always return at least 1 node (the start node).
+ */
+ function $dfsIterator(startNode, endNode) {
+@@ -572,7 +621,7 @@ function $dfsCaretIterator(direction, startNode, endNode) {
+ const start = startNode || root;
+ const startCaret = lexical.$isElementNode(start) ? lexical.$getChildCaret(start, direction) : lexical.$getSiblingCaret(start, direction);
+ const startDepth = $getDepth(start);
+- const endCaret = endNode ? lexical.$getAdjacentChildCaret(lexical.$getChildCaretOrSelf(lexical.$getSiblingCaret(endNode, direction))) : $getEndCaret(start, direction);
++ const endCaret = endNode ? lexical.$getAdjacentChildCaret(lexical.$getChildCaretOrSelf(lexical.$getSiblingCaret(endNode, direction))) || $getEndCaret(endNode, direction) : $getEndCaret(start, direction);
+ let depth = startDepth;
+ return lexical.makeStepwiseIterator({
+ hasNext: state => state !== null,
+diff --git a/node_modules/@lexical/utils/LexicalUtils.dev.mjs b/node_modules/@lexical/utils/LexicalUtils.dev.mjs
+index 1ea9327..4ba0e8a 100644
+--- a/node_modules/@lexical/utils/LexicalUtils.dev.mjs
++++ b/node_modules/@lexical/utils/LexicalUtils.dev.mjs
+@@ -6,7 +6,7 @@
+ *
+ */
+
+-import { isHTMLElement, $getSelection, $isRangeSelection, $isElementNode, getDOMTextNode, $getAdjacentSiblingOrParentSiblingCaret, $getSiblingCaret, $getChildCaretOrSelf, $findMatchingParent, $cloneWithProperties, $setSelection, $getPreviousSelection, $caretFromPoint, $getChildCaret, $getRoot, $createParagraphNode, $getAdjacentChildCaret, $isChildCaret, $normalizeCaret, $setSelectionFromCaretRange, $getCollapsedCaretRange, $getCaretInDirection, $splitAtPointCaretNext, $isTextPointCaret, $rewindSiblingCaret, $getState, $setState, makeStepwiseIterator, $isSiblingCaret } from 'lexical';
++import { isHTMLElement, $getSelection, $isRangeSelection, $isElementNode, getDOMTextNode, isDocumentFragment, $getAdjacentSiblingOrParentSiblingCaret, $getSiblingCaret, $getChildCaretOrSelf, $findMatchingParent, $cloneWithProperties, $setSelection, $getPreviousSelection, $caretFromPoint, $getChildCaret, $getRoot, $createParagraphNode, $getAdjacentChildCaret, $isChildCaret, $normalizeCaret, $setSelectionFromCaretRange, $getCollapsedCaretRange, $getCaretInDirection, $splitAtPointCaretNext, $isTextPointCaret, $rewindSiblingCaret, $getState, $setState, makeStepwiseIterator, $isSiblingCaret } from 'lexical';
+ export { $findMatchingParent, $getAdjacentSiblingOrParentSiblingCaret, $splitNode, isBlockDomNode, isHTMLAnchorElement, isHTMLElement, isInlineDomNode } from 'lexical';
+ import { createRectsFromDOMRange } from '@lexical/selection';
+
+@@ -392,9 +392,39 @@ function markSelection(editor, onReposition) {
+ function selectionAlwaysOnDisplay(editor) {
+ let removeSelectionMark = null;
+ const onSelectionChange = () => {
+- const domSelection = getSelection();
+- const domAnchorNode = domSelection && domSelection.anchorNode;
+ const editorRootElement = editor.getRootElement();
++ if (!editorRootElement) {
++ return;
++ }
++
++ // Get selection from the proper context (shadow DOM or document)
++ let domSelection = null;
++ let current = editorRootElement;
++ while (current) {
++ if (isDocumentFragment(current.nodeType)) {
++ const shadowRoot = current;
++
++ // Try modern getComposedRanges API first
++ if ('getComposedRanges' in Selection.prototype) {
++ const globalSelection = window.getSelection();
++ if (globalSelection) {
++ const ranges = globalSelection.getComposedRanges({
++ shadowRoots: [shadowRoot]
++ });
++ if (ranges.length > 0) {
++ // Use the global selection with composed ranges context
++ domSelection = globalSelection;
++ }
++ }
++ }
++ break;
++ }
++ current = current.parentNode;
++ }
++ if (!domSelection) {
++ domSelection = getSelection();
++ }
++ const domAnchorNode = domSelection && domSelection.anchorNode;
+ const isSelectionInsideEditor = domAnchorNode !== null && editorRootElement !== null && editorRootElement.contains(domAnchorNode);
+ if (isSelectionInsideEditor) {
+ if (removeSelectionMark !== null) {
+@@ -407,12 +437,27 @@ function selectionAlwaysOnDisplay(editor) {
+ }
+ }
+ };
+- document.addEventListener('selectionchange', onSelectionChange);
++
++ // Get the proper document context for event listeners
++ const editorRootElement = editor.getRootElement();
++ let targetDocument = document;
++ if (editorRootElement) {
++ let current = editorRootElement;
++ while (current) {
++ if (isDocumentFragment(current.nodeType)) {
++ targetDocument = current.ownerDocument || document;
++ break;
++ }
++ current = current.parentNode;
++ }
++ targetDocument = editorRootElement.ownerDocument || document;
++ }
++ targetDocument.addEventListener('selectionchange', onSelectionChange);
+ return () => {
+ if (removeSelectionMark !== null) {
+ removeSelectionMark();
+ }
+- document.removeEventListener('selectionchange', onSelectionChange);
++ targetDocument.removeEventListener('selectionchange', onSelectionChange);
+ };
+ }
+
+@@ -524,8 +569,10 @@ function mediaFileReader(files, acceptableMimeTypes) {
+ * before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a
+ * branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.
+ * It will then return all the nodes found in the search in an array of objects.
+- * @param startNode - The node to start the search, if omitted, it will start at the root node.
+- * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode.
++ * Preorder traversal is used, meaning that nodes are listed in the order of when they are FIRST encountered.
++ * @param startNode - The node to start the search (inclusive), if omitted, it will start at the root node.
++ * @param endNode - The node to end the search (inclusive), if omitted, it will find all descendants of the startingNode. If endNode
++ * is an ElementNode, it will stop before visiting any of its children.
+ * @returns An array of objects of all the nodes found by the search, including their depth into the tree.
+ * \\{depth: number, node: LexicalNode\\} It will always return at least 1 node (the start node).
+ */
+@@ -555,8 +602,10 @@ function $reverseDfs(startNode, endNode) {
+
+ /**
+ * $dfs iterator (left to right). Tree traversal is done on the fly as new values are requested with O(1) memory.
+- * @param startNode - The node to start the search, if omitted, it will start at the root node.
+- * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode.
++ * Preorder traversal is used, meaning that nodes are iterated over in the order of when they are FIRST encountered.
++ * @param startNode - The node to start the search (inclusive), if omitted, it will start at the root node.
++ * @param endNode - The node to end the search (inclusive), if omitted, it will find all descendants of the startingNode.
++ * If endNode is an ElementNode, the iterator will end as soon as it reaches the endNode (no children will be visited).
+ * @returns An iterator, each yielded value is a DFSNode. It will always return at least 1 node (the start node).
+ */
+ function $dfsIterator(startNode, endNode) {
+@@ -571,7 +620,7 @@ function $dfsCaretIterator(direction, startNode, endNode) {
+ const start = startNode || root;
+ const startCaret = $isElementNode(start) ? $getChildCaret(start, direction) : $getSiblingCaret(start, direction);
+ const startDepth = $getDepth(start);
+- const endCaret = endNode ? $getAdjacentChildCaret($getChildCaretOrSelf($getSiblingCaret(endNode, direction))) : $getEndCaret(start, direction);
++ const endCaret = endNode ? $getAdjacentChildCaret($getChildCaretOrSelf($getSiblingCaret(endNode, direction))) || $getEndCaret(endNode, direction) : $getEndCaret(start, direction);
+ let depth = startDepth;
+ return makeStepwiseIterator({
+ hasNext: state => state !== null,
+diff --git a/node_modules/@lexical/utils/LexicalUtils.prod.js b/node_modules/@lexical/utils/LexicalUtils.prod.js
+index a85716c..f3ee24d 100644
+--- a/node_modules/@lexical/utils/LexicalUtils.prod.js
++++ b/node_modules/@lexical/utils/LexicalUtils.prod.js
+@@ -6,4 +6,4 @@
+ *
+ */
+
+-"use strict";var e=require("lexical"),t=require("@lexical/selection");function n(e,...t){const n=new URL("https://lexical.dev/docs/error"),r=new URLSearchParams;r.append("code",e);for(const e of t)r.append("v",e);throw n.search=r.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}const r="undefined"!=typeof window&&void 0!==window.document&&void 0!==window.document.createElement,o=r&&"documentMode"in document?document.documentMode:null,i=r&&/Mac|iPod|iPhone|iPad/.test(navigator.platform),s=r&&/^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent),l=!(!r||!("InputEvent"in window)||o)&&"getTargetRanges"in new window.InputEvent("input"),a=r&&/Version\/[\d.]+.*Safari/.test(navigator.userAgent),c=r&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream,u=r&&/Android/.test(navigator.userAgent),d=r&&/^(?=.*Chrome).*/i.test(navigator.userAgent),g=r&&u&&d,f=r&&/AppleWebKit\/[\d.]+/.test(navigator.userAgent)&&i&&!d;function p(...e){const t=[];for(const n of e)if(n&&"string"==typeof n)for(const[e]of n.matchAll(/\S+/g))t.push(e);return t}function m(...e){return()=>{for(let t=e.length-1;t>=0;t--)e[t]();e.length=0}}function h(e){return`${e}px`}const $={attributes:!0,characterData:!0,childList:!0,subtree:!0};function x(r,o,i){let s=null,l=null,a=null,c=[];const u=document.createElement("div");function d(){null===s&&n(182),null===l&&n(183);const{left:e,top:a}=l.getBoundingClientRect(),d=t.createRectsFromDOMRange(r,o);var g,f;u.isConnected||(f=u,(g=l).insertBefore(f,g.firstChild));let p=!1;for(let t=0;td.length;)c.pop();p&&i(c)}function g(){l=null,s=null,null!==a&&a.disconnect(),a=null,u.remove();for(const e of c)e.remove();c=[]}u.style.position="relative";const f=r.registerRootListener(function t(){const n=r.getRootElement();if(null===n)return g();const o=n.parentElement;if(!e.isHTMLElement(o))return g();g(),s=n,l=o,a=new MutationObserver(e=>{const n=r.getRootElement(),o=n&&n.parentElement;if(n!==s||o!==l)return t();for(const t of e)if(!u.contains(t.target))return d()}),a.observe(o,$),d()});return()=>{f(),g()}}function S(t,n,r){if("text"!==t.type&&e.$isElementNode(n)){const e=n.getDOMSlot(r);return[e.element,e.getFirstChildOffset()+t.offset]}return[e.getDOMTextNode(r)||r,t.offset]}function C(t,n){let r=null,o=null,i=null,s=null,l=null,a=null,c=()=>{};function u(u){u.read(()=>{const u=e.$getSelection();if(!e.$isRangeSelection(u))return r=null,i=null,s=null,a=null,c(),void(c=()=>{});const[d,g]=function(e){const t=e.getStartEndPoints();return e.isBackward()?[t[1],t[0]]:t}(u),f=d.getNode(),p=f.getKey(),m=d.offset,$=g.getNode(),C=$.getKey(),E=g.offset,v=t.getElementByKey(p),N=t.getElementByKey(C),A=null===r||v!==o||m!==i||p!==r.getKey(),w=null===s||N!==l||E!==a||C!==s.getKey();if((A||w)&&null!==v&&null!==N){const e=function(e,t,n,r,o,i,s){const l=(e._window?e._window.document:document).createRange();return l.setStart(...S(t,n,r)),l.setEnd(...S(o,i,s)),l}(t,d,f,v,g,$,N);c(),c=x(t,e,e=>{if(void 0===n)for(const t of e){const e=t.style;"Highlight"!==e.background&&(e.background="Highlight"),"HighlightText"!==e.color&&(e.color="HighlightText"),e.marginTop!==h(-1.5)&&(e.marginTop=h(-1.5)),e.paddingTop!==h(4)&&(e.paddingTop=h(4)),e.paddingBottom!==h(0)&&(e.paddingBottom=h(0))}else n(e)})}r=f,o=v,i=m,s=$,l=N,a=E})}return u(t.getEditorState()),m(t.registerUpdateListener(({editorState:e})=>u(e)),()=>{c()})}const E=l,v=r,N=u,A=g,w=i,b=f,y=d,P=s,R=c,I=a;function O(e,t){for(const n of t)if(e.type.startsWith(n))return!0;return!1}function M(e,t){return T("next",e,t)}function T(t,n,r){const o=e.$getRoot(),i=n||o,s=e.$isElementNode(i)?e.$getChildCaret(i,t):e.$getSiblingCaret(i,t),l=L(i),a=r?e.$getAdjacentChildCaret(e.$getChildCaretOrSelf(e.$getSiblingCaret(r,t))):function(t,n){const r=e.$getAdjacentSiblingOrParentSiblingCaret(e.$getSiblingCaret(t,n));return r&&r[0]}(i,t);let c=l;return e.makeStepwiseIterator({hasNext:e=>null!==e,initial:s,map:e=>({depth:c,node:e.origin}),step:t=>{if(t.isSameNodeCaret(a))return null;e.$isChildCaret(t)&&c++;const n=e.$getAdjacentSiblingOrParentSiblingCaret(t);return!n||n[0].isSameNodeCaret(a)?null:(c+=n[1],n[0])}})}function L(e){let t=-1;for(let n=e;null!==n;n=n.getParent())t++;return t}function _(e,t){return T("previous",e,t)}function D(t,r,o){let i=e.$getCaretInDirection(r,"next");for(let t=i;t;t=e.$splitAtPointCaretNext(t,o))i=t;return e.$isTextPointCaret(i)&&n(283),i.insert(t.isInline()?e.$createParagraphNode().append(t):t),e.$getCaretInDirection(e.$getSiblingCaret(t.getLatest(),"next"),r.direction)}let F=!(P||!v)&&void 0;function B(t,n,r){let o=!1;for(const i of j(t))n(i)?null!==r&&r(i):(o=!0,e.$isElementNode(i)&&B(i,n,r||(e=>i.insertAfter(e))),i.remove());return o}function j(t){return k(e.$getChildCaret(t,"previous"))}function k(t){return e.makeStepwiseIterator({hasNext:e.$isSiblingCaret,initial:t.getAdjacentCaret(),map:e=>e.origin.getLatest(),step:e=>e.getAdjacentCaret()})}exports.$findMatchingParent=e.$findMatchingParent,exports.$getAdjacentSiblingOrParentSiblingCaret=e.$getAdjacentSiblingOrParentSiblingCaret,exports.$splitNode=e.$splitNode,exports.isBlockDomNode=e.isBlockDomNode,exports.isHTMLAnchorElement=e.isHTMLAnchorElement,exports.isHTMLElement=e.isHTMLElement,exports.isInlineDomNode=e.isInlineDomNode,exports.$descendantsMatching=function(t,n){const r=[],o=Array.from(t).reverse();for(let t=o.pop();void 0!==t;t=o.pop())if(n(t))r.push(t);else if(e.$isElementNode(t))for(const e of j(t))o.push(e);return r},exports.$dfs=function(e,t){return Array.from(M(e,t))},exports.$dfsIterator=M,exports.$filter=function(e,t){const n=[];for(let r=0;re.$isElementNode(t)&&!t.isInline());return e.$isElementNode(r)||n(4,t.__key),r},exports.$getNearestNodeOfType=function(e,t){let n=e;for(;null!=n;){if(n instanceof t)return n;n=n.getParent()}return null},exports.$getNextRightPreorderNode=function(t){const n=e.$getChildCaretOrSelf(e.$getSiblingCaret(t,"previous")),r=e.$getAdjacentSiblingOrParentSiblingCaret(n,"root");return r&&r[0].origin},exports.$getNextSiblingOrParentSibling=function(t){const n=e.$getAdjacentSiblingOrParentSiblingCaret(e.$getSiblingCaret(t,"next"));return n&&[n[0].origin,n[1]]},exports.$insertFirst=function(t,n){e.$getChildCaret(t,"next").insert(n)},exports.$insertNodeToNearestRoot=function(t){const n=e.$getSelection()||e.$getPreviousSelection();let r;if(e.$isRangeSelection(n))r=e.$caretFromPoint(n.focus,"next");else{if(null!=n){const t=n.getNodes(),o=t[t.length-1];o&&(r=e.$getSiblingCaret(o,"next"))}r=r||e.$getChildCaret(e.$getRoot(),"previous").getFlipped().insert(e.$createParagraphNode())}const o=D(t,r),i=e.$getAdjacentChildCaret(o),s=e.$isChildCaret(i)?e.$normalizeCaret(i):o;return e.$setSelectionFromCaretRange(e.$getCollapsedCaretRange(s)),t.getLatest()},exports.$insertNodeToNearestRootAtCaret=D,exports.$isEditorIsNestedEditor=function(e){return null!==e._parentEditor},exports.$lastToFirstIterator=j,exports.$restoreEditorState=function(t,n){const r=new Map,o=t._pendingEditorState;for(const[t,o]of n._nodeMap)r.set(t,e.$cloneWithProperties(o));o&&(o._nodeMap=r),t._dirtyType=2;const i=n._selection;e.$setSelection(null===i?null:i.clone())},exports.$reverseDfs=function(e,t){return Array.from(_(e,t))},exports.$reverseDfsIterator=_,exports.$unwrapAndFilterDescendants=function(e,t){return B(e,t,null)},exports.$unwrapNode=function(t){e.$rewindSiblingCaret(e.$getSiblingCaret(t,"next")).splice(1,t.getChildren())},exports.$wrapNodeInElement=function(e,t){const n=t();return e.replace(n),n.append(e),n},exports.CAN_USE_BEFORE_INPUT=E,exports.CAN_USE_DOM=v,exports.IS_ANDROID=N,exports.IS_ANDROID_CHROME=A,exports.IS_APPLE=w,exports.IS_APPLE_WEBKIT=b,exports.IS_CHROME=y,exports.IS_FIREFOX=P,exports.IS_IOS=R,exports.IS_SAFARI=I,exports.addClassNamesToElement=function(e,...t){const n=p(...t);n.length>0&&e.classList.add(...n)},exports.calculateZoomLevel=function(e){let t=1;if(function(){if(void 0===F){const e=document.createElement("div");e.style.cssText="position: absolute; opacity: 0; width: 100px; left: -1000px;",document.body.appendChild(e);const t=e.getBoundingClientRect();e.style.setProperty("zoom","2"),F=e.getBoundingClientRect().width===t.width,document.body.removeChild(e)}return F}())for(;e;)t*=Number(window.getComputedStyle(e).getPropertyValue("zoom")),e=e.parentElement;return t},exports.isMimeType=O,exports.makeStateWrapper=function(t){const n=n=>e.$getState(n,t),r=(n,r)=>e.$setState(n,t,r);return{$get:n,$set:r,accessors:[n,r],makeGetterMethod:()=>function(){return n(this)},makeSetterMethod:()=>function(e){return r(this,e)},stateConfig:t}},exports.markSelection=C,exports.mediaFileReader=function(e,t){const n=e[Symbol.iterator]();return new Promise((e,r)=>{const o=[],i=()=>{const{done:s,value:l}=n.next();if(s)return e(o);const a=new FileReader;a.addEventListener("error",r),a.addEventListener("load",()=>{const e=a.result;"string"==typeof e&&o.push({file:l,result:e}),i()}),O(l,t)?a.readAsDataURL(l):i()};i()})},exports.mergeRegister=m,exports.objectKlassEquals=function(e,t){return null!==e&&Object.getPrototypeOf(e).constructor.name===t.name},exports.positionNodeOnRange=x,exports.registerNestedElementResolver=function(e,t,n,r){const o=e=>e instanceof t;return e.registerNodeTransform(t,e=>{const t=(e=>{const t=e.getChildren();for(let e=0;e0&&e.classList.remove(...n)},exports.selectionAlwaysOnDisplay=function(e){let t=null;const n=()=>{const n=getSelection(),r=n&&n.anchorNode,o=e.getRootElement();null!==r&&null!==o&&o.contains(r)?null!==t&&(t(),t=null):null===t&&(t=C(e))};return document.addEventListener("selectionchange",n),()=>{null!==t&&t(),document.removeEventListener("selectionchange",n)}};
++"use strict";var e=require("lexical"),t=require("@lexical/selection");function n(e,...t){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",e);for(const e of t)o.append("v",e);throw n.search=o.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}const o="undefined"!=typeof window&&void 0!==window.document&&void 0!==window.document.createElement,r=o&&"documentMode"in document?document.documentMode:null,i=o&&/Mac|iPod|iPhone|iPad/.test(navigator.platform),s=o&&/^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent),l=!(!o||!("InputEvent"in window)||r)&&"getTargetRanges"in new window.InputEvent("input"),a=o&&/Version\/[\d.]+.*Safari/.test(navigator.userAgent),c=o&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream,u=o&&/Android/.test(navigator.userAgent),d=o&&/^(?=.*Chrome).*/i.test(navigator.userAgent),g=o&&u&&d,f=o&&/AppleWebKit\/[\d.]+/.test(navigator.userAgent)&&i&&!d;function p(...e){const t=[];for(const n of e)if(n&&"string"==typeof n)for(const[e]of n.matchAll(/\S+/g))t.push(e);return t}function m(...e){return()=>{for(let t=e.length-1;t>=0;t--)e[t]();e.length=0}}function h(e){return`${e}px`}const $={attributes:!0,characterData:!0,childList:!0,subtree:!0};function x(o,r,i){let s=null,l=null,a=null,c=[];const u=document.createElement("div");function d(){null===s&&n(182),null===l&&n(183);const{left:e,top:a}=l.getBoundingClientRect(),d=t.createRectsFromDOMRange(o,r);var g,f;u.isConnected||(f=u,(g=l).insertBefore(f,g.firstChild));let p=!1;for(let t=0;td.length;)c.pop();p&&i(c)}function g(){l=null,s=null,null!==a&&a.disconnect(),a=null,u.remove();for(const e of c)e.remove();c=[]}u.style.position="relative";const f=o.registerRootListener(function t(){const n=o.getRootElement();if(null===n)return g();const r=n.parentElement;if(!e.isHTMLElement(r))return g();g(),s=n,l=r,a=new MutationObserver(e=>{const n=o.getRootElement(),r=n&&n.parentElement;if(n!==s||r!==l)return t();for(const t of e)if(!u.contains(t.target))return d()}),a.observe(r,$),d()});return()=>{f(),g()}}function S(t,n,o){if("text"!==t.type&&e.$isElementNode(n)){const e=n.getDOMSlot(o);return[e.element,e.getFirstChildOffset()+t.offset]}return[e.getDOMTextNode(o)||o,t.offset]}function C(t,n){let o=null,r=null,i=null,s=null,l=null,a=null,c=()=>{};function u(u){u.read(()=>{const u=e.$getSelection();if(!e.$isRangeSelection(u))return o=null,i=null,s=null,a=null,c(),void(c=()=>{});const[d,g]=function(e){const t=e.getStartEndPoints();return e.isBackward()?[t[1],t[0]]:t}(u),f=d.getNode(),p=f.getKey(),m=d.offset,$=g.getNode(),C=$.getKey(),E=g.offset,v=t.getElementByKey(p),N=t.getElementByKey(C),w=null===o||v!==r||m!==i||p!==o.getKey(),y=null===s||N!==l||E!==a||C!==s.getKey();if((w||y)&&null!==v&&null!==N){const e=function(e,t,n,o,r,i,s){const l=(e._window?e._window.document:document).createRange();return l.setStart(...S(t,n,o)),l.setEnd(...S(r,i,s)),l}(t,d,f,v,g,$,N);c(),c=x(t,e,e=>{if(void 0===n)for(const t of e){const e=t.style;"Highlight"!==e.background&&(e.background="Highlight"),"HighlightText"!==e.color&&(e.color="HighlightText"),e.marginTop!==h(-1.5)&&(e.marginTop=h(-1.5)),e.paddingTop!==h(4)&&(e.paddingTop=h(4)),e.paddingBottom!==h(0)&&(e.paddingBottom=h(0))}else n(e)})}o=f,r=v,i=m,s=$,l=N,a=E})}return u(t.getEditorState()),m(t.registerUpdateListener(({editorState:e})=>u(e)),()=>{c()})}const E=l,v=o,N=u,w=g,y=i,b=f,A=d,R=s,P=c,I=a;function T(e,t){for(const n of t)if(e.type.startsWith(n))return!0;return!1}function O(e,t){return D("next",e,t)}function M(t,n){const o=e.$getAdjacentSiblingOrParentSiblingCaret(e.$getSiblingCaret(t,n));return o&&o[0]}function D(t,n,o){const r=e.$getRoot(),i=n||r,s=e.$isElementNode(i)?e.$getChildCaret(i,t):e.$getSiblingCaret(i,t),l=L(i),a=o?e.$getAdjacentChildCaret(e.$getChildCaretOrSelf(e.$getSiblingCaret(o,t)))||M(o,t):M(i,t);let c=l;return e.makeStepwiseIterator({hasNext:e=>null!==e,initial:s,map:e=>({depth:c,node:e.origin}),step:t=>{if(t.isSameNodeCaret(a))return null;e.$isChildCaret(t)&&c++;const n=e.$getAdjacentSiblingOrParentSiblingCaret(t);return!n||n[0].isSameNodeCaret(a)?null:(c+=n[1],n[0])}})}function L(e){let t=-1;for(let n=e;null!==n;n=n.getParent())t++;return t}function _(e,t){return D("previous",e,t)}function F(t,o,r){let i=e.$getCaretInDirection(o,"next");for(let t=i;t;t=e.$splitAtPointCaretNext(t,r))i=t;return e.$isTextPointCaret(i)&&n(283),i.insert(t.isInline()?e.$createParagraphNode().append(t):t),e.$getCaretInDirection(e.$getSiblingCaret(t.getLatest(),"next"),o.direction)}let k=!(R||!v)&&void 0;function B(t,n,o){let r=!1;for(const i of j(t))n(i)?null!==o&&o(i):(r=!0,e.$isElementNode(i)&&B(i,n,o||(e=>i.insertAfter(e))),i.remove());return r}function j(t){return H(e.$getChildCaret(t,"previous"))}function H(t){return e.makeStepwiseIterator({hasNext:e.$isSiblingCaret,initial:t.getAdjacentCaret(),map:e=>e.origin.getLatest(),step:e=>e.getAdjacentCaret()})}exports.$findMatchingParent=e.$findMatchingParent,exports.$getAdjacentSiblingOrParentSiblingCaret=e.$getAdjacentSiblingOrParentSiblingCaret,exports.$splitNode=e.$splitNode,exports.isBlockDomNode=e.isBlockDomNode,exports.isHTMLAnchorElement=e.isHTMLAnchorElement,exports.isHTMLElement=e.isHTMLElement,exports.isInlineDomNode=e.isInlineDomNode,exports.$descendantsMatching=function(t,n){const o=[],r=Array.from(t).reverse();for(let t=r.pop();void 0!==t;t=r.pop())if(n(t))o.push(t);else if(e.$isElementNode(t))for(const e of j(t))r.push(e);return o},exports.$dfs=function(e,t){return Array.from(O(e,t))},exports.$dfsIterator=O,exports.$filter=function(e,t){const n=[];for(let o=0;oe.$isElementNode(t)&&!t.isInline());return e.$isElementNode(o)||n(4,t.__key),o},exports.$getNearestNodeOfType=function(e,t){let n=e;for(;null!=n;){if(n instanceof t)return n;n=n.getParent()}return null},exports.$getNextRightPreorderNode=function(t){const n=e.$getChildCaretOrSelf(e.$getSiblingCaret(t,"previous")),o=e.$getAdjacentSiblingOrParentSiblingCaret(n,"root");return o&&o[0].origin},exports.$getNextSiblingOrParentSibling=function(t){const n=e.$getAdjacentSiblingOrParentSiblingCaret(e.$getSiblingCaret(t,"next"));return n&&[n[0].origin,n[1]]},exports.$insertFirst=function(t,n){e.$getChildCaret(t,"next").insert(n)},exports.$insertNodeToNearestRoot=function(t){const n=e.$getSelection()||e.$getPreviousSelection();let o;if(e.$isRangeSelection(n))o=e.$caretFromPoint(n.focus,"next");else{if(null!=n){const t=n.getNodes(),r=t[t.length-1];r&&(o=e.$getSiblingCaret(r,"next"))}o=o||e.$getChildCaret(e.$getRoot(),"previous").getFlipped().insert(e.$createParagraphNode())}const r=F(t,o),i=e.$getAdjacentChildCaret(r),s=e.$isChildCaret(i)?e.$normalizeCaret(i):r;return e.$setSelectionFromCaretRange(e.$getCollapsedCaretRange(s)),t.getLatest()},exports.$insertNodeToNearestRootAtCaret=F,exports.$isEditorIsNestedEditor=function(e){return null!==e._parentEditor},exports.$lastToFirstIterator=j,exports.$restoreEditorState=function(t,n){const o=new Map,r=t._pendingEditorState;for(const[t,r]of n._nodeMap)o.set(t,e.$cloneWithProperties(r));r&&(r._nodeMap=o),t._dirtyType=2;const i=n._selection;e.$setSelection(null===i?null:i.clone())},exports.$reverseDfs=function(e,t){return Array.from(_(e,t))},exports.$reverseDfsIterator=_,exports.$unwrapAndFilterDescendants=function(e,t){return B(e,t,null)},exports.$unwrapNode=function(t){e.$rewindSiblingCaret(e.$getSiblingCaret(t,"next")).splice(1,t.getChildren())},exports.$wrapNodeInElement=function(e,t){const n=t();return e.replace(n),n.append(e),n},exports.CAN_USE_BEFORE_INPUT=E,exports.CAN_USE_DOM=v,exports.IS_ANDROID=N,exports.IS_ANDROID_CHROME=w,exports.IS_APPLE=y,exports.IS_APPLE_WEBKIT=b,exports.IS_CHROME=A,exports.IS_FIREFOX=R,exports.IS_IOS=P,exports.IS_SAFARI=I,exports.addClassNamesToElement=function(e,...t){const n=p(...t);n.length>0&&e.classList.add(...n)},exports.calculateZoomLevel=function(e){let t=1;if(function(){if(void 0===k){const e=document.createElement("div");e.style.cssText="position: absolute; opacity: 0; width: 100px; left: -1000px;",document.body.appendChild(e);const t=e.getBoundingClientRect();e.style.setProperty("zoom","2"),k=e.getBoundingClientRect().width===t.width,document.body.removeChild(e)}return k}())for(;e;)t*=Number(window.getComputedStyle(e).getPropertyValue("zoom")),e=e.parentElement;return t},exports.isMimeType=T,exports.makeStateWrapper=function(t){const n=n=>e.$getState(n,t),o=(n,o)=>e.$setState(n,t,o);return{$get:n,$set:o,accessors:[n,o],makeGetterMethod:()=>function(){return n(this)},makeSetterMethod:()=>function(e){return o(this,e)},stateConfig:t}},exports.markSelection=C,exports.mediaFileReader=function(e,t){const n=e[Symbol.iterator]();return new Promise((e,o)=>{const r=[],i=()=>{const{done:s,value:l}=n.next();if(s)return e(r);const a=new FileReader;a.addEventListener("error",o),a.addEventListener("load",()=>{const e=a.result;"string"==typeof e&&r.push({file:l,result:e}),i()}),T(l,t)?a.readAsDataURL(l):i()};i()})},exports.mergeRegister=m,exports.objectKlassEquals=function(e,t){return null!==e&&Object.getPrototypeOf(e).constructor.name===t.name},exports.positionNodeOnRange=x,exports.registerNestedElementResolver=function(e,t,n,o){const r=e=>e instanceof t;return e.registerNodeTransform(t,e=>{const t=(e=>{const t=e.getChildren();for(let e=0;e0&&e.classList.remove(...n)},exports.selectionAlwaysOnDisplay=function(t){let n=null;const o=()=>{const o=t.getRootElement();if(!o)return;let r=null,i=o;for(;i;){if(e.isDocumentFragment(i.nodeType)){const e=i;if("getComposedRanges"in Selection.prototype){const t=window.getSelection();if(t){t.getComposedRanges({shadowRoots:[e]}).length>0&&(r=t)}}break}i=i.parentNode}r||(r=getSelection());const s=r&&r.anchorNode;null!==s&&null!==o&&o.contains(s)?null!==n&&(n(),n=null):null===n&&(n=C(t))},r=t.getRootElement();let i=document;if(r){let t=r;for(;t;){if(e.isDocumentFragment(t.nodeType)){i=t.ownerDocument||document;break}t=t.parentNode}i=r.ownerDocument||document}return i.addEventListener("selectionchange",o),()=>{null!==n&&n(),i.removeEventListener("selectionchange",o)}};
+diff --git a/node_modules/@lexical/utils/LexicalUtils.prod.mjs b/node_modules/@lexical/utils/LexicalUtils.prod.mjs
+index 012f410..f8b373d 100644
+--- a/node_modules/@lexical/utils/LexicalUtils.prod.mjs
++++ b/node_modules/@lexical/utils/LexicalUtils.prod.mjs
+@@ -6,4 +6,4 @@
+ *
+ */
+
+-import{isHTMLElement as t,$getSelection as e,$isRangeSelection as n,$isElementNode as o,getDOMTextNode as r,$getAdjacentSiblingOrParentSiblingCaret as i,$getSiblingCaret as l,$getChildCaretOrSelf as u,$findMatchingParent as s,$cloneWithProperties as c,$setSelection as a,$getPreviousSelection as f,$caretFromPoint as d,$getChildCaret as g,$getRoot as p,$createParagraphNode as m,$getAdjacentChildCaret as h,$isChildCaret as v,$normalizeCaret as y,$setSelectionFromCaretRange as w,$getCollapsedCaretRange as x,$getCaretInDirection as E,$splitAtPointCaretNext as S,$isTextPointCaret as A,$rewindSiblingCaret as C,$getState as b,$setState as L,makeStepwiseIterator as P,$isSiblingCaret as N}from"lexical";export{$findMatchingParent,$getAdjacentSiblingOrParentSiblingCaret,$splitNode,isBlockDomNode,isHTMLAnchorElement,isHTMLElement,isInlineDomNode}from"lexical";import{createRectsFromDOMRange as M}from"@lexical/selection";function R(t,...e){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",t);for(const t of e)o.append("v",t);throw n.search=o.toString(),Error(`Minified Lexical error #${t}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}const T="undefined"!=typeof window&&void 0!==window.document&&void 0!==window.document.createElement,B=T&&"documentMode"in document?document.documentMode:null,_=T&&/Mac|iPod|iPhone|iPad/.test(navigator.platform),k=T&&/^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent),$=!(!T||!("InputEvent"in window)||B)&&"getTargetRanges"in new window.InputEvent("input"),K=T&&/Version\/[\d.]+.*Safari/.test(navigator.userAgent),O=T&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream,H=T&&/Android/.test(navigator.userAgent),j=T&&/^(?=.*Chrome).*/i.test(navigator.userAgent),D=T&&H&&j,I=T&&/AppleWebKit\/[\d.]+/.test(navigator.userAgent)&&_&&!j;function F(...t){const e=[];for(const n of t)if(n&&"string"==typeof n)for(const[t]of n.matchAll(/\S+/g))e.push(t);return e}function U(...t){return()=>{for(let e=t.length-1;e>=0;e--)t[e]();t.length=0}}function z(t){return`${t}px`}const V={attributes:!0,characterData:!0,childList:!0,subtree:!0};function W(e,n,o){let r=null,i=null,l=null,u=[];const s=document.createElement("div");function c(){null===r&&R(182),null===i&&R(183);const{left:t,top:l}=i.getBoundingClientRect(),c=M(e,n);var a,f;s.isConnected||(f=s,(a=i).insertBefore(f,a.firstChild));let d=!1;for(let e=0;ec.length;)u.pop();d&&o(u)}function a(){i=null,r=null,null!==l&&l.disconnect(),l=null,s.remove();for(const t of u)t.remove();u=[]}s.style.position="relative";const f=e.registerRootListener(function n(){const o=e.getRootElement();if(null===o)return a();const u=o.parentElement;if(!t(u))return a();a(),r=o,i=u,l=new MutationObserver(t=>{const o=e.getRootElement(),l=o&&o.parentElement;if(o!==r||l!==i)return n();for(const e of t)if(!s.contains(e.target))return c()}),l.observe(u,V),c()});return()=>{f(),a()}}function G(t,e,n){if("text"!==t.type&&o(e)){const o=e.getDOMSlot(n);return[o.element,o.getFirstChildOffset()+t.offset]}return[r(n)||n,t.offset]}function q(t,o){let r=null,i=null,l=null,u=null,s=null,c=null,a=()=>{};function f(f){f.read(()=>{const f=e();if(!n(f))return r=null,l=null,u=null,c=null,a(),void(a=()=>{});const[d,g]=function(t){const e=t.getStartEndPoints();return t.isBackward()?[e[1],e[0]]:e}(f),p=d.getNode(),m=p.getKey(),h=d.offset,v=g.getNode(),y=v.getKey(),w=g.offset,x=t.getElementByKey(m),E=t.getElementByKey(y),S=null===r||x!==i||h!==l||m!==r.getKey(),A=null===u||E!==s||w!==c||y!==u.getKey();if((S||A)&&null!==x&&null!==E){const e=function(t,e,n,o,r,i,l){const u=(t._window?t._window.document:document).createRange();return u.setStart(...G(e,n,o)),u.setEnd(...G(r,i,l)),u}(t,d,p,x,g,v,E);a(),a=W(t,e,t=>{if(void 0===o)for(const e of t){const t=e.style;"Highlight"!==t.background&&(t.background="Highlight"),"HighlightText"!==t.color&&(t.color="HighlightText"),t.marginTop!==z(-1.5)&&(t.marginTop=z(-1.5)),t.paddingTop!==z(4)&&(t.paddingTop=z(4)),t.paddingBottom!==z(0)&&(t.paddingBottom=z(0))}else o(t)})}r=p,i=x,l=h,u=v,s=E,c=w})}return f(t.getEditorState()),U(t.registerUpdateListener(({editorState:t})=>f(t)),()=>{a()})}function J(t){let e=null;const n=()=>{const n=getSelection(),o=n&&n.anchorNode,r=t.getRootElement();null!==o&&null!==r&&r.contains(o)?null!==e&&(e(),e=null):null===e&&(e=q(t))};return document.addEventListener("selectionchange",n),()=>{null!==e&&e(),document.removeEventListener("selectionchange",n)}}const Q=$,X=T,Y=H,Z=D,tt=_,et=I,nt=j,ot=k,rt=O,it=K;function lt(t,...e){const n=F(...e);n.length>0&&t.classList.add(...n)}function ut(t,...e){const n=F(...e);n.length>0&&t.classList.remove(...n)}function st(t,e){for(const n of e)if(t.type.startsWith(n))return!0;return!1}function ct(t,e){const n=t[Symbol.iterator]();return new Promise((t,o)=>{const r=[],i=()=>{const{done:l,value:u}=n.next();if(l)return t(r);const s=new FileReader;s.addEventListener("error",o),s.addEventListener("load",()=>{const t=s.result;"string"==typeof t&&r.push({file:u,result:t}),i()}),st(u,e)?s.readAsDataURL(u):i()};i()})}function at(t,e){return Array.from(gt(t,e))}function ft(t){return t?t.getAdjacentCaret():null}function dt(t,e){return Array.from(yt(t,e))}function gt(t,e){return pt("next",t,e)}function pt(t,e,n){const r=p(),s=e||r,c=o(s)?g(s,t):l(s,t),a=ht(s),f=n?h(u(l(n,t))):function(t,e){const n=i(l(t,e));return n&&n[0]}(s,t);let d=a;return P({hasNext:t=>null!==t,initial:c,map:t=>({depth:d,node:t.origin}),step:t=>{if(t.isSameNodeCaret(f))return null;v(t)&&d++;const e=i(t);return!e||e[0].isSameNodeCaret(f)?null:(d+=e[1],e[0])}})}function mt(t){const e=i(l(t,"next"));return e&&[e[0].origin,e[1]]}function ht(t){let e=-1;for(let n=t;null!==n;n=n.getParent())e++;return e}function vt(t){const e=u(l(t,"previous")),n=i(e,"root");return n&&n[0].origin}function yt(t,e){return pt("previous",t,e)}function wt(t,e){let n=t;for(;null!=n;){if(n instanceof e)return n;n=n.getParent()}return null}function xt(t){const e=s(t,t=>o(t)&&!t.isInline());return o(e)||R(4,t.__key),e}function Et(t,e,n,o){const r=t=>t instanceof e;return t.registerNodeTransform(e,t=>{const e=(t=>{const e=t.getChildren();for(let t=0;ti.insertAfter(t))),i.remove());return r}function kt(t,e){const n=[],r=Array.from(t).reverse();for(let t=r.pop();void 0!==t;t=r.pop())if(e(t))n.push(t);else if(o(t))for(const e of Kt(t))r.push(e);return n}function $t(t){return Ot(g(t,"next"))}function Kt(t){return Ot(g(t,"previous"))}function Ot(t){return P({hasNext:N,initial:t.getAdjacentCaret(),map:t=>t.origin.getLatest(),step:t=>t.getAdjacentCaret()})}function Ht(t){C(l(t,"next")).splice(1,t.getChildren())}function jt(t){const e=e=>b(e,t),n=(e,n)=>L(e,t,n);return{$get:e,$set:n,accessors:[e,n],makeGetterMethod:()=>function(){return e(this)},makeSetterMethod:()=>function(t){return n(this,t)},stateConfig:t}}export{kt as $descendantsMatching,at as $dfs,gt as $dfsIterator,Pt as $filter,$t as $firstToLastIterator,ft as $getAdjacentCaret,ht as $getDepth,xt as $getNearestBlockElementAncestorOrThrow,wt as $getNearestNodeOfType,vt as $getNextRightPreorderNode,mt as $getNextSiblingOrParentSibling,Nt as $insertFirst,At as $insertNodeToNearestRoot,Ct as $insertNodeToNearestRootAtCaret,Tt as $isEditorIsNestedEditor,Kt as $lastToFirstIterator,St as $restoreEditorState,dt as $reverseDfs,yt as $reverseDfsIterator,Bt as $unwrapAndFilterDescendants,Ht as $unwrapNode,bt as $wrapNodeInElement,Q as CAN_USE_BEFORE_INPUT,X as CAN_USE_DOM,Y as IS_ANDROID,Z as IS_ANDROID_CHROME,tt as IS_APPLE,et as IS_APPLE_WEBKIT,nt as IS_CHROME,ot as IS_FIREFOX,rt as IS_IOS,it as IS_SAFARI,lt as addClassNamesToElement,Rt as calculateZoomLevel,st as isMimeType,jt as makeStateWrapper,q as markSelection,ct as mediaFileReader,U as mergeRegister,Lt as objectKlassEquals,W as positionNodeOnRange,Et as registerNestedElementResolver,ut as removeClassNamesFromElement,J as selectionAlwaysOnDisplay};
++import{isHTMLElement as t,$getSelection as e,$isRangeSelection as n,$isElementNode as o,getDOMTextNode as r,isDocumentFragment as i,$getAdjacentSiblingOrParentSiblingCaret as l,$getSiblingCaret as s,$getChildCaretOrSelf as u,$findMatchingParent as c,$cloneWithProperties as f,$setSelection as a,$getPreviousSelection as d,$caretFromPoint as g,$getChildCaret as p,$getRoot as m,$createParagraphNode as h,$getAdjacentChildCaret as v,$isChildCaret as y,$normalizeCaret as w,$setSelectionFromCaretRange as x,$getCollapsedCaretRange as E,$getCaretInDirection as S,$splitAtPointCaretNext as C,$isTextPointCaret as A,$rewindSiblingCaret as b,$getState as L,$setState as N,makeStepwiseIterator as R,$isSiblingCaret as P}from"lexical";export{$findMatchingParent,$getAdjacentSiblingOrParentSiblingCaret,$splitNode,isBlockDomNode,isHTMLAnchorElement,isHTMLElement,isInlineDomNode}from"lexical";import{createRectsFromDOMRange as M}from"@lexical/selection";function T(t,...e){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",t);for(const t of e)o.append("v",t);throw n.search=o.toString(),Error(`Minified Lexical error #${t}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}const B="undefined"!=typeof window&&void 0!==window.document&&void 0!==window.document.createElement,k=B&&"documentMode"in document?document.documentMode:null,_=B&&/Mac|iPod|iPhone|iPad/.test(navigator.platform),$=B&&/^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent),D=!(!B||!("InputEvent"in window)||k)&&"getTargetRanges"in new window.InputEvent("input"),K=B&&/Version\/[\d.]+.*Safari/.test(navigator.userAgent),O=B&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream,H=B&&/Android/.test(navigator.userAgent),j=B&&/^(?=.*Chrome).*/i.test(navigator.userAgent),I=B&&H&&j,F=B&&/AppleWebKit\/[\d.]+/.test(navigator.userAgent)&&_&&!j;function U(...t){const e=[];for(const n of t)if(n&&"string"==typeof n)for(const[t]of n.matchAll(/\S+/g))e.push(t);return e}function z(...t){return()=>{for(let e=t.length-1;e>=0;e--)t[e]();t.length=0}}function V(t){return`${t}px`}const W={attributes:!0,characterData:!0,childList:!0,subtree:!0};function G(e,n,o){let r=null,i=null,l=null,s=[];const u=document.createElement("div");function c(){null===r&&T(182),null===i&&T(183);const{left:t,top:l}=i.getBoundingClientRect(),c=M(e,n);var f,a;u.isConnected||(a=u,(f=i).insertBefore(a,f.firstChild));let d=!1;for(let e=0;ec.length;)s.pop();d&&o(s)}function f(){i=null,r=null,null!==l&&l.disconnect(),l=null,u.remove();for(const t of s)t.remove();s=[]}u.style.position="relative";const a=e.registerRootListener(function n(){const o=e.getRootElement();if(null===o)return f();const s=o.parentElement;if(!t(s))return f();f(),r=o,i=s,l=new MutationObserver(t=>{const o=e.getRootElement(),l=o&&o.parentElement;if(o!==r||l!==i)return n();for(const e of t)if(!u.contains(e.target))return c()}),l.observe(s,W),c()});return()=>{a(),f()}}function q(t,e,n){if("text"!==t.type&&o(e)){const o=e.getDOMSlot(n);return[o.element,o.getFirstChildOffset()+t.offset]}return[r(n)||n,t.offset]}function J(t,o){let r=null,i=null,l=null,s=null,u=null,c=null,f=()=>{};function a(a){a.read(()=>{const a=e();if(!n(a))return r=null,l=null,s=null,c=null,f(),void(f=()=>{});const[d,g]=function(t){const e=t.getStartEndPoints();return t.isBackward()?[e[1],e[0]]:e}(a),p=d.getNode(),m=p.getKey(),h=d.offset,v=g.getNode(),y=v.getKey(),w=g.offset,x=t.getElementByKey(m),E=t.getElementByKey(y),S=null===r||x!==i||h!==l||m!==r.getKey(),C=null===s||E!==u||w!==c||y!==s.getKey();if((S||C)&&null!==x&&null!==E){const e=function(t,e,n,o,r,i,l){const s=(t._window?t._window.document:document).createRange();return s.setStart(...q(e,n,o)),s.setEnd(...q(r,i,l)),s}(t,d,p,x,g,v,E);f(),f=G(t,e,t=>{if(void 0===o)for(const e of t){const t=e.style;"Highlight"!==t.background&&(t.background="Highlight"),"HighlightText"!==t.color&&(t.color="HighlightText"),t.marginTop!==V(-1.5)&&(t.marginTop=V(-1.5)),t.paddingTop!==V(4)&&(t.paddingTop=V(4)),t.paddingBottom!==V(0)&&(t.paddingBottom=V(0))}else o(t)})}r=p,i=x,l=h,s=v,u=E,c=w})}return a(t.getEditorState()),z(t.registerUpdateListener(({editorState:t})=>a(t)),()=>{f()})}function Q(t){let e=null;const n=()=>{const n=t.getRootElement();if(!n)return;let o=null,r=n;for(;r;){if(i(r.nodeType)){const t=r;if("getComposedRanges"in Selection.prototype){const e=window.getSelection();if(e){e.getComposedRanges({shadowRoots:[t]}).length>0&&(o=e)}}break}r=r.parentNode}o||(o=getSelection());const l=o&&o.anchorNode;null!==l&&null!==n&&n.contains(l)?null!==e&&(e(),e=null):null===e&&(e=J(t))},o=t.getRootElement();let r=document;if(o){let t=o;for(;t;){if(i(t.nodeType)){r=t.ownerDocument||document;break}t=t.parentNode}r=o.ownerDocument||document}return r.addEventListener("selectionchange",n),()=>{null!==e&&e(),r.removeEventListener("selectionchange",n)}}const X=D,Y=B,Z=H,tt=I,et=_,nt=F,ot=j,rt=$,it=O,lt=K;function st(t,...e){const n=U(...e);n.length>0&&t.classList.add(...n)}function ut(t,...e){const n=U(...e);n.length>0&&t.classList.remove(...n)}function ct(t,e){for(const n of e)if(t.type.startsWith(n))return!0;return!1}function ft(t,e){const n=t[Symbol.iterator]();return new Promise((t,o)=>{const r=[],i=()=>{const{done:l,value:s}=n.next();if(l)return t(r);const u=new FileReader;u.addEventListener("error",o),u.addEventListener("load",()=>{const t=u.result;"string"==typeof t&&r.push({file:s,result:t}),i()}),ct(s,e)?u.readAsDataURL(s):i()};i()})}function at(t,e){return Array.from(pt(t,e))}function dt(t){return t?t.getAdjacentCaret():null}function gt(t,e){return Array.from(xt(t,e))}function pt(t,e){return ht("next",t,e)}function mt(t,e){const n=l(s(t,e));return n&&n[0]}function ht(t,e,n){const r=m(),i=e||r,c=o(i)?p(i,t):s(i,t),f=yt(i),a=n?v(u(s(n,t)))||mt(n,t):mt(i,t);let d=f;return R({hasNext:t=>null!==t,initial:c,map:t=>({depth:d,node:t.origin}),step:t=>{if(t.isSameNodeCaret(a))return null;y(t)&&d++;const e=l(t);return!e||e[0].isSameNodeCaret(a)?null:(d+=e[1],e[0])}})}function vt(t){const e=l(s(t,"next"));return e&&[e[0].origin,e[1]]}function yt(t){let e=-1;for(let n=t;null!==n;n=n.getParent())e++;return e}function wt(t){const e=u(s(t,"previous")),n=l(e,"root");return n&&n[0].origin}function xt(t,e){return ht("previous",t,e)}function Et(t,e){let n=t;for(;null!=n;){if(n instanceof e)return n;n=n.getParent()}return null}function St(t){const e=c(t,t=>o(t)&&!t.isInline());return o(e)||T(4,t.__key),e}function Ct(t,e,n,o){const r=t=>t instanceof e;return t.registerNodeTransform(e,t=>{const e=(t=>{const e=t.getChildren();for(let t=0;ti.insertAfter(t))),i.remove());return r}function Dt(t,e){const n=[],r=Array.from(t).reverse();for(let t=r.pop();void 0!==t;t=r.pop())if(e(t))n.push(t);else if(o(t))for(const e of Ot(t))r.push(e);return n}function Kt(t){return Ht(p(t,"next"))}function Ot(t){return Ht(p(t,"previous"))}function Ht(t){return R({hasNext:P,initial:t.getAdjacentCaret(),map:t=>t.origin.getLatest(),step:t=>t.getAdjacentCaret()})}function jt(t){b(s(t,"next")).splice(1,t.getChildren())}function It(t){const e=e=>L(e,t),n=(e,n)=>N(e,t,n);return{$get:e,$set:n,accessors:[e,n],makeGetterMethod:()=>function(){return e(this)},makeSetterMethod:()=>function(t){return n(this,t)},stateConfig:t}}export{Dt as $descendantsMatching,at as $dfs,pt as $dfsIterator,Pt as $filter,Kt as $firstToLastIterator,dt as $getAdjacentCaret,yt as $getDepth,St as $getNearestBlockElementAncestorOrThrow,Et as $getNearestNodeOfType,wt as $getNextRightPreorderNode,vt as $getNextSiblingOrParentSibling,Mt as $insertFirst,bt as $insertNodeToNearestRoot,Lt as $insertNodeToNearestRootAtCaret,kt as $isEditorIsNestedEditor,Ot as $lastToFirstIterator,At as $restoreEditorState,gt as $reverseDfs,xt as $reverseDfsIterator,_t as $unwrapAndFilterDescendants,jt as $unwrapNode,Nt as $wrapNodeInElement,X as CAN_USE_BEFORE_INPUT,Y as CAN_USE_DOM,Z as IS_ANDROID,tt as IS_ANDROID_CHROME,et as IS_APPLE,nt as IS_APPLE_WEBKIT,ot as IS_CHROME,rt as IS_FIREFOX,it as IS_IOS,lt as IS_SAFARI,st as addClassNamesToElement,Bt as calculateZoomLevel,ct as isMimeType,It as makeStateWrapper,J as markSelection,ft as mediaFileReader,z as mergeRegister,Rt as objectKlassEquals,G as positionNodeOnRange,Ct as registerNestedElementResolver,ut as removeClassNamesFromElement,Q as selectionAlwaysOnDisplay};
+diff --git a/node_modules/@lexical/utils/index.d.ts b/node_modules/@lexical/utils/index.d.ts
+index 5b130fc..9bb7f02 100644
+--- a/node_modules/@lexical/utils/index.d.ts
++++ b/node_modules/@lexical/utils/index.d.ts
+@@ -72,8 +72,10 @@ export interface DFSNode {
+ * before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a
+ * branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.
+ * It will then return all the nodes found in the search in an array of objects.
+- * @param startNode - The node to start the search, if omitted, it will start at the root node.
+- * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode.
++ * Preorder traversal is used, meaning that nodes are listed in the order of when they are FIRST encountered.
++ * @param startNode - The node to start the search (inclusive), if omitted, it will start at the root node.
++ * @param endNode - The node to end the search (inclusive), if omitted, it will find all descendants of the startingNode. If endNode
++ * is an ElementNode, it will stop before visiting any of its children.
+ * @returns An array of objects of all the nodes found by the search, including their depth into the tree.
+ * \\{depth: number, node: LexicalNode\\} It will always return at least 1 node (the start node).
+ */
+@@ -94,8 +96,10 @@ export declare function $getAdjacentCaret(caret: null
+ export declare function $reverseDfs(startNode?: LexicalNode, endNode?: LexicalNode): Array;
+ /**
+ * $dfs iterator (left to right). Tree traversal is done on the fly as new values are requested with O(1) memory.
+- * @param startNode - The node to start the search, if omitted, it will start at the root node.
+- * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode.
++ * Preorder traversal is used, meaning that nodes are iterated over in the order of when they are FIRST encountered.
++ * @param startNode - The node to start the search (inclusive), if omitted, it will start at the root node.
++ * @param endNode - The node to end the search (inclusive), if omitted, it will find all descendants of the startingNode.
++ * If endNode is an ElementNode, the iterator will end as soon as it reaches the endNode (no children will be visited).
+ * @returns An iterator, each yielded value is a DFSNode. It will always return at least 1 node (the start node).
+ */
+ export declare function $dfsIterator(startNode?: LexicalNode, endNode?: LexicalNode): IterableIterator;
+diff --git a/node_modules/@lexical/utils/selectionAlwaysOnDisplay.d.ts b/node_modules/@lexical/utils/selectionAlwaysOnDisplay.d.ts
+index 8c9c157..92c2b14 100644
+--- a/node_modules/@lexical/utils/selectionAlwaysOnDisplay.d.ts
++++ b/node_modules/@lexical/utils/selectionAlwaysOnDisplay.d.ts
+@@ -5,5 +5,5 @@
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+-import { LexicalEditor } from 'lexical';
++import { type LexicalEditor } from 'lexical';
+ export default function selectionAlwaysOnDisplay(editor: LexicalEditor): () => void;
diff --git a/patches/lexical+0.38.2.patch b/patches/lexical+0.38.2.patch
new file mode 100644
index 00000000..6b99fdb5
--- /dev/null
+++ b/patches/lexical+0.38.2.patch
@@ -0,0 +1,3461 @@
+diff --git a/node_modules/lexical/Lexical.dev.js b/node_modules/lexical/Lexical.dev.js
+index 34827a5..e7d2916 100644
+--- a/node_modules/lexical/Lexical.dev.js
++++ b/node_modules/lexical/Lexical.dev.js
+@@ -308,7 +308,7 @@ function getLastSelection(editor) {
+ });
+ }
+ function $handleTextMutation(target, node, editor) {
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ let anchorOffset = null;
+ let focusOffset = null;
+ if (domSelection !== null && domSelection.anchorNode === target) {
+@@ -1328,7 +1328,7 @@ function $normalizePoint(point) {
+
+ let subTreeTextContent = '';
+ let subTreeTextFormat = null;
+-let subTreeTextStyle = '';
++let subTreeTextStyle = null;
+ let editorTextContent = '';
+ let activeEditorConfig;
+ let activeEditor$1;
+@@ -1504,8 +1504,6 @@ function $createChildren(children, element, _startIndex, endIndex, slot) {
+ if (node !== null && $isTextNode(node)) {
+ if (subTreeTextFormat === null) {
+ subTreeTextFormat = node.getFormat();
+- }
+- if (subTreeTextStyle === '') {
+ subTreeTextStyle = node.getStyle();
+ }
+ }
+@@ -1545,13 +1543,13 @@ function reconcileTextFormat(element) {
+ }
+ }
+ function reconcileTextStyle(element) {
+- if (subTreeTextStyle !== '' && subTreeTextStyle !== element.__textStyle && !activeEditorStateReadOnly) {
++ if (subTreeTextStyle != null && subTreeTextStyle !== element.__textStyle && !activeEditorStateReadOnly) {
+ element.setTextStyle(subTreeTextStyle);
+ }
+ }
+ function $reconcileChildrenWithDirection(prevElement, nextElement, dom) {
+ subTreeTextFormat = null;
+- subTreeTextStyle = '';
++ subTreeTextStyle = null;
+ $reconcileChildren(prevElement, nextElement, nextElement.getDOMSlot(dom));
+ reconcileTextFormat(nextElement);
+ reconcileTextStyle(nextElement);
+@@ -1601,8 +1599,6 @@ function $reconcileChildren(prevElement, nextElement, slot) {
+ if ($isTextNode(nextChildNode)) {
+ if (subTreeTextFormat === null) {
+ subTreeTextFormat = nextChildNode.getFormat();
+- }
+- if (subTreeTextStyle === '') {
+ subTreeTextStyle = nextChildNode.getStyle();
+ }
+ }
+@@ -1810,8 +1806,6 @@ function $reconcileNodeChildren(nextElement, prevChildren, nextChildren, prevChi
+ if (node !== null && $isTextNode(node)) {
+ if (subTreeTextFormat === null) {
+ subTreeTextFormat = node.getFormat();
+- }
+- if (subTreeTextStyle === '') {
+ subTreeTextStyle = node.getStyle();
+ }
+ }
+@@ -1935,6 +1929,10 @@ function createCommand(type) {
+ const SELECTION_CHANGE_COMMAND = createCommand('SELECTION_CHANGE_COMMAND');
+ const SELECTION_INSERT_CLIPBOARD_NODES_COMMAND = createCommand('SELECTION_INSERT_CLIPBOARD_NODES_COMMAND');
+ const CLICK_COMMAND = createCommand('CLICK_COMMAND');
++const BEFORE_INPUT_COMMAND = createCommand('BEFORE_INPUT_COMMAND');
++const INPUT_COMMAND = createCommand('INPUT_COMMAND');
++const COMPOSITION_START_COMMAND = createCommand('COMPOSITION_START_COMMAND');
++const COMPOSITION_END_COMMAND = createCommand('COMPOSITION_END_COMMAND');
+ /**
+ * Dispatched to delete a character, the payload will be `true` if the deletion
+ * is backwards (backspace or delete on macOS) and `false` if forwards
+@@ -2110,7 +2108,7 @@ function $shouldPreventDefaultAndInsertText(selection, domTargetRange, text, tim
+ const focus = selection.focus;
+ const anchorNode = anchor.getNode();
+ const editor = getActiveEditor();
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;
+ const anchorKey = anchor.key;
+ const backingAnchorElement = editor.getElementByKey(anchorKey);
+@@ -2296,7 +2294,7 @@ function $updateSelectionFormatStyleFromElementNode(selection, node) {
+ function onClick(event, editor) {
+ updateEditorSync(editor, () => {
+ const selection = $getSelection();
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ const lastSelection = $getPreviousSelection();
+ if (domSelection) {
+ if ($isRangeSelection(selection)) {
+@@ -2366,9 +2364,15 @@ function $canRemoveText(anchorNode, focusNode) {
+ function isPossiblyAndroidKeyPress(timeStamp) {
+ return lastKeyCode === 'MediaLast' && timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY;
+ }
++function registerDefaultCommandHandlers(editor) {
++ editor.registerCommand(BEFORE_INPUT_COMMAND, $handleBeforeInput, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(INPUT_COMMAND, $handleInput, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(COMPOSITION_START_COMMAND, $handleCompositionStart, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(COMPOSITION_END_COMMAND, $handleCompositionEnd, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(KEY_DOWN_COMMAND, $handleKeyDown, COMMAND_PRIORITY_EDITOR);
++}
+ function onBeforeInput(event, editor) {
+ const inputType = event.inputType;
+- const targetRange = getTargetRange(event);
+
+ // We let the browser do its own thing for composition.
+ if (inputType === 'deleteCompositionText' ||
+@@ -2382,237 +2386,242 @@ function onBeforeInput(event, editor) {
+ } else if (inputType === 'insertCompositionText') {
+ return;
+ }
+- updateEditorSync(editor, () => {
+- const selection = $getSelection();
+- if (inputType === 'deleteContentBackward') {
+- if (selection === null) {
+- // Use previous selection
+- const prevSelection = $getPreviousSelection();
+- if (!$isRangeSelection(prevSelection)) {
+- return;
+- }
+- $setSelection(prevSelection.clone());
++ dispatchCommand(editor, BEFORE_INPUT_COMMAND, event);
++}
++function $handleBeforeInput(event) {
++ const inputType = event.inputType;
++ const targetRange = getTargetRange(event);
++ const editor = getActiveEditor();
++ const selection = $getSelection();
++ if (inputType === 'deleteContentBackward') {
++ if (selection === null) {
++ // Use previous selection
++ const prevSelection = $getPreviousSelection();
++ if (!$isRangeSelection(prevSelection)) {
++ return true;
+ }
+- if ($isRangeSelection(selection)) {
+- const isSelectionAnchorSameAsFocus = selection.anchor.key === selection.focus.key;
+- if (isPossiblyAndroidKeyPress(event.timeStamp) && editor.isComposing() && isSelectionAnchorSameAsFocus) {
+- $setCompositionKey(null);
+- lastKeyDownTimeStamp = 0;
+- // Fixes an Android bug where selection flickers when backspacing
+- setTimeout(() => {
+- updateEditorSync(editor, () => {
+- $setCompositionKey(null);
+- });
+- }, ANDROID_COMPOSITION_LATENCY);
+- if ($isRangeSelection(selection)) {
+- const anchorNode = selection.anchor.getNode();
+- anchorNode.markDirty();
+- if (!$isTextNode(anchorNode)) {
+- formatDevErrorMessage(`Anchor node must be a TextNode`);
+- }
+- $updateSelectionFormatStyleFromTextNode(selection, anchorNode);
+- }
+- } else {
+- $setCompositionKey(null);
+- event.preventDefault();
+- // Chromium Android at the moment seems to ignore the preventDefault
+- // on 'deleteContentBackward' and still deletes the content. Which leads
+- // to multiple deletions. So we let the browser handle the deletion in this case.
+- const selectedNode = selection.anchor.getNode();
+- const selectedNodeText = selectedNode.getTextContent();
+- // When the target node has `canInsertTextAfter` set to false, the first deletion
+- // doesn't have an effect, so we need to handle it with Lexical.
+- const selectedNodeCanInsertTextAfter = selectedNode.canInsertTextAfter();
+- const hasSelectedAllTextInNode = selection.anchor.offset === 0 && selection.focus.offset === selectedNodeText.length;
+- let shouldLetBrowserHandleDelete = IS_ANDROID_CHROME && isSelectionAnchorSameAsFocus && !hasSelectedAllTextInNode && selectedNodeCanInsertTextAfter;
+- // Check if selection is collapsed and if the previous node is a decorator node
+- // If so, the browser will not be able to handle the deletion
+- if (shouldLetBrowserHandleDelete && selection.isCollapsed()) {
+- shouldLetBrowserHandleDelete = !$isDecoratorNode($getAdjacentNode(selection.anchor, true));
++ $setSelection(prevSelection.clone());
++ }
++ if ($isRangeSelection(selection)) {
++ const isSelectionAnchorSameAsFocus = selection.anchor.key === selection.focus.key;
++ if (isPossiblyAndroidKeyPress(event.timeStamp) && editor.isComposing() && isSelectionAnchorSameAsFocus) {
++ $setCompositionKey(null);
++ lastKeyDownTimeStamp = 0;
++ // Fixes an Android bug where selection flickers when backspacing
++ setTimeout(() => {
++ updateEditorSync(editor, () => {
++ $setCompositionKey(null);
++ });
++ }, ANDROID_COMPOSITION_LATENCY);
++ if ($isRangeSelection(selection)) {
++ const anchorNode = selection.anchor.getNode();
++ anchorNode.markDirty();
++ if (!$isTextNode(anchorNode)) {
++ formatDevErrorMessage(`Anchor node must be a TextNode`);
+ }
+- if (!shouldLetBrowserHandleDelete) {
+- dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);
+- // When deleting across paragraphs, Chrome on Android incorrectly shifts the selection rightwards
+- // We save the correct selection to restore later during handling of selectionchange event
+- const selectionAfterDelete = $getSelection();
+- if (IS_ANDROID_CHROME && $isRangeSelection(selectionAfterDelete) && selectionAfterDelete.isCollapsed()) {
+- postDeleteSelectionToRestore = selectionAfterDelete;
+- // Cleanup in case selectionchange does not fire
+- setTimeout(() => postDeleteSelectionToRestore = null);
+- }
++ $updateSelectionFormatStyleFromTextNode(selection, anchorNode);
++ }
++ } else {
++ $setCompositionKey(null);
++ event.preventDefault();
++ // Chromium Android at the moment seems to ignore the preventDefault
++ // on 'deleteContentBackward' and still deletes the content. Which leads
++ // to multiple deletions. So we let the browser handle the deletion in this case.
++ const selectedNode = selection.anchor.getNode();
++ const selectedNodeText = selectedNode.getTextContent();
++ // When the target node has `canInsertTextAfter` set to false, the first deletion
++ // doesn't have an effect, so we need to handle it with Lexical.
++ const selectedNodeCanInsertTextAfter = selectedNode.canInsertTextAfter();
++ const hasSelectedAllTextInNode = selection.anchor.offset === 0 && selection.focus.offset === selectedNodeText.length;
++ let shouldLetBrowserHandleDelete = IS_ANDROID_CHROME && isSelectionAnchorSameAsFocus && !hasSelectedAllTextInNode && selectedNodeCanInsertTextAfter;
++ // Check if selection is collapsed and if the previous node is a decorator node
++ // If so, the browser will not be able to handle the deletion
++ if (shouldLetBrowserHandleDelete && selection.isCollapsed()) {
++ shouldLetBrowserHandleDelete = !$isDecoratorNode($getAdjacentNode(selection.anchor, true));
++ }
++ if (!shouldLetBrowserHandleDelete) {
++ dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);
++ // When deleting across paragraphs, Chrome on Android incorrectly shifts the selection rightwards
++ // We save the correct selection to restore later during handling of selectionchange event
++ const selectionAfterDelete = $getSelection();
++ if (IS_ANDROID_CHROME && $isRangeSelection(selectionAfterDelete) && selectionAfterDelete.isCollapsed()) {
++ postDeleteSelectionToRestore = selectionAfterDelete;
++ // Cleanup in case selectionchange does not fire
++ setTimeout(() => postDeleteSelectionToRestore = null);
+ }
+ }
+- return;
+ }
++ return true;
+ }
+- if (!$isRangeSelection(selection)) {
+- return;
+- }
+- const data = event.data;
++ }
++ if (!$isRangeSelection(selection)) {
++ return true;
++ }
++ const data = event.data;
+
+- // This represents the case when two beforeinput events are triggered at the same time (without a
+- // full event loop ending at input). This happens with MacOS with the default keyboard settings,
+- // a combination of autocorrection + autocapitalization.
+- // Having Lexical run everything in controlled mode would fix the issue without additional code
+- // but this would kill the massive performance win from the most common typing event.
+- // Alternatively, when this happens we can prematurely update our EditorState based on the DOM
+- // content, a job that would usually be the input event's responsibility.
+- if (unprocessedBeforeInputData !== null) {
+- $updateSelectedTextFromDOM(false, editor, unprocessedBeforeInputData);
+- }
+- if ((!selection.dirty || unprocessedBeforeInputData !== null) && selection.isCollapsed() && !$isRootNode(selection.anchor.getNode()) && targetRange !== null) {
+- selection.applyDOMRange(targetRange);
++ // This represents the case when two beforeinput events are triggered at the same time (without a
++ // full event loop ending at input). This happens with MacOS with the default keyboard settings,
++ // a combination of autocorrection + autocapitalization.
++ // Having Lexical run everything in controlled mode would fix the issue without additional code
++ // but this would kill the massive performance win from the most common typing event.
++ // Alternatively, when this happens we can prematurely update our EditorState based on the DOM
++ // content, a job that would usually be the input event's responsibility.
++ if (unprocessedBeforeInputData !== null) {
++ $updateSelectedTextFromDOM(false, editor, unprocessedBeforeInputData);
++ }
++ if ((!selection.dirty || unprocessedBeforeInputData !== null) && selection.isCollapsed() && !$isRootNode(selection.anchor.getNode()) && targetRange !== null) {
++ selection.applyDOMRange(targetRange);
++ }
++ unprocessedBeforeInputData = null;
++ const anchor = selection.anchor;
++ const focus = selection.focus;
++ const anchorNode = anchor.getNode();
++ const focusNode = focus.getNode();
++ if (inputType === 'insertText' || inputType === 'insertTranspose') {
++ if (data === '\n') {
++ event.preventDefault();
++ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
++ } else if (data === DOUBLE_LINE_BREAK) {
++ event.preventDefault();
++ dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
++ } else if (data == null && event.dataTransfer) {
++ // Gets around a Safari text replacement bug.
++ const text = event.dataTransfer.getData('text/plain');
++ event.preventDefault();
++ selection.insertRawText(text);
++ } else if (data != null && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, true)) {
++ event.preventDefault();
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
++ } else {
++ unprocessedBeforeInputData = data;
+ }
+- unprocessedBeforeInputData = null;
+- const anchor = selection.anchor;
+- const focus = selection.focus;
+- const anchorNode = anchor.getNode();
+- const focusNode = focus.getNode();
+- if (inputType === 'insertText' || inputType === 'insertTranspose') {
+- if (data === '\n') {
+- event.preventDefault();
++ lastBeforeInputInsertTextTimeStamp = event.timeStamp;
++ return true;
++ }
++
++ // Prevent the browser from carrying out
++ // the input event, so we can control the
++ // output.
++ event.preventDefault();
++ switch (inputType) {
++ case 'insertFromYank':
++ case 'insertFromDrop':
++ case 'insertReplacementText':
++ {
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
++ break;
++ }
++ case 'insertFromComposition':
++ {
++ // This is the end of composition
++ $setCompositionKey(null);
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
++ break;
++ }
++ case 'insertLineBreak':
++ {
++ // Used for Android
++ $setCompositionKey(null);
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
+- } else if (data === DOUBLE_LINE_BREAK) {
+- event.preventDefault();
+- dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
+- } else if (data == null && event.dataTransfer) {
+- // Gets around a Safari text replacement bug.
+- const text = event.dataTransfer.getData('text/plain');
+- event.preventDefault();
+- selection.insertRawText(text);
+- } else if (data != null && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, true)) {
+- event.preventDefault();
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
+- } else {
+- unprocessedBeforeInputData = data;
++ break;
+ }
+- lastBeforeInputInsertTextTimeStamp = event.timeStamp;
+- return;
+- }
++ case 'insertParagraph':
++ {
++ // Used for Android
++ $setCompositionKey(null);
+
+- // Prevent the browser from carrying out
+- // the input event, so we can control the
+- // output.
+- event.preventDefault();
+- switch (inputType) {
+- case 'insertFromYank':
+- case 'insertFromDrop':
+- case 'insertReplacementText':
+- {
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
+- break;
+- }
+- case 'insertFromComposition':
+- {
+- // This is the end of composition
+- $setCompositionKey(null);
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
+- break;
+- }
+- case 'insertLineBreak':
+- {
+- // Used for Android
+- $setCompositionKey(null);
++ // Safari does not provide the type "insertLineBreak".
++ // So instead, we need to infer it from the keyboard event.
++ // We do not apply this logic to iOS to allow newline auto-capitalization
++ // work without creating linebreaks when pressing Enter
++ if (isInsertLineBreak && !IS_IOS) {
++ isInsertLineBreak = false;
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
+- break;
+- }
+- case 'insertParagraph':
+- {
+- // Used for Android
+- $setCompositionKey(null);
+-
+- // Safari does not provide the type "insertLineBreak".
+- // So instead, we need to infer it from the keyboard event.
+- // We do not apply this logic to iOS to allow newline auto-capitalization
+- // work without creating linebreaks when pressing Enter
+- if (isInsertLineBreak && !IS_IOS) {
+- isInsertLineBreak = false;
+- dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
+- } else {
+- dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
+- }
+- break;
+- }
+- case 'insertFromPaste':
+- case 'insertFromPasteAsQuotation':
+- {
+- dispatchCommand(editor, PASTE_COMMAND, event);
+- break;
+- }
+- case 'deleteByComposition':
+- {
+- if ($canRemoveText(anchorNode, focusNode)) {
+- dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
+- }
+- break;
++ } else {
++ dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
+ }
+- case 'deleteByDrag':
+- case 'deleteByCut':
+- {
++ break;
++ }
++ case 'insertFromPaste':
++ case 'insertFromPasteAsQuotation':
++ {
++ dispatchCommand(editor, PASTE_COMMAND, event);
++ break;
++ }
++ case 'deleteByComposition':
++ {
++ if ($canRemoveText(anchorNode, focusNode)) {
+ dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
+- break;
+- }
+- case 'deleteContent':
+- {
+- dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);
+- break;
+- }
+- case 'deleteWordBackward':
+- {
+- dispatchCommand(editor, DELETE_WORD_COMMAND, true);
+- break;
+- }
+- case 'deleteWordForward':
+- {
+- dispatchCommand(editor, DELETE_WORD_COMMAND, false);
+- break;
+- }
+- case 'deleteHardLineBackward':
+- case 'deleteSoftLineBackward':
+- {
+- dispatchCommand(editor, DELETE_LINE_COMMAND, true);
+- break;
+- }
+- case 'deleteContentForward':
+- case 'deleteHardLineForward':
+- case 'deleteSoftLineForward':
+- {
+- dispatchCommand(editor, DELETE_LINE_COMMAND, false);
+- break;
+- }
+- case 'formatStrikeThrough':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'strikethrough');
+- break;
+- }
+- case 'formatBold':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');
+- break;
+- }
+- case 'formatItalic':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');
+- break;
+- }
+- case 'formatUnderline':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');
+- break;
+- }
+- case 'historyUndo':
+- {
+- dispatchCommand(editor, UNDO_COMMAND, undefined);
+- break;
+ }
+- case 'historyRedo':
+- {
+- dispatchCommand(editor, REDO_COMMAND, undefined);
+- break;
+- }
+- // NO-OP
+- }
+- });
++ break;
++ }
++ case 'deleteByDrag':
++ case 'deleteByCut':
++ {
++ dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
++ break;
++ }
++ case 'deleteContent':
++ {
++ dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);
++ break;
++ }
++ case 'deleteWordBackward':
++ {
++ dispatchCommand(editor, DELETE_WORD_COMMAND, true);
++ break;
++ }
++ case 'deleteWordForward':
++ {
++ dispatchCommand(editor, DELETE_WORD_COMMAND, false);
++ break;
++ }
++ case 'deleteHardLineBackward':
++ case 'deleteSoftLineBackward':
++ {
++ dispatchCommand(editor, DELETE_LINE_COMMAND, true);
++ break;
++ }
++ case 'deleteContentForward':
++ case 'deleteHardLineForward':
++ case 'deleteSoftLineForward':
++ {
++ dispatchCommand(editor, DELETE_LINE_COMMAND, false);
++ break;
++ }
++ case 'formatStrikeThrough':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'strikethrough');
++ break;
++ }
++ case 'formatBold':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');
++ break;
++ }
++ case 'formatItalic':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');
++ break;
++ }
++ case 'formatUnderline':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');
++ break;
++ }
++ case 'historyUndo':
++ {
++ dispatchCommand(editor, UNDO_COMMAND, undefined);
++ break;
++ }
++ case 'historyRedo':
++ {
++ dispatchCommand(editor, REDO_COMMAND, undefined);
++ break;
++ }
++ // NO-OP
++ }
++ return true;
+ }
+ function onInput(event, editor) {
+ // Note that the MutationObserver may or may not have already fired,
+@@ -2624,90 +2633,103 @@ function onInput(event, editor) {
+ // We don't want the onInput to bubble, in the case of nested editors.
+ event.stopPropagation();
+ updateEditorSync(editor, () => {
+- if (isHTMLElement(event.target) && $isSelectionCapturedInDecorator(event.target)) {
+- return;
++ editor.dispatchCommand(INPUT_COMMAND, event);
++ }, {
++ event
++ });
++ unprocessedBeforeInputData = null;
++}
++function $handleInput(event) {
++ if (isHTMLElement(event.target) && $isSelectionCapturedInDecorator(event.target)) {
++ return true;
++ }
++ const editor = getActiveEditor();
++ const selection = $getSelection();
++ const data = event.data;
++ const targetRange = getTargetRange(event);
++ if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, false)) {
++ // Given we're over-riding the default behavior, we will need
++ // to ensure to disable composition before dispatching the
++ // insertText command for when changing the sequence for FF.
++ if (isFirefoxEndingComposition) {
++ $onCompositionEndImpl(editor, data);
++ isFirefoxEndingComposition = false;
+ }
+- const selection = $getSelection();
+- const data = event.data;
+- const targetRange = getTargetRange(event);
+- if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, false)) {
+- // Given we're over-riding the default behavior, we will need
+- // to ensure to disable composition before dispatching the
+- // insertText command for when changing the sequence for FF.
+- if (isFirefoxEndingComposition) {
+- $onCompositionEndImpl(editor, data);
+- isFirefoxEndingComposition = false;
+- }
+- const anchor = selection.anchor;
+- const anchorNode = anchor.getNode();
+- const domSelection = getDOMSelection(getWindow(editor));
+- if (domSelection === null) {
+- return;
+- }
+- const isBackward = selection.isBackward();
+- const startOffset = isBackward ? selection.anchor.offset : selection.focus.offset;
+- const endOffset = isBackward ? selection.focus.offset : selection.anchor.offset;
+- // If the content is the same as inserted, then don't dispatch an insertion.
+- // Given onInput doesn't take the current selection (it uses the previous)
+- // we can compare that against what the DOM currently says.
+- if (!CAN_USE_BEFORE_INPUT || selection.isCollapsed() || !$isTextNode(anchorNode) || domSelection.anchorNode === null || anchorNode.getTextContent().slice(0, startOffset) + data + anchorNode.getTextContent().slice(startOffset + endOffset) !== getAnchorTextFromDOM(domSelection.anchorNode)) {
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
+- }
+- const textLength = data.length;
++ const anchor = selection.anchor;
++ const anchorNode = anchor.getNode();
++ const domSelection = getDOMSelection(getWindow(editor));
++ if (domSelection === null) {
++ return true;
++ }
++ const isBackward = selection.isBackward();
++ const startOffset = isBackward ? selection.anchor.offset : selection.focus.offset;
++ const endOffset = isBackward ? selection.focus.offset : selection.anchor.offset;
++ // If the content is the same as inserted, then don't dispatch an insertion.
++ // Given onInput doesn't take the current selection (it uses the previous)
++ // we can compare that against what the DOM currently says.
++ if (!CAN_USE_BEFORE_INPUT || selection.isCollapsed() || !$isTextNode(anchorNode) || domSelection.anchorNode === null || anchorNode.getTextContent().slice(0, startOffset) + data + anchorNode.getTextContent().slice(startOffset + endOffset) !== getAnchorTextFromDOM(domSelection.anchorNode)) {
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
++ }
++ const textLength = data.length;
+
+- // Another hack for FF, as it's possible that the IME is still
+- // open, even though compositionend has already fired (sigh).
+- if (IS_FIREFOX && textLength > 1 && event.inputType === 'insertCompositionText' && !editor.isComposing()) {
+- selection.anchor.offset -= textLength;
+- }
++ // Another hack for FF, as it's possible that the IME is still
++ // open, even though compositionend has already fired (sigh).
++ if (IS_FIREFOX && textLength > 1 && event.inputType === 'insertCompositionText' && !editor.isComposing()) {
++ selection.anchor.offset -= textLength;
++ }
+
+- // This ensures consistency on Android.
+- if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT && editor.isComposing()) {
+- lastKeyDownTimeStamp = 0;
+- $setCompositionKey(null);
+- }
+- } else {
+- const characterData = data !== null ? data : undefined;
+- $updateSelectedTextFromDOM(false, editor, characterData);
++ // This ensures consistency on Android.
++ if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT && editor.isComposing()) {
++ lastKeyDownTimeStamp = 0;
++ $setCompositionKey(null);
++ }
++ } else {
++ const characterData = data !== null ? data : undefined;
++ $updateSelectedTextFromDOM(false, editor, characterData);
+
+- // onInput always fires after onCompositionEnd for FF.
+- if (isFirefoxEndingComposition) {
+- $onCompositionEndImpl(editor, data || undefined);
+- isFirefoxEndingComposition = false;
+- }
++ // onInput always fires after onCompositionEnd for FF.
++ if (isFirefoxEndingComposition) {
++ $onCompositionEndImpl(editor, data || undefined);
++ isFirefoxEndingComposition = false;
+ }
++ }
+
+- // Also flush any other mutations that might have occurred
+- // since the change.
+- $flushMutations();
+- }, {
+- event
+- });
+- unprocessedBeforeInputData = null;
++ // Also flush any other mutations that might have occurred
++ // since the change.
++ $flushMutations();
++ return true;
+ }
+ function onCompositionStart(event, editor) {
+- updateEditorSync(editor, () => {
+- const selection = $getSelection();
+- if ($isRangeSelection(selection) && !editor.isComposing()) {
+- const anchor = selection.anchor;
+- const node = selection.anchor.getNode();
+- $setCompositionKey(anchor.key);
+- if (
+- // If it has been 30ms since the last keydown, then we should
+- // apply the empty space heuristic. We can't do this for Safari,
+- // as the keydown fires after composition start.
+- event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY ||
+- // FF has issues around composing multibyte characters, so we also
+- // need to invoke the empty space heuristic below.
+- anchor.type === 'element' || !selection.isCollapsed() || node.getFormat() !== selection.format || $isTextNode(node) && node.getStyle() !== selection.style) {
+- // We insert a zero width character, ready for the composition
+- // to get inserted into the new node we create. If
+- // we don't do this, Safari will fail on us because
+- // there is no text node matching the selection.
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, COMPOSITION_START_CHAR);
+- }
++ dispatchCommand(editor, COMPOSITION_START_COMMAND, event);
++}
++function $handleCompositionStart(event) {
++ const editor = getActiveEditor();
++ const selection = $getSelection();
++ if ($isRangeSelection(selection) && !editor.isComposing()) {
++ const anchor = selection.anchor;
++ const node = selection.anchor.getNode();
++ $setCompositionKey(anchor.key);
++ if (
++ // If it has been 30ms since the last keydown, then we should
++ // apply the empty space heuristic. We can't do this for Safari,
++ // as the keydown fires after composition start.
++ event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY ||
++ // FF has issues around composing multibyte characters, so we also
++ // need to invoke the empty space heuristic below.
++ anchor.type === 'element' || !selection.isCollapsed() || node.getFormat() !== selection.format || $isTextNode(node) && node.getStyle() !== selection.style) {
++ // We insert a zero width character, ready for the composition
++ // to get inserted into the new node we create. If
++ // we don't do this, Safari will fail on us because
++ // there is no text node matching the selection.
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, COMPOSITION_START_CHAR);
+ }
+- });
++ }
++ return true;
++}
++function $handleCompositionEnd(event) {
++ const editor = getActiveEditor();
++ $onCompositionEndImpl(editor, event.data);
++ return true;
+ }
+ function $onCompositionEndImpl(editor, data) {
+ const compositionKey = editor._compositionKey;
+@@ -2759,9 +2781,7 @@ function onCompositionEnd(event, editor) {
+ isSafariEndingComposition = true;
+ safariEndCompositionEventData = event.data;
+ } else {
+- updateEditorSync(editor, () => {
+- $onCompositionEndImpl(editor, event.data);
+- });
++ dispatchCommand(editor, COMPOSITION_END_COMMAND, event);
+ }
+ }
+ function onKeyDown(event, editor) {
+@@ -2770,11 +2790,12 @@ function onKeyDown(event, editor) {
+ if (editor.isComposing()) {
+ return;
+ }
+- if (dispatchCommand(editor, KEY_DOWN_COMMAND, event)) {
+- return;
+- }
++ dispatchCommand(editor, KEY_DOWN_COMMAND, event);
++}
++function $handleKeyDown(event) {
++ const editor = getActiveEditor();
+ if (event.key == null) {
+- return;
++ return true;
+ }
+ if (isSafariEndingComposition && isBackspace(event)) {
+ updateEditorSync(editor, () => {
+@@ -2782,7 +2803,7 @@ function onKeyDown(event, editor) {
+ });
+ isSafariEndingComposition = false;
+ safariEndCompositionEventData = '';
+- return;
++ return true;
+ }
+ if (isMoveForward(event)) {
+ dispatchCommand(editor, KEY_ARROW_RIGHT_COMMAND, event);
+@@ -2873,8 +2894,9 @@ function onKeyDown(event, editor) {
+ }
+ }
+ if (isModifier(event)) {
+- dispatchCommand(editor, KEY_MODIFIER_COMMAND, event);
++ editor.dispatchCommand(KEY_MODIFIER_COMMAND, event);
+ }
++ return true;
+ }
+ function getRootElementRemoveHandles(rootElement) {
+ // @ts-expect-error: internal field
+@@ -2949,7 +2971,7 @@ function hasStoppedLexicalPropagation(event) {
+ function addRootElementEvents(rootElement, editor) {
+ // We only want to have a single global selectionchange event handler, shared
+ // between all editor instances.
+- const doc = rootElement.ownerDocument;
++ const doc = getShadowRootOrDocument(rootElement);
+ rootElementToDocument.set(rootElement, doc);
+ const documentRootElementsCount = rootElementsRegistered.get(doc) ?? 0;
+ if (documentRootElementsCount < 1) {
+@@ -3007,7 +3029,7 @@ function addRootElementEvents(rootElement, editor) {
+ }
+ const rootElementNotRegisteredWarning = warnOnlyOnce('Root element not registered');
+ function removeRootElementEvents(rootElement) {
+- const doc = rootElementToDocument.get(rootElement);
++ const doc = getShadowRootOrDocument(rootElement);
+ if (doc === undefined) {
+ rootElementNotRegisteredWarning();
+ return;
+@@ -6760,7 +6782,7 @@ class RangeSelection {
+ }
+ const collapse = alter === 'move';
+ const editor = getActiveEditor();
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ if (!domSelection) {
+ return;
+ }
+@@ -6848,11 +6870,12 @@ class RangeSelection {
+ }
+ /**
+ * Helper for handling forward character and word deletion that prevents element nodes
+- * like a table, columns layout being destroyed
++ * like a table, columns layout being destroyed. Also prevents deletion into shadow roots.
+ *
+ * @param anchor the anchor
+ * @param anchorNode the anchor node in the selection
+ * @param isBackward whether or not selection is backwards
++ * @returns true if deletion should be prevented
+ */
+ forwardDeletion(anchor, anchorNode, isBackward) {
+ if (!isBackward && (
+@@ -6956,7 +6979,26 @@ class RangeSelection {
+
+ // Handle the deletion around decorators.
+ const focus = this.focus;
++ const initialAnchorKey = anchor.key;
++ const initialAnchorOffset = anchor.offset;
++ const initialFocusKey = focus.key;
++ const initialFocusOffset = focus.offset;
+ this.modify('extend', isBackward, 'character');
++
++ // Check if modify actually changed the selection (it might not in shadow DOM)
++ const selectionChanged = this.anchor.key !== initialAnchorKey || this.anchor.offset !== initialAnchorOffset || this.focus.key !== initialFocusKey || this.focus.offset !== initialFocusOffset;
++ if (!selectionChanged && anchor.type === 'text' && $isTextNode(anchorNode)) {
++ // Fallback for environments where modify doesn't work (e.g., shadow DOM)
++ const textContent = anchorNode.getTextContent();
++ const offset = anchor.offset;
++ if (isBackward && offset > 0) {
++ // Select the character before cursor
++ this.anchor.set(anchor.key, offset - 1, 'text');
++ } else if (!isBackward && offset < textContent.length) {
++ // Select the character after cursor
++ this.focus.set(focus.key, offset + 1, 'text');
++ }
++ }
+ if (!this.isCollapsed()) {
+ const focusNode = focus.type === 'text' ? focus.getNode() : null;
+ anchorNode = anchor.type === 'text' ? anchor.getNode() : null;
+@@ -7000,7 +7042,31 @@ class RangeSelection {
+ */
+ deleteLine(isBackward) {
+ if (this.isCollapsed()) {
++ const anchor = this.anchor;
++ const focus = this.focus;
++ const initialAnchorKey = anchor.key;
++ const initialAnchorOffset = anchor.offset;
++ const initialFocusKey = focus.key;
++ const initialFocusOffset = focus.offset;
+ this.modify('extend', isBackward, 'lineboundary');
++
++ // Check if modify actually changed the selection (it might not in shadow DOM)
++ const selectionChanged = this.anchor.key !== initialAnchorKey || this.anchor.offset !== initialAnchorOffset || this.focus.key !== initialFocusKey || this.focus.offset !== initialFocusOffset;
++ if (!selectionChanged && anchor.type === 'text') {
++ // Fallback for environments where modify doesn't work (e.g., shadow DOM)
++ const anchorNode = anchor.getNode();
++ if ($isTextNode(anchorNode)) {
++ const textContent = anchorNode.getTextContent();
++ const offset = anchor.offset;
++ if (isBackward && offset > 0) {
++ // Delete from beginning of line to cursor
++ this.anchor.set(anchor.key, 0, 'text');
++ } else if (!isBackward && offset < textContent.length) {
++ // Delete from cursor to end of line
++ this.focus.set(focus.key, textContent.length, 'text');
++ }
++ }
++ }
+ }
+ if (this.isCollapsed()) {
+ // If the selection was already collapsed at the lineboundary,
+@@ -7012,6 +7078,57 @@ class RangeSelection {
+ }
+ }
+
++ /**
++ * Helper function to determine if a character is a word boundary (whitespace).
++ * @param char the character to check
++ * @returns true if the character is a word boundary
++ */
++ isWordBoundary(char) {
++ return char === ' ' || char === '\t' || char === '\n' || char === '\r';
++ }
++
++ /**
++ * Find the start of a word going backward from the given offset in text.
++ * @param text the text to search in
++ * @param offset the starting offset
++ * @returns the offset of the word start
++ */
++ findWordStart(text, offset) {
++ let position = offset - 1;
++
++ // Skip spaces
++ while (position >= 0 && this.isWordBoundary(text[position])) {
++ position--;
++ }
++
++ // Find word start
++ while (position > 0 && !this.isWordBoundary(text[position - 1])) {
++ position--;
++ }
++ return position >= 0 ? position : 0;
++ }
++
++ /**
++ * Find the end of a word going forward from the given offset in text.
++ * @param text the text to search in
++ * @param offset the starting offset
++ * @returns the offset of the word end
++ */
++ findWordEnd(text, offset) {
++ let position = offset;
++
++ // Skip spaces
++ while (position < text.length && this.isWordBoundary(text[position])) {
++ position++;
++ }
++
++ // Find word end
++ while (position < text.length && !this.isWordBoundary(text[position])) {
++ position++;
++ }
++ return position;
++ }
++
+ /**
+ * Performs one logical word deletion operation on the EditorState based on the current Selection.
+ * Handles different node types.
+@@ -7025,7 +7142,69 @@ class RangeSelection {
+ if (this.forwardDeletion(anchor, anchorNode, isBackward)) {
+ return;
+ }
++ const initialAnchorKey = anchor.key;
++ const initialAnchorOffset = anchor.offset;
++ const focus = this.focus;
++ const initialFocusKey = focus.key;
++ const initialFocusOffset = focus.offset;
+ this.modify('extend', isBackward, 'word');
++
++ // Check if modify actually changed the selection (it might not in shadow DOM)
++ const selectionChanged = this.anchor.key !== initialAnchorKey || this.anchor.offset !== initialAnchorOffset || this.focus.key !== initialFocusKey || this.focus.offset !== initialFocusOffset;
++ if (!selectionChanged && anchor.type === 'text' && $isTextNode(anchorNode)) {
++ // Fallback for environments where modify doesn't work (e.g., shadow DOM)
++ const textContent = anchorNode.getTextContent();
++ const offset = anchor.offset;
++ if (isBackward) {
++ // Backward: find start of word before cursor
++ if (offset === 0) {
++ // At node start, check previous sibling
++ const prevSibling = anchorNode.getPreviousSibling();
++ if ($isTextNode(prevSibling)) {
++ const prevText = prevSibling.getTextContent();
++ const position = this.findWordStart(prevText, prevText.length);
++ this.anchor.set(prevSibling.__key, position, 'text');
++ }
++ } else {
++ const position = this.findWordStart(textContent, offset);
++ if (position === 0 && this.isWordBoundary(textContent[0])) {
++ // Only spaces in this node, try previous sibling
++ const prevSibling = anchorNode.getPreviousSibling();
++ if ($isTextNode(prevSibling)) {
++ const prevText = prevSibling.getTextContent();
++ const prevPosition = this.findWordStart(prevText, prevText.length);
++ this.anchor.set(prevSibling.__key, prevPosition, 'text');
++ return;
++ }
++ }
++ this.anchor.set(anchor.key, position, 'text');
++ }
++ } else {
++ // Forward: find end of word after cursor
++ if (offset === textContent.length) {
++ // At node end, check next sibling
++ const nextSibling = anchorNode.getNextSibling();
++ if ($isTextNode(nextSibling)) {
++ const nextText = nextSibling.getTextContent();
++ const position = this.findWordEnd(nextText, 0);
++ this.focus.set(nextSibling.__key, position, 'text');
++ }
++ } else {
++ const position = this.findWordEnd(textContent, offset);
++ if (position === textContent.length && this.isWordBoundary(textContent[textContent.length - 1])) {
++ // Only spaces in this node, try next sibling
++ const nextSibling = anchorNode.getNextSibling();
++ if ($isTextNode(nextSibling)) {
++ const nextText = nextSibling.getTextContent();
++ const nextPosition = this.findWordEnd(nextText, 0);
++ this.focus.set(nextSibling.__key, nextPosition, 'text');
++ return;
++ }
++ }
++ this.focus.set(focus.key, position, 'text');
++ }
++ }
++ }
+ }
+ this.removeText();
+ }
+@@ -7474,7 +7653,7 @@ function $createNodeSelection() {
+ function $internalCreateSelection(editor, event) {
+ const currentEditorState = editor.getEditorState();
+ const lastSelection = currentEditorState._selection;
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ if ($isRangeSelection(lastSelection) || lastSelection == null) {
+ return $internalCreateRangeSelection(lastSelection, domSelection, editor, event);
+ }
+@@ -7732,7 +7911,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
+ const focusDOMNode = domSelection.focusNode;
+ const anchorOffset = domSelection.anchorOffset;
+ const focusOffset = domSelection.focusOffset;
+- const activeElement = document.activeElement;
++ const activeElement = getActiveElement(rootElement);
+
+ // TODO: make this not hard-coded, and add another config option
+ // that makes this configurable.
+@@ -8459,7 +8638,7 @@ function $commitPendingUpdates(editor, recoveryEditorState) {
+ // Reconciliation has finished. Now update selection and trigger listeners.
+ // ======
+
+- const domSelection = shouldSkipDOM ? null : getDOMSelection(getWindow(editor));
++ const domSelection = shouldSkipDOM ? null : getDOMSelectionForEditor(editor);
+
+ // Attempt to update the DOM selection, including focusing of the root element,
+ // and scroll into view if needed.
+@@ -9491,11 +9670,15 @@ class ElementNode extends LexicalNode {
+ };
+ const textFormat = this.getTextFormat();
+ const textStyle = this.getTextStyle();
+- if (textFormat !== 0) {
+- json.textFormat = textFormat;
+- }
+- if (textStyle !== '') {
+- json.textStyle = textStyle;
++ // Only persist for cases when there are no TextNode children from which
++ // these would be set on reconcile (#7968)
++ if ((textFormat !== 0 || textStyle !== '') && !$isRootOrShadowRoot(this) && !this.getChildren().some($isTextNode)) {
++ if (textFormat !== 0) {
++ json.textFormat = textFormat;
++ }
++ if (textStyle !== '') {
++ json.textStyle = textStyle;
++ }
+ }
+ return json;
+ }
+@@ -9892,12 +10075,20 @@ class ParagraphNode extends ElementNode {
+ return $createParagraphNode().updateFromJSON(serializedNode);
+ }
+ exportJSON() {
+- return {
+- ...super.exportJSON(),
+- // These are included explicitly for backwards compatibility
+- textFormat: this.getTextFormat(),
+- textStyle: this.getTextStyle()
+- };
++ const json = super.exportJSON();
++ // Provide backwards compatible values, see #7971
++ if (json.textFormat === undefined || json.textStyle === undefined) {
++ // Compute the same value that the reconciler would
++ const firstTextNode = this.getChildren().find($isTextNode);
++ if (firstTextNode) {
++ json.textFormat = firstTextNode.getFormat();
++ json.textStyle = firstTextNode.getStyle();
++ } else {
++ json.textFormat = this.getTextFormat();
++ json.textStyle = this.getTextStyle();
++ }
++ }
++ return json;
+ }
+
+ // Mutation
+@@ -10180,6 +10371,7 @@ function createEditor(editorConfig) {
+ editor._pendingEditorState = initialEditorState;
+ editor._dirtyType = FULL_RECONCILE;
+ }
++ registerDefaultCommandHandlers(editor);
+ return editor;
+ }
+ class LexicalEditor {
+@@ -10821,7 +11013,7 @@ class LexicalEditor {
+ if (rootElement !== null) {
+ rootElement.blur();
+ }
+- const domSelection = getDOMSelection(this._window);
++ const domSelection = getDOMSelectionForEditor(this);
+ if (domSelection !== null) {
+ domSelection.removeAllRanges();
+ }
+@@ -10907,7 +11099,9 @@ function $isSelectionCapturedInDecorator(node) {
+ return $isDecoratorNode($getNearestNodeFromDOMNode(node));
+ }
+ function isSelectionCapturedInDecoratorInput(anchorDOM) {
+- const activeElement = document.activeElement;
++ const editor = getNearestEditorFromDOMNode(anchorDOM);
++ const rootElement = editor ? editor.getRootElement() : null;
++ const activeElement = rootElement ? getActiveElement(rootElement) : document.activeElement;
+ if (!isHTMLElement(activeElement)) {
+ return false;
+ }
+@@ -11354,7 +11548,7 @@ function getAnchorTextFromDOM(anchorNode) {
+ }
+ function $updateSelectedTextFromDOM(isCompositionEnd, editor, data) {
+ // Update the text content with the latest composition text
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ if (domSelection === null) {
+ return;
+ }
+@@ -11799,9 +11993,23 @@ function getElementByKeyOrThrow(editor, key) {
+ }
+ return element;
+ }
++
++/**
++ * Type guard function that checks if a node is a ShadowRoot. This function performs
++ * runtime validation to safely narrow types and enable type-safe Shadow DOM operations.
++ * It checks both the nodeType and the presence of the 'host' property to distinguish
++ * ShadowRoot from regular DocumentFragment nodes.
++ *
++ * @param node - The Node to check (can be null)
++ * @returns True if the node is a ShadowRoot, false otherwise. When true, TypeScript
++ * will narrow the type to ShadowRoot for subsequent operations.
++ */
++function isShadowRoot(node) {
++ return isDocumentFragment(node) && 'host' in node;
++}
+ function getParentElement(node) {
+ const parentElement = node.assignedSlot || node.parentElement;
+- return isDocumentFragment(parentElement) ? parentElement.host : parentElement;
++ return isShadowRoot(parentElement) ? parentElement.host : parentElement;
+ }
+ function getDOMOwnerDocument(target) {
+ return isDOMDocumentNode(target) ? target : isHTMLElement(target) ? target.ownerDocument : null;
+@@ -11909,7 +12117,7 @@ function getDefaultView(domElem) {
+ return ownerDoc ? ownerDoc.defaultView : null;
+ }
+ function getWindow(editor) {
+- const windowObj = editor._window;
++ const windowObj = editor._window || window;
+ if (windowObj === null) {
+ {
+ formatDevErrorMessage(`window object not found`);
+@@ -12028,7 +12236,7 @@ function removeDOMBlockCursorElement(blockCursorElement, editor, rootElement) {
+ }
+ function updateDOMBlockCursorElement(editor, rootElement, nextSelection) {
+ let blockCursorElement = editor._blockCursorElement;
+- if ($isRangeSelection(nextSelection) && nextSelection.isCollapsed() && nextSelection.anchor.type === 'element' && rootElement.contains(document.activeElement)) {
++ if ($isRangeSelection(nextSelection) && nextSelection.isCollapsed() && nextSelection.anchor.type === 'element' && rootElement.contains(getActiveElement(rootElement))) {
+ const anchor = nextSelection.anchor;
+ const elementNode = anchor.getNode();
+ const offset = anchor.offset;
+@@ -12071,23 +12279,290 @@ function updateDOMBlockCursorElement(editor, rootElement, nextSelection) {
+ }
+
+ /**
+- * Returns the selection for the given window, or the global window if null.
+- * Will return null if {@link CAN_USE_DOM} is false.
++ * Returns a Selection object from a ShadowRoot using the best available API.
++ *
++ * This function attempts to get selection from Shadow DOM contexts using modern
++ * getComposedRanges API when available. If the API is not supported or returns
++ * empty ranges, it falls back to the global window selection.
++ *
++ * **Selection Proxy:**
++ * When getComposedRanges returns valid ranges, this function creates a Selection proxy
++ * that properly handles text selection across Shadow DOM boundaries. The proxy
++ * provides all standard Selection methods while ensuring correct behavior with
++ * composed ranges.
++ *
++ * **Browser Support:**
++ * - Modern browsers with getComposedRanges: Full Shadow DOM selection support
++ * - Older browsers: Falls back to window.getSelection()
++ *
++ * @param shadowRoot - The ShadowRoot to get selection from
++ * @returns A Selection object (either a proxy with composed ranges or the global selection),
++ * or null if no selection is available
++ */
++function getDOMSelectionFromShadowRoot(shadowRoot) {
++ const globalSelection = window.getSelection();
++ if (!globalSelection) {
++ return null;
++ }
++ if ('getComposedRanges' in Selection.prototype) {
++ const ranges = globalSelection.getComposedRanges({
++ shadowRoots: [shadowRoot]
++ });
++ if (ranges.length > 0) {
++ return createSelectionWithComposedRanges(globalSelection, ranges);
++ }
++ }
++ return globalSelection;
++}
++
++/**
++ * Returns the selection for the given window, with Shadow DOM support.
++ *
++ * This function provides a unified API for getting selections in both regular DOM
++ * and Shadow DOM contexts. When a rootElement is provided, it checks if the element
++ * is within a Shadow DOM and uses the appropriate selection API.
++ *
++ * **Behavior:**
++ * - If CAN_USE_DOM is false: Returns null
++ * - If rootElement is in Shadow DOM: Uses getDOMSelectionFromShadowRoot
++ * - Otherwise: Returns window.getSelection() from the target or global window
+ *
+- * @param targetWindow The window to get the selection from
+- * @returns a Selection or null
++ * @param targetWindow - The window to get the selection from (defaults to global window if null)
++ * @param rootElement - Optional root element to check for Shadow DOM context
++ * @returns A Selection object appropriate for the context, or null if selection is unavailable
+ */
+-function getDOMSelection(targetWindow) {
+- return !CAN_USE_DOM ? null : (targetWindow || window).getSelection();
++function getDOMSelection(targetWindow, rootElement) {
++ if (!CAN_USE_DOM) {
++ return null;
++ }
++
++ // Check if we're inside a shadow DOM
++ if (rootElement) {
++ const shadowRoot = getShadowRootOrDocument(rootElement);
++ if (shadowRoot && isShadowRoot(shadowRoot)) {
++ return getDOMSelectionFromShadowRoot(shadowRoot);
++ }
++ }
++ return (targetWindow || window).getSelection();
+ }
+
+ /**
+- * Returns the selection for the defaultView of the ownerDocument of given EventTarget.
++ * Creates a Selection-like proxy object that properly handles StaticRange objects
++ * from the getComposedRanges API for Shadow DOM compatibility.
++ *
++ * This function creates a proxy that:
++ * - Provides all standard Selection properties and methods
++ * - Correctly handles anchor/focus nodes from StaticRange data
++ * - Implements the `type` property ('None', 'Caret', or 'Range')
++ * - Converts StaticRange to Range objects in getRangeAt method
++ * - Delegates other methods to the base Selection object
++ *
++ * **Validation:**
++ * The function validates that composedRanges is a non-empty array with valid
++ * StaticRange objects before creating the proxy. If validation fails, it
++ * returns the base selection unchanged.
+ *
+- * @param eventTarget The node to get the selection from
+- * @returns a Selection or null
++ * @param baseSelection - The base Selection object to enhance
++ * @param composedRanges - Array of StaticRange objects from getComposedRanges
++ * @returns A proxy Selection object that correctly handles Shadow DOM ranges,
++ * or the base selection if composedRanges is invalid
++ */
++
++function createSelectionWithComposedRanges(baseSelection, composedRanges) {
++ if (composedRanges.length === 0) {
++ return baseSelection;
++ }
++ const firstRange = composedRanges[0];
++ const selectionLike = Object.create(Selection.prototype);
++
++ // Copy all methods and properties from base selection
++ const descriptors = Object.getOwnPropertyDescriptors(Selection.prototype);
++ Object.keys(descriptors).forEach(prop => {
++ if (prop === 'constructor') {
++ return;
++ }
++ const descriptor = descriptors[prop];
++ if (descriptor.value && typeof descriptor.value === 'function') {
++ // It's a method - bind it to base selection
++ const method = baseSelection[prop];
++ if (typeof method === 'function') {
++ selectionLike[prop] = method.bind(baseSelection);
++ }
++ } else if (!descriptor.get) {
++ // It's a regular property, not a getter - copy the value from base selection
++ const value = baseSelection[prop];
++ if (value !== undefined) {
++ selectionLike[prop] = value;
++ }
++ }
++ });
++
++ // Override specific properties with composed ranges data
++ Object.defineProperty(selectionLike, 'anchorNode', {
++ enumerable: true,
++ get: () => firstRange.startContainer
++ });
++ Object.defineProperty(selectionLike, 'anchorOffset', {
++ enumerable: true,
++ get: () => firstRange.startOffset
++ });
++ Object.defineProperty(selectionLike, 'focusNode', {
++ enumerable: true,
++ get: () => firstRange.endContainer
++ });
++ Object.defineProperty(selectionLike, 'focusOffset', {
++ enumerable: true,
++ get: () => firstRange.endOffset
++ });
++ Object.defineProperty(selectionLike, 'isCollapsed', {
++ enumerable: true,
++ get: () => firstRange.collapsed
++ });
++ Object.defineProperty(selectionLike, 'rangeCount', {
++ enumerable: true,
++ get: () => composedRanges.length
++ });
++ Object.defineProperty(selectionLike, 'type', {
++ enumerable: true,
++ get: () => {
++ const range = composedRanges[0];
++ if (!range) {
++ return 'None';
++ }
++ return range.collapsed ? 'Caret' : 'Range';
++ }
++ });
++
++ // Override getRangeAt to return a proper Range object from StaticRange
++ selectionLike.getRangeAt = function (index) {
++ if (index < 0 || index >= composedRanges.length) {
++ throw new DOMException('Index out of range', 'IndexSizeError');
++ }
++ const staticRange = composedRanges[index];
++ const range = document.createRange();
++ range.setStart(staticRange.startContainer, staticRange.startOffset);
++ range.setEnd(staticRange.endContainer, staticRange.endOffset);
++ return range;
++ };
++
++ // If the original selection has getComposedRanges, preserve it
++ if ('getComposedRanges' in baseSelection) {
++ selectionLike.getComposedRanges = function () {
++ return composedRanges;
++ };
++ }
++ return selectionLike;
++}
++function getDOMSelectionForEditor(editor) {
++ return getDOMSelection(getWindow(editor), editor.getRootElement());
++}
++
++/**
++ * Traverses up the DOM tree to find a ShadowRoot if the element is inside a shadow DOM.
++ * This function helps determine whether the given element is rendered within Shadow DOM
++ * encapsulation.
++ *
++ * @param element - The HTMLElement to start traversing from
++ * @returns The ShadowRoot if found, or Document if the element is not in shadow DOM
++ */
++function getShadowRootOrDocument(element) {
++ const shadowRoot = element.getRootNode({
++ composed: false
++ });
++ if (isShadowRoot(shadowRoot)) {
++ return shadowRoot;
++ }
++ return document;
++}
++
++/**
++ * Checks if the Lexical editor is running within a Shadow DOM context.
++ *
++ * This function determines whether the editor's root element is contained within
++ * a ShadowRoot, which is essential for enabling Shadow DOM-specific functionality
++ * like specialized deletion commands and selection handling.
++ *
++ * @param editor - The Lexical editor instance to check
++ * @returns `true` if the editor is in Shadow DOM, `false` otherwise
++ */
++function $isInShadowDOMContext(editor) {
++ const rootElement = editor.getRootElement();
++ return rootElement ? isShadowRoot(getShadowRootOrDocument(rootElement)) : false;
++}
++
++/**
++ * Gets the appropriate Document object for an element, accounting for shadow DOM.
++ * Returns the ownerDocument of the ShadowRoot if the element is in shadow DOM,
++ * otherwise returns the element's ownerDocument or the global document.
++ *
++ * @param element - The HTMLElement to get the document for
++ * @returns The Document object that should be used for DOM operations
++ */
++function getDocumentFromElement(element) {
++ if (!element || !CAN_USE_DOM) {
++ return document;
++ }
++ const rootNode = element.getRootNode({
++ composed: true
++ });
++
++ // If the element is not connected to a document, return the default document
++ if (rootNode === element || rootNode.nodeType !== Node.DOCUMENT_NODE) {
++ return element.ownerDocument || document;
++ }
++ return rootNode;
++}
++
++/**
++ * Gets the currently active (focused) element, accounting for shadow DOM encapsulation.
++ * In shadow DOM, the activeElement is tracked separately within the ShadowRoot.
++ * Falls back to the document's activeElement if not in shadow DOM.
++ *
++ * @param rootElement - The root element to check for shadow DOM context
++ * @returns The currently active Element or null if no element is focused
++ */
++function getActiveElement(rootElement) {
++ const shadowRoot = getShadowRootOrDocument(rootElement);
++ if (shadowRoot && isShadowRoot(shadowRoot) && shadowRoot.activeElement) {
++ return shadowRoot.activeElement;
++ }
++ return getDocumentFromElement(rootElement).activeElement;
++}
++
++/**
++ * Returns the selection for the defaultView of the ownerDocument of given EventTarget,
++ * with full Shadow DOM support.
++ *
++ * This function determines the appropriate selection context based on whether the
++ * EventTarget is within a Shadow DOM or regular DOM:
++ *
++ * **Shadow DOM Elements:**
++ * Uses getDOMSelectionFromShadowRoot to get a selection that properly handles
++ * Shadow DOM boundaries using the getComposedRanges API when available.
++ *
++ * **Regular DOM Elements:**
++ * Returns the standard window.getSelection() from the element's defaultView.
++ *
++ * **Edge Cases:**
++ * - Returns null for null EventTarget
++ * - Returns null for EventTargets without a valid defaultView
++ * - Handles non-HTML EventTargets gracefully
++ *
++ * @param eventTarget - The EventTarget (typically a DOM node) to get the selection from
++ * @returns A Selection object from the appropriate context or null if unavailable
+ */
+ function getDOMSelectionFromTarget(eventTarget) {
++ if (!eventTarget) {
++ return null;
++ }
++
++ // Check if eventTarget is in shadow DOM
++ if (isHTMLElement(eventTarget)) {
++ const shadowRoot = getShadowRootOrDocument(eventTarget);
++ if (shadowRoot && isShadowRoot(shadowRoot)) {
++ return getDOMSelectionFromShadowRoot(shadowRoot);
++ }
++ }
+ const defaultView = getDefaultView(eventTarget);
+ return defaultView ? defaultView.getSelection() : null;
+ }
+@@ -14039,6 +14514,7 @@ exports.$isDecoratorNode = $isDecoratorNode;
+ exports.$isEditorState = $isEditorState;
+ exports.$isElementNode = $isElementNode;
+ exports.$isExtendableTextPointCaret = $isExtendableTextPointCaret;
++exports.$isInShadowDOMContext = $isInShadowDOMContext;
+ exports.$isInlineElementOrDecoratorNode = $isInlineElementOrDecoratorNode;
+ exports.$isLeafNode = $isLeafNode;
+ exports.$isLineBreakNode = $isLineBreakNode;
+@@ -14072,6 +14548,7 @@ exports.$splitAtPointCaretNext = $splitAtPointCaretNext;
+ exports.$splitNode = $splitNode;
+ exports.$updateRangeSelectionFromCaretRange = $updateRangeSelectionFromCaretRange;
+ exports.ArtificialNode__DO_NOT_USE = ArtificialNode__DO_NOT_USE;
++exports.BEFORE_INPUT_COMMAND = BEFORE_INPUT_COMMAND;
+ exports.BLUR_COMMAND = BLUR_COMMAND;
+ exports.CAN_REDO_COMMAND = CAN_REDO_COMMAND;
+ exports.CAN_UNDO_COMMAND = CAN_UNDO_COMMAND;
+@@ -14084,6 +14561,8 @@ exports.COMMAND_PRIORITY_EDITOR = COMMAND_PRIORITY_EDITOR;
+ exports.COMMAND_PRIORITY_HIGH = COMMAND_PRIORITY_HIGH;
+ exports.COMMAND_PRIORITY_LOW = COMMAND_PRIORITY_LOW;
+ exports.COMMAND_PRIORITY_NORMAL = COMMAND_PRIORITY_NORMAL;
++exports.COMPOSITION_END_COMMAND = COMPOSITION_END_COMMAND;
++exports.COMPOSITION_START_COMMAND = COMPOSITION_START_COMMAND;
+ exports.CONTROLLED_TEXT_INSERTION_COMMAND = CONTROLLED_TEXT_INSERTION_COMMAND;
+ exports.COPY_COMMAND = COPY_COMMAND;
+ exports.CUT_COMMAND = CUT_COMMAND;
+@@ -14103,6 +14582,7 @@ exports.HISTORIC_TAG = HISTORIC_TAG;
+ exports.HISTORY_MERGE_TAG = HISTORY_MERGE_TAG;
+ exports.HISTORY_PUSH_TAG = HISTORY_PUSH_TAG;
+ exports.INDENT_CONTENT_COMMAND = INDENT_CONTENT_COMMAND;
++exports.INPUT_COMMAND = INPUT_COMMAND;
+ exports.INSERT_LINE_BREAK_COMMAND = INSERT_LINE_BREAK_COMMAND;
+ exports.INSERT_PARAGRAPH_COMMAND = INSERT_PARAGRAPH_COMMAND;
+ exports.INSERT_TAB_COMMAND = INSERT_TAB_COMMAND;
+@@ -14161,15 +14641,20 @@ exports.defineExtension = defineExtension;
+ exports.flipDirection = flipDirection;
+ exports.getDOMOwnerDocument = getDOMOwnerDocument;
+ exports.getDOMSelection = getDOMSelection;
++exports.getDOMSelectionForEditor = getDOMSelectionForEditor;
++exports.getDOMSelectionFromShadowRoot = getDOMSelectionFromShadowRoot;
+ exports.getDOMSelectionFromTarget = getDOMSelectionFromTarget;
+ exports.getDOMTextNode = getDOMTextNode;
++exports.getDocumentFromElement = getDocumentFromElement;
+ exports.getEditorPropertyFromDOMNode = getEditorPropertyFromDOMNode;
+ exports.getNearestEditorFromDOMNode = getNearestEditorFromDOMNode;
+ exports.getRegisteredNode = getRegisteredNode;
+ exports.getRegisteredNodeOrThrow = getRegisteredNodeOrThrow;
++exports.getShadowRootOrDocument = getShadowRootOrDocument;
+ exports.getStaticNodeConfig = getStaticNodeConfig;
+ exports.getTextDirection = getTextDirection;
+ exports.getTransformSetFromKlass = getTransformSetFromKlass;
++exports.getWindow = getWindow;
+ exports.isBlockDomNode = isBlockDomNode;
+ exports.isCurrentlyReadOnlyMode = isCurrentlyReadOnlyMode;
+ exports.isDOMDocumentNode = isDOMDocumentNode;
+@@ -14185,6 +14670,7 @@ exports.isLexicalEditor = isLexicalEditor;
+ exports.isModifierMatch = isModifierMatch;
+ exports.isSelectionCapturedInDecoratorInput = isSelectionCapturedInDecoratorInput;
+ exports.isSelectionWithinEditor = isSelectionWithinEditor;
++exports.isShadowRoot = isShadowRoot;
+ exports.makeStepwiseIterator = makeStepwiseIterator;
+ exports.removeFromParent = removeFromParent;
+ exports.resetRandomKey = resetRandomKey;
+diff --git a/node_modules/lexical/Lexical.dev.mjs b/node_modules/lexical/Lexical.dev.mjs
+index 37151a9..e2037e9 100644
+--- a/node_modules/lexical/Lexical.dev.mjs
++++ b/node_modules/lexical/Lexical.dev.mjs
+@@ -306,7 +306,7 @@ function getLastSelection(editor) {
+ });
+ }
+ function $handleTextMutation(target, node, editor) {
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ let anchorOffset = null;
+ let focusOffset = null;
+ if (domSelection !== null && domSelection.anchorNode === target) {
+@@ -1326,7 +1326,7 @@ function $normalizePoint(point) {
+
+ let subTreeTextContent = '';
+ let subTreeTextFormat = null;
+-let subTreeTextStyle = '';
++let subTreeTextStyle = null;
+ let editorTextContent = '';
+ let activeEditorConfig;
+ let activeEditor$1;
+@@ -1502,8 +1502,6 @@ function $createChildren(children, element, _startIndex, endIndex, slot) {
+ if (node !== null && $isTextNode(node)) {
+ if (subTreeTextFormat === null) {
+ subTreeTextFormat = node.getFormat();
+- }
+- if (subTreeTextStyle === '') {
+ subTreeTextStyle = node.getStyle();
+ }
+ }
+@@ -1543,13 +1541,13 @@ function reconcileTextFormat(element) {
+ }
+ }
+ function reconcileTextStyle(element) {
+- if (subTreeTextStyle !== '' && subTreeTextStyle !== element.__textStyle && !activeEditorStateReadOnly) {
++ if (subTreeTextStyle != null && subTreeTextStyle !== element.__textStyle && !activeEditorStateReadOnly) {
+ element.setTextStyle(subTreeTextStyle);
+ }
+ }
+ function $reconcileChildrenWithDirection(prevElement, nextElement, dom) {
+ subTreeTextFormat = null;
+- subTreeTextStyle = '';
++ subTreeTextStyle = null;
+ $reconcileChildren(prevElement, nextElement, nextElement.getDOMSlot(dom));
+ reconcileTextFormat(nextElement);
+ reconcileTextStyle(nextElement);
+@@ -1599,8 +1597,6 @@ function $reconcileChildren(prevElement, nextElement, slot) {
+ if ($isTextNode(nextChildNode)) {
+ if (subTreeTextFormat === null) {
+ subTreeTextFormat = nextChildNode.getFormat();
+- }
+- if (subTreeTextStyle === '') {
+ subTreeTextStyle = nextChildNode.getStyle();
+ }
+ }
+@@ -1808,8 +1804,6 @@ function $reconcileNodeChildren(nextElement, prevChildren, nextChildren, prevChi
+ if (node !== null && $isTextNode(node)) {
+ if (subTreeTextFormat === null) {
+ subTreeTextFormat = node.getFormat();
+- }
+- if (subTreeTextStyle === '') {
+ subTreeTextStyle = node.getStyle();
+ }
+ }
+@@ -1933,6 +1927,10 @@ function createCommand(type) {
+ const SELECTION_CHANGE_COMMAND = createCommand('SELECTION_CHANGE_COMMAND');
+ const SELECTION_INSERT_CLIPBOARD_NODES_COMMAND = createCommand('SELECTION_INSERT_CLIPBOARD_NODES_COMMAND');
+ const CLICK_COMMAND = createCommand('CLICK_COMMAND');
++const BEFORE_INPUT_COMMAND = createCommand('BEFORE_INPUT_COMMAND');
++const INPUT_COMMAND = createCommand('INPUT_COMMAND');
++const COMPOSITION_START_COMMAND = createCommand('COMPOSITION_START_COMMAND');
++const COMPOSITION_END_COMMAND = createCommand('COMPOSITION_END_COMMAND');
+ /**
+ * Dispatched to delete a character, the payload will be `true` if the deletion
+ * is backwards (backspace or delete on macOS) and `false` if forwards
+@@ -2108,7 +2106,7 @@ function $shouldPreventDefaultAndInsertText(selection, domTargetRange, text, tim
+ const focus = selection.focus;
+ const anchorNode = anchor.getNode();
+ const editor = getActiveEditor();
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;
+ const anchorKey = anchor.key;
+ const backingAnchorElement = editor.getElementByKey(anchorKey);
+@@ -2294,7 +2292,7 @@ function $updateSelectionFormatStyleFromElementNode(selection, node) {
+ function onClick(event, editor) {
+ updateEditorSync(editor, () => {
+ const selection = $getSelection();
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ const lastSelection = $getPreviousSelection();
+ if (domSelection) {
+ if ($isRangeSelection(selection)) {
+@@ -2364,9 +2362,15 @@ function $canRemoveText(anchorNode, focusNode) {
+ function isPossiblyAndroidKeyPress(timeStamp) {
+ return lastKeyCode === 'MediaLast' && timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY;
+ }
++function registerDefaultCommandHandlers(editor) {
++ editor.registerCommand(BEFORE_INPUT_COMMAND, $handleBeforeInput, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(INPUT_COMMAND, $handleInput, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(COMPOSITION_START_COMMAND, $handleCompositionStart, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(COMPOSITION_END_COMMAND, $handleCompositionEnd, COMMAND_PRIORITY_EDITOR);
++ editor.registerCommand(KEY_DOWN_COMMAND, $handleKeyDown, COMMAND_PRIORITY_EDITOR);
++}
+ function onBeforeInput(event, editor) {
+ const inputType = event.inputType;
+- const targetRange = getTargetRange(event);
+
+ // We let the browser do its own thing for composition.
+ if (inputType === 'deleteCompositionText' ||
+@@ -2380,237 +2384,242 @@ function onBeforeInput(event, editor) {
+ } else if (inputType === 'insertCompositionText') {
+ return;
+ }
+- updateEditorSync(editor, () => {
+- const selection = $getSelection();
+- if (inputType === 'deleteContentBackward') {
+- if (selection === null) {
+- // Use previous selection
+- const prevSelection = $getPreviousSelection();
+- if (!$isRangeSelection(prevSelection)) {
+- return;
+- }
+- $setSelection(prevSelection.clone());
++ dispatchCommand(editor, BEFORE_INPUT_COMMAND, event);
++}
++function $handleBeforeInput(event) {
++ const inputType = event.inputType;
++ const targetRange = getTargetRange(event);
++ const editor = getActiveEditor();
++ const selection = $getSelection();
++ if (inputType === 'deleteContentBackward') {
++ if (selection === null) {
++ // Use previous selection
++ const prevSelection = $getPreviousSelection();
++ if (!$isRangeSelection(prevSelection)) {
++ return true;
+ }
+- if ($isRangeSelection(selection)) {
+- const isSelectionAnchorSameAsFocus = selection.anchor.key === selection.focus.key;
+- if (isPossiblyAndroidKeyPress(event.timeStamp) && editor.isComposing() && isSelectionAnchorSameAsFocus) {
+- $setCompositionKey(null);
+- lastKeyDownTimeStamp = 0;
+- // Fixes an Android bug where selection flickers when backspacing
+- setTimeout(() => {
+- updateEditorSync(editor, () => {
+- $setCompositionKey(null);
+- });
+- }, ANDROID_COMPOSITION_LATENCY);
+- if ($isRangeSelection(selection)) {
+- const anchorNode = selection.anchor.getNode();
+- anchorNode.markDirty();
+- if (!$isTextNode(anchorNode)) {
+- formatDevErrorMessage(`Anchor node must be a TextNode`);
+- }
+- $updateSelectionFormatStyleFromTextNode(selection, anchorNode);
+- }
+- } else {
+- $setCompositionKey(null);
+- event.preventDefault();
+- // Chromium Android at the moment seems to ignore the preventDefault
+- // on 'deleteContentBackward' and still deletes the content. Which leads
+- // to multiple deletions. So we let the browser handle the deletion in this case.
+- const selectedNode = selection.anchor.getNode();
+- const selectedNodeText = selectedNode.getTextContent();
+- // When the target node has `canInsertTextAfter` set to false, the first deletion
+- // doesn't have an effect, so we need to handle it with Lexical.
+- const selectedNodeCanInsertTextAfter = selectedNode.canInsertTextAfter();
+- const hasSelectedAllTextInNode = selection.anchor.offset === 0 && selection.focus.offset === selectedNodeText.length;
+- let shouldLetBrowserHandleDelete = IS_ANDROID_CHROME && isSelectionAnchorSameAsFocus && !hasSelectedAllTextInNode && selectedNodeCanInsertTextAfter;
+- // Check if selection is collapsed and if the previous node is a decorator node
+- // If so, the browser will not be able to handle the deletion
+- if (shouldLetBrowserHandleDelete && selection.isCollapsed()) {
+- shouldLetBrowserHandleDelete = !$isDecoratorNode($getAdjacentNode(selection.anchor, true));
++ $setSelection(prevSelection.clone());
++ }
++ if ($isRangeSelection(selection)) {
++ const isSelectionAnchorSameAsFocus = selection.anchor.key === selection.focus.key;
++ if (isPossiblyAndroidKeyPress(event.timeStamp) && editor.isComposing() && isSelectionAnchorSameAsFocus) {
++ $setCompositionKey(null);
++ lastKeyDownTimeStamp = 0;
++ // Fixes an Android bug where selection flickers when backspacing
++ setTimeout(() => {
++ updateEditorSync(editor, () => {
++ $setCompositionKey(null);
++ });
++ }, ANDROID_COMPOSITION_LATENCY);
++ if ($isRangeSelection(selection)) {
++ const anchorNode = selection.anchor.getNode();
++ anchorNode.markDirty();
++ if (!$isTextNode(anchorNode)) {
++ formatDevErrorMessage(`Anchor node must be a TextNode`);
+ }
+- if (!shouldLetBrowserHandleDelete) {
+- dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);
+- // When deleting across paragraphs, Chrome on Android incorrectly shifts the selection rightwards
+- // We save the correct selection to restore later during handling of selectionchange event
+- const selectionAfterDelete = $getSelection();
+- if (IS_ANDROID_CHROME && $isRangeSelection(selectionAfterDelete) && selectionAfterDelete.isCollapsed()) {
+- postDeleteSelectionToRestore = selectionAfterDelete;
+- // Cleanup in case selectionchange does not fire
+- setTimeout(() => postDeleteSelectionToRestore = null);
+- }
++ $updateSelectionFormatStyleFromTextNode(selection, anchorNode);
++ }
++ } else {
++ $setCompositionKey(null);
++ event.preventDefault();
++ // Chromium Android at the moment seems to ignore the preventDefault
++ // on 'deleteContentBackward' and still deletes the content. Which leads
++ // to multiple deletions. So we let the browser handle the deletion in this case.
++ const selectedNode = selection.anchor.getNode();
++ const selectedNodeText = selectedNode.getTextContent();
++ // When the target node has `canInsertTextAfter` set to false, the first deletion
++ // doesn't have an effect, so we need to handle it with Lexical.
++ const selectedNodeCanInsertTextAfter = selectedNode.canInsertTextAfter();
++ const hasSelectedAllTextInNode = selection.anchor.offset === 0 && selection.focus.offset === selectedNodeText.length;
++ let shouldLetBrowserHandleDelete = IS_ANDROID_CHROME && isSelectionAnchorSameAsFocus && !hasSelectedAllTextInNode && selectedNodeCanInsertTextAfter;
++ // Check if selection is collapsed and if the previous node is a decorator node
++ // If so, the browser will not be able to handle the deletion
++ if (shouldLetBrowserHandleDelete && selection.isCollapsed()) {
++ shouldLetBrowserHandleDelete = !$isDecoratorNode($getAdjacentNode(selection.anchor, true));
++ }
++ if (!shouldLetBrowserHandleDelete) {
++ dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);
++ // When deleting across paragraphs, Chrome on Android incorrectly shifts the selection rightwards
++ // We save the correct selection to restore later during handling of selectionchange event
++ const selectionAfterDelete = $getSelection();
++ if (IS_ANDROID_CHROME && $isRangeSelection(selectionAfterDelete) && selectionAfterDelete.isCollapsed()) {
++ postDeleteSelectionToRestore = selectionAfterDelete;
++ // Cleanup in case selectionchange does not fire
++ setTimeout(() => postDeleteSelectionToRestore = null);
+ }
+ }
+- return;
+ }
++ return true;
+ }
+- if (!$isRangeSelection(selection)) {
+- return;
+- }
+- const data = event.data;
++ }
++ if (!$isRangeSelection(selection)) {
++ return true;
++ }
++ const data = event.data;
+
+- // This represents the case when two beforeinput events are triggered at the same time (without a
+- // full event loop ending at input). This happens with MacOS with the default keyboard settings,
+- // a combination of autocorrection + autocapitalization.
+- // Having Lexical run everything in controlled mode would fix the issue without additional code
+- // but this would kill the massive performance win from the most common typing event.
+- // Alternatively, when this happens we can prematurely update our EditorState based on the DOM
+- // content, a job that would usually be the input event's responsibility.
+- if (unprocessedBeforeInputData !== null) {
+- $updateSelectedTextFromDOM(false, editor, unprocessedBeforeInputData);
+- }
+- if ((!selection.dirty || unprocessedBeforeInputData !== null) && selection.isCollapsed() && !$isRootNode(selection.anchor.getNode()) && targetRange !== null) {
+- selection.applyDOMRange(targetRange);
++ // This represents the case when two beforeinput events are triggered at the same time (without a
++ // full event loop ending at input). This happens with MacOS with the default keyboard settings,
++ // a combination of autocorrection + autocapitalization.
++ // Having Lexical run everything in controlled mode would fix the issue without additional code
++ // but this would kill the massive performance win from the most common typing event.
++ // Alternatively, when this happens we can prematurely update our EditorState based on the DOM
++ // content, a job that would usually be the input event's responsibility.
++ if (unprocessedBeforeInputData !== null) {
++ $updateSelectedTextFromDOM(false, editor, unprocessedBeforeInputData);
++ }
++ if ((!selection.dirty || unprocessedBeforeInputData !== null) && selection.isCollapsed() && !$isRootNode(selection.anchor.getNode()) && targetRange !== null) {
++ selection.applyDOMRange(targetRange);
++ }
++ unprocessedBeforeInputData = null;
++ const anchor = selection.anchor;
++ const focus = selection.focus;
++ const anchorNode = anchor.getNode();
++ const focusNode = focus.getNode();
++ if (inputType === 'insertText' || inputType === 'insertTranspose') {
++ if (data === '\n') {
++ event.preventDefault();
++ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
++ } else if (data === DOUBLE_LINE_BREAK) {
++ event.preventDefault();
++ dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
++ } else if (data == null && event.dataTransfer) {
++ // Gets around a Safari text replacement bug.
++ const text = event.dataTransfer.getData('text/plain');
++ event.preventDefault();
++ selection.insertRawText(text);
++ } else if (data != null && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, true)) {
++ event.preventDefault();
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
++ } else {
++ unprocessedBeforeInputData = data;
+ }
+- unprocessedBeforeInputData = null;
+- const anchor = selection.anchor;
+- const focus = selection.focus;
+- const anchorNode = anchor.getNode();
+- const focusNode = focus.getNode();
+- if (inputType === 'insertText' || inputType === 'insertTranspose') {
+- if (data === '\n') {
+- event.preventDefault();
++ lastBeforeInputInsertTextTimeStamp = event.timeStamp;
++ return true;
++ }
++
++ // Prevent the browser from carrying out
++ // the input event, so we can control the
++ // output.
++ event.preventDefault();
++ switch (inputType) {
++ case 'insertFromYank':
++ case 'insertFromDrop':
++ case 'insertReplacementText':
++ {
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
++ break;
++ }
++ case 'insertFromComposition':
++ {
++ // This is the end of composition
++ $setCompositionKey(null);
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
++ break;
++ }
++ case 'insertLineBreak':
++ {
++ // Used for Android
++ $setCompositionKey(null);
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
+- } else if (data === DOUBLE_LINE_BREAK) {
+- event.preventDefault();
+- dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
+- } else if (data == null && event.dataTransfer) {
+- // Gets around a Safari text replacement bug.
+- const text = event.dataTransfer.getData('text/plain');
+- event.preventDefault();
+- selection.insertRawText(text);
+- } else if (data != null && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, true)) {
+- event.preventDefault();
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
+- } else {
+- unprocessedBeforeInputData = data;
++ break;
+ }
+- lastBeforeInputInsertTextTimeStamp = event.timeStamp;
+- return;
+- }
++ case 'insertParagraph':
++ {
++ // Used for Android
++ $setCompositionKey(null);
+
+- // Prevent the browser from carrying out
+- // the input event, so we can control the
+- // output.
+- event.preventDefault();
+- switch (inputType) {
+- case 'insertFromYank':
+- case 'insertFromDrop':
+- case 'insertReplacementText':
+- {
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
+- break;
+- }
+- case 'insertFromComposition':
+- {
+- // This is the end of composition
+- $setCompositionKey(null);
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
+- break;
+- }
+- case 'insertLineBreak':
+- {
+- // Used for Android
+- $setCompositionKey(null);
++ // Safari does not provide the type "insertLineBreak".
++ // So instead, we need to infer it from the keyboard event.
++ // We do not apply this logic to iOS to allow newline auto-capitalization
++ // work without creating linebreaks when pressing Enter
++ if (isInsertLineBreak && !IS_IOS) {
++ isInsertLineBreak = false;
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
+- break;
+- }
+- case 'insertParagraph':
+- {
+- // Used for Android
+- $setCompositionKey(null);
+-
+- // Safari does not provide the type "insertLineBreak".
+- // So instead, we need to infer it from the keyboard event.
+- // We do not apply this logic to iOS to allow newline auto-capitalization
+- // work without creating linebreaks when pressing Enter
+- if (isInsertLineBreak && !IS_IOS) {
+- isInsertLineBreak = false;
+- dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
+- } else {
+- dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
+- }
+- break;
+- }
+- case 'insertFromPaste':
+- case 'insertFromPasteAsQuotation':
+- {
+- dispatchCommand(editor, PASTE_COMMAND, event);
+- break;
+- }
+- case 'deleteByComposition':
+- {
+- if ($canRemoveText(anchorNode, focusNode)) {
+- dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
+- }
+- break;
++ } else {
++ dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
+ }
+- case 'deleteByDrag':
+- case 'deleteByCut':
+- {
++ break;
++ }
++ case 'insertFromPaste':
++ case 'insertFromPasteAsQuotation':
++ {
++ dispatchCommand(editor, PASTE_COMMAND, event);
++ break;
++ }
++ case 'deleteByComposition':
++ {
++ if ($canRemoveText(anchorNode, focusNode)) {
+ dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
+- break;
+- }
+- case 'deleteContent':
+- {
+- dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);
+- break;
+- }
+- case 'deleteWordBackward':
+- {
+- dispatchCommand(editor, DELETE_WORD_COMMAND, true);
+- break;
+- }
+- case 'deleteWordForward':
+- {
+- dispatchCommand(editor, DELETE_WORD_COMMAND, false);
+- break;
+- }
+- case 'deleteHardLineBackward':
+- case 'deleteSoftLineBackward':
+- {
+- dispatchCommand(editor, DELETE_LINE_COMMAND, true);
+- break;
+- }
+- case 'deleteContentForward':
+- case 'deleteHardLineForward':
+- case 'deleteSoftLineForward':
+- {
+- dispatchCommand(editor, DELETE_LINE_COMMAND, false);
+- break;
+- }
+- case 'formatStrikeThrough':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'strikethrough');
+- break;
+- }
+- case 'formatBold':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');
+- break;
+- }
+- case 'formatItalic':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');
+- break;
+- }
+- case 'formatUnderline':
+- {
+- dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');
+- break;
+- }
+- case 'historyUndo':
+- {
+- dispatchCommand(editor, UNDO_COMMAND, undefined);
+- break;
+ }
+- case 'historyRedo':
+- {
+- dispatchCommand(editor, REDO_COMMAND, undefined);
+- break;
+- }
+- // NO-OP
+- }
+- });
++ break;
++ }
++ case 'deleteByDrag':
++ case 'deleteByCut':
++ {
++ dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
++ break;
++ }
++ case 'deleteContent':
++ {
++ dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);
++ break;
++ }
++ case 'deleteWordBackward':
++ {
++ dispatchCommand(editor, DELETE_WORD_COMMAND, true);
++ break;
++ }
++ case 'deleteWordForward':
++ {
++ dispatchCommand(editor, DELETE_WORD_COMMAND, false);
++ break;
++ }
++ case 'deleteHardLineBackward':
++ case 'deleteSoftLineBackward':
++ {
++ dispatchCommand(editor, DELETE_LINE_COMMAND, true);
++ break;
++ }
++ case 'deleteContentForward':
++ case 'deleteHardLineForward':
++ case 'deleteSoftLineForward':
++ {
++ dispatchCommand(editor, DELETE_LINE_COMMAND, false);
++ break;
++ }
++ case 'formatStrikeThrough':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'strikethrough');
++ break;
++ }
++ case 'formatBold':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');
++ break;
++ }
++ case 'formatItalic':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');
++ break;
++ }
++ case 'formatUnderline':
++ {
++ dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');
++ break;
++ }
++ case 'historyUndo':
++ {
++ dispatchCommand(editor, UNDO_COMMAND, undefined);
++ break;
++ }
++ case 'historyRedo':
++ {
++ dispatchCommand(editor, REDO_COMMAND, undefined);
++ break;
++ }
++ // NO-OP
++ }
++ return true;
+ }
+ function onInput(event, editor) {
+ // Note that the MutationObserver may or may not have already fired,
+@@ -2622,90 +2631,103 @@ function onInput(event, editor) {
+ // We don't want the onInput to bubble, in the case of nested editors.
+ event.stopPropagation();
+ updateEditorSync(editor, () => {
+- if (isHTMLElement(event.target) && $isSelectionCapturedInDecorator(event.target)) {
+- return;
++ editor.dispatchCommand(INPUT_COMMAND, event);
++ }, {
++ event
++ });
++ unprocessedBeforeInputData = null;
++}
++function $handleInput(event) {
++ if (isHTMLElement(event.target) && $isSelectionCapturedInDecorator(event.target)) {
++ return true;
++ }
++ const editor = getActiveEditor();
++ const selection = $getSelection();
++ const data = event.data;
++ const targetRange = getTargetRange(event);
++ if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, false)) {
++ // Given we're over-riding the default behavior, we will need
++ // to ensure to disable composition before dispatching the
++ // insertText command for when changing the sequence for FF.
++ if (isFirefoxEndingComposition) {
++ $onCompositionEndImpl(editor, data);
++ isFirefoxEndingComposition = false;
+ }
+- const selection = $getSelection();
+- const data = event.data;
+- const targetRange = getTargetRange(event);
+- if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, targetRange, data, event.timeStamp, false)) {
+- // Given we're over-riding the default behavior, we will need
+- // to ensure to disable composition before dispatching the
+- // insertText command for when changing the sequence for FF.
+- if (isFirefoxEndingComposition) {
+- $onCompositionEndImpl(editor, data);
+- isFirefoxEndingComposition = false;
+- }
+- const anchor = selection.anchor;
+- const anchorNode = anchor.getNode();
+- const domSelection = getDOMSelection(getWindow(editor));
+- if (domSelection === null) {
+- return;
+- }
+- const isBackward = selection.isBackward();
+- const startOffset = isBackward ? selection.anchor.offset : selection.focus.offset;
+- const endOffset = isBackward ? selection.focus.offset : selection.anchor.offset;
+- // If the content is the same as inserted, then don't dispatch an insertion.
+- // Given onInput doesn't take the current selection (it uses the previous)
+- // we can compare that against what the DOM currently says.
+- if (!CAN_USE_BEFORE_INPUT || selection.isCollapsed() || !$isTextNode(anchorNode) || domSelection.anchorNode === null || anchorNode.getTextContent().slice(0, startOffset) + data + anchorNode.getTextContent().slice(startOffset + endOffset) !== getAnchorTextFromDOM(domSelection.anchorNode)) {
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
+- }
+- const textLength = data.length;
++ const anchor = selection.anchor;
++ const anchorNode = anchor.getNode();
++ const domSelection = getDOMSelection(getWindow(editor));
++ if (domSelection === null) {
++ return true;
++ }
++ const isBackward = selection.isBackward();
++ const startOffset = isBackward ? selection.anchor.offset : selection.focus.offset;
++ const endOffset = isBackward ? selection.focus.offset : selection.anchor.offset;
++ // If the content is the same as inserted, then don't dispatch an insertion.
++ // Given onInput doesn't take the current selection (it uses the previous)
++ // we can compare that against what the DOM currently says.
++ if (!CAN_USE_BEFORE_INPUT || selection.isCollapsed() || !$isTextNode(anchorNode) || domSelection.anchorNode === null || anchorNode.getTextContent().slice(0, startOffset) + data + anchorNode.getTextContent().slice(startOffset + endOffset) !== getAnchorTextFromDOM(domSelection.anchorNode)) {
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
++ }
++ const textLength = data.length;
+
+- // Another hack for FF, as it's possible that the IME is still
+- // open, even though compositionend has already fired (sigh).
+- if (IS_FIREFOX && textLength > 1 && event.inputType === 'insertCompositionText' && !editor.isComposing()) {
+- selection.anchor.offset -= textLength;
+- }
++ // Another hack for FF, as it's possible that the IME is still
++ // open, even though compositionend has already fired (sigh).
++ if (IS_FIREFOX && textLength > 1 && event.inputType === 'insertCompositionText' && !editor.isComposing()) {
++ selection.anchor.offset -= textLength;
++ }
+
+- // This ensures consistency on Android.
+- if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT && editor.isComposing()) {
+- lastKeyDownTimeStamp = 0;
+- $setCompositionKey(null);
+- }
+- } else {
+- const characterData = data !== null ? data : undefined;
+- $updateSelectedTextFromDOM(false, editor, characterData);
++ // This ensures consistency on Android.
++ if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT && editor.isComposing()) {
++ lastKeyDownTimeStamp = 0;
++ $setCompositionKey(null);
++ }
++ } else {
++ const characterData = data !== null ? data : undefined;
++ $updateSelectedTextFromDOM(false, editor, characterData);
+
+- // onInput always fires after onCompositionEnd for FF.
+- if (isFirefoxEndingComposition) {
+- $onCompositionEndImpl(editor, data || undefined);
+- isFirefoxEndingComposition = false;
+- }
++ // onInput always fires after onCompositionEnd for FF.
++ if (isFirefoxEndingComposition) {
++ $onCompositionEndImpl(editor, data || undefined);
++ isFirefoxEndingComposition = false;
+ }
++ }
+
+- // Also flush any other mutations that might have occurred
+- // since the change.
+- $flushMutations();
+- }, {
+- event
+- });
+- unprocessedBeforeInputData = null;
++ // Also flush any other mutations that might have occurred
++ // since the change.
++ $flushMutations();
++ return true;
+ }
+ function onCompositionStart(event, editor) {
+- updateEditorSync(editor, () => {
+- const selection = $getSelection();
+- if ($isRangeSelection(selection) && !editor.isComposing()) {
+- const anchor = selection.anchor;
+- const node = selection.anchor.getNode();
+- $setCompositionKey(anchor.key);
+- if (
+- // If it has been 30ms since the last keydown, then we should
+- // apply the empty space heuristic. We can't do this for Safari,
+- // as the keydown fires after composition start.
+- event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY ||
+- // FF has issues around composing multibyte characters, so we also
+- // need to invoke the empty space heuristic below.
+- anchor.type === 'element' || !selection.isCollapsed() || node.getFormat() !== selection.format || $isTextNode(node) && node.getStyle() !== selection.style) {
+- // We insert a zero width character, ready for the composition
+- // to get inserted into the new node we create. If
+- // we don't do this, Safari will fail on us because
+- // there is no text node matching the selection.
+- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, COMPOSITION_START_CHAR);
+- }
++ dispatchCommand(editor, COMPOSITION_START_COMMAND, event);
++}
++function $handleCompositionStart(event) {
++ const editor = getActiveEditor();
++ const selection = $getSelection();
++ if ($isRangeSelection(selection) && !editor.isComposing()) {
++ const anchor = selection.anchor;
++ const node = selection.anchor.getNode();
++ $setCompositionKey(anchor.key);
++ if (
++ // If it has been 30ms since the last keydown, then we should
++ // apply the empty space heuristic. We can't do this for Safari,
++ // as the keydown fires after composition start.
++ event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY ||
++ // FF has issues around composing multibyte characters, so we also
++ // need to invoke the empty space heuristic below.
++ anchor.type === 'element' || !selection.isCollapsed() || node.getFormat() !== selection.format || $isTextNode(node) && node.getStyle() !== selection.style) {
++ // We insert a zero width character, ready for the composition
++ // to get inserted into the new node we create. If
++ // we don't do this, Safari will fail on us because
++ // there is no text node matching the selection.
++ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, COMPOSITION_START_CHAR);
+ }
+- });
++ }
++ return true;
++}
++function $handleCompositionEnd(event) {
++ const editor = getActiveEditor();
++ $onCompositionEndImpl(editor, event.data);
++ return true;
+ }
+ function $onCompositionEndImpl(editor, data) {
+ const compositionKey = editor._compositionKey;
+@@ -2757,9 +2779,7 @@ function onCompositionEnd(event, editor) {
+ isSafariEndingComposition = true;
+ safariEndCompositionEventData = event.data;
+ } else {
+- updateEditorSync(editor, () => {
+- $onCompositionEndImpl(editor, event.data);
+- });
++ dispatchCommand(editor, COMPOSITION_END_COMMAND, event);
+ }
+ }
+ function onKeyDown(event, editor) {
+@@ -2768,11 +2788,12 @@ function onKeyDown(event, editor) {
+ if (editor.isComposing()) {
+ return;
+ }
+- if (dispatchCommand(editor, KEY_DOWN_COMMAND, event)) {
+- return;
+- }
++ dispatchCommand(editor, KEY_DOWN_COMMAND, event);
++}
++function $handleKeyDown(event) {
++ const editor = getActiveEditor();
+ if (event.key == null) {
+- return;
++ return true;
+ }
+ if (isSafariEndingComposition && isBackspace(event)) {
+ updateEditorSync(editor, () => {
+@@ -2780,7 +2801,7 @@ function onKeyDown(event, editor) {
+ });
+ isSafariEndingComposition = false;
+ safariEndCompositionEventData = '';
+- return;
++ return true;
+ }
+ if (isMoveForward(event)) {
+ dispatchCommand(editor, KEY_ARROW_RIGHT_COMMAND, event);
+@@ -2871,8 +2892,9 @@ function onKeyDown(event, editor) {
+ }
+ }
+ if (isModifier(event)) {
+- dispatchCommand(editor, KEY_MODIFIER_COMMAND, event);
++ editor.dispatchCommand(KEY_MODIFIER_COMMAND, event);
+ }
++ return true;
+ }
+ function getRootElementRemoveHandles(rootElement) {
+ // @ts-expect-error: internal field
+@@ -2947,7 +2969,7 @@ function hasStoppedLexicalPropagation(event) {
+ function addRootElementEvents(rootElement, editor) {
+ // We only want to have a single global selectionchange event handler, shared
+ // between all editor instances.
+- const doc = rootElement.ownerDocument;
++ const doc = getShadowRootOrDocument(rootElement);
+ rootElementToDocument.set(rootElement, doc);
+ const documentRootElementsCount = rootElementsRegistered.get(doc) ?? 0;
+ if (documentRootElementsCount < 1) {
+@@ -3005,7 +3027,7 @@ function addRootElementEvents(rootElement, editor) {
+ }
+ const rootElementNotRegisteredWarning = warnOnlyOnce('Root element not registered');
+ function removeRootElementEvents(rootElement) {
+- const doc = rootElementToDocument.get(rootElement);
++ const doc = getShadowRootOrDocument(rootElement);
+ if (doc === undefined) {
+ rootElementNotRegisteredWarning();
+ return;
+@@ -6758,7 +6780,7 @@ class RangeSelection {
+ }
+ const collapse = alter === 'move';
+ const editor = getActiveEditor();
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ if (!domSelection) {
+ return;
+ }
+@@ -6846,11 +6868,12 @@ class RangeSelection {
+ }
+ /**
+ * Helper for handling forward character and word deletion that prevents element nodes
+- * like a table, columns layout being destroyed
++ * like a table, columns layout being destroyed. Also prevents deletion into shadow roots.
+ *
+ * @param anchor the anchor
+ * @param anchorNode the anchor node in the selection
+ * @param isBackward whether or not selection is backwards
++ * @returns true if deletion should be prevented
+ */
+ forwardDeletion(anchor, anchorNode, isBackward) {
+ if (!isBackward && (
+@@ -6954,7 +6977,26 @@ class RangeSelection {
+
+ // Handle the deletion around decorators.
+ const focus = this.focus;
++ const initialAnchorKey = anchor.key;
++ const initialAnchorOffset = anchor.offset;
++ const initialFocusKey = focus.key;
++ const initialFocusOffset = focus.offset;
+ this.modify('extend', isBackward, 'character');
++
++ // Check if modify actually changed the selection (it might not in shadow DOM)
++ const selectionChanged = this.anchor.key !== initialAnchorKey || this.anchor.offset !== initialAnchorOffset || this.focus.key !== initialFocusKey || this.focus.offset !== initialFocusOffset;
++ if (!selectionChanged && anchor.type === 'text' && $isTextNode(anchorNode)) {
++ // Fallback for environments where modify doesn't work (e.g., shadow DOM)
++ const textContent = anchorNode.getTextContent();
++ const offset = anchor.offset;
++ if (isBackward && offset > 0) {
++ // Select the character before cursor
++ this.anchor.set(anchor.key, offset - 1, 'text');
++ } else if (!isBackward && offset < textContent.length) {
++ // Select the character after cursor
++ this.focus.set(focus.key, offset + 1, 'text');
++ }
++ }
+ if (!this.isCollapsed()) {
+ const focusNode = focus.type === 'text' ? focus.getNode() : null;
+ anchorNode = anchor.type === 'text' ? anchor.getNode() : null;
+@@ -6998,7 +7040,31 @@ class RangeSelection {
+ */
+ deleteLine(isBackward) {
+ if (this.isCollapsed()) {
++ const anchor = this.anchor;
++ const focus = this.focus;
++ const initialAnchorKey = anchor.key;
++ const initialAnchorOffset = anchor.offset;
++ const initialFocusKey = focus.key;
++ const initialFocusOffset = focus.offset;
+ this.modify('extend', isBackward, 'lineboundary');
++
++ // Check if modify actually changed the selection (it might not in shadow DOM)
++ const selectionChanged = this.anchor.key !== initialAnchorKey || this.anchor.offset !== initialAnchorOffset || this.focus.key !== initialFocusKey || this.focus.offset !== initialFocusOffset;
++ if (!selectionChanged && anchor.type === 'text') {
++ // Fallback for environments where modify doesn't work (e.g., shadow DOM)
++ const anchorNode = anchor.getNode();
++ if ($isTextNode(anchorNode)) {
++ const textContent = anchorNode.getTextContent();
++ const offset = anchor.offset;
++ if (isBackward && offset > 0) {
++ // Delete from beginning of line to cursor
++ this.anchor.set(anchor.key, 0, 'text');
++ } else if (!isBackward && offset < textContent.length) {
++ // Delete from cursor to end of line
++ this.focus.set(focus.key, textContent.length, 'text');
++ }
++ }
++ }
+ }
+ if (this.isCollapsed()) {
+ // If the selection was already collapsed at the lineboundary,
+@@ -7010,6 +7076,57 @@ class RangeSelection {
+ }
+ }
+
++ /**
++ * Helper function to determine if a character is a word boundary (whitespace).
++ * @param char the character to check
++ * @returns true if the character is a word boundary
++ */
++ isWordBoundary(char) {
++ return char === ' ' || char === '\t' || char === '\n' || char === '\r';
++ }
++
++ /**
++ * Find the start of a word going backward from the given offset in text.
++ * @param text the text to search in
++ * @param offset the starting offset
++ * @returns the offset of the word start
++ */
++ findWordStart(text, offset) {
++ let position = offset - 1;
++
++ // Skip spaces
++ while (position >= 0 && this.isWordBoundary(text[position])) {
++ position--;
++ }
++
++ // Find word start
++ while (position > 0 && !this.isWordBoundary(text[position - 1])) {
++ position--;
++ }
++ return position >= 0 ? position : 0;
++ }
++
++ /**
++ * Find the end of a word going forward from the given offset in text.
++ * @param text the text to search in
++ * @param offset the starting offset
++ * @returns the offset of the word end
++ */
++ findWordEnd(text, offset) {
++ let position = offset;
++
++ // Skip spaces
++ while (position < text.length && this.isWordBoundary(text[position])) {
++ position++;
++ }
++
++ // Find word end
++ while (position < text.length && !this.isWordBoundary(text[position])) {
++ position++;
++ }
++ return position;
++ }
++
+ /**
+ * Performs one logical word deletion operation on the EditorState based on the current Selection.
+ * Handles different node types.
+@@ -7023,7 +7140,69 @@ class RangeSelection {
+ if (this.forwardDeletion(anchor, anchorNode, isBackward)) {
+ return;
+ }
++ const initialAnchorKey = anchor.key;
++ const initialAnchorOffset = anchor.offset;
++ const focus = this.focus;
++ const initialFocusKey = focus.key;
++ const initialFocusOffset = focus.offset;
+ this.modify('extend', isBackward, 'word');
++
++ // Check if modify actually changed the selection (it might not in shadow DOM)
++ const selectionChanged = this.anchor.key !== initialAnchorKey || this.anchor.offset !== initialAnchorOffset || this.focus.key !== initialFocusKey || this.focus.offset !== initialFocusOffset;
++ if (!selectionChanged && anchor.type === 'text' && $isTextNode(anchorNode)) {
++ // Fallback for environments where modify doesn't work (e.g., shadow DOM)
++ const textContent = anchorNode.getTextContent();
++ const offset = anchor.offset;
++ if (isBackward) {
++ // Backward: find start of word before cursor
++ if (offset === 0) {
++ // At node start, check previous sibling
++ const prevSibling = anchorNode.getPreviousSibling();
++ if ($isTextNode(prevSibling)) {
++ const prevText = prevSibling.getTextContent();
++ const position = this.findWordStart(prevText, prevText.length);
++ this.anchor.set(prevSibling.__key, position, 'text');
++ }
++ } else {
++ const position = this.findWordStart(textContent, offset);
++ if (position === 0 && this.isWordBoundary(textContent[0])) {
++ // Only spaces in this node, try previous sibling
++ const prevSibling = anchorNode.getPreviousSibling();
++ if ($isTextNode(prevSibling)) {
++ const prevText = prevSibling.getTextContent();
++ const prevPosition = this.findWordStart(prevText, prevText.length);
++ this.anchor.set(prevSibling.__key, prevPosition, 'text');
++ return;
++ }
++ }
++ this.anchor.set(anchor.key, position, 'text');
++ }
++ } else {
++ // Forward: find end of word after cursor
++ if (offset === textContent.length) {
++ // At node end, check next sibling
++ const nextSibling = anchorNode.getNextSibling();
++ if ($isTextNode(nextSibling)) {
++ const nextText = nextSibling.getTextContent();
++ const position = this.findWordEnd(nextText, 0);
++ this.focus.set(nextSibling.__key, position, 'text');
++ }
++ } else {
++ const position = this.findWordEnd(textContent, offset);
++ if (position === textContent.length && this.isWordBoundary(textContent[textContent.length - 1])) {
++ // Only spaces in this node, try next sibling
++ const nextSibling = anchorNode.getNextSibling();
++ if ($isTextNode(nextSibling)) {
++ const nextText = nextSibling.getTextContent();
++ const nextPosition = this.findWordEnd(nextText, 0);
++ this.focus.set(nextSibling.__key, nextPosition, 'text');
++ return;
++ }
++ }
++ this.focus.set(focus.key, position, 'text');
++ }
++ }
++ }
+ }
+ this.removeText();
+ }
+@@ -7472,7 +7651,7 @@ function $createNodeSelection() {
+ function $internalCreateSelection(editor, event) {
+ const currentEditorState = editor.getEditorState();
+ const lastSelection = currentEditorState._selection;
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ if ($isRangeSelection(lastSelection) || lastSelection == null) {
+ return $internalCreateRangeSelection(lastSelection, domSelection, editor, event);
+ }
+@@ -7730,7 +7909,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
+ const focusDOMNode = domSelection.focusNode;
+ const anchorOffset = domSelection.anchorOffset;
+ const focusOffset = domSelection.focusOffset;
+- const activeElement = document.activeElement;
++ const activeElement = getActiveElement(rootElement);
+
+ // TODO: make this not hard-coded, and add another config option
+ // that makes this configurable.
+@@ -8457,7 +8636,7 @@ function $commitPendingUpdates(editor, recoveryEditorState) {
+ // Reconciliation has finished. Now update selection and trigger listeners.
+ // ======
+
+- const domSelection = shouldSkipDOM ? null : getDOMSelection(getWindow(editor));
++ const domSelection = shouldSkipDOM ? null : getDOMSelectionForEditor(editor);
+
+ // Attempt to update the DOM selection, including focusing of the root element,
+ // and scroll into view if needed.
+@@ -9489,11 +9668,15 @@ class ElementNode extends LexicalNode {
+ };
+ const textFormat = this.getTextFormat();
+ const textStyle = this.getTextStyle();
+- if (textFormat !== 0) {
+- json.textFormat = textFormat;
+- }
+- if (textStyle !== '') {
+- json.textStyle = textStyle;
++ // Only persist for cases when there are no TextNode children from which
++ // these would be set on reconcile (#7968)
++ if ((textFormat !== 0 || textStyle !== '') && !$isRootOrShadowRoot(this) && !this.getChildren().some($isTextNode)) {
++ if (textFormat !== 0) {
++ json.textFormat = textFormat;
++ }
++ if (textStyle !== '') {
++ json.textStyle = textStyle;
++ }
+ }
+ return json;
+ }
+@@ -9890,12 +10073,20 @@ class ParagraphNode extends ElementNode {
+ return $createParagraphNode().updateFromJSON(serializedNode);
+ }
+ exportJSON() {
+- return {
+- ...super.exportJSON(),
+- // These are included explicitly for backwards compatibility
+- textFormat: this.getTextFormat(),
+- textStyle: this.getTextStyle()
+- };
++ const json = super.exportJSON();
++ // Provide backwards compatible values, see #7971
++ if (json.textFormat === undefined || json.textStyle === undefined) {
++ // Compute the same value that the reconciler would
++ const firstTextNode = this.getChildren().find($isTextNode);
++ if (firstTextNode) {
++ json.textFormat = firstTextNode.getFormat();
++ json.textStyle = firstTextNode.getStyle();
++ } else {
++ json.textFormat = this.getTextFormat();
++ json.textStyle = this.getTextStyle();
++ }
++ }
++ return json;
+ }
+
+ // Mutation
+@@ -10178,6 +10369,7 @@ function createEditor(editorConfig) {
+ editor._pendingEditorState = initialEditorState;
+ editor._dirtyType = FULL_RECONCILE;
+ }
++ registerDefaultCommandHandlers(editor);
+ return editor;
+ }
+ class LexicalEditor {
+@@ -10819,7 +11011,7 @@ class LexicalEditor {
+ if (rootElement !== null) {
+ rootElement.blur();
+ }
+- const domSelection = getDOMSelection(this._window);
++ const domSelection = getDOMSelectionForEditor(this);
+ if (domSelection !== null) {
+ domSelection.removeAllRanges();
+ }
+@@ -10905,7 +11097,9 @@ function $isSelectionCapturedInDecorator(node) {
+ return $isDecoratorNode($getNearestNodeFromDOMNode(node));
+ }
+ function isSelectionCapturedInDecoratorInput(anchorDOM) {
+- const activeElement = document.activeElement;
++ const editor = getNearestEditorFromDOMNode(anchorDOM);
++ const rootElement = editor ? editor.getRootElement() : null;
++ const activeElement = rootElement ? getActiveElement(rootElement) : document.activeElement;
+ if (!isHTMLElement(activeElement)) {
+ return false;
+ }
+@@ -11352,7 +11546,7 @@ function getAnchorTextFromDOM(anchorNode) {
+ }
+ function $updateSelectedTextFromDOM(isCompositionEnd, editor, data) {
+ // Update the text content with the latest composition text
+- const domSelection = getDOMSelection(getWindow(editor));
++ const domSelection = getDOMSelectionForEditor(editor);
+ if (domSelection === null) {
+ return;
+ }
+@@ -11797,9 +11991,23 @@ function getElementByKeyOrThrow(editor, key) {
+ }
+ return element;
+ }
++
++/**
++ * Type guard function that checks if a node is a ShadowRoot. This function performs
++ * runtime validation to safely narrow types and enable type-safe Shadow DOM operations.
++ * It checks both the nodeType and the presence of the 'host' property to distinguish
++ * ShadowRoot from regular DocumentFragment nodes.
++ *
++ * @param node - The Node to check (can be null)
++ * @returns True if the node is a ShadowRoot, false otherwise. When true, TypeScript
++ * will narrow the type to ShadowRoot for subsequent operations.
++ */
++function isShadowRoot(node) {
++ return isDocumentFragment(node) && 'host' in node;
++}
+ function getParentElement(node) {
+ const parentElement = node.assignedSlot || node.parentElement;
+- return isDocumentFragment(parentElement) ? parentElement.host : parentElement;
++ return isShadowRoot(parentElement) ? parentElement.host : parentElement;
+ }
+ function getDOMOwnerDocument(target) {
+ return isDOMDocumentNode(target) ? target : isHTMLElement(target) ? target.ownerDocument : null;
+@@ -11907,7 +12115,7 @@ function getDefaultView(domElem) {
+ return ownerDoc ? ownerDoc.defaultView : null;
+ }
+ function getWindow(editor) {
+- const windowObj = editor._window;
++ const windowObj = editor._window || window;
+ if (windowObj === null) {
+ {
+ formatDevErrorMessage(`window object not found`);
+@@ -12026,7 +12234,7 @@ function removeDOMBlockCursorElement(blockCursorElement, editor, rootElement) {
+ }
+ function updateDOMBlockCursorElement(editor, rootElement, nextSelection) {
+ let blockCursorElement = editor._blockCursorElement;
+- if ($isRangeSelection(nextSelection) && nextSelection.isCollapsed() && nextSelection.anchor.type === 'element' && rootElement.contains(document.activeElement)) {
++ if ($isRangeSelection(nextSelection) && nextSelection.isCollapsed() && nextSelection.anchor.type === 'element' && rootElement.contains(getActiveElement(rootElement))) {
+ const anchor = nextSelection.anchor;
+ const elementNode = anchor.getNode();
+ const offset = anchor.offset;
+@@ -12069,23 +12277,290 @@ function updateDOMBlockCursorElement(editor, rootElement, nextSelection) {
+ }
+
+ /**
+- * Returns the selection for the given window, or the global window if null.
+- * Will return null if {@link CAN_USE_DOM} is false.
++ * Returns a Selection object from a ShadowRoot using the best available API.
++ *
++ * This function attempts to get selection from Shadow DOM contexts using modern
++ * getComposedRanges API when available. If the API is not supported or returns
++ * empty ranges, it falls back to the global window selection.
++ *
++ * **Selection Proxy:**
++ * When getComposedRanges returns valid ranges, this function creates a Selection proxy
++ * that properly handles text selection across Shadow DOM boundaries. The proxy
++ * provides all standard Selection methods while ensuring correct behavior with
++ * composed ranges.
++ *
++ * **Browser Support:**
++ * - Modern browsers with getComposedRanges: Full Shadow DOM selection support
++ * - Older browsers: Falls back to window.getSelection()
++ *
++ * @param shadowRoot - The ShadowRoot to get selection from
++ * @returns A Selection object (either a proxy with composed ranges or the global selection),
++ * or null if no selection is available
++ */
++function getDOMSelectionFromShadowRoot(shadowRoot) {
++ const globalSelection = window.getSelection();
++ if (!globalSelection) {
++ return null;
++ }
++ if ('getComposedRanges' in Selection.prototype) {
++ const ranges = globalSelection.getComposedRanges({
++ shadowRoots: [shadowRoot]
++ });
++ if (ranges.length > 0) {
++ return createSelectionWithComposedRanges(globalSelection, ranges);
++ }
++ }
++ return globalSelection;
++}
++
++/**
++ * Returns the selection for the given window, with Shadow DOM support.
++ *
++ * This function provides a unified API for getting selections in both regular DOM
++ * and Shadow DOM contexts. When a rootElement is provided, it checks if the element
++ * is within a Shadow DOM and uses the appropriate selection API.
++ *
++ * **Behavior:**
++ * - If CAN_USE_DOM is false: Returns null
++ * - If rootElement is in Shadow DOM: Uses getDOMSelectionFromShadowRoot
++ * - Otherwise: Returns window.getSelection() from the target or global window
+ *
+- * @param targetWindow The window to get the selection from
+- * @returns a Selection or null
++ * @param targetWindow - The window to get the selection from (defaults to global window if null)
++ * @param rootElement - Optional root element to check for Shadow DOM context
++ * @returns A Selection object appropriate for the context, or null if selection is unavailable
+ */
+-function getDOMSelection(targetWindow) {
+- return !CAN_USE_DOM ? null : (targetWindow || window).getSelection();
++function getDOMSelection(targetWindow, rootElement) {
++ if (!CAN_USE_DOM) {
++ return null;
++ }
++
++ // Check if we're inside a shadow DOM
++ if (rootElement) {
++ const shadowRoot = getShadowRootOrDocument(rootElement);
++ if (shadowRoot && isShadowRoot(shadowRoot)) {
++ return getDOMSelectionFromShadowRoot(shadowRoot);
++ }
++ }
++ return (targetWindow || window).getSelection();
+ }
+
+ /**
+- * Returns the selection for the defaultView of the ownerDocument of given EventTarget.
++ * Creates a Selection-like proxy object that properly handles StaticRange objects
++ * from the getComposedRanges API for Shadow DOM compatibility.
++ *
++ * This function creates a proxy that:
++ * - Provides all standard Selection properties and methods
++ * - Correctly handles anchor/focus nodes from StaticRange data
++ * - Implements the `type` property ('None', 'Caret', or 'Range')
++ * - Converts StaticRange to Range objects in getRangeAt method
++ * - Delegates other methods to the base Selection object
++ *
++ * **Validation:**
++ * The function validates that composedRanges is a non-empty array with valid
++ * StaticRange objects before creating the proxy. If validation fails, it
++ * returns the base selection unchanged.
+ *
+- * @param eventTarget The node to get the selection from
+- * @returns a Selection or null
++ * @param baseSelection - The base Selection object to enhance
++ * @param composedRanges - Array of StaticRange objects from getComposedRanges
++ * @returns A proxy Selection object that correctly handles Shadow DOM ranges,
++ * or the base selection if composedRanges is invalid
++ */
++
++function createSelectionWithComposedRanges(baseSelection, composedRanges) {
++ if (composedRanges.length === 0) {
++ return baseSelection;
++ }
++ const firstRange = composedRanges[0];
++ const selectionLike = Object.create(Selection.prototype);
++
++ // Copy all methods and properties from base selection
++ const descriptors = Object.getOwnPropertyDescriptors(Selection.prototype);
++ Object.keys(descriptors).forEach(prop => {
++ if (prop === 'constructor') {
++ return;
++ }
++ const descriptor = descriptors[prop];
++ if (descriptor.value && typeof descriptor.value === 'function') {
++ // It's a method - bind it to base selection
++ const method = baseSelection[prop];
++ if (typeof method === 'function') {
++ selectionLike[prop] = method.bind(baseSelection);
++ }
++ } else if (!descriptor.get) {
++ // It's a regular property, not a getter - copy the value from base selection
++ const value = baseSelection[prop];
++ if (value !== undefined) {
++ selectionLike[prop] = value;
++ }
++ }
++ });
++
++ // Override specific properties with composed ranges data
++ Object.defineProperty(selectionLike, 'anchorNode', {
++ enumerable: true,
++ get: () => firstRange.startContainer
++ });
++ Object.defineProperty(selectionLike, 'anchorOffset', {
++ enumerable: true,
++ get: () => firstRange.startOffset
++ });
++ Object.defineProperty(selectionLike, 'focusNode', {
++ enumerable: true,
++ get: () => firstRange.endContainer
++ });
++ Object.defineProperty(selectionLike, 'focusOffset', {
++ enumerable: true,
++ get: () => firstRange.endOffset
++ });
++ Object.defineProperty(selectionLike, 'isCollapsed', {
++ enumerable: true,
++ get: () => firstRange.collapsed
++ });
++ Object.defineProperty(selectionLike, 'rangeCount', {
++ enumerable: true,
++ get: () => composedRanges.length
++ });
++ Object.defineProperty(selectionLike, 'type', {
++ enumerable: true,
++ get: () => {
++ const range = composedRanges[0];
++ if (!range) {
++ return 'None';
++ }
++ return range.collapsed ? 'Caret' : 'Range';
++ }
++ });
++
++ // Override getRangeAt to return a proper Range object from StaticRange
++ selectionLike.getRangeAt = function (index) {
++ if (index < 0 || index >= composedRanges.length) {
++ throw new DOMException('Index out of range', 'IndexSizeError');
++ }
++ const staticRange = composedRanges[index];
++ const range = document.createRange();
++ range.setStart(staticRange.startContainer, staticRange.startOffset);
++ range.setEnd(staticRange.endContainer, staticRange.endOffset);
++ return range;
++ };
++
++ // If the original selection has getComposedRanges, preserve it
++ if ('getComposedRanges' in baseSelection) {
++ selectionLike.getComposedRanges = function () {
++ return composedRanges;
++ };
++ }
++ return selectionLike;
++}
++function getDOMSelectionForEditor(editor) {
++ return getDOMSelection(getWindow(editor), editor.getRootElement());
++}
++
++/**
++ * Traverses up the DOM tree to find a ShadowRoot if the element is inside a shadow DOM.
++ * This function helps determine whether the given element is rendered within Shadow DOM
++ * encapsulation.
++ *
++ * @param element - The HTMLElement to start traversing from
++ * @returns The ShadowRoot if found, or Document if the element is not in shadow DOM
++ */
++function getShadowRootOrDocument(element) {
++ const shadowRoot = element.getRootNode({
++ composed: false
++ });
++ if (isShadowRoot(shadowRoot)) {
++ return shadowRoot;
++ }
++ return document;
++}
++
++/**
++ * Checks if the Lexical editor is running within a Shadow DOM context.
++ *
++ * This function determines whether the editor's root element is contained within
++ * a ShadowRoot, which is essential for enabling Shadow DOM-specific functionality
++ * like specialized deletion commands and selection handling.
++ *
++ * @param editor - The Lexical editor instance to check
++ * @returns `true` if the editor is in Shadow DOM, `false` otherwise
++ */
++function $isInShadowDOMContext(editor) {
++ const rootElement = editor.getRootElement();
++ return rootElement ? isShadowRoot(getShadowRootOrDocument(rootElement)) : false;
++}
++
++/**
++ * Gets the appropriate Document object for an element, accounting for shadow DOM.
++ * Returns the ownerDocument of the ShadowRoot if the element is in shadow DOM,
++ * otherwise returns the element's ownerDocument or the global document.
++ *
++ * @param element - The HTMLElement to get the document for
++ * @returns The Document object that should be used for DOM operations
++ */
++function getDocumentFromElement(element) {
++ if (!element || !CAN_USE_DOM) {
++ return document;
++ }
++ const rootNode = element.getRootNode({
++ composed: true
++ });
++
++ // If the element is not connected to a document, return the default document
++ if (rootNode === element || rootNode.nodeType !== Node.DOCUMENT_NODE) {
++ return element.ownerDocument || document;
++ }
++ return rootNode;
++}
++
++/**
++ * Gets the currently active (focused) element, accounting for shadow DOM encapsulation.
++ * In shadow DOM, the activeElement is tracked separately within the ShadowRoot.
++ * Falls back to the document's activeElement if not in shadow DOM.
++ *
++ * @param rootElement - The root element to check for shadow DOM context
++ * @returns The currently active Element or null if no element is focused
++ */
++function getActiveElement(rootElement) {
++ const shadowRoot = getShadowRootOrDocument(rootElement);
++ if (shadowRoot && isShadowRoot(shadowRoot) && shadowRoot.activeElement) {
++ return shadowRoot.activeElement;
++ }
++ return getDocumentFromElement(rootElement).activeElement;
++}
++
++/**
++ * Returns the selection for the defaultView of the ownerDocument of given EventTarget,
++ * with full Shadow DOM support.
++ *
++ * This function determines the appropriate selection context based on whether the
++ * EventTarget is within a Shadow DOM or regular DOM:
++ *
++ * **Shadow DOM Elements:**
++ * Uses getDOMSelectionFromShadowRoot to get a selection that properly handles
++ * Shadow DOM boundaries using the getComposedRanges API when available.
++ *
++ * **Regular DOM Elements:**
++ * Returns the standard window.getSelection() from the element's defaultView.
++ *
++ * **Edge Cases:**
++ * - Returns null for null EventTarget
++ * - Returns null for EventTargets without a valid defaultView
++ * - Handles non-HTML EventTargets gracefully
++ *
++ * @param eventTarget - The EventTarget (typically a DOM node) to get the selection from
++ * @returns A Selection object from the appropriate context or null if unavailable
+ */
+ function getDOMSelectionFromTarget(eventTarget) {
++ if (!eventTarget) {
++ return null;
++ }
++
++ // Check if eventTarget is in shadow DOM
++ if (isHTMLElement(eventTarget)) {
++ const shadowRoot = getShadowRootOrDocument(eventTarget);
++ if (shadowRoot && isShadowRoot(shadowRoot)) {
++ return getDOMSelectionFromShadowRoot(shadowRoot);
++ }
++ }
+ const defaultView = getDefaultView(eventTarget);
+ return defaultView ? defaultView.getSelection() : null;
+ }
+@@ -13979,4 +14454,4 @@ function shallowMergeConfig(config, overrides) {
+ return config;
+ }
+
+-export { $addUpdateTag, $applyNodeReplacement, $caretFromPoint, $caretRangeFromSelection, $cloneWithProperties, $cloneWithPropertiesEphemeral, $comparePointCaretNext, $copyNode, $create, $createLineBreakNode, $createNodeSelection, $createParagraphNode, $createPoint, $createRangeSelection, $createRangeSelectionFromDom, $createTabNode, $createTextNode, $extendCaretToRange, $findMatchingParent, $getAdjacentChildCaret, $getAdjacentNode, $getAdjacentSiblingOrParentSiblingCaret, $getCaretInDirection, $getCaretRange, $getCaretRangeInDirection, $getCharacterOffsets, $getChildCaret, $getChildCaretAtIndex, $getChildCaretOrSelf, $getCollapsedCaretRange, $getCommonAncestor, $getCommonAncestorResultBranchOrder, $getEditor, $getNearestNodeFromDOMNode, $getNearestRootOrShadowRoot, $getNodeByKey, $getNodeByKeyOrThrow, $getNodeFromDOMNode, $getPreviousSelection, $getRoot, $getSelection, $getSiblingCaret, $getState, $getStateChange, $getTextContent, $getTextNodeOffset, $getTextPointCaret, $getTextPointCaretSlice, $getWritableNodeState, $hasAncestor, $hasUpdateTag, $insertNodes, $isBlockElementNode, $isChildCaret, $isDecoratorNode, $isEditorState, $isElementNode, $isExtendableTextPointCaret, $isInlineElementOrDecoratorNode, $isLeafNode, $isLineBreakNode, $isNodeCaret, $isNodeSelection, $isParagraphNode, $isRangeSelection, $isRootNode, $isRootOrShadowRoot, $isSiblingCaret, $isTabNode, $isTextNode, $isTextPointCaret, $isTextPointCaretSlice, $isTokenOrSegmented, $isTokenOrTab, $nodesOfType, $normalizeCaret, $normalizeSelection as $normalizeSelection__EXPERIMENTAL, $onUpdate, $parseSerializedNode, $removeTextFromCaretRange, $rewindSiblingCaret, $selectAll, $setCompositionKey, $setPointFromCaret, $setSelection, $setSelectionFromCaretRange, $setState, $splitAtPointCaretNext, $splitNode, $updateRangeSelectionFromCaretRange, ArtificialNode__DO_NOT_USE, BLUR_COMMAND, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, CLICK_COMMAND, COLLABORATION_TAG, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, CONTROLLED_TEXT_INSERTION_COMMAND, COPY_COMMAND, CUT_COMMAND, DELETE_CHARACTER_COMMAND, DELETE_LINE_COMMAND, DELETE_WORD_COMMAND, DRAGEND_COMMAND, DRAGOVER_COMMAND, DRAGSTART_COMMAND, DROP_COMMAND, DecoratorNode, ElementNode, FOCUS_COMMAND, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, INDENT_CONTENT_COMMAND, INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, INSERT_TAB_COMMAND, INTERNAL_$isBlock, IS_ALL_FORMATTING, IS_BOLD, IS_CODE, IS_HIGHLIGHT, IS_ITALIC, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_DOWN_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_MODIFIER_COMMAND, KEY_SPACE_COMMAND, KEY_TAB_COMMAND, LineBreakNode, MOVE_TO_END, MOVE_TO_START, NODE_STATE_KEY, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, PASTE_TAG, ParagraphNode, REDO_COMMAND, REMOVE_TEXT_COMMAND, RootNode, SELECTION_CHANGE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, SELECT_ALL_COMMAND, SKIP_COLLAB_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG, SKIP_SELECTION_FOCUS_TAG, TEXT_TYPE_TO_FORMAT, TabNode, TextNode, UNDO_COMMAND, buildImportMap, configExtension, createCommand, createEditor, createSharedNodeState, createState, declarePeerDependency, defineExtension, flipDirection, getDOMOwnerDocument, getDOMSelection, getDOMSelectionFromTarget, getDOMTextNode, getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, getRegisteredNode, getRegisteredNodeOrThrow, getStaticNodeConfig, getTextDirection, getTransformSetFromKlass, isBlockDomNode, isCurrentlyReadOnlyMode, isDOMDocumentNode, isDOMNode, isDOMTextNode, isDOMUnmanaged, isDocumentFragment, isExactShortcutMatch, isHTMLAnchorElement, isHTMLElement, isInlineDomNode, isLexicalEditor, isModifierMatch, isSelectionCapturedInDecoratorInput, isSelectionWithinEditor, makeStepwiseIterator, removeFromParent, resetRandomKey, safeCast, setDOMUnmanaged, setNodeIndentFromDOM, shallowMergeConfig };
++export { $addUpdateTag, $applyNodeReplacement, $caretFromPoint, $caretRangeFromSelection, $cloneWithProperties, $cloneWithPropertiesEphemeral, $comparePointCaretNext, $copyNode, $create, $createLineBreakNode, $createNodeSelection, $createParagraphNode, $createPoint, $createRangeSelection, $createRangeSelectionFromDom, $createTabNode, $createTextNode, $extendCaretToRange, $findMatchingParent, $getAdjacentChildCaret, $getAdjacentNode, $getAdjacentSiblingOrParentSiblingCaret, $getCaretInDirection, $getCaretRange, $getCaretRangeInDirection, $getCharacterOffsets, $getChildCaret, $getChildCaretAtIndex, $getChildCaretOrSelf, $getCollapsedCaretRange, $getCommonAncestor, $getCommonAncestorResultBranchOrder, $getEditor, $getNearestNodeFromDOMNode, $getNearestRootOrShadowRoot, $getNodeByKey, $getNodeByKeyOrThrow, $getNodeFromDOMNode, $getPreviousSelection, $getRoot, $getSelection, $getSiblingCaret, $getState, $getStateChange, $getTextContent, $getTextNodeOffset, $getTextPointCaret, $getTextPointCaretSlice, $getWritableNodeState, $hasAncestor, $hasUpdateTag, $insertNodes, $isBlockElementNode, $isChildCaret, $isDecoratorNode, $isEditorState, $isElementNode, $isExtendableTextPointCaret, $isInShadowDOMContext, $isInlineElementOrDecoratorNode, $isLeafNode, $isLineBreakNode, $isNodeCaret, $isNodeSelection, $isParagraphNode, $isRangeSelection, $isRootNode, $isRootOrShadowRoot, $isSiblingCaret, $isTabNode, $isTextNode, $isTextPointCaret, $isTextPointCaretSlice, $isTokenOrSegmented, $isTokenOrTab, $nodesOfType, $normalizeCaret, $normalizeSelection as $normalizeSelection__EXPERIMENTAL, $onUpdate, $parseSerializedNode, $removeTextFromCaretRange, $rewindSiblingCaret, $selectAll, $setCompositionKey, $setPointFromCaret, $setSelection, $setSelectionFromCaretRange, $setState, $splitAtPointCaretNext, $splitNode, $updateRangeSelectionFromCaretRange, ArtificialNode__DO_NOT_USE, BEFORE_INPUT_COMMAND, BLUR_COMMAND, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, CLICK_COMMAND, COLLABORATION_TAG, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, COMPOSITION_END_COMMAND, COMPOSITION_START_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, COPY_COMMAND, CUT_COMMAND, DELETE_CHARACTER_COMMAND, DELETE_LINE_COMMAND, DELETE_WORD_COMMAND, DRAGEND_COMMAND, DRAGOVER_COMMAND, DRAGSTART_COMMAND, DROP_COMMAND, DecoratorNode, ElementNode, FOCUS_COMMAND, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, INDENT_CONTENT_COMMAND, INPUT_COMMAND, INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, INSERT_TAB_COMMAND, INTERNAL_$isBlock, IS_ALL_FORMATTING, IS_BOLD, IS_CODE, IS_HIGHLIGHT, IS_ITALIC, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_DOWN_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_MODIFIER_COMMAND, KEY_SPACE_COMMAND, KEY_TAB_COMMAND, LineBreakNode, MOVE_TO_END, MOVE_TO_START, NODE_STATE_KEY, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, PASTE_TAG, ParagraphNode, REDO_COMMAND, REMOVE_TEXT_COMMAND, RootNode, SELECTION_CHANGE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, SELECT_ALL_COMMAND, SKIP_COLLAB_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG, SKIP_SELECTION_FOCUS_TAG, TEXT_TYPE_TO_FORMAT, TabNode, TextNode, UNDO_COMMAND, buildImportMap, configExtension, createCommand, createEditor, createSharedNodeState, createState, declarePeerDependency, defineExtension, flipDirection, getDOMOwnerDocument, getDOMSelection, getDOMSelectionForEditor, getDOMSelectionFromShadowRoot, getDOMSelectionFromTarget, getDOMTextNode, getDocumentFromElement, getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, getRegisteredNode, getRegisteredNodeOrThrow, getShadowRootOrDocument, getStaticNodeConfig, getTextDirection, getTransformSetFromKlass, getWindow, isBlockDomNode, isCurrentlyReadOnlyMode, isDOMDocumentNode, isDOMNode, isDOMTextNode, isDOMUnmanaged, isDocumentFragment, isExactShortcutMatch, isHTMLAnchorElement, isHTMLElement, isInlineDomNode, isLexicalEditor, isModifierMatch, isSelectionCapturedInDecoratorInput, isSelectionWithinEditor, isShadowRoot, makeStepwiseIterator, removeFromParent, resetRandomKey, safeCast, setDOMUnmanaged, setNodeIndentFromDOM, shallowMergeConfig };
+diff --git a/node_modules/lexical/Lexical.js.flow b/node_modules/lexical/Lexical.js.flow
+index f560067..6a5c126 100644
+--- a/node_modules/lexical/Lexical.js.flow
++++ b/node_modules/lexical/Lexical.js.flow
+@@ -1633,13 +1633,13 @@ export type LexicalExtensionArgument<
+ > =
+ | LexicalExtension
+ | NormalizedLexicalExtensionArgument;
+-export type LexicalExtensionConfig =
++export type LexicalExtensionConfig<+Extension: AnyLexicalExtension> =
+ $NonMaybeType;
+ export interface LexicalExtensionDependency<
+ +Dependency: AnyLexicalExtension,
+ > {
+- config: LexicalExtensionConfig;
+- output: LexicalExtensionOutput;
++ +config: LexicalExtensionConfig;
++ +output: LexicalExtensionOutput;
+ }
+ export type LexicalExtensionInit =
+ $NonMaybeType;
+@@ -1656,7 +1656,7 @@ interface LexicalExtensionInternalOutput