lexical 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Lexical.dev.js CHANGED
@@ -148,6 +148,10 @@ const TEXT_TYPE_TO_FORMAT = {
148
148
  superscript: IS_SUPERSCRIPT,
149
149
  underline: IS_UNDERLINE
150
150
  };
151
+ const DETAIL_TYPE_TO_DETAIL = {
152
+ directionless: IS_DIRECTIONLESS,
153
+ unmergeable: IS_UNMERGEABLE
154
+ };
151
155
  const ELEMENT_TYPE_TO_FORMAT = {
152
156
  center: IS_ALIGN_CENTER,
153
157
  justify: IS_ALIGN_JUSTIFY,
@@ -200,7 +204,8 @@ function initTextEntryListener() {
200
204
 
201
205
  function isManagedLineBreak(dom, target, editor) {
202
206
  return (// @ts-expect-error: internal field
203
- target.__lexicalLineBreak === dom || dom['__lexicalKey_' + editor._key] !== undefined
207
+ target.__lexicalLineBreak === dom || // @ts-ignore We intentionally add this to the Node.
208
+ dom[`__lexicalKey_${editor._key}`] !== undefined
204
209
  );
205
210
  }
206
211
 
@@ -222,7 +227,10 @@ function handleTextMutation(target, node, editor) {
222
227
  }
223
228
 
224
229
  const text = target.nodeValue;
225
- $updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
230
+
231
+ if (text !== null) {
232
+ $updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
233
+ }
226
234
  }
227
235
 
228
236
  function shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode) {
@@ -661,8 +669,9 @@ function $getNodeByKey(key, _editorState) {
661
669
  return node;
662
670
  }
663
671
  function getNodeFromDOMNode(dom, editorState) {
664
- const editor = getActiveEditor();
665
- const key = dom['__lexicalKey_' + editor._key];
672
+ const editor = getActiveEditor(); // @ts-ignore We intentionally add this to the Node.
673
+
674
+ const key = dom[`__lexicalKey_${editor._key}`];
666
675
 
667
676
  if (key !== undefined) {
668
677
  return $getNodeByKey(key, editorState);
@@ -771,7 +780,8 @@ dom, editor) {
771
780
  let node = dom;
772
781
 
773
782
  while (node != null) {
774
- const key = node['__lexicalKey_' + editor._key];
783
+ // @ts-ignore We intentionally add this to the Node.
784
+ const key = node[`__lexicalKey_${editor._key}`];
775
785
 
776
786
  if (key !== undefined) {
777
787
  return key;
@@ -827,7 +837,9 @@ function $updateSelectedTextFromDOM(editor, isCompositionEnd, data) {
827
837
  focusOffset = offset;
828
838
  }
829
839
 
830
- $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, isCompositionEnd);
840
+ if (textContent !== null) {
841
+ $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, isCompositionEnd);
842
+ }
831
843
  }
832
844
  }
833
845
  }
@@ -897,6 +909,11 @@ function $updateTextNodeFromDOMContent(textNode, textContent, anchorOffset, focu
897
909
  }
898
910
  }
899
911
 
912
+ function $previousSiblingDoesNotAcceptText(node) {
913
+ const previousSibling = node.getPreviousSibling();
914
+ return ($isTextNode(previousSibling) || $isElementNode(previousSibling) && previousSibling.isInline()) && !previousSibling.canInsertTextAfter();
915
+ }
916
+
900
917
  function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
901
918
  if (node.isSegmented()) {
902
919
  return true;
@@ -909,7 +926,7 @@ function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
909
926
  const offset = selection.anchor.offset;
910
927
  const parent = node.getParentOrThrow();
911
928
  const isToken = node.isToken();
912
- const shouldInsertTextBefore = offset === 0 && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken);
929
+ const shouldInsertTextBefore = offset === 0 && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken || $previousSiblingDoesNotAcceptText(node));
913
930
  const shouldInsertTextAfter = node.getTextContentSize() === offset && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken);
914
931
  return shouldInsertTextBefore || shouldInsertTextAfter;
915
932
  } // This function is used to determine if Lexical should attempt to override
@@ -1023,6 +1040,8 @@ function isCopy(keyCode, shiftKey, metaKey, ctrlKey) {
1023
1040
  if (keyCode === 67) {
1024
1041
  return IS_APPLE ? metaKey : ctrlKey;
1025
1042
  }
1043
+
1044
+ return false;
1026
1045
  }
1027
1046
  function isCut(keyCode, shiftKey, metaKey, ctrlKey) {
1028
1047
  if (shiftKey) {
@@ -1032,6 +1051,8 @@ function isCut(keyCode, shiftKey, metaKey, ctrlKey) {
1032
1051
  if (keyCode === 88) {
1033
1052
  return IS_APPLE ? metaKey : ctrlKey;
1034
1053
  }
1054
+
1055
+ return false;
1035
1056
  }
1036
1057
 
1037
1058
  function isArrowLeft(keyCode) {
@@ -1137,8 +1158,7 @@ function setMutatedNode(mutatedNodes, registeredNodes, mutationListeners, node,
1137
1158
  }
1138
1159
  function $nodesOfType(klass) {
1139
1160
  const editorState = getActiveEditorState();
1140
- const readOnly = editorState._readOnly; // @ts-expect-error TODO Replace Class utility type with InstanceType
1141
-
1161
+ const readOnly = editorState._readOnly;
1142
1162
  const klassType = klass.getType();
1143
1163
  const nodes = editorState._nodeMap;
1144
1164
  const nodesOfType = [];
@@ -1658,10 +1678,9 @@ function reconcileBlockDirection(element, dom) {
1658
1678
 
1659
1679
  if (previousDirectionTheme !== undefined) {
1660
1680
  if (typeof previousDirectionTheme === 'string') {
1661
- const classNamesArr = previousDirectionTheme.split(' '); // @ts-expect-error: intentional
1662
-
1681
+ const classNamesArr = previousDirectionTheme.split(' ');
1663
1682
  previousDirectionTheme = theme[previousDirection] = classNamesArr;
1664
- } // @ts-expect-error: intentional
1683
+ } // @ts-ignore: intentional
1665
1684
 
1666
1685
 
1667
1686
  classList.remove(...previousDirectionTheme);
@@ -1679,7 +1698,9 @@ function reconcileBlockDirection(element, dom) {
1679
1698
  nextDirectionTheme = theme[direction] = classNamesArr;
1680
1699
  }
1681
1700
 
1682
- classList.add(...nextDirectionTheme);
1701
+ if (nextDirectionTheme !== undefined) {
1702
+ classList.add(...nextDirectionTheme);
1703
+ }
1683
1704
  } // Update direction
1684
1705
 
1685
1706
 
@@ -2001,20 +2022,30 @@ function reconcileRoot(prevEditorState, nextEditorState, editor, dirtyType, dirt
2001
2022
  // so instead we make it seem that these values are always set.
2002
2023
  // We also want to make sure we clear them down, otherwise we
2003
2024
  // can leak memory.
2025
+ // @ts-ignore
2026
+
2027
+ activeEditor$1 = undefined; // @ts-ignore
2028
+
2029
+ activeEditorNodes = undefined; // @ts-ignore
2030
+
2031
+ activeDirtyElements = undefined; // @ts-ignore
2032
+
2033
+ activeDirtyLeaves = undefined; // @ts-ignore
2034
+
2035
+ activePrevNodeMap = undefined; // @ts-ignore
2036
+
2037
+ activeNextNodeMap = undefined; // @ts-ignore
2038
+
2039
+ activeEditorConfig = undefined; // @ts-ignore
2040
+
2041
+ activePrevKeyToDOMMap = undefined; // @ts-ignore
2004
2042
 
2005
- activeEditor$1 = undefined;
2006
- activeEditorNodes = undefined;
2007
- activeDirtyElements = undefined;
2008
- activeDirtyLeaves = undefined;
2009
- activePrevNodeMap = undefined;
2010
- activeNextNodeMap = undefined;
2011
- activeEditorConfig = undefined;
2012
- activePrevKeyToDOMMap = undefined;
2013
2043
  mutatedNodes = undefined;
2014
2044
  return currentMutatedNodes;
2015
2045
  }
2016
2046
  function storeDOMWithKey(key, dom, editor) {
2017
- const keyToDOMMap = editor._keyToDOMMap;
2047
+ const keyToDOMMap = editor._keyToDOMMap; // @ts-ignore We intentionally add this to the Node.
2048
+
2018
2049
  dom['__lexicalKey_' + editor._key] = key;
2019
2050
  keyToDOMMap.set(key, dom);
2020
2051
  }
@@ -2043,7 +2074,7 @@ const ANDROID_COMPOSITION_LATENCY = 30;
2043
2074
  const rootElementEvents = [['keydown', onKeyDown], ['compositionstart', onCompositionStart], ['compositionend', onCompositionEnd], ['input', onInput], ['click', onClick], ['cut', PASS_THROUGH_COMMAND], ['copy', PASS_THROUGH_COMMAND], ['dragstart', PASS_THROUGH_COMMAND], ['dragover', PASS_THROUGH_COMMAND], ['paste', PASS_THROUGH_COMMAND], ['focus', PASS_THROUGH_COMMAND], ['blur', PASS_THROUGH_COMMAND], ['drop', PASS_THROUGH_COMMAND]];
2044
2075
 
2045
2076
  if (CAN_USE_BEFORE_INPUT) {
2046
- rootElementEvents.push(['beforeinput', onBeforeInput]);
2077
+ rootElementEvents.push(['beforeinput', (event, editor) => onBeforeInput(event, editor)]);
2047
2078
  }
2048
2079
 
2049
2080
  let lastKeyDownTimeStamp = 0;
@@ -2055,7 +2086,7 @@ let isFirefoxEndingComposition = false;
2055
2086
  let collapsedSelectionFormat = [0, 0, 'root', 0];
2056
2087
 
2057
2088
  function shouldSkipSelectionChange(domNode, offset) {
2058
- return domNode !== null && domNode.nodeType === DOM_TEXT_TYPE && offset !== 0 && offset !== domNode.nodeValue.length;
2089
+ return domNode !== null && domNode.nodeValue !== null && domNode.nodeType === DOM_TEXT_TYPE && offset !== 0 && offset !== domNode.nodeValue.length;
2059
2090
  }
2060
2091
 
2061
2092
  function onSelectionChange(domSelection, editor, isActive) {
@@ -2157,15 +2188,19 @@ function onClick(event, editor) {
2157
2188
  const anchor = selection.anchor;
2158
2189
  const anchorNode = anchor.getNode();
2159
2190
 
2160
- if (anchor.type === 'element' && anchor.offset === 0 && selection.isCollapsed() && !$isRootNode(anchorNode) && $getRoot().getChildrenSize() === 1 && anchorNode.getTopLevelElementOrThrow().isEmpty() && lastSelection !== null && selection.is(lastSelection)) {
2191
+ if (domSelection && anchor.type === 'element' && anchor.offset === 0 && selection.isCollapsed() && !$isRootNode(anchorNode) && $getRoot().getChildrenSize() === 1 && anchorNode.getTopLevelElementOrThrow().isEmpty() && lastSelection !== null && selection.is(lastSelection)) {
2161
2192
  domSelection.removeAllRanges();
2162
2193
  selection.dirty = true;
2163
2194
  }
2164
- } else if ($isNodeSelection(selection) && domSelection.isCollapsed) {
2195
+ } else if (domSelection && $isNodeSelection(selection) && domSelection.isCollapsed) {
2165
2196
  const domAnchor = domSelection.anchorNode; // If the user is attempting to click selection back onto text, then
2166
2197
  // we should attempt create a range selection.
2198
+ // When we click on an empty paragraph node or the end of a paragraph that ends
2199
+ // with an image/poll, the nodeType will be ELEMENT_NODE
2200
+
2201
+ const allowedNodeType = [DOM_ELEMENT_TYPE, DOM_TEXT_TYPE];
2167
2202
 
2168
- if (domAnchor !== null && domAnchor.nodeType === DOM_TEXT_TYPE) {
2203
+ if (domAnchor !== null && allowedNodeType.includes(domAnchor.nodeType)) {
2169
2204
  const newSelection = internalCreateRangeSelection(lastSelection, domSelection, editor);
2170
2205
  $setSelection(newSelection);
2171
2206
  }
@@ -2476,7 +2511,14 @@ function onInput(event, editor) {
2476
2511
  isFirefoxEndingComposition = false;
2477
2512
  }
2478
2513
 
2479
- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data); // This ensures consistency on Android.
2514
+ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2515
+ const textLength = data.length; // Another hack for FF, as it's possible that the IME is still
2516
+ // open, even though compositionend has already fired (sigh).
2517
+
2518
+ if (IS_FIREFOX && textLength > 1 && event.inputType === 'insertCompositionText' && !editor.isComposing()) {
2519
+ selection.anchor.offset -= textLength;
2520
+ } // This ensures consistency on Android.
2521
+
2480
2522
 
2481
2523
  if (!IS_SAFARI && !IS_IOS && editor.isComposing()) {
2482
2524
  lastKeyDownTimeStamp = 0;
@@ -2486,7 +2528,7 @@ function onInput(event, editor) {
2486
2528
  $updateSelectedTextFromDOM(editor, false); // onInput always fires after onCompositionEnd for FF.
2487
2529
 
2488
2530
  if (isFirefoxEndingComposition) {
2489
- onCompositionEndImpl(editor, data);
2531
+ onCompositionEndImpl(editor, data || undefined);
2490
2532
  isFirefoxEndingComposition = false;
2491
2533
  }
2492
2534
  } // Also flush any other mutations that might have occurred
@@ -2532,7 +2574,7 @@ function onCompositionEndImpl(editor, data) {
2532
2574
  const node = $getNodeByKey(compositionKey);
2533
2575
  const textNode = getDOMTextNode(editor.getElementByKey(compositionKey));
2534
2576
 
2535
- if (textNode !== null && $isTextNode(node)) {
2577
+ if (textNode !== null && textNode.nodeValue !== null && $isTextNode(node)) {
2536
2578
  $updateTextNodeFromDOMContent(node, textNode.nodeValue, null, null, true);
2537
2579
  }
2538
2580
 
@@ -2696,6 +2738,11 @@ const activeNestedEditorsMap = new Map();
2696
2738
 
2697
2739
  function onDocumentSelectionChange(event) {
2698
2740
  const selection = getDOMSelection();
2741
+
2742
+ if (!selection) {
2743
+ return;
2744
+ }
2745
+
2699
2746
  const nextActiveEditor = getNearestEditorFromDOMNode(selection.anchorNode);
2700
2747
 
2701
2748
  if (nextActiveEditor === null) {
@@ -2795,7 +2842,7 @@ function removeRootElementEvents(rootElement) {
2795
2842
 
2796
2843
  const editor = rootElement.__lexicalEditor;
2797
2844
 
2798
- if (editor !== null || editor !== undefined) {
2845
+ if (editor !== null && editor !== undefined) {
2799
2846
  cleanActiveNestedEditorsMap(editor); // @ts-expect-error: internal field
2800
2847
 
2801
2848
  rootElement.__lexicalEditor = null;
@@ -3472,7 +3519,7 @@ class RangeSelection {
3472
3519
  const lastIndex = selectedNodesLength - 1;
3473
3520
  let lastNode = selectedNodes[lastIndex];
3474
3521
 
3475
- if (this.isCollapsed() && startOffset === firstNodeTextLength && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextAfter() || !firstNodeParent.canInsertTextAfter())) {
3522
+ if (this.isCollapsed() && startOffset === firstNodeTextLength && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextAfter() || !firstNodeParent.canInsertTextAfter() && firstNode.getNextSibling() === null)) {
3476
3523
  let nextSibling = firstNode.getNextSibling();
3477
3524
 
3478
3525
  if (!$isTextNode(nextSibling) || $isTokenOrInertOrSegmented(nextSibling)) {
@@ -3493,7 +3540,7 @@ class RangeSelection {
3493
3540
  this.insertText(text);
3494
3541
  return;
3495
3542
  }
3496
- } else if (this.isCollapsed() && startOffset === 0 && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextBefore() || !firstNodeParent.canInsertTextBefore())) {
3543
+ } else if (this.isCollapsed() && startOffset === 0 && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextBefore() || !firstNodeParent.canInsertTextBefore() && firstNode.getPreviousSibling() === null)) {
3497
3544
  let prevSibling = firstNode.getPreviousSibling();
3498
3545
 
3499
3546
  if (!$isTextNode(prevSibling) || $isTokenOrInertOrSegmented(prevSibling)) {
@@ -3717,10 +3764,19 @@ class RangeSelection {
3717
3764
  formatText(formatType) {
3718
3765
  // TODO I wonder if this methods use selection.extract() instead?
3719
3766
  const selectedNodes = this.getNodes();
3720
- const selectedNodesLength = selectedNodes.length;
3721
- const lastIndex = selectedNodesLength - 1;
3722
- let firstNode = selectedNodes[0];
3723
- let lastNode = selectedNodes[lastIndex];
3767
+ const selectedTextNodes = [];
3768
+
3769
+ for (const selectedNode of selectedNodes) {
3770
+ if ($isTextNode(selectedNode)) {
3771
+ selectedTextNodes.push(selectedNode);
3772
+ }
3773
+ }
3774
+
3775
+ const selectedTextNodesLength = selectedTextNodes.length;
3776
+ let firstIndex = 0;
3777
+ const lastIndex = selectedTextNodesLength - 1;
3778
+ let firstNode = selectedTextNodes[0];
3779
+ let lastNode = selectedTextNodes[lastIndex];
3724
3780
 
3725
3781
  if (this.isCollapsed()) {
3726
3782
  this.toggleFormat(formatType); // When changing format, we should stop composition
@@ -3731,42 +3787,22 @@ class RangeSelection {
3731
3787
 
3732
3788
  const anchor = this.anchor;
3733
3789
  const focus = this.focus;
3790
+ const anchorOffset = anchor.offset;
3734
3791
  const focusOffset = focus.offset;
3735
- let firstNextFormat = 0;
3792
+ let firstNextFormat = firstNode.getFormatFlags(formatType, null);
3736
3793
  let firstNodeTextLength = firstNode.getTextContent().length;
3737
-
3738
- for (let i = 0; i < selectedNodes.length; i++) {
3739
- const selectedNode = selectedNodes[i];
3740
-
3741
- if ($isTextNode(selectedNode)) {
3742
- firstNextFormat = selectedNode.getFormatFlags(formatType, null);
3743
- break;
3744
- }
3745
- }
3746
-
3747
- let anchorOffset = anchor.offset;
3748
- let startOffset;
3749
- let endOffset;
3750
3794
  const isBefore = anchor.isBefore(focus);
3751
- startOffset = isBefore ? anchorOffset : focusOffset;
3752
- endOffset = isBefore ? focusOffset : anchorOffset; // This is the case where the user only selected the very end of the
3795
+ const endOffset = isBefore ? focusOffset : anchorOffset;
3796
+ let startOffset = isBefore ? anchorOffset : focusOffset; // This is the case where the user only selected the very end of the
3753
3797
  // first node so we don't want to include it in the formatting change.
3754
3798
 
3755
- if (startOffset === firstNode.getTextContentSize()) {
3756
- let nextSibling = firstNode.getNextSibling();
3757
-
3758
- if ($isElementNode(nextSibling) && nextSibling.isInline()) {
3759
- nextSibling = nextSibling.getFirstChild();
3760
- }
3761
-
3762
- if ($isTextNode(nextSibling)) {
3763
- // we basically make the second node the firstNode, changing offsets accordingly
3764
- anchorOffset = 0;
3765
- startOffset = 0;
3766
- firstNode = nextSibling;
3767
- firstNodeTextLength = nextSibling.getTextContent().length;
3768
- firstNextFormat = firstNode.getFormatFlags(formatType, null);
3769
- }
3799
+ if (startOffset === firstNode.getTextContentSize() && selectedTextNodes.length > 1) {
3800
+ const nextNode = selectedTextNodes[1];
3801
+ startOffset = 0;
3802
+ firstIndex = 1;
3803
+ firstNode = nextNode;
3804
+ firstNodeTextLength = nextNode.getTextContentSize();
3805
+ firstNextFormat = nextNode.getFormatFlags(formatType, null);
3770
3806
  } // This is the case where we only selected a single node
3771
3807
 
3772
3808
 
@@ -3777,10 +3813,8 @@ class RangeSelection {
3777
3813
  firstNode.select(startOffset, endOffset);
3778
3814
  this.format = firstNextFormat;
3779
3815
  return;
3780
- }
3816
+ } // No actual text is selected, so do nothing.
3781
3817
 
3782
- startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
3783
- endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset; // No actual text is selected, so do nothing.
3784
3818
 
3785
3819
  if (startOffset === endOffset) {
3786
3820
  return;
@@ -3803,7 +3837,9 @@ class RangeSelection {
3803
3837
  } // multiple nodes selected.
3804
3838
 
3805
3839
  } else {
3806
- if ($isTextNode(firstNode)) {
3840
+ // Note: startOffset !== firstNodeTextLength should only occur within rare programatic
3841
+ // update functions; transforms normalization ensure there's no empty text nodes.
3842
+ if ($isTextNode(firstNode) && startOffset !== firstNodeTextLength) {
3807
3843
  if (startOffset !== 0) {
3808
3844
  // the entire first node isn't selected, so split it
3809
3845
  [, firstNode] = firstNode.splitText(startOffset);
@@ -3829,11 +3865,12 @@ class RangeSelection {
3829
3865
 
3830
3866
  lastNode.setFormat(lastNextFormat);
3831
3867
  }
3832
- } // deal with all the nodes in between
3868
+ }
3833
3869
 
3870
+ this.format = firstNextFormat | lastNextFormat; // deal with all the nodes in between
3834
3871
 
3835
- for (let i = 1; i < lastIndex; i++) {
3836
- const selectedNode = selectedNodes[i];
3872
+ for (let i = firstIndex + 1; i < lastIndex; i++) {
3873
+ const selectedNode = selectedTextNodes[i];
3837
3874
  const selectedNodeKey = selectedNode.__key;
3838
3875
 
3839
3876
  if ($isTextNode(selectedNode) && selectedNodeKey !== firstNode.__key && selectedNodeKey !== lastNode.__key && !selectedNode.isToken()) {
@@ -3905,7 +3942,7 @@ class RangeSelection {
3905
3942
  siblings.push(...nextSiblings);
3906
3943
  const firstNode = nodes[0];
3907
3944
  let didReplaceOrMerge = false;
3908
- let lastNodeInserted = null; // Time to insert the nodes!
3945
+ let lastNode = null; // Time to insert the nodes!
3909
3946
 
3910
3947
  for (let i = 0; i < nodes.length; i++) {
3911
3948
  const node = nodes[i];
@@ -3968,18 +4005,17 @@ class RangeSelection {
3968
4005
 
3969
4006
  if ($isElementNode(target)) {
3970
4007
  for (let s = 0; s < childrenLength; s++) {
3971
- lastNodeInserted = children[s];
3972
- target.append(lastNodeInserted);
4008
+ target.append(children[s]);
3973
4009
  }
3974
4010
  } else {
3975
4011
  for (let s = childrenLength - 1; s >= 0; s--) {
3976
- lastNodeInserted = children[s];
3977
- target.insertAfter(lastNodeInserted);
4012
+ target.insertAfter(children[s]);
3978
4013
  }
3979
4014
 
3980
4015
  target = target.getParentOrThrow();
3981
4016
  }
3982
4017
 
4018
+ lastNode = children[childrenLength - 1];
3983
4019
  element.remove();
3984
4020
  didReplaceOrMerge = true;
3985
4021
 
@@ -4007,7 +4043,7 @@ class RangeSelection {
4007
4043
  didReplaceOrMerge = false;
4008
4044
 
4009
4045
  if ($isElementNode(target) && !target.isInline()) {
4010
- lastNodeInserted = node;
4046
+ lastNode = node;
4011
4047
 
4012
4048
  if ($isDecoratorNode(node) && node.isTopLevel()) {
4013
4049
  target = target.insertAfter(node);
@@ -4040,8 +4076,8 @@ class RangeSelection {
4040
4076
  target = target.insertAfter(node);
4041
4077
  }
4042
4078
  }
4043
- } else if (!$isElementNode(node) || $isElementNode(node) && node.isInline() || $isDecoratorNode(target) && target.isTopLevel()) {
4044
- lastNodeInserted = node;
4079
+ } else if (!$isElementNode(node) || $isElementNode(node) && node.isInline() || $isDecoratorNode(target) && target.isTopLevel() || $isLineBreakNode(target)) {
4080
+ lastNode = node;
4045
4081
  target = target.insertAfter(node);
4046
4082
  } else {
4047
4083
  target = node.getParentOrThrow(); // Re-try again with the target being the parent
@@ -4070,7 +4106,7 @@ class RangeSelection {
4070
4106
  if ($isElementNode(target)) {
4071
4107
  // If the last node to be inserted was a text node,
4072
4108
  // then we should attempt to move selection to that.
4073
- const lastChild = $isTextNode(lastNodeInserted) ? lastNodeInserted : target.getLastDescendant();
4109
+ const lastChild = $isTextNode(lastNode) ? lastNode : target.getLastDescendant();
4074
4110
 
4075
4111
  if (!selectStart) {
4076
4112
  // Handle moving selection to end for elements
@@ -4084,18 +4120,26 @@ class RangeSelection {
4084
4120
  }
4085
4121
 
4086
4122
  if (siblings.length !== 0) {
4123
+ const originalTarget = target;
4124
+
4087
4125
  for (let i = siblings.length - 1; i >= 0; i--) {
4088
4126
  const sibling = siblings[i];
4089
4127
  const prevParent = sibling.getParentOrThrow();
4090
4128
 
4091
- if ($isElementNode(target) && !$isElementNode(sibling)) {
4092
- target.append(sibling);
4129
+ if ($isElementNode(target) && !$isBlockElementNode(sibling)) {
4130
+ if (originalTarget === target) {
4131
+ target.append(sibling);
4132
+ } else {
4133
+ target.insertBefore(sibling);
4134
+ }
4135
+
4093
4136
  target = sibling;
4094
- } else if (!$isElementNode(target) && !$isElementNode(sibling)) {
4137
+ } else if (!$isElementNode(target) && !$isBlockElementNode(sibling)) {
4095
4138
  target.insertBefore(sibling);
4096
4139
  target = sibling;
4097
4140
  } else {
4098
4141
  if ($isElementNode(sibling) && !sibling.canInsertAfter(target)) {
4142
+ // @ts-ignore The clone method does exist on the constructor.
4099
4143
  const prevParentClone = prevParent.constructor.clone(prevParent);
4100
4144
 
4101
4145
  if (!$isElementNode(prevParentClone)) {
@@ -4378,13 +4422,18 @@ class RangeSelection {
4378
4422
  }
4379
4423
  }
4380
4424
 
4381
- const domSelection = getDOMSelection(); // We use the DOM selection.modify API here to "tell" us what the selection
4425
+ const domSelection = getDOMSelection();
4426
+
4427
+ if (!domSelection) {
4428
+ return;
4429
+ } // We use the DOM selection.modify API here to "tell" us what the selection
4382
4430
  // will be. We then use it to update the Lexical selection accordingly. This
4383
4431
  // is much more reliable than waiting for a beforeinput and using the ranges
4384
4432
  // from getTargetRanges(), and is also better than trying to do it ourselves
4385
4433
  // using Intl.Segmenter or other workarounds that struggle with word segments
4386
4434
  // and line segments (especially with word wrapping and non-Roman languages).
4387
4435
 
4436
+
4388
4437
  $moveNativeSelection(domSelection, alter, isBackward ? 'backward' : 'forward', granularity); // Guard against no ranges
4389
4438
 
4390
4439
  if (domSelection.rangeCount > 0) {
@@ -4408,7 +4457,7 @@ class RangeSelection {
4408
4457
  let anchorNode = anchor.getNode();
4409
4458
 
4410
4459
  if (!isBackward && ( // Delete forward handle case
4411
- anchor.type === 'element' && anchor.offset === anchorNode.getChildrenSize() || anchor.type === 'text' && anchor.offset === anchorNode.getTextContentSize())) {
4460
+ anchor.type === 'element' && $isElementNode(anchorNode) && anchor.offset === anchorNode.getChildrenSize() || anchor.type === 'text' && anchor.offset === anchorNode.getTextContentSize())) {
4412
4461
  const nextSibling = anchorNode.getNextSibling() || anchorNode.getParentOrThrow().getNextSibling();
4413
4462
 
4414
4463
  if ($isElementNode(nextSibling) && !nextSibling.canExtractContents()) {
@@ -4509,6 +4558,8 @@ function $swapPoints(selection) {
4509
4558
  }
4510
4559
 
4511
4560
  function $moveNativeSelection(domSelection, alter, direction, granularity) {
4561
+ // @ts-expect-error Selection.modify() method applies a change to the current selection or cursor position,
4562
+ // but is still non-standard in some browsers.
4512
4563
  domSelection.modify(alter, direction, granularity);
4513
4564
  }
4514
4565
 
@@ -4768,6 +4819,10 @@ function internalResolveSelectionPoints(anchorDOM, anchorOffset, focusDOM, focus
4768
4819
 
4769
4820
  normalizeSelectionPointsForBoundaries(resolvedAnchorPoint, resolvedFocusPoint, lastSelection);
4770
4821
  return [resolvedAnchorPoint, resolvedFocusPoint];
4822
+ }
4823
+
4824
+ function $isBlockElementNode(node) {
4825
+ return $isElementNode(node) && !node.isInline();
4771
4826
  } // This is used to make a selection when the existing
4772
4827
  // selection is null, i.e. forcing selection on the editor
4773
4828
  // when it current exists outside the editor.
@@ -5116,7 +5171,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5116
5171
  if (anchorOffset === nextAnchorOffset && focusOffset === nextFocusOffset && anchorDOMNode === nextAnchorNode && focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
5117
5172
  !(domSelection.type === 'Range' && isCollapsed)) {
5118
5173
  // If the root element does not have focus, ensure it has focus
5119
- if (activeElement === null || !rootElement.contains(activeElement)) {
5174
+ if (rootElement !== null && (activeElement === null || !rootElement.contains(activeElement))) {
5120
5175
  rootElement.focus({
5121
5176
  preventScroll: true
5122
5177
  });
@@ -5135,7 +5190,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5135
5190
  try {
5136
5191
  domSelection.setBaseAndExtent(nextAnchorNode, nextAnchorOffset, nextFocusNode, nextFocusOffset);
5137
5192
 
5138
- if (nextSelection.isCollapsed() && rootElement === activeElement) {
5193
+ if (nextSelection.isCollapsed() && rootElement !== null && rootElement === activeElement) {
5139
5194
  scrollIntoViewIfNeeded(editor, anchor, rootElement, tags);
5140
5195
  }
5141
5196
 
@@ -5317,7 +5372,6 @@ function $applyAllTransforms(editorState, editor) {
5317
5372
  }
5318
5373
 
5319
5374
  function $parseSerializedNode(serializedNode) {
5320
- // $FlowFixMe: intentional cast to our internal type
5321
5375
  const internalSerializedNode = serializedNode;
5322
5376
  return $parseSerializedNodeImpl(internalSerializedNode, getActiveEditor()._nodes);
5323
5377
  }
@@ -5332,14 +5386,13 @@ function $parseSerializedNodeImpl(serializedNode, registeredNodes) {
5332
5386
  }
5333
5387
  }
5334
5388
 
5335
- const nodeClass = registeredNode.klass; // @ts-expect-error TODO Replace Class utility type with InstanceType
5389
+ const nodeClass = registeredNode.klass;
5336
5390
 
5337
5391
  if (serializedNode.type !== nodeClass.getType()) {
5338
5392
  {
5339
5393
  throw Error(`LexicalNode: Node ${nodeClass.name} does not implement .importJSON().`);
5340
5394
  }
5341
- } // @ts-expect-error TODO Replace Class utility type with InstanceType
5342
-
5395
+ }
5343
5396
 
5344
5397
  const node = nodeClass.importJSON(serializedNode);
5345
5398
  const children = serializedNode.children;
@@ -5477,7 +5530,9 @@ function commitPendingUpdates(editor) {
5477
5530
  mutatedNodes = reconcileRoot(currentEditorState, pendingEditorState, editor, dirtyType, dirtyElements, dirtyLeaves);
5478
5531
  } catch (error) {
5479
5532
  // Report errors
5480
- editor._onError(error); // Reset editor and restore incoming editor state to the DOM
5533
+ if (error instanceof Error) {
5534
+ editor._onError(error);
5535
+ } // Reset editor and restore incoming editor state to the DOM
5481
5536
 
5482
5537
 
5483
5538
  if (!isAttemptingToRecoverFromReconcilerError) {
@@ -5610,6 +5665,7 @@ function triggerListeners(type, editor, isCurrentlyEnqueuingUpdates, ...payload)
5610
5665
  const listeners = Array.from(editor._listeners[type]);
5611
5666
 
5612
5667
  for (let i = 0; i < listeners.length; i++) {
5668
+ // @ts-ignore
5613
5669
  listeners[i].apply(null, payload);
5614
5670
  }
5615
5671
  } finally {
@@ -5654,8 +5710,12 @@ function triggerEnqueuedUpdates(editor) {
5654
5710
  const queuedUpdates = editor._updates;
5655
5711
 
5656
5712
  if (queuedUpdates.length !== 0) {
5657
- const [updateFn, options] = queuedUpdates.shift();
5658
- beginUpdate(editor, updateFn, options);
5713
+ const queuedUpdate = queuedUpdates.shift();
5714
+
5715
+ if (queuedUpdate) {
5716
+ const [updateFn, options] = queuedUpdate;
5717
+ beginUpdate(editor, updateFn, options);
5718
+ }
5659
5719
  }
5660
5720
  }
5661
5721
 
@@ -5683,28 +5743,32 @@ function processNestedUpdates(editor, initialSkipTransforms) {
5683
5743
  // empty.
5684
5744
 
5685
5745
  while (queuedUpdates.length !== 0) {
5686
- const [nextUpdateFn, options] = queuedUpdates.shift();
5687
- let onUpdate;
5688
- let tag;
5746
+ const queuedUpdate = queuedUpdates.shift();
5689
5747
 
5690
- if (options !== undefined) {
5691
- onUpdate = options.onUpdate;
5692
- tag = options.tag;
5748
+ if (queuedUpdate) {
5749
+ const [nextUpdateFn, options] = queuedUpdate;
5750
+ let onUpdate;
5751
+ let tag;
5693
5752
 
5694
- if (options.skipTransforms) {
5695
- skipTransforms = true;
5696
- }
5753
+ if (options !== undefined) {
5754
+ onUpdate = options.onUpdate;
5755
+ tag = options.tag;
5697
5756
 
5698
- if (onUpdate) {
5699
- editor._deferred.push(onUpdate);
5700
- }
5757
+ if (options.skipTransforms) {
5758
+ skipTransforms = true;
5759
+ }
5760
+
5761
+ if (onUpdate) {
5762
+ editor._deferred.push(onUpdate);
5763
+ }
5701
5764
 
5702
- if (tag) {
5703
- editor._updateTags.add(tag);
5765
+ if (tag) {
5766
+ editor._updateTags.add(tag);
5767
+ }
5704
5768
  }
5705
- }
5706
5769
 
5707
- nextUpdateFn();
5770
+ nextUpdateFn();
5771
+ }
5708
5772
  }
5709
5773
 
5710
5774
  return skipTransforms;
@@ -5724,7 +5788,7 @@ function beginUpdate(editor, updateFn, options) {
5724
5788
  updateTags.add(tag);
5725
5789
  }
5726
5790
 
5727
- skipTransforms = options.skipTransforms;
5791
+ skipTransforms = options.skipTransforms || false;
5728
5792
  }
5729
5793
 
5730
5794
  if (onUpdate) {
@@ -5796,7 +5860,9 @@ function beginUpdate(editor, updateFn, options) {
5796
5860
  }
5797
5861
  } catch (error) {
5798
5862
  // Report errors
5799
- editor._onError(error); // Restore existing editor state to the DOM
5863
+ if (error instanceof Error) {
5864
+ editor._onError(error);
5865
+ } // Restore existing editor state to the DOM
5800
5866
 
5801
5867
 
5802
5868
  editor._pendingEditorState = currentEditorState;
@@ -6158,7 +6224,6 @@ class LexicalNode {
6158
6224
  const b = node.getParents();
6159
6225
 
6160
6226
  if ($isElementNode(this)) {
6161
- // @ts-expect-error
6162
6227
  a.unshift(this);
6163
6228
  }
6164
6229
 
@@ -6871,8 +6936,12 @@ class ElementNode extends LexicalNode {
6871
6936
  }
6872
6937
 
6873
6938
  hasFormat(type) {
6874
- const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
6875
- return (this.getFormat() & formatFlag) !== 0;
6939
+ if (type !== '') {
6940
+ const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
6941
+ return (this.getFormat() & formatFlag) !== 0;
6942
+ }
6943
+
6944
+ return false;
6876
6945
  } // Mutators
6877
6946
 
6878
6947
 
@@ -6957,7 +7026,7 @@ class ElementNode extends LexicalNode {
6957
7026
  setFormat(type) {
6958
7027
  errorOnReadOnly();
6959
7028
  const self = this.getWritable();
6960
- self.__format = ELEMENT_TYPE_TO_FORMAT[type] || 0;
7029
+ self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0;
6961
7030
  return this;
6962
7031
  }
6963
7032
 
@@ -7550,20 +7619,24 @@ function setTextContent(nextText, dom, node) {
7550
7619
  dom.textContent = text;
7551
7620
  } else {
7552
7621
  const nodeValue = firstChild.nodeValue;
7553
- if (nodeValue !== text) if (isComposing || IS_FIREFOX) {
7554
- // We also use the diff composed text for general text in FF to avoid
7555
- // the spellcheck red line from flickering.
7556
- const [index, remove, insert] = diffComposedText(nodeValue, text);
7557
7622
 
7558
- if (remove !== 0) {
7559
- // @ts-expect-error
7560
- firstChild.deleteData(index, remove);
7561
- } // @ts-expect-error
7623
+ if (nodeValue !== text) {
7624
+ if (isComposing || IS_FIREFOX) {
7625
+ // We also use the diff composed text for general text in FF to avoid
7626
+ // We also use the diff composed text for general text in FF to avoid
7627
+ // the spellcheck red line from flickering.
7628
+ const [index, remove, insert] = diffComposedText(nodeValue, text);
7562
7629
 
7630
+ if (remove !== 0) {
7631
+ // @ts-expect-error
7632
+ firstChild.deleteData(index, remove);
7633
+ } // @ts-expect-error
7563
7634
 
7564
- firstChild.insertData(index, insert);
7565
- } else {
7566
- firstChild.nodeValue = text;
7635
+
7636
+ firstChild.insertData(index, insert);
7637
+ } else {
7638
+ firstChild.nodeValue = text;
7639
+ }
7567
7640
  }
7568
7641
  }
7569
7642
  }
@@ -7816,19 +7889,21 @@ class TextNode extends LexicalNode {
7816
7889
 
7817
7890
  selectionTransform(prevSelection, nextSelection) {
7818
7891
  return;
7819
- }
7892
+ } // TODO 0.4 This should just be a `string`.
7893
+
7820
7894
 
7821
7895
  setFormat(format) {
7822
7896
  errorOnReadOnly();
7823
7897
  const self = this.getWritable();
7824
- self.__format = format;
7898
+ self.__format = typeof format === 'string' ? TEXT_TYPE_TO_FORMAT[format] : format;
7825
7899
  return self;
7826
- }
7900
+ } // TODO 0.4 This should just be a `string`.
7901
+
7827
7902
 
7828
7903
  setDetail(detail) {
7829
7904
  errorOnReadOnly();
7830
7905
  const self = this.getWritable();
7831
- self.__detail = detail;
7906
+ self.__detail = typeof detail === 'string' ? DETAIL_TYPE_TO_DETAIL[detail] : detail;
7832
7907
  return self;
7833
7908
  }
7834
7909
 
@@ -8118,11 +8193,13 @@ function convertSpanElement(domNode) {
8118
8193
  // domNode is a <span> since we matched it by nodeName
8119
8194
  const span = domNode; // Google Docs uses span tags + font-weight for bold text
8120
8195
 
8121
- const hasBoldFontWeight = span.style.fontWeight === '700'; // Google Docs uses span tags + text-decoration for strikethrough text
8196
+ const hasBoldFontWeight = span.style.fontWeight === '700'; // Google Docs uses span tags + text-decoration: line-through for strikethrough text
8122
8197
 
8123
8198
  const hasLinethroughTextDecoration = span.style.textDecoration === 'line-through'; // Google Docs uses span tags + font-style for italic text
8124
8199
 
8125
- const hasItalicFontStyle = span.style.fontStyle === 'italic';
8200
+ const hasItalicFontStyle = span.style.fontStyle === 'italic'; // Google Docs uses span tags + text-decoration: underline for underline text
8201
+
8202
+ const hasUnderlineTextDecoration = span.style.textDecoration === 'underline';
8126
8203
  return {
8127
8204
  forChild: lexicalNode => {
8128
8205
  if ($isTextNode(lexicalNode) && hasBoldFontWeight) {
@@ -8137,6 +8214,10 @@ function convertSpanElement(domNode) {
8137
8214
  lexicalNode.toggleFormat('italic');
8138
8215
  }
8139
8216
 
8217
+ if ($isTextNode(lexicalNode) && hasUnderlineTextDecoration) {
8218
+ lexicalNode.toggleFormat('underline');
8219
+ }
8220
+
8140
8221
  return lexicalNode;
8141
8222
  },
8142
8223
  node: null
@@ -8162,9 +8243,9 @@ function convertBringAttentionToElement(domNode) {
8162
8243
 
8163
8244
  function convertTextDOMNode(domNode) {
8164
8245
  const {
8165
- parentElement,
8166
- textContent
8246
+ parentElement
8167
8247
  } = domNode;
8248
+ const textContent = domNode.textContent || '';
8168
8249
  const textContentTrim = textContent.trim();
8169
8250
  const isPre = parentElement != null && parentElement.tagName.toLowerCase() === 'pre';
8170
8251
 
@@ -8261,10 +8342,8 @@ class ParagraphNode extends ElementNode {
8261
8342
  element
8262
8343
  } = super.exportDOM(editor);
8263
8344
 
8264
- if (element) {
8265
- if (this.getTextContentSize() === 0) {
8266
- element.append(document.createElement('br'));
8267
- }
8345
+ if (element && this.isEmpty()) {
8346
+ element.append(document.createElement('br'));
8268
8347
  }
8269
8348
 
8270
8349
  return {
@@ -8388,9 +8467,7 @@ function initializeConversionCache(nodes) {
8388
8467
  const conversionCache = new Map();
8389
8468
  const handledConversions = new Set();
8390
8469
  nodes.forEach(node => {
8391
- const importDOM = // @ts-expect-error TODO Replace Class utility type with InstanceType
8392
- node.klass.importDOM != null ? // @ts-expect-error TODO Replace Class utility type with InstanceType
8393
- node.klass.importDOM.bind(node.klass) : null;
8470
+ const importDOM = node.klass.importDOM != null ? node.klass.importDOM.bind(node.klass) : null;
8394
8471
 
8395
8472
  if (importDOM == null || handledConversions.has(importDOM)) {
8396
8473
  return;
@@ -8458,7 +8535,7 @@ function createEditor(editorConfig) {
8458
8535
  if (proto instanceof DecoratorNode) {
8459
8536
  // eslint-disable-next-line no-prototype-builtins
8460
8537
  if (!proto.hasOwnProperty('decorate')) {
8461
- console.warn(`${this.constructor.name} must implement "decorate" method`);
8538
+ console.warn(`${proto.constructor.name} must implement "decorate" method`);
8462
8539
  }
8463
8540
  }
8464
8541
 
@@ -8472,8 +8549,7 @@ function createEditor(editorConfig) {
8472
8549
  console.warn(`${name} should implement "exportJSON" method to ensure JSON and default HTML serialization works as expected`);
8473
8550
  }
8474
8551
  }
8475
- } // @ts-expect-error TODO Replace Class utility type with InstanceType
8476
-
8552
+ }
8477
8553
 
8478
8554
  const type = klass.getType();
8479
8555
  registeredNodes.set(type, {
@@ -8487,7 +8563,7 @@ function createEditor(editorConfig) {
8487
8563
  disableEvents,
8488
8564
  namespace,
8489
8565
  theme
8490
- }, onError, initializeConversionCache(registeredNodes), isReadOnly);
8566
+ }, onError ? onError : console.error, initializeConversionCache(registeredNodes), isReadOnly);
8491
8567
 
8492
8568
  if (initialEditorState !== undefined) {
8493
8569
  editor._pendingEditorState = initialEditorState;
@@ -8626,7 +8702,6 @@ class LexicalEditor {
8626
8702
  }
8627
8703
 
8628
8704
  registerMutationListener(klass, listener) {
8629
- // @ts-expect-error TODO Replace Class utility type with InstanceType
8630
8705
  const registeredNode = this._nodes.get(klass.getType());
8631
8706
 
8632
8707
  if (registeredNode === undefined) {
@@ -8643,7 +8718,6 @@ class LexicalEditor {
8643
8718
  }
8644
8719
 
8645
8720
  registerNodeTransform(klass, listener) {
8646
- // @ts-expect-error TODO Replace Class utility type with InstanceType
8647
8721
  const type = klass.getType();
8648
8722
 
8649
8723
  const registeredNode = this._nodes.get(type);
@@ -8664,8 +8738,7 @@ class LexicalEditor {
8664
8738
 
8665
8739
  hasNodes(nodes) {
8666
8740
  for (let i = 0; i < nodes.length; i++) {
8667
- const klass = nodes[i]; // @ts-expect-error
8668
-
8741
+ const klass = nodes[i];
8669
8742
  const type = klass.getType();
8670
8743
 
8671
8744
  if (!this._nodes.has(type)) {
@@ -8827,8 +8900,10 @@ class LexicalEditor {
8827
8900
  }
8828
8901
 
8829
8902
  setReadOnly(readOnly) {
8830
- this._readOnly = readOnly;
8831
- triggerListeners('readonly', this, true, readOnly);
8903
+ if (this._readOnly !== readOnly) {
8904
+ this._readOnly = readOnly;
8905
+ triggerListeners('readonly', this, true, readOnly);
8906
+ }
8832
8907
  }
8833
8908
 
8834
8909
  toJSON() {
@@ -8846,7 +8921,7 @@ class LexicalEditor {
8846
8921
  * LICENSE file in the root directory of this source tree.
8847
8922
  *
8848
8923
  */
8849
- const VERSION = '0.3.5';
8924
+ const VERSION = '0.3.6';
8850
8925
 
8851
8926
  /**
8852
8927
  * Copyright (c) Meta Platforms, Inc. and affiliates.