lexical 0.3.5 → 0.3.8

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
@@ -115,7 +115,8 @@ const IS_STRIKETHROUGH = 1 << 2;
115
115
  const IS_UNDERLINE = 1 << 3;
116
116
  const IS_CODE = 1 << 4;
117
117
  const IS_SUBSCRIPT = 1 << 5;
118
- const IS_SUPERSCRIPT = 1 << 6; // Text node details
118
+ const IS_SUPERSCRIPT = 1 << 6;
119
+ const IS_ALL_FORMATTING = IS_BOLD | IS_ITALIC | IS_STRIKETHROUGH | IS_UNDERLINE | IS_CODE | IS_SUBSCRIPT | IS_SUPERSCRIPT; // Text node details
119
120
 
120
121
  const IS_DIRECTIONLESS = 1;
121
122
  const IS_UNMERGEABLE = 1 << 1; // Element node formatting
@@ -125,10 +126,11 @@ const IS_ALIGN_CENTER = 2;
125
126
  const IS_ALIGN_RIGHT = 3;
126
127
  const IS_ALIGN_JUSTIFY = 4; // Reconciliation
127
128
 
128
- const NON_BREAKING_SPACE = '\u00A0'; // For iOS/Safari we use a non breaking space, otherwise the cursor appears
129
+ const NON_BREAKING_SPACE = '\u00A0';
130
+ const ZERO_WIDTH_SPACE = '\u200b'; // For iOS/Safari we use a non breaking space, otherwise the cursor appears
129
131
  // overlapping the composed text.
130
132
 
131
- const COMPOSITION_SUFFIX = IS_SAFARI || IS_IOS ? NON_BREAKING_SPACE : '\u200b';
133
+ const COMPOSITION_SUFFIX = IS_SAFARI || IS_IOS ? NON_BREAKING_SPACE : ZERO_WIDTH_SPACE;
132
134
  const DOUBLE_LINE_BREAK = '\n\n'; // For FF, we need to use a non-breaking space, or it gets composition
133
135
  // in a stuck state.
134
136
 
@@ -148,6 +150,10 @@ const TEXT_TYPE_TO_FORMAT = {
148
150
  superscript: IS_SUPERSCRIPT,
149
151
  underline: IS_UNDERLINE
150
152
  };
153
+ const DETAIL_TYPE_TO_DETAIL = {
154
+ directionless: IS_DIRECTIONLESS,
155
+ unmergeable: IS_UNMERGEABLE
156
+ };
151
157
  const ELEMENT_TYPE_TO_FORMAT = {
152
158
  center: IS_ALIGN_CENTER,
153
159
  justify: IS_ALIGN_JUSTIFY,
@@ -200,7 +206,8 @@ function initTextEntryListener() {
200
206
 
201
207
  function isManagedLineBreak(dom, target, editor) {
202
208
  return (// @ts-expect-error: internal field
203
- target.__lexicalLineBreak === dom || dom['__lexicalKey_' + editor._key] !== undefined
209
+ target.__lexicalLineBreak === dom || // @ts-ignore We intentionally add this to the Node.
210
+ dom[`__lexicalKey_${editor._key}`] !== undefined
204
211
  );
205
212
  }
206
213
 
@@ -222,7 +229,10 @@ function handleTextMutation(target, node, editor) {
222
229
  }
223
230
 
224
231
  const text = target.nodeValue;
225
- $updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
232
+
233
+ if (text !== null) {
234
+ $updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
235
+ }
226
236
  }
227
237
 
228
238
  function shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode) {
@@ -245,7 +255,7 @@ function $flushMutations$1(editor, mutations, observer) {
245
255
  updateEditor(editor, () => {
246
256
  const selection = $getSelection() || getLastSelection(editor);
247
257
  const badDOMTargets = new Map();
248
- const rootElement = editor.getRootElement(); // We use the current edtior state, as that reflects what is
258
+ const rootElement = editor.getRootElement(); // We use the current editor state, as that reflects what is
249
259
  // actually "on screen".
250
260
 
251
261
  const currentEditorState = editor._editorState;
@@ -661,8 +671,9 @@ function $getNodeByKey(key, _editorState) {
661
671
  return node;
662
672
  }
663
673
  function getNodeFromDOMNode(dom, editorState) {
664
- const editor = getActiveEditor();
665
- const key = dom['__lexicalKey_' + editor._key];
674
+ const editor = getActiveEditor(); // @ts-ignore We intentionally add this to the Node.
675
+
676
+ const key = dom[`__lexicalKey_${editor._key}`];
666
677
 
667
678
  if (key !== undefined) {
668
679
  return $getNodeByKey(key, editorState);
@@ -771,7 +782,8 @@ dom, editor) {
771
782
  let node = dom;
772
783
 
773
784
  while (node != null) {
774
- const key = node['__lexicalKey_' + editor._key];
785
+ // @ts-ignore We intentionally add this to the Node.
786
+ const key = node[`__lexicalKey_${editor._key}`];
775
787
 
776
788
  if (key !== undefined) {
777
789
  return key;
@@ -827,7 +839,9 @@ function $updateSelectedTextFromDOM(editor, isCompositionEnd, data) {
827
839
  focusOffset = offset;
828
840
  }
829
841
 
830
- $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, isCompositionEnd);
842
+ if (textContent !== null) {
843
+ $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, isCompositionEnd);
844
+ }
831
845
  }
832
846
  }
833
847
  }
@@ -897,6 +911,11 @@ function $updateTextNodeFromDOMContent(textNode, textContent, anchorOffset, focu
897
911
  }
898
912
  }
899
913
 
914
+ function $previousSiblingDoesNotAcceptText(node) {
915
+ const previousSibling = node.getPreviousSibling();
916
+ return ($isTextNode(previousSibling) || $isElementNode(previousSibling) && previousSibling.isInline()) && !previousSibling.canInsertTextAfter();
917
+ }
918
+
900
919
  function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
901
920
  if (node.isSegmented()) {
902
921
  return true;
@@ -909,7 +928,7 @@ function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
909
928
  const offset = selection.anchor.offset;
910
929
  const parent = node.getParentOrThrow();
911
930
  const isToken = node.isToken();
912
- const shouldInsertTextBefore = offset === 0 && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken);
931
+ const shouldInsertTextBefore = offset === 0 && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken || $previousSiblingDoesNotAcceptText(node));
913
932
  const shouldInsertTextAfter = node.getTextContentSize() === offset && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken);
914
933
  return shouldInsertTextBefore || shouldInsertTextAfter;
915
934
  } // This function is used to determine if Lexical should attempt to override
@@ -1023,6 +1042,8 @@ function isCopy(keyCode, shiftKey, metaKey, ctrlKey) {
1023
1042
  if (keyCode === 67) {
1024
1043
  return IS_APPLE ? metaKey : ctrlKey;
1025
1044
  }
1045
+
1046
+ return false;
1026
1047
  }
1027
1048
  function isCut(keyCode, shiftKey, metaKey, ctrlKey) {
1028
1049
  if (shiftKey) {
@@ -1032,6 +1053,8 @@ function isCut(keyCode, shiftKey, metaKey, ctrlKey) {
1032
1053
  if (keyCode === 88) {
1033
1054
  return IS_APPLE ? metaKey : ctrlKey;
1034
1055
  }
1056
+
1057
+ return false;
1035
1058
  }
1036
1059
 
1037
1060
  function isArrowLeft(keyCode) {
@@ -1137,8 +1160,7 @@ function setMutatedNode(mutatedNodes, registeredNodes, mutationListeners, node,
1137
1160
  }
1138
1161
  function $nodesOfType(klass) {
1139
1162
  const editorState = getActiveEditorState();
1140
- const readOnly = editorState._readOnly; // @ts-expect-error TODO Replace Class utility type with InstanceType
1141
-
1163
+ const readOnly = editorState._readOnly;
1142
1164
  const klassType = klass.getType();
1143
1165
  const nodes = editorState._nodeMap;
1144
1166
  const nodesOfType = [];
@@ -1658,10 +1680,9 @@ function reconcileBlockDirection(element, dom) {
1658
1680
 
1659
1681
  if (previousDirectionTheme !== undefined) {
1660
1682
  if (typeof previousDirectionTheme === 'string') {
1661
- const classNamesArr = previousDirectionTheme.split(' '); // @ts-expect-error: intentional
1662
-
1683
+ const classNamesArr = previousDirectionTheme.split(' ');
1663
1684
  previousDirectionTheme = theme[previousDirection] = classNamesArr;
1664
- } // @ts-expect-error: intentional
1685
+ } // @ts-ignore: intentional
1665
1686
 
1666
1687
 
1667
1688
  classList.remove(...previousDirectionTheme);
@@ -1679,7 +1700,9 @@ function reconcileBlockDirection(element, dom) {
1679
1700
  nextDirectionTheme = theme[direction] = classNamesArr;
1680
1701
  }
1681
1702
 
1682
- classList.add(...nextDirectionTheme);
1703
+ if (nextDirectionTheme !== undefined) {
1704
+ classList.add(...nextDirectionTheme);
1705
+ }
1683
1706
  } // Update direction
1684
1707
 
1685
1708
 
@@ -2001,20 +2024,30 @@ function reconcileRoot(prevEditorState, nextEditorState, editor, dirtyType, dirt
2001
2024
  // so instead we make it seem that these values are always set.
2002
2025
  // We also want to make sure we clear them down, otherwise we
2003
2026
  // can leak memory.
2027
+ // @ts-ignore
2028
+
2029
+ activeEditor$1 = undefined; // @ts-ignore
2030
+
2031
+ activeEditorNodes = undefined; // @ts-ignore
2032
+
2033
+ activeDirtyElements = undefined; // @ts-ignore
2034
+
2035
+ activeDirtyLeaves = undefined; // @ts-ignore
2036
+
2037
+ activePrevNodeMap = undefined; // @ts-ignore
2038
+
2039
+ activeNextNodeMap = undefined; // @ts-ignore
2040
+
2041
+ activeEditorConfig = undefined; // @ts-ignore
2042
+
2043
+ activePrevKeyToDOMMap = undefined; // @ts-ignore
2004
2044
 
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
2045
  mutatedNodes = undefined;
2014
2046
  return currentMutatedNodes;
2015
2047
  }
2016
2048
  function storeDOMWithKey(key, dom, editor) {
2017
- const keyToDOMMap = editor._keyToDOMMap;
2049
+ const keyToDOMMap = editor._keyToDOMMap; // @ts-ignore We intentionally add this to the Node.
2050
+
2018
2051
  dom['__lexicalKey_' + editor._key] = key;
2019
2052
  keyToDOMMap.set(key, dom);
2020
2053
  }
@@ -2040,10 +2073,10 @@ function getPrevElementByKeyOrThrow(key) {
2040
2073
  */
2041
2074
  const PASS_THROUGH_COMMAND = Object.freeze({});
2042
2075
  const ANDROID_COMPOSITION_LATENCY = 30;
2043
- 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]];
2076
+ 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], ['dragend', PASS_THROUGH_COMMAND], ['paste', PASS_THROUGH_COMMAND], ['focus', PASS_THROUGH_COMMAND], ['blur', PASS_THROUGH_COMMAND], ['drop', PASS_THROUGH_COMMAND]];
2044
2077
 
2045
2078
  if (CAN_USE_BEFORE_INPUT) {
2046
- rootElementEvents.push(['beforeinput', onBeforeInput]);
2079
+ rootElementEvents.push(['beforeinput', (event, editor) => onBeforeInput(event, editor)]);
2047
2080
  }
2048
2081
 
2049
2082
  let lastKeyDownTimeStamp = 0;
@@ -2055,7 +2088,7 @@ let isFirefoxEndingComposition = false;
2055
2088
  let collapsedSelectionFormat = [0, 0, 'root', 0];
2056
2089
 
2057
2090
  function shouldSkipSelectionChange(domNode, offset) {
2058
- return domNode !== null && domNode.nodeType === DOM_TEXT_TYPE && offset !== 0 && offset !== domNode.nodeValue.length;
2091
+ return domNode !== null && domNode.nodeValue !== null && domNode.nodeType === DOM_TEXT_TYPE && offset !== 0 && offset !== domNode.nodeValue.length;
2059
2092
  }
2060
2093
 
2061
2094
  function onSelectionChange(domSelection, editor, isActive) {
@@ -2122,16 +2155,20 @@ function onSelectionChange(domSelection, editor, isActive) {
2122
2155
  }
2123
2156
  }
2124
2157
  } else {
2125
- const focus = selection.focus;
2126
- const focusNode = focus.getNode();
2127
- let combinedFormat = 0;
2158
+ let combinedFormat = IS_ALL_FORMATTING;
2159
+ const nodes = selection.getNodes();
2160
+ const nodesLength = nodes.length;
2128
2161
 
2129
- if (anchor.type === 'text') {
2130
- combinedFormat |= anchorNode.getFormat();
2131
- }
2162
+ for (let i = 0; i < nodesLength; i++) {
2163
+ const node = nodes[i];
2132
2164
 
2133
- if (focus.type === 'text' && !anchorNode.is(focusNode)) {
2134
- combinedFormat |= focusNode.getFormat();
2165
+ if ($isTextNode(node)) {
2166
+ combinedFormat &= node.getFormat();
2167
+
2168
+ if (combinedFormat === 0) {
2169
+ break;
2170
+ }
2171
+ }
2135
2172
  }
2136
2173
 
2137
2174
  selection.format = combinedFormat;
@@ -2157,15 +2194,19 @@ function onClick(event, editor) {
2157
2194
  const anchor = selection.anchor;
2158
2195
  const anchorNode = anchor.getNode();
2159
2196
 
2160
- if (anchor.type === 'element' && anchor.offset === 0 && selection.isCollapsed() && !$isRootNode(anchorNode) && $getRoot().getChildrenSize() === 1 && anchorNode.getTopLevelElementOrThrow().isEmpty() && lastSelection !== null && selection.is(lastSelection)) {
2197
+ if (domSelection && anchor.type === 'element' && anchor.offset === 0 && selection.isCollapsed() && !$isRootNode(anchorNode) && $getRoot().getChildrenSize() === 1 && anchorNode.getTopLevelElementOrThrow().isEmpty() && lastSelection !== null && selection.is(lastSelection)) {
2161
2198
  domSelection.removeAllRanges();
2162
2199
  selection.dirty = true;
2163
2200
  }
2164
- } else if ($isNodeSelection(selection) && domSelection.isCollapsed) {
2201
+ } else if (domSelection && $isNodeSelection(selection) && domSelection.isCollapsed) {
2165
2202
  const domAnchor = domSelection.anchorNode; // If the user is attempting to click selection back onto text, then
2166
2203
  // we should attempt create a range selection.
2204
+ // When we click on an empty paragraph node or the end of a paragraph that ends
2205
+ // with an image/poll, the nodeType will be ELEMENT_NODE
2206
+
2207
+ const allowedNodeType = [DOM_ELEMENT_TYPE, DOM_TEXT_TYPE];
2167
2208
 
2168
- if (domAnchor !== null && domAnchor.nodeType === DOM_TEXT_TYPE) {
2209
+ if (domAnchor !== null && allowedNodeType.includes(domAnchor.nodeType)) {
2169
2210
  const newSelection = internalCreateRangeSelection(lastSelection, domSelection, editor);
2170
2211
  $setSelection(newSelection);
2171
2212
  }
@@ -2204,41 +2245,6 @@ function onBeforeInput(event, editor) {
2204
2245
  IS_FIREFOX && isFirefoxClipboardEvents()) {
2205
2246
  return;
2206
2247
  } else if (inputType === 'insertCompositionText') {
2207
- // This logic handles insertion of text between different
2208
- // format text types. We have to detect a change in type
2209
- // during composition and see if the previous text contains
2210
- // part of the composed text to work out the actual text that
2211
- // we need to insert.
2212
- const composedText = event.data; // TODO: evaluate if this is Android only. It doesn't always seem
2213
- // to have any real impact, so could probably be refactored or removed
2214
- // for an alternative approach.
2215
-
2216
- if (composedText) {
2217
- updateEditor(editor, () => {
2218
- const selection = $getSelection();
2219
-
2220
- if ($isRangeSelection(selection)) {
2221
- const anchor = selection.anchor;
2222
- const node = anchor.getNode();
2223
- const prevNode = node.getPreviousSibling();
2224
-
2225
- if (anchor.offset === 0 && $isTextNode(node) && $isTextNode(prevNode) && node.getTextContent() === COMPOSITION_START_CHAR && prevNode.getFormat() !== selection.format) {
2226
- const prevTextContent = prevNode.getTextContent();
2227
-
2228
- if (composedText.indexOf(prevTextContent) === 0) {
2229
- const insertedText = composedText.slice(prevTextContent.length);
2230
- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, insertedText);
2231
- setTimeout(() => {
2232
- updateEditor(editor, () => {
2233
- node.select();
2234
- });
2235
- }, ANDROID_COMPOSITION_LATENCY);
2236
- }
2237
- }
2238
- }
2239
- });
2240
- }
2241
-
2242
2248
  return;
2243
2249
  }
2244
2250
 
@@ -2301,7 +2307,7 @@ function onBeforeInput(event, editor) {
2301
2307
  if (inputType === 'insertText') {
2302
2308
  if (data === '\n') {
2303
2309
  event.preventDefault();
2304
- dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, undefined);
2310
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
2305
2311
  } else if (data === DOUBLE_LINE_BREAK) {
2306
2312
  event.preventDefault();
2307
2313
  dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
@@ -2344,7 +2350,7 @@ function onBeforeInput(event, editor) {
2344
2350
  {
2345
2351
  // Used for Android
2346
2352
  $setCompositionKey(null);
2347
- dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, undefined);
2353
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
2348
2354
  break;
2349
2355
  }
2350
2356
 
@@ -2356,7 +2362,7 @@ function onBeforeInput(event, editor) {
2356
2362
 
2357
2363
  if (isInsertLineBreak) {
2358
2364
  isInsertLineBreak = false;
2359
- dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, undefined);
2365
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
2360
2366
  } else {
2361
2367
  dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
2362
2368
  }
@@ -2476,7 +2482,14 @@ function onInput(event, editor) {
2476
2482
  isFirefoxEndingComposition = false;
2477
2483
  }
2478
2484
 
2479
- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data); // This ensures consistency on Android.
2485
+ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2486
+ const textLength = data.length; // Another hack for FF, as it's possible that the IME is still
2487
+ // open, even though compositionend has already fired (sigh).
2488
+
2489
+ if (IS_FIREFOX && textLength > 1 && event.inputType === 'insertCompositionText' && !editor.isComposing()) {
2490
+ selection.anchor.offset -= textLength;
2491
+ } // This ensures consistency on Android.
2492
+
2480
2493
 
2481
2494
  if (!IS_SAFARI && !IS_IOS && editor.isComposing()) {
2482
2495
  lastKeyDownTimeStamp = 0;
@@ -2486,7 +2499,7 @@ function onInput(event, editor) {
2486
2499
  $updateSelectedTextFromDOM(editor, false); // onInput always fires after onCompositionEnd for FF.
2487
2500
 
2488
2501
  if (isFirefoxEndingComposition) {
2489
- onCompositionEndImpl(editor, data);
2502
+ onCompositionEndImpl(editor, data || undefined);
2490
2503
  isFirefoxEndingComposition = false;
2491
2504
  }
2492
2505
  } // Also flush any other mutations that might have occurred
@@ -2532,7 +2545,7 @@ function onCompositionEndImpl(editor, data) {
2532
2545
  const node = $getNodeByKey(compositionKey);
2533
2546
  const textNode = getDOMTextNode(editor.getElementByKey(compositionKey));
2534
2547
 
2535
- if (textNode !== null && $isTextNode(node)) {
2548
+ if (textNode !== null && textNode.nodeValue !== null && $isTextNode(node)) {
2536
2549
  $updateTextNodeFromDOMContent(node, textNode.nodeValue, null, null, true);
2537
2550
  }
2538
2551
 
@@ -2696,6 +2709,11 @@ const activeNestedEditorsMap = new Map();
2696
2709
 
2697
2710
  function onDocumentSelectionChange(event) {
2698
2711
  const selection = getDOMSelection();
2712
+
2713
+ if (!selection) {
2714
+ return;
2715
+ }
2716
+
2699
2717
  const nextActiveEditor = getNearestEditorFromDOMNode(selection.anchorNode);
2700
2718
 
2701
2719
  if (nextActiveEditor === null) {
@@ -2795,7 +2813,7 @@ function removeRootElementEvents(rootElement) {
2795
2813
 
2796
2814
  const editor = rootElement.__lexicalEditor;
2797
2815
 
2798
- if (editor !== null || editor !== undefined) {
2816
+ if (editor !== null && editor !== undefined) {
2799
2817
  cleanActiveNestedEditorsMap(editor); // @ts-expect-error: internal field
2800
2818
 
2801
2819
  rootElement.__lexicalEditor = null;
@@ -3472,7 +3490,7 @@ class RangeSelection {
3472
3490
  const lastIndex = selectedNodesLength - 1;
3473
3491
  let lastNode = selectedNodes[lastIndex];
3474
3492
 
3475
- if (this.isCollapsed() && startOffset === firstNodeTextLength && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextAfter() || !firstNodeParent.canInsertTextAfter())) {
3493
+ if (this.isCollapsed() && startOffset === firstNodeTextLength && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextAfter() || !firstNodeParent.canInsertTextAfter() && firstNode.getNextSibling() === null)) {
3476
3494
  let nextSibling = firstNode.getNextSibling();
3477
3495
 
3478
3496
  if (!$isTextNode(nextSibling) || $isTokenOrInertOrSegmented(nextSibling)) {
@@ -3493,7 +3511,7 @@ class RangeSelection {
3493
3511
  this.insertText(text);
3494
3512
  return;
3495
3513
  }
3496
- } else if (this.isCollapsed() && startOffset === 0 && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextBefore() || !firstNodeParent.canInsertTextBefore())) {
3514
+ } else if (this.isCollapsed() && startOffset === 0 && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextBefore() || !firstNodeParent.canInsertTextBefore() && firstNode.getPreviousSibling() === null)) {
3497
3515
  let prevSibling = firstNode.getPreviousSibling();
3498
3516
 
3499
3517
  if (!$isTextNode(prevSibling) || $isTokenOrInertOrSegmented(prevSibling)) {
@@ -3717,10 +3735,19 @@ class RangeSelection {
3717
3735
  formatText(formatType) {
3718
3736
  // TODO I wonder if this methods use selection.extract() instead?
3719
3737
  const selectedNodes = this.getNodes();
3720
- const selectedNodesLength = selectedNodes.length;
3721
- const lastIndex = selectedNodesLength - 1;
3722
- let firstNode = selectedNodes[0];
3723
- let lastNode = selectedNodes[lastIndex];
3738
+ const selectedTextNodes = [];
3739
+
3740
+ for (const selectedNode of selectedNodes) {
3741
+ if ($isTextNode(selectedNode)) {
3742
+ selectedTextNodes.push(selectedNode);
3743
+ }
3744
+ }
3745
+
3746
+ const selectedTextNodesLength = selectedTextNodes.length;
3747
+ let firstIndex = 0;
3748
+ const lastIndex = selectedTextNodesLength - 1;
3749
+ let firstNode = selectedTextNodes[0];
3750
+ let lastNode = selectedTextNodes[lastIndex];
3724
3751
 
3725
3752
  if (this.isCollapsed()) {
3726
3753
  this.toggleFormat(formatType); // When changing format, we should stop composition
@@ -3731,42 +3758,22 @@ class RangeSelection {
3731
3758
 
3732
3759
  const anchor = this.anchor;
3733
3760
  const focus = this.focus;
3761
+ const anchorOffset = anchor.offset;
3734
3762
  const focusOffset = focus.offset;
3735
- let firstNextFormat = 0;
3763
+ let firstNextFormat = firstNode.getFormatFlags(formatType, null);
3736
3764
  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
3765
  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
3766
+ const endOffset = isBefore ? focusOffset : anchorOffset;
3767
+ let startOffset = isBefore ? anchorOffset : focusOffset; // This is the case where the user only selected the very end of the
3753
3768
  // first node so we don't want to include it in the formatting change.
3754
3769
 
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
- }
3770
+ if (startOffset === firstNode.getTextContentSize() && selectedTextNodes.length > 1) {
3771
+ const nextNode = selectedTextNodes[1];
3772
+ startOffset = 0;
3773
+ firstIndex = 1;
3774
+ firstNode = nextNode;
3775
+ firstNodeTextLength = nextNode.getTextContentSize();
3776
+ firstNextFormat = nextNode.getFormatFlags(formatType, null);
3770
3777
  } // This is the case where we only selected a single node
3771
3778
 
3772
3779
 
@@ -3777,10 +3784,8 @@ class RangeSelection {
3777
3784
  firstNode.select(startOffset, endOffset);
3778
3785
  this.format = firstNextFormat;
3779
3786
  return;
3780
- }
3787
+ } // No actual text is selected, so do nothing.
3781
3788
 
3782
- startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
3783
- endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset; // No actual text is selected, so do nothing.
3784
3789
 
3785
3790
  if (startOffset === endOffset) {
3786
3791
  return;
@@ -3803,7 +3808,9 @@ class RangeSelection {
3803
3808
  } // multiple nodes selected.
3804
3809
 
3805
3810
  } else {
3806
- if ($isTextNode(firstNode)) {
3811
+ // Note: startOffset !== firstNodeTextLength should only occur within rare programatic
3812
+ // update functions; transforms normalization ensure there's no empty text nodes.
3813
+ if ($isTextNode(firstNode) && startOffset !== firstNodeTextLength) {
3807
3814
  if (startOffset !== 0) {
3808
3815
  // the entire first node isn't selected, so split it
3809
3816
  [, firstNode] = firstNode.splitText(startOffset);
@@ -3829,11 +3836,12 @@ class RangeSelection {
3829
3836
 
3830
3837
  lastNode.setFormat(lastNextFormat);
3831
3838
  }
3832
- } // deal with all the nodes in between
3839
+ }
3833
3840
 
3841
+ this.format = firstNextFormat | lastNextFormat; // deal with all the nodes in between
3834
3842
 
3835
- for (let i = 1; i < lastIndex; i++) {
3836
- const selectedNode = selectedNodes[i];
3843
+ for (let i = firstIndex + 1; i < lastIndex; i++) {
3844
+ const selectedNode = selectedTextNodes[i];
3837
3845
  const selectedNodeKey = selectedNode.__key;
3838
3846
 
3839
3847
  if ($isTextNode(selectedNode) && selectedNodeKey !== firstNode.__key && selectedNodeKey !== lastNode.__key && !selectedNode.isToken()) {
@@ -3905,7 +3913,7 @@ class RangeSelection {
3905
3913
  siblings.push(...nextSiblings);
3906
3914
  const firstNode = nodes[0];
3907
3915
  let didReplaceOrMerge = false;
3908
- let lastNodeInserted = null; // Time to insert the nodes!
3916
+ let lastNode = null; // Time to insert the nodes!
3909
3917
 
3910
3918
  for (let i = 0; i < nodes.length; i++) {
3911
3919
  const node = nodes[i];
@@ -3968,18 +3976,17 @@ class RangeSelection {
3968
3976
 
3969
3977
  if ($isElementNode(target)) {
3970
3978
  for (let s = 0; s < childrenLength; s++) {
3971
- lastNodeInserted = children[s];
3972
- target.append(lastNodeInserted);
3979
+ target.append(children[s]);
3973
3980
  }
3974
3981
  } else {
3975
3982
  for (let s = childrenLength - 1; s >= 0; s--) {
3976
- lastNodeInserted = children[s];
3977
- target.insertAfter(lastNodeInserted);
3983
+ target.insertAfter(children[s]);
3978
3984
  }
3979
3985
 
3980
3986
  target = target.getParentOrThrow();
3981
3987
  }
3982
3988
 
3989
+ lastNode = children[childrenLength - 1];
3983
3990
  element.remove();
3984
3991
  didReplaceOrMerge = true;
3985
3992
 
@@ -4007,7 +4014,7 @@ class RangeSelection {
4007
4014
  didReplaceOrMerge = false;
4008
4015
 
4009
4016
  if ($isElementNode(target) && !target.isInline()) {
4010
- lastNodeInserted = node;
4017
+ lastNode = node;
4011
4018
 
4012
4019
  if ($isDecoratorNode(node) && node.isTopLevel()) {
4013
4020
  target = target.insertAfter(node);
@@ -4040,8 +4047,8 @@ class RangeSelection {
4040
4047
  target = target.insertAfter(node);
4041
4048
  }
4042
4049
  }
4043
- } else if (!$isElementNode(node) || $isElementNode(node) && node.isInline() || $isDecoratorNode(target) && target.isTopLevel()) {
4044
- lastNodeInserted = node;
4050
+ } else if (!$isElementNode(node) || $isElementNode(node) && node.isInline() || $isDecoratorNode(target) && target.isTopLevel() || $isLineBreakNode(target)) {
4051
+ lastNode = node;
4045
4052
  target = target.insertAfter(node);
4046
4053
  } else {
4047
4054
  target = node.getParentOrThrow(); // Re-try again with the target being the parent
@@ -4070,7 +4077,7 @@ class RangeSelection {
4070
4077
  if ($isElementNode(target)) {
4071
4078
  // If the last node to be inserted was a text node,
4072
4079
  // then we should attempt to move selection to that.
4073
- const lastChild = $isTextNode(lastNodeInserted) ? lastNodeInserted : target.getLastDescendant();
4080
+ const lastChild = $isTextNode(lastNode) ? lastNode : target.getLastDescendant();
4074
4081
 
4075
4082
  if (!selectStart) {
4076
4083
  // Handle moving selection to end for elements
@@ -4084,18 +4091,26 @@ class RangeSelection {
4084
4091
  }
4085
4092
 
4086
4093
  if (siblings.length !== 0) {
4094
+ const originalTarget = target;
4095
+
4087
4096
  for (let i = siblings.length - 1; i >= 0; i--) {
4088
4097
  const sibling = siblings[i];
4089
4098
  const prevParent = sibling.getParentOrThrow();
4090
4099
 
4091
- if ($isElementNode(target) && !$isElementNode(sibling)) {
4092
- target.append(sibling);
4100
+ if ($isElementNode(target) && !$isBlockElementNode(sibling)) {
4101
+ if (originalTarget === target) {
4102
+ target.append(sibling);
4103
+ } else {
4104
+ target.insertBefore(sibling);
4105
+ }
4106
+
4093
4107
  target = sibling;
4094
- } else if (!$isElementNode(target) && !$isElementNode(sibling)) {
4108
+ } else if (!$isElementNode(target) && !$isBlockElementNode(sibling)) {
4095
4109
  target.insertBefore(sibling);
4096
4110
  target = sibling;
4097
4111
  } else {
4098
4112
  if ($isElementNode(sibling) && !sibling.canInsertAfter(target)) {
4113
+ // @ts-ignore The clone method does exist on the constructor.
4099
4114
  const prevParentClone = prevParent.constructor.clone(prevParent);
4100
4115
 
4101
4116
  if (!$isElementNode(prevParentClone)) {
@@ -4291,7 +4306,7 @@ class RangeSelection {
4291
4306
  if (selectedNodesLength === 0) {
4292
4307
  return [];
4293
4308
  } else if (selectedNodesLength === 1) {
4294
- if ($isTextNode(firstNode)) {
4309
+ if ($isTextNode(firstNode) && !this.isCollapsed()) {
4295
4310
  const startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
4296
4311
  const endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
4297
4312
  const splitNodes = firstNode.splitText(startOffset, endOffset);
@@ -4378,13 +4393,18 @@ class RangeSelection {
4378
4393
  }
4379
4394
  }
4380
4395
 
4381
- const domSelection = getDOMSelection(); // We use the DOM selection.modify API here to "tell" us what the selection
4396
+ const domSelection = getDOMSelection();
4397
+
4398
+ if (!domSelection) {
4399
+ return;
4400
+ } // We use the DOM selection.modify API here to "tell" us what the selection
4382
4401
  // will be. We then use it to update the Lexical selection accordingly. This
4383
4402
  // is much more reliable than waiting for a beforeinput and using the ranges
4384
4403
  // from getTargetRanges(), and is also better than trying to do it ourselves
4385
4404
  // using Intl.Segmenter or other workarounds that struggle with word segments
4386
4405
  // and line segments (especially with word wrapping and non-Roman languages).
4387
4406
 
4407
+
4388
4408
  $moveNativeSelection(domSelection, alter, isBackward ? 'backward' : 'forward', granularity); // Guard against no ranges
4389
4409
 
4390
4410
  if (domSelection.rangeCount > 0) {
@@ -4408,7 +4428,7 @@ class RangeSelection {
4408
4428
  let anchorNode = anchor.getNode();
4409
4429
 
4410
4430
  if (!isBackward && ( // Delete forward handle case
4411
- anchor.type === 'element' && anchor.offset === anchorNode.getChildrenSize() || anchor.type === 'text' && anchor.offset === anchorNode.getTextContentSize())) {
4431
+ anchor.type === 'element' && $isElementNode(anchorNode) && anchor.offset === anchorNode.getChildrenSize() || anchor.type === 'text' && anchor.offset === anchorNode.getTextContentSize())) {
4412
4432
  const nextSibling = anchorNode.getNextSibling() || anchorNode.getParentOrThrow().getNextSibling();
4413
4433
 
4414
4434
  if ($isElementNode(nextSibling) && !nextSibling.canExtractContents()) {
@@ -4509,6 +4529,8 @@ function $swapPoints(selection) {
4509
4529
  }
4510
4530
 
4511
4531
  function $moveNativeSelection(domSelection, alter, direction, granularity) {
4532
+ // @ts-expect-error Selection.modify() method applies a change to the current selection or cursor position,
4533
+ // but is still non-standard in some browsers.
4512
4534
  domSelection.modify(alter, direction, granularity);
4513
4535
  }
4514
4536
 
@@ -4768,6 +4790,10 @@ function internalResolveSelectionPoints(anchorDOM, anchorOffset, focusDOM, focus
4768
4790
 
4769
4791
  normalizeSelectionPointsForBoundaries(resolvedAnchorPoint, resolvedFocusPoint, lastSelection);
4770
4792
  return [resolvedAnchorPoint, resolvedFocusPoint];
4793
+ }
4794
+
4795
+ function $isBlockElementNode(node) {
4796
+ return $isElementNode(node) && !node.isInline();
4771
4797
  } // This is used to make a selection when the existing
4772
4798
  // selection is null, i.e. forcing selection on the editor
4773
4799
  // when it current exists outside the editor.
@@ -5116,7 +5142,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5116
5142
  if (anchorOffset === nextAnchorOffset && focusOffset === nextFocusOffset && anchorDOMNode === nextAnchorNode && focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
5117
5143
  !(domSelection.type === 'Range' && isCollapsed)) {
5118
5144
  // If the root element does not have focus, ensure it has focus
5119
- if (activeElement === null || !rootElement.contains(activeElement)) {
5145
+ if (rootElement !== null && (activeElement === null || !rootElement.contains(activeElement))) {
5120
5146
  rootElement.focus({
5121
5147
  preventScroll: true
5122
5148
  });
@@ -5135,7 +5161,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5135
5161
  try {
5136
5162
  domSelection.setBaseAndExtent(nextAnchorNode, nextAnchorOffset, nextFocusNode, nextFocusOffset);
5137
5163
 
5138
- if (nextSelection.isCollapsed() && rootElement === activeElement) {
5164
+ if (nextSelection.isCollapsed() && rootElement !== null && rootElement === activeElement) {
5139
5165
  scrollIntoViewIfNeeded(editor, anchor, rootElement, tags);
5140
5166
  }
5141
5167
 
@@ -5317,7 +5343,6 @@ function $applyAllTransforms(editorState, editor) {
5317
5343
  }
5318
5344
 
5319
5345
  function $parseSerializedNode(serializedNode) {
5320
- // $FlowFixMe: intentional cast to our internal type
5321
5346
  const internalSerializedNode = serializedNode;
5322
5347
  return $parseSerializedNodeImpl(internalSerializedNode, getActiveEditor()._nodes);
5323
5348
  }
@@ -5332,14 +5357,13 @@ function $parseSerializedNodeImpl(serializedNode, registeredNodes) {
5332
5357
  }
5333
5358
  }
5334
5359
 
5335
- const nodeClass = registeredNode.klass; // @ts-expect-error TODO Replace Class utility type with InstanceType
5360
+ const nodeClass = registeredNode.klass;
5336
5361
 
5337
5362
  if (serializedNode.type !== nodeClass.getType()) {
5338
5363
  {
5339
5364
  throw Error(`LexicalNode: Node ${nodeClass.name} does not implement .importJSON().`);
5340
5365
  }
5341
- } // @ts-expect-error TODO Replace Class utility type with InstanceType
5342
-
5366
+ }
5343
5367
 
5344
5368
  const node = nodeClass.importJSON(serializedNode);
5345
5369
  const children = serializedNode.children;
@@ -5477,7 +5501,9 @@ function commitPendingUpdates(editor) {
5477
5501
  mutatedNodes = reconcileRoot(currentEditorState, pendingEditorState, editor, dirtyType, dirtyElements, dirtyLeaves);
5478
5502
  } catch (error) {
5479
5503
  // Report errors
5480
- editor._onError(error); // Reset editor and restore incoming editor state to the DOM
5504
+ if (error instanceof Error) {
5505
+ editor._onError(error);
5506
+ } // Reset editor and restore incoming editor state to the DOM
5481
5507
 
5482
5508
 
5483
5509
  if (!isAttemptingToRecoverFromReconcilerError) {
@@ -5590,16 +5616,17 @@ function triggerTextContentListeners(editor, currentEditorState, pendingEditorSt
5590
5616
  }
5591
5617
 
5592
5618
  function triggerMutationListeners(editor, currentEditorState, pendingEditorState, mutatedNodes) {
5593
- const listeners = editor._listeners.mutation;
5594
- listeners.forEach((klass, listener) => {
5619
+ const listeners = Array.from(editor._listeners.mutation);
5620
+ const listenersLength = listeners.length;
5621
+
5622
+ for (let i = 0; i < listenersLength; i++) {
5623
+ const [listener, klass] = listeners[i];
5595
5624
  const mutatedNodesByType = mutatedNodes.get(klass);
5596
5625
 
5597
- if (mutatedNodesByType === undefined) {
5598
- return;
5626
+ if (mutatedNodesByType !== undefined) {
5627
+ listener(mutatedNodesByType);
5599
5628
  }
5600
-
5601
- listener(mutatedNodesByType);
5602
- });
5629
+ }
5603
5630
  }
5604
5631
 
5605
5632
  function triggerListeners(type, editor, isCurrentlyEnqueuingUpdates, ...payload) {
@@ -5610,6 +5637,7 @@ function triggerListeners(type, editor, isCurrentlyEnqueuingUpdates, ...payload)
5610
5637
  const listeners = Array.from(editor._listeners[type]);
5611
5638
 
5612
5639
  for (let i = 0; i < listeners.length; i++) {
5640
+ // @ts-ignore
5613
5641
  listeners[i].apply(null, payload);
5614
5642
  }
5615
5643
  } finally {
@@ -5634,11 +5662,14 @@ function triggerCommandListeners(editor, type, payload) {
5634
5662
  const listenerInPriorityOrder = commandListeners.get(type);
5635
5663
 
5636
5664
  if (listenerInPriorityOrder !== undefined) {
5637
- const listeners = listenerInPriorityOrder[i];
5665
+ const listenersSet = listenerInPriorityOrder[i];
5638
5666
 
5639
- if (listeners !== undefined) {
5640
- for (const listener of listeners) {
5641
- if (listener(payload, editor) === true) {
5667
+ if (listenersSet !== undefined) {
5668
+ const listeners = Array.from(listenersSet);
5669
+ const listenersLength = listeners.length;
5670
+
5671
+ for (let j = 0; j < listenersLength; j++) {
5672
+ if (listeners[j](payload, editor) === true) {
5642
5673
  return true;
5643
5674
  }
5644
5675
  }
@@ -5654,8 +5685,12 @@ function triggerEnqueuedUpdates(editor) {
5654
5685
  const queuedUpdates = editor._updates;
5655
5686
 
5656
5687
  if (queuedUpdates.length !== 0) {
5657
- const [updateFn, options] = queuedUpdates.shift();
5658
- beginUpdate(editor, updateFn, options);
5688
+ const queuedUpdate = queuedUpdates.shift();
5689
+
5690
+ if (queuedUpdate) {
5691
+ const [updateFn, options] = queuedUpdate;
5692
+ beginUpdate(editor, updateFn, options);
5693
+ }
5659
5694
  }
5660
5695
  }
5661
5696
 
@@ -5683,28 +5718,32 @@ function processNestedUpdates(editor, initialSkipTransforms) {
5683
5718
  // empty.
5684
5719
 
5685
5720
  while (queuedUpdates.length !== 0) {
5686
- const [nextUpdateFn, options] = queuedUpdates.shift();
5687
- let onUpdate;
5688
- let tag;
5721
+ const queuedUpdate = queuedUpdates.shift();
5689
5722
 
5690
- if (options !== undefined) {
5691
- onUpdate = options.onUpdate;
5692
- tag = options.tag;
5723
+ if (queuedUpdate) {
5724
+ const [nextUpdateFn, options] = queuedUpdate;
5725
+ let onUpdate;
5726
+ let tag;
5693
5727
 
5694
- if (options.skipTransforms) {
5695
- skipTransforms = true;
5696
- }
5728
+ if (options !== undefined) {
5729
+ onUpdate = options.onUpdate;
5730
+ tag = options.tag;
5697
5731
 
5698
- if (onUpdate) {
5699
- editor._deferred.push(onUpdate);
5700
- }
5732
+ if (options.skipTransforms) {
5733
+ skipTransforms = true;
5734
+ }
5735
+
5736
+ if (onUpdate) {
5737
+ editor._deferred.push(onUpdate);
5738
+ }
5701
5739
 
5702
- if (tag) {
5703
- editor._updateTags.add(tag);
5740
+ if (tag) {
5741
+ editor._updateTags.add(tag);
5742
+ }
5704
5743
  }
5705
- }
5706
5744
 
5707
- nextUpdateFn();
5745
+ nextUpdateFn();
5746
+ }
5708
5747
  }
5709
5748
 
5710
5749
  return skipTransforms;
@@ -5724,7 +5763,7 @@ function beginUpdate(editor, updateFn, options) {
5724
5763
  updateTags.add(tag);
5725
5764
  }
5726
5765
 
5727
- skipTransforms = options.skipTransforms;
5766
+ skipTransforms = options.skipTransforms || false;
5728
5767
  }
5729
5768
 
5730
5769
  if (onUpdate) {
@@ -5796,7 +5835,9 @@ function beginUpdate(editor, updateFn, options) {
5796
5835
  }
5797
5836
  } catch (error) {
5798
5837
  // Report errors
5799
- editor._onError(error); // Restore existing editor state to the DOM
5838
+ if (error instanceof Error) {
5839
+ editor._onError(error);
5840
+ } // Restore existing editor state to the DOM
5800
5841
 
5801
5842
 
5802
5843
  editor._pendingEditorState = currentEditorState;
@@ -6158,7 +6199,6 @@ class LexicalNode {
6158
6199
  const b = node.getParents();
6159
6200
 
6160
6201
  if ($isElementNode(this)) {
6161
- // @ts-expect-error
6162
6202
  a.unshift(this);
6163
6203
  }
6164
6204
 
@@ -6338,7 +6378,7 @@ class LexicalNode {
6338
6378
 
6339
6379
  if (latest === null) {
6340
6380
  {
6341
- throw Error(`Lexical node does not exist in active edtior state. Avoid using the same node references between nested closures from editor.read/editor.update.`);
6381
+ throw Error(`Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editor.read/editor.update.`);
6342
6382
  }
6343
6383
  }
6344
6384
 
@@ -6871,8 +6911,12 @@ class ElementNode extends LexicalNode {
6871
6911
  }
6872
6912
 
6873
6913
  hasFormat(type) {
6874
- const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
6875
- return (this.getFormat() & formatFlag) !== 0;
6914
+ if (type !== '') {
6915
+ const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
6916
+ return (this.getFormat() & formatFlag) !== 0;
6917
+ }
6918
+
6919
+ return false;
6876
6920
  } // Mutators
6877
6921
 
6878
6922
 
@@ -6957,7 +7001,7 @@ class ElementNode extends LexicalNode {
6957
7001
  setFormat(type) {
6958
7002
  errorOnReadOnly();
6959
7003
  const self = this.getWritable();
6960
- self.__format = ELEMENT_TYPE_TO_FORMAT[type] || 0;
7004
+ self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0;
6961
7005
  return this;
6962
7006
  }
6963
7007
 
@@ -7550,20 +7594,24 @@ function setTextContent(nextText, dom, node) {
7550
7594
  dom.textContent = text;
7551
7595
  } else {
7552
7596
  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
7597
 
7558
- if (remove !== 0) {
7559
- // @ts-expect-error
7560
- firstChild.deleteData(index, remove);
7561
- } // @ts-expect-error
7598
+ if (nodeValue !== text) {
7599
+ if (isComposing || IS_FIREFOX) {
7600
+ // We also use the diff composed text for general text in FF to avoid
7601
+ // We also use the diff composed text for general text in FF to avoid
7602
+ // the spellcheck red line from flickering.
7603
+ const [index, remove, insert] = diffComposedText(nodeValue, text);
7562
7604
 
7605
+ if (remove !== 0) {
7606
+ // @ts-expect-error
7607
+ firstChild.deleteData(index, remove);
7608
+ } // @ts-expect-error
7563
7609
 
7564
- firstChild.insertData(index, insert);
7565
- } else {
7566
- firstChild.nodeValue = text;
7610
+
7611
+ firstChild.insertData(index, insert);
7612
+ } else {
7613
+ firstChild.nodeValue = text;
7614
+ }
7567
7615
  }
7568
7616
  }
7569
7617
  }
@@ -7769,6 +7817,10 @@ class TextNode extends LexicalNode {
7769
7817
  conversion: convertBringAttentionToElement,
7770
7818
  priority: 0
7771
7819
  }),
7820
+ code: node => ({
7821
+ conversion: convertTextFormatElement,
7822
+ priority: 0
7823
+ }),
7772
7824
  em: node => ({
7773
7825
  conversion: convertTextFormatElement,
7774
7826
  priority: 0
@@ -7816,19 +7868,21 @@ class TextNode extends LexicalNode {
7816
7868
 
7817
7869
  selectionTransform(prevSelection, nextSelection) {
7818
7870
  return;
7819
- }
7871
+ } // TODO 0.4 This should just be a `string`.
7872
+
7820
7873
 
7821
7874
  setFormat(format) {
7822
7875
  errorOnReadOnly();
7823
7876
  const self = this.getWritable();
7824
- self.__format = format;
7877
+ self.__format = typeof format === 'string' ? TEXT_TYPE_TO_FORMAT[format] : format;
7825
7878
  return self;
7826
- }
7879
+ } // TODO 0.4 This should just be a `string`.
7880
+
7827
7881
 
7828
7882
  setDetail(detail) {
7829
7883
  errorOnReadOnly();
7830
7884
  const self = this.getWritable();
7831
- self.__detail = detail;
7885
+ self.__detail = typeof detail === 'string' ? DETAIL_TYPE_TO_DETAIL[detail] : detail;
7832
7886
  return self;
7833
7887
  }
7834
7888
 
@@ -8118,25 +8172,45 @@ function convertSpanElement(domNode) {
8118
8172
  // domNode is a <span> since we matched it by nodeName
8119
8173
  const span = domNode; // Google Docs uses span tags + font-weight for bold text
8120
8174
 
8121
- const hasBoldFontWeight = span.style.fontWeight === '700'; // Google Docs uses span tags + text-decoration for strikethrough text
8175
+ const hasBoldFontWeight = span.style.fontWeight === '700'; // Google Docs uses span tags + text-decoration: line-through for strikethrough text
8122
8176
 
8123
8177
  const hasLinethroughTextDecoration = span.style.textDecoration === 'line-through'; // Google Docs uses span tags + font-style for italic text
8124
8178
 
8125
- const hasItalicFontStyle = span.style.fontStyle === 'italic';
8179
+ const hasItalicFontStyle = span.style.fontStyle === 'italic'; // Google Docs uses span tags + text-decoration: underline for underline text
8180
+
8181
+ const hasUnderlineTextDecoration = span.style.textDecoration === 'underline'; // Google Docs uses span tags + vertical-align to specify subscript and superscript
8182
+
8183
+ const verticalAlign = span.style.verticalAlign;
8126
8184
  return {
8127
8185
  forChild: lexicalNode => {
8128
- if ($isTextNode(lexicalNode) && hasBoldFontWeight) {
8186
+ if (!$isTextNode(lexicalNode)) {
8187
+ return lexicalNode;
8188
+ }
8189
+
8190
+ if (hasBoldFontWeight) {
8129
8191
  lexicalNode.toggleFormat('bold');
8130
8192
  }
8131
8193
 
8132
- if ($isTextNode(lexicalNode) && hasLinethroughTextDecoration) {
8194
+ if (hasLinethroughTextDecoration) {
8133
8195
  lexicalNode.toggleFormat('strikethrough');
8134
8196
  }
8135
8197
 
8136
- if ($isTextNode(lexicalNode) && hasItalicFontStyle) {
8198
+ if (hasItalicFontStyle) {
8137
8199
  lexicalNode.toggleFormat('italic');
8138
8200
  }
8139
8201
 
8202
+ if (hasUnderlineTextDecoration) {
8203
+ lexicalNode.toggleFormat('underline');
8204
+ }
8205
+
8206
+ if (verticalAlign === 'sub') {
8207
+ lexicalNode.toggleFormat('subscript');
8208
+ }
8209
+
8210
+ if (verticalAlign === 'super') {
8211
+ lexicalNode.toggleFormat('superscript');
8212
+ }
8213
+
8140
8214
  return lexicalNode;
8141
8215
  },
8142
8216
  node: null
@@ -8162,9 +8236,9 @@ function convertBringAttentionToElement(domNode) {
8162
8236
 
8163
8237
  function convertTextDOMNode(domNode) {
8164
8238
  const {
8165
- parentElement,
8166
- textContent
8239
+ parentElement
8167
8240
  } = domNode;
8241
+ const textContent = domNode.textContent || '';
8168
8242
  const textContentTrim = textContent.trim();
8169
8243
  const isPre = parentElement != null && parentElement.tagName.toLowerCase() === 'pre';
8170
8244
 
@@ -8180,6 +8254,7 @@ function convertTextDOMNode(domNode) {
8180
8254
  }
8181
8255
 
8182
8256
  const nodeNameToTextFormat = {
8257
+ code: 'code',
8183
8258
  em: 'italic',
8184
8259
  i: 'italic',
8185
8260
  strong: 'bold',
@@ -8261,10 +8336,8 @@ class ParagraphNode extends ElementNode {
8261
8336
  element
8262
8337
  } = super.exportDOM(editor);
8263
8338
 
8264
- if (element) {
8265
- if (this.getTextContentSize() === 0) {
8266
- element.append(document.createElement('br'));
8267
- }
8339
+ if (element && this.isEmpty()) {
8340
+ element.append(document.createElement('br'));
8268
8341
  }
8269
8342
 
8270
8343
  return {
@@ -8388,9 +8461,7 @@ function initializeConversionCache(nodes) {
8388
8461
  const conversionCache = new Map();
8389
8462
  const handledConversions = new Set();
8390
8463
  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;
8464
+ const importDOM = node.klass.importDOM != null ? node.klass.importDOM.bind(node.klass) : null;
8394
8465
 
8395
8466
  if (importDOM == null || handledConversions.has(importDOM)) {
8396
8467
  return;
@@ -8458,7 +8529,7 @@ function createEditor(editorConfig) {
8458
8529
  if (proto instanceof DecoratorNode) {
8459
8530
  // eslint-disable-next-line no-prototype-builtins
8460
8531
  if (!proto.hasOwnProperty('decorate')) {
8461
- console.warn(`${this.constructor.name} must implement "decorate" method`);
8532
+ console.warn(`${proto.constructor.name} must implement "decorate" method`);
8462
8533
  }
8463
8534
  }
8464
8535
 
@@ -8472,8 +8543,7 @@ function createEditor(editorConfig) {
8472
8543
  console.warn(`${name} should implement "exportJSON" method to ensure JSON and default HTML serialization works as expected`);
8473
8544
  }
8474
8545
  }
8475
- } // @ts-expect-error TODO Replace Class utility type with InstanceType
8476
-
8546
+ }
8477
8547
 
8478
8548
  const type = klass.getType();
8479
8549
  registeredNodes.set(type, {
@@ -8487,7 +8557,7 @@ function createEditor(editorConfig) {
8487
8557
  disableEvents,
8488
8558
  namespace,
8489
8559
  theme
8490
- }, onError, initializeConversionCache(registeredNodes), isReadOnly);
8560
+ }, onError ? onError : console.error, initializeConversionCache(registeredNodes), isReadOnly);
8491
8561
 
8492
8562
  if (initialEditorState !== undefined) {
8493
8563
  editor._pendingEditorState = initialEditorState;
@@ -8626,7 +8696,6 @@ class LexicalEditor {
8626
8696
  }
8627
8697
 
8628
8698
  registerMutationListener(klass, listener) {
8629
- // @ts-expect-error TODO Replace Class utility type with InstanceType
8630
8699
  const registeredNode = this._nodes.get(klass.getType());
8631
8700
 
8632
8701
  if (registeredNode === undefined) {
@@ -8643,7 +8712,6 @@ class LexicalEditor {
8643
8712
  }
8644
8713
 
8645
8714
  registerNodeTransform(klass, listener) {
8646
- // @ts-expect-error TODO Replace Class utility type with InstanceType
8647
8715
  const type = klass.getType();
8648
8716
 
8649
8717
  const registeredNode = this._nodes.get(type);
@@ -8664,8 +8732,7 @@ class LexicalEditor {
8664
8732
 
8665
8733
  hasNodes(nodes) {
8666
8734
  for (let i = 0; i < nodes.length; i++) {
8667
- const klass = nodes[i]; // @ts-expect-error
8668
-
8735
+ const klass = nodes[i];
8669
8736
  const type = klass.getType();
8670
8737
 
8671
8738
  if (!this._nodes.has(type)) {
@@ -8780,7 +8847,7 @@ class LexicalEditor {
8780
8847
  updateEditor(this, updateFn, options);
8781
8848
  }
8782
8849
 
8783
- focus(callbackFn) {
8850
+ focus(callbackFn, options = {}) {
8784
8851
  const rootElement = this._rootElement;
8785
8852
 
8786
8853
  if (rootElement !== null) {
@@ -8794,7 +8861,11 @@ class LexicalEditor {
8794
8861
  // Marking the selection dirty will force the selection back to it
8795
8862
  selection.dirty = true;
8796
8863
  } else if (root.getChildrenSize() !== 0) {
8797
- root.selectEnd();
8864
+ if (options.defaultSelection === 'rootStart') {
8865
+ root.selectStart();
8866
+ } else {
8867
+ root.selectEnd();
8868
+ }
8798
8869
  }
8799
8870
  }, {
8800
8871
  onUpdate: () => {
@@ -8827,8 +8898,10 @@ class LexicalEditor {
8827
8898
  }
8828
8899
 
8829
8900
  setReadOnly(readOnly) {
8830
- this._readOnly = readOnly;
8831
- triggerListeners('readonly', this, true, readOnly);
8901
+ if (this._readOnly !== readOnly) {
8902
+ this._readOnly = readOnly;
8903
+ triggerListeners('readonly', this, true, readOnly);
8904
+ }
8832
8905
  }
8833
8906
 
8834
8907
  toJSON() {
@@ -8846,7 +8919,7 @@ class LexicalEditor {
8846
8919
  * LICENSE file in the root directory of this source tree.
8847
8920
  *
8848
8921
  */
8849
- const VERSION = '0.3.5';
8922
+ const VERSION = '0.3.8';
8850
8923
 
8851
8924
  /**
8852
8925
  * Copyright (c) Meta Platforms, Inc. and affiliates.