lexical 0.3.3 → 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
@@ -6,6 +6,58 @@
6
6
  */
7
7
  'use strict';
8
8
 
9
+ /**
10
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
11
+ *
12
+ * This source code is licensed under the MIT license found in the
13
+ * LICENSE file in the root directory of this source tree.
14
+ *
15
+ */
16
+ function createCommand() {
17
+ return {};
18
+ }
19
+ const SELECTION_CHANGE_COMMAND = createCommand();
20
+ const CLICK_COMMAND = createCommand();
21
+ const DELETE_CHARACTER_COMMAND = createCommand();
22
+ const INSERT_LINE_BREAK_COMMAND = createCommand();
23
+ const INSERT_PARAGRAPH_COMMAND = createCommand();
24
+ const CONTROLLED_TEXT_INSERTION_COMMAND = createCommand();
25
+ const PASTE_COMMAND = createCommand();
26
+ const REMOVE_TEXT_COMMAND = createCommand();
27
+ const DELETE_WORD_COMMAND = createCommand();
28
+ const DELETE_LINE_COMMAND = createCommand();
29
+ const FORMAT_TEXT_COMMAND = createCommand();
30
+ const UNDO_COMMAND = createCommand();
31
+ const REDO_COMMAND = createCommand();
32
+ const KEY_ARROW_RIGHT_COMMAND = createCommand();
33
+ const MOVE_TO_END = createCommand();
34
+ const KEY_ARROW_LEFT_COMMAND = createCommand();
35
+ const MOVE_TO_START = createCommand();
36
+ const KEY_ARROW_UP_COMMAND = createCommand();
37
+ const KEY_ARROW_DOWN_COMMAND = createCommand();
38
+ const KEY_ENTER_COMMAND = createCommand();
39
+ const KEY_SPACE_COMMAND = createCommand();
40
+ const KEY_BACKSPACE_COMMAND = createCommand();
41
+ const KEY_ESCAPE_COMMAND = createCommand();
42
+ const KEY_DELETE_COMMAND = createCommand();
43
+ const KEY_TAB_COMMAND = createCommand();
44
+ const INDENT_CONTENT_COMMAND = createCommand();
45
+ const OUTDENT_CONTENT_COMMAND = createCommand();
46
+ const DROP_COMMAND = createCommand();
47
+ const FORMAT_ELEMENT_COMMAND = createCommand();
48
+ const DRAGSTART_COMMAND = createCommand();
49
+ const DRAGOVER_COMMAND = createCommand();
50
+ const DRAGEND_COMMAND = createCommand();
51
+ const COPY_COMMAND = createCommand();
52
+ const CUT_COMMAND = createCommand();
53
+ const CLEAR_EDITOR_COMMAND = createCommand();
54
+ const CLEAR_HISTORY_COMMAND = createCommand();
55
+ const CAN_REDO_COMMAND = createCommand();
56
+ const CAN_UNDO_COMMAND = createCommand();
57
+ const FOCUS_COMMAND = createCommand();
58
+ const BLUR_COMMAND = createCommand();
59
+ const KEY_MODIFIER_COMMAND = createCommand();
60
+
9
61
  /**
10
62
  * Copyright (c) Meta Platforms, Inc. and affiliates.
11
63
  *
@@ -96,6 +148,10 @@ const TEXT_TYPE_TO_FORMAT = {
96
148
  superscript: IS_SUPERSCRIPT,
97
149
  underline: IS_UNDERLINE
98
150
  };
151
+ const DETAIL_TYPE_TO_DETAIL = {
152
+ directionless: IS_DIRECTIONLESS,
153
+ unmergeable: IS_UNMERGEABLE
154
+ };
99
155
  const ELEMENT_TYPE_TO_FORMAT = {
100
156
  center: IS_ALIGN_CENTER,
101
157
  justify: IS_ALIGN_JUSTIFY,
@@ -148,7 +204,8 @@ function initTextEntryListener() {
148
204
 
149
205
  function isManagedLineBreak(dom, target, editor) {
150
206
  return (// @ts-expect-error: internal field
151
- 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
152
209
  );
153
210
  }
154
211
 
@@ -170,7 +227,22 @@ function handleTextMutation(target, node, editor) {
170
227
  }
171
228
 
172
229
  const text = target.nodeValue;
173
- $updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
230
+
231
+ if (text !== null) {
232
+ $updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
233
+ }
234
+ }
235
+
236
+ function shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode) {
237
+ if ($isRangeSelection(selection)) {
238
+ const anchorNode = selection.anchor.getNode();
239
+
240
+ if (anchorNode.is(targetNode) && selection.format !== anchorNode.getFormat()) {
241
+ return false;
242
+ }
243
+ }
244
+
245
+ return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();
174
246
  }
175
247
 
176
248
  function $flushMutations$1(editor, mutations, observer) {
@@ -179,6 +251,7 @@ function $flushMutations$1(editor, mutations, observer) {
179
251
 
180
252
  try {
181
253
  updateEditor(editor, () => {
254
+ const selection = $getSelection() || getLastSelection(editor);
182
255
  const badDOMTargets = new Map();
183
256
  const rootElement = editor.getRootElement(); // We use the current edtior state, as that reflects what is
184
257
  // actually "on screen".
@@ -200,7 +273,7 @@ function $flushMutations$1(editor, mutations, observer) {
200
273
  if (type === 'characterData') {
201
274
  // Text mutations are deferred and passed to mutation listeners to be
202
275
  // processed outside of the Lexical engine.
203
- if (shouldFlushTextMutations && targetDOM.nodeType === DOM_TEXT_TYPE && $isTextNode(targetNode) && targetNode.isAttached()) {
276
+ if (shouldFlushTextMutations && $isTextNode(targetNode) && shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode)) {
204
277
  handleTextMutation( // nodeType === DOM_TEXT_TYPE is a Text DOM node
205
278
  targetDOM, targetNode);
206
279
  }
@@ -315,8 +388,6 @@ function $flushMutations$1(editor, mutations, observer) {
315
388
  observer.takeRecords();
316
389
  }
317
390
 
318
- const selection = $getSelection() || getLastSelection(editor);
319
-
320
391
  if (selection !== null) {
321
392
  if (shouldRevertSelection) {
322
393
  selection.dirty = true;
@@ -598,8 +669,9 @@ function $getNodeByKey(key, _editorState) {
598
669
  return node;
599
670
  }
600
671
  function getNodeFromDOMNode(dom, editorState) {
601
- const editor = getActiveEditor();
602
- 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}`];
603
675
 
604
676
  if (key !== undefined) {
605
677
  return $getNodeByKey(key, editorState);
@@ -664,7 +736,6 @@ function $setSelection(selection) {
664
736
  const editorState = getActiveEditorState();
665
737
 
666
738
  if (selection !== null) {
667
- // @ts-ignore
668
739
  {
669
740
  if (Object.isFrozen(selection)) {
670
741
  {
@@ -709,7 +780,8 @@ dom, editor) {
709
780
  let node = dom;
710
781
 
711
782
  while (node != null) {
712
- const key = node['__lexicalKey_' + editor._key];
783
+ // @ts-ignore We intentionally add this to the Node.
784
+ const key = node[`__lexicalKey_${editor._key}`];
713
785
 
714
786
  if (key !== undefined) {
715
787
  return key;
@@ -765,7 +837,9 @@ function $updateSelectedTextFromDOM(editor, isCompositionEnd, data) {
765
837
  focusOffset = offset;
766
838
  }
767
839
 
768
- $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, isCompositionEnd);
840
+ if (textContent !== null) {
841
+ $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, isCompositionEnd);
842
+ }
769
843
  }
770
844
  }
771
845
  }
@@ -835,6 +909,11 @@ function $updateTextNodeFromDOMContent(textNode, textContent, anchorOffset, focu
835
909
  }
836
910
  }
837
911
 
912
+ function $previousSiblingDoesNotAcceptText(node) {
913
+ const previousSibling = node.getPreviousSibling();
914
+ return ($isTextNode(previousSibling) || $isElementNode(previousSibling) && previousSibling.isInline()) && !previousSibling.canInsertTextAfter();
915
+ }
916
+
838
917
  function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
839
918
  if (node.isSegmented()) {
840
919
  return true;
@@ -847,7 +926,7 @@ function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
847
926
  const offset = selection.anchor.offset;
848
927
  const parent = node.getParentOrThrow();
849
928
  const isToken = node.isToken();
850
- const shouldInsertTextBefore = offset === 0 && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken);
929
+ const shouldInsertTextBefore = offset === 0 && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken || $previousSiblingDoesNotAcceptText(node));
851
930
  const shouldInsertTextAfter = node.getTextContentSize() === offset && (!node.canInsertTextBefore() || !parent.canInsertTextBefore() || isToken);
852
931
  return shouldInsertTextBefore || shouldInsertTextAfter;
853
932
  } // This function is used to determine if Lexical should attempt to override
@@ -961,6 +1040,8 @@ function isCopy(keyCode, shiftKey, metaKey, ctrlKey) {
961
1040
  if (keyCode === 67) {
962
1041
  return IS_APPLE ? metaKey : ctrlKey;
963
1042
  }
1043
+
1044
+ return false;
964
1045
  }
965
1046
  function isCut(keyCode, shiftKey, metaKey, ctrlKey) {
966
1047
  if (shiftKey) {
@@ -970,6 +1051,8 @@ function isCut(keyCode, shiftKey, metaKey, ctrlKey) {
970
1051
  if (keyCode === 88) {
971
1052
  return IS_APPLE ? metaKey : ctrlKey;
972
1053
  }
1054
+
1055
+ return false;
973
1056
  }
974
1057
 
975
1058
  function isArrowLeft(keyCode) {
@@ -1075,8 +1158,7 @@ function setMutatedNode(mutatedNodes, registeredNodes, mutationListeners, node,
1075
1158
  }
1076
1159
  function $nodesOfType(klass) {
1077
1160
  const editorState = getActiveEditorState();
1078
- const readOnly = editorState._readOnly; // @ts-expect-error TODO Replace Class utility type with InstanceType
1079
-
1161
+ const readOnly = editorState._readOnly;
1080
1162
  const klassType = klass.getType();
1081
1163
  const nodes = editorState._nodeMap;
1082
1164
  const nodesOfType = [];
@@ -1474,7 +1556,7 @@ function createNode(key, parentDOM, insertDOM) {
1474
1556
  const text = node.getTextContent();
1475
1557
 
1476
1558
  if ($isDecoratorNode(node)) {
1477
- const decorator = node.decorate(activeEditor$1);
1559
+ const decorator = node.decorate(activeEditor$1, activeEditorConfig);
1478
1560
 
1479
1561
  if (decorator !== null) {
1480
1562
  reconcileDecorator(key, decorator);
@@ -1514,8 +1596,7 @@ function createNode(key, parentDOM, insertDOM) {
1514
1596
  parentDOM.appendChild(dom);
1515
1597
  }
1516
1598
  }
1517
- } // @ts-ignore
1518
-
1599
+ }
1519
1600
 
1520
1601
  {
1521
1602
  // Freeze the node in DEV to prevent accidental mutations
@@ -1597,10 +1678,9 @@ function reconcileBlockDirection(element, dom) {
1597
1678
 
1598
1679
  if (previousDirectionTheme !== undefined) {
1599
1680
  if (typeof previousDirectionTheme === 'string') {
1600
- const classNamesArr = previousDirectionTheme.split(' '); // @ts-expect-error: intentional
1601
-
1681
+ const classNamesArr = previousDirectionTheme.split(' ');
1602
1682
  previousDirectionTheme = theme[previousDirection] = classNamesArr;
1603
- } // @ts-expect-error: intentional
1683
+ } // @ts-ignore: intentional
1604
1684
 
1605
1685
 
1606
1686
  classList.remove(...previousDirectionTheme);
@@ -1618,7 +1698,9 @@ function reconcileBlockDirection(element, dom) {
1618
1698
  nextDirectionTheme = theme[direction] = classNamesArr;
1619
1699
  }
1620
1700
 
1621
- classList.add(...nextDirectionTheme);
1701
+ if (nextDirectionTheme !== undefined) {
1702
+ classList.add(...nextDirectionTheme);
1703
+ }
1622
1704
  } // Update direction
1623
1705
 
1624
1706
 
@@ -1790,7 +1872,7 @@ function reconcileNode(key, parentDOM) {
1790
1872
  const text = nextNode.getTextContent();
1791
1873
 
1792
1874
  if ($isDecoratorNode(nextNode)) {
1793
- const decorator = nextNode.decorate(activeEditor$1);
1875
+ const decorator = nextNode.decorate(activeEditor$1, activeEditorConfig);
1794
1876
 
1795
1877
  if (decorator !== null) {
1796
1878
  reconcileDecorator(key, decorator);
@@ -1811,8 +1893,7 @@ function reconcileNode(key, parentDOM) {
1811
1893
  // Cache the latest text content.
1812
1894
  nextNode = nextNode.getWritable();
1813
1895
  nextNode.__cachedText = editorTextContent;
1814
- } // @ts-ignore
1815
-
1896
+ }
1816
1897
 
1817
1898
  {
1818
1899
  // Freeze the node in DEV to prevent accidental mutations
@@ -1941,20 +2022,30 @@ function reconcileRoot(prevEditorState, nextEditorState, editor, dirtyType, dirt
1941
2022
  // so instead we make it seem that these values are always set.
1942
2023
  // We also want to make sure we clear them down, otherwise we
1943
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
1944
2042
 
1945
- activeEditor$1 = undefined;
1946
- activeEditorNodes = undefined;
1947
- activeDirtyElements = undefined;
1948
- activeDirtyLeaves = undefined;
1949
- activePrevNodeMap = undefined;
1950
- activeNextNodeMap = undefined;
1951
- activeEditorConfig = undefined;
1952
- activePrevKeyToDOMMap = undefined;
1953
2043
  mutatedNodes = undefined;
1954
2044
  return currentMutatedNodes;
1955
2045
  }
1956
2046
  function storeDOMWithKey(key, dom, editor) {
1957
- const keyToDOMMap = editor._keyToDOMMap;
2047
+ const keyToDOMMap = editor._keyToDOMMap; // @ts-ignore We intentionally add this to the Node.
2048
+
1958
2049
  dom['__lexicalKey_' + editor._key] = key;
1959
2050
  keyToDOMMap.set(key, dom);
1960
2051
  }
@@ -1971,58 +2062,6 @@ function getPrevElementByKeyOrThrow(key) {
1971
2062
  return element;
1972
2063
  }
1973
2064
 
1974
- /**
1975
- * Copyright (c) Meta Platforms, Inc. and affiliates.
1976
- *
1977
- * This source code is licensed under the MIT license found in the
1978
- * LICENSE file in the root directory of this source tree.
1979
- *
1980
- */
1981
- function createCommand() {
1982
- return {};
1983
- }
1984
- const SELECTION_CHANGE_COMMAND = createCommand();
1985
- const CLICK_COMMAND = createCommand();
1986
- const DELETE_CHARACTER_COMMAND = createCommand();
1987
- const INSERT_LINE_BREAK_COMMAND = createCommand();
1988
- const INSERT_PARAGRAPH_COMMAND = createCommand();
1989
- const CONTROLLED_TEXT_INSERTION_COMMAND = createCommand();
1990
- const PASTE_COMMAND = createCommand();
1991
- const REMOVE_TEXT_COMMAND = createCommand();
1992
- const DELETE_WORD_COMMAND = createCommand();
1993
- const DELETE_LINE_COMMAND = createCommand();
1994
- const FORMAT_TEXT_COMMAND = createCommand();
1995
- const UNDO_COMMAND = createCommand();
1996
- const REDO_COMMAND = createCommand();
1997
- const KEY_ARROW_RIGHT_COMMAND = createCommand();
1998
- const MOVE_TO_END = createCommand();
1999
- const KEY_ARROW_LEFT_COMMAND = createCommand();
2000
- const MOVE_TO_START = createCommand();
2001
- const KEY_ARROW_UP_COMMAND = createCommand();
2002
- const KEY_ARROW_DOWN_COMMAND = createCommand();
2003
- const KEY_ENTER_COMMAND = createCommand();
2004
- const KEY_SPACE_COMMAND = createCommand();
2005
- const KEY_BACKSPACE_COMMAND = createCommand();
2006
- const KEY_ESCAPE_COMMAND = createCommand();
2007
- const KEY_DELETE_COMMAND = createCommand();
2008
- const KEY_TAB_COMMAND = createCommand();
2009
- const INDENT_CONTENT_COMMAND = createCommand();
2010
- const OUTDENT_CONTENT_COMMAND = createCommand();
2011
- const DROP_COMMAND = createCommand();
2012
- const FORMAT_ELEMENT_COMMAND = createCommand();
2013
- const DRAGSTART_COMMAND = createCommand();
2014
- const DRAGOVER_COMMAND = createCommand();
2015
- const DRAGEND_COMMAND = createCommand();
2016
- const COPY_COMMAND = createCommand();
2017
- const CUT_COMMAND = createCommand();
2018
- const CLEAR_EDITOR_COMMAND = createCommand();
2019
- const CLEAR_HISTORY_COMMAND = createCommand();
2020
- const CAN_REDO_COMMAND = createCommand();
2021
- const CAN_UNDO_COMMAND = createCommand();
2022
- const FOCUS_COMMAND = createCommand();
2023
- const BLUR_COMMAND = createCommand();
2024
- const KEY_MODIFIER_COMMAND = createCommand();
2025
-
2026
2065
  /**
2027
2066
  * Copyright (c) Meta Platforms, Inc. and affiliates.
2028
2067
  *
@@ -2035,10 +2074,11 @@ const ANDROID_COMPOSITION_LATENCY = 30;
2035
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]];
2036
2075
 
2037
2076
  if (CAN_USE_BEFORE_INPUT) {
2038
- rootElementEvents.push(['beforeinput', onBeforeInput]);
2077
+ rootElementEvents.push(['beforeinput', (event, editor) => onBeforeInput(event, editor)]);
2039
2078
  }
2040
2079
 
2041
2080
  let lastKeyDownTimeStamp = 0;
2081
+ let lastKeyCode = 0;
2042
2082
  let rootElementsRegistered = 0;
2043
2083
  let isSelectionChangeFromDOMUpdate = false;
2044
2084
  let isInsertLineBreak = false;
@@ -2046,7 +2086,7 @@ let isFirefoxEndingComposition = false;
2046
2086
  let collapsedSelectionFormat = [0, 0, 'root', 0];
2047
2087
 
2048
2088
  function shouldSkipSelectionChange(domNode, offset) {
2049
- 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;
2050
2090
  }
2051
2091
 
2052
2092
  function onSelectionChange(domSelection, editor, isActive) {
@@ -2148,15 +2188,19 @@ function onClick(event, editor) {
2148
2188
  const anchor = selection.anchor;
2149
2189
  const anchorNode = anchor.getNode();
2150
2190
 
2151
- 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)) {
2152
2192
  domSelection.removeAllRanges();
2153
2193
  selection.dirty = true;
2154
2194
  }
2155
- } else if ($isNodeSelection(selection) && domSelection.isCollapsed) {
2195
+ } else if (domSelection && $isNodeSelection(selection) && domSelection.isCollapsed) {
2156
2196
  const domAnchor = domSelection.anchorNode; // If the user is attempting to click selection back onto text, then
2157
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
2158
2200
 
2159
- if (domAnchor !== null && domAnchor.nodeType === DOM_TEXT_TYPE) {
2201
+ const allowedNodeType = [DOM_ELEMENT_TYPE, DOM_TEXT_TYPE];
2202
+
2203
+ if (domAnchor !== null && allowedNodeType.includes(domAnchor.nodeType)) {
2160
2204
  const newSelection = internalCreateRangeSelection(lastSelection, domSelection, editor);
2161
2205
  $setSelection(newSelection);
2162
2206
  }
@@ -2180,6 +2224,10 @@ function $canRemoveText(anchorNode, focusNode) {
2180
2224
  return anchorNode !== focusNode || $isElementNode(anchorNode) || $isElementNode(focusNode) || !$isTokenOrInert(anchorNode) || !$isTokenOrInert(focusNode);
2181
2225
  }
2182
2226
 
2227
+ function isPossiblyAndroidKeyPress(timeStamp) {
2228
+ return lastKeyCode === 229 && timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY;
2229
+ }
2230
+
2183
2231
  function onBeforeInput(event, editor) {
2184
2232
  const inputType = event.inputType; // We let the browser do its own thing for composition.
2185
2233
 
@@ -2196,7 +2244,9 @@ function onBeforeInput(event, editor) {
2196
2244
  // during composition and see if the previous text contains
2197
2245
  // part of the composed text to work out the actual text that
2198
2246
  // we need to insert.
2199
- const composedText = event.data;
2247
+ const composedText = event.data; // TODO: evaluate if this is Android only. It doesn't always seem
2248
+ // to have any real impact, so could probably be refactored or removed
2249
+ // for an alternative approach.
2200
2250
 
2201
2251
  if (composedText) {
2202
2252
  updateEditor(editor, () => {
@@ -2240,20 +2290,32 @@ function onBeforeInput(event, editor) {
2240
2290
  }
2241
2291
 
2242
2292
  $setSelection(prevSelection.clone());
2243
- } // Used for Android
2293
+ }
2244
2294
 
2295
+ if ($isRangeSelection(selection)) {
2296
+ // Used for handling backspace in Android.
2297
+ if (isPossiblyAndroidKeyPress(event.timeStamp) && selection.anchor.key === selection.focus.key) {
2298
+ $setCompositionKey(null);
2299
+ lastKeyDownTimeStamp = 0; // Fixes an Android bug where selection flickers when backspacing
2245
2300
 
2246
- $setCompositionKey(null);
2247
- event.preventDefault();
2248
- lastKeyDownTimeStamp = 0;
2249
- dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true); // Fixes an Android bug where selection flickers when backspacing
2301
+ setTimeout(() => {
2302
+ updateEditor(editor, () => {
2303
+ $setCompositionKey(null);
2304
+ });
2305
+ }, ANDROID_COMPOSITION_LATENCY);
2250
2306
 
2251
- setTimeout(() => {
2252
- updateEditor(editor, () => {
2253
- $setCompositionKey(null);
2254
- });
2255
- }, ANDROID_COMPOSITION_LATENCY);
2256
- return;
2307
+ if ($isRangeSelection(selection)) {
2308
+ const anchorNode = selection.anchor.getNode();
2309
+ anchorNode.markDirty();
2310
+ selection.format = anchorNode.getFormat();
2311
+ }
2312
+ } else {
2313
+ event.preventDefault();
2314
+ dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);
2315
+ }
2316
+
2317
+ return;
2318
+ }
2257
2319
  }
2258
2320
 
2259
2321
  if (!$isRangeSelection(selection)) {
@@ -2449,7 +2511,14 @@ function onInput(event, editor) {
2449
2511
  isFirefoxEndingComposition = false;
2450
2512
  }
2451
2513
 
2452
- 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
+
2453
2522
 
2454
2523
  if (!IS_SAFARI && !IS_IOS && editor.isComposing()) {
2455
2524
  lastKeyDownTimeStamp = 0;
@@ -2459,7 +2528,7 @@ function onInput(event, editor) {
2459
2528
  $updateSelectedTextFromDOM(editor, false); // onInput always fires after onCompositionEnd for FF.
2460
2529
 
2461
2530
  if (isFirefoxEndingComposition) {
2462
- onCompositionEndImpl(editor, data);
2531
+ onCompositionEndImpl(editor, data || undefined);
2463
2532
  isFirefoxEndingComposition = false;
2464
2533
  }
2465
2534
  } // Also flush any other mutations that might have occurred
@@ -2505,7 +2574,7 @@ function onCompositionEndImpl(editor, data) {
2505
2574
  const node = $getNodeByKey(compositionKey);
2506
2575
  const textNode = getDOMTextNode(editor.getElementByKey(compositionKey));
2507
2576
 
2508
- if (textNode !== null && $isTextNode(node)) {
2577
+ if (textNode !== null && textNode.nodeValue !== null && $isTextNode(node)) {
2509
2578
  $updateTextNodeFromDOMContent(node, textNode.nodeValue, null, null, true);
2510
2579
  }
2511
2580
 
@@ -2548,6 +2617,7 @@ function onCompositionEnd(event, editor) {
2548
2617
 
2549
2618
  function onKeyDown(event, editor) {
2550
2619
  lastKeyDownTimeStamp = event.timeStamp;
2620
+ lastKeyCode = event.keyCode;
2551
2621
 
2552
2622
  if (editor.isComposing()) {
2553
2623
  return;
@@ -2668,6 +2738,11 @@ const activeNestedEditorsMap = new Map();
2668
2738
 
2669
2739
  function onDocumentSelectionChange(event) {
2670
2740
  const selection = getDOMSelection();
2741
+
2742
+ if (!selection) {
2743
+ return;
2744
+ }
2745
+
2671
2746
  const nextActiveEditor = getNearestEditorFromDOMNode(selection.anchorNode);
2672
2747
 
2673
2748
  if (nextActiveEditor === null) {
@@ -2767,7 +2842,7 @@ function removeRootElementEvents(rootElement) {
2767
2842
 
2768
2843
  const editor = rootElement.__lexicalEditor;
2769
2844
 
2770
- if (editor !== null || editor !== undefined) {
2845
+ if (editor !== null && editor !== undefined) {
2771
2846
  cleanActiveNestedEditorsMap(editor); // @ts-expect-error: internal field
2772
2847
 
2773
2848
  rootElement.__lexicalEditor = null;
@@ -2813,7 +2888,6 @@ function markCollapsedSelectionFormat(format, offset, key, timeStamp) {
2813
2888
  * LICENSE file in the root directory of this source tree.
2814
2889
  *
2815
2890
  */
2816
-
2817
2891
  class Point {
2818
2892
  constructor(key, offset, type) {
2819
2893
  this._selection = null;
@@ -3445,11 +3519,12 @@ class RangeSelection {
3445
3519
  const lastIndex = selectedNodesLength - 1;
3446
3520
  let lastNode = selectedNodes[lastIndex];
3447
3521
 
3448
- 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)) {
3449
3523
  let nextSibling = firstNode.getNextSibling();
3450
3524
 
3451
3525
  if (!$isTextNode(nextSibling) || $isTokenOrInertOrSegmented(nextSibling)) {
3452
3526
  nextSibling = $createTextNode();
3527
+ nextSibling.setFormat(format);
3453
3528
 
3454
3529
  if (!firstNodeParent.canInsertTextAfter()) {
3455
3530
  firstNodeParent.insertAfter(nextSibling);
@@ -3465,11 +3540,12 @@ class RangeSelection {
3465
3540
  this.insertText(text);
3466
3541
  return;
3467
3542
  }
3468
- } 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)) {
3469
3544
  let prevSibling = firstNode.getPreviousSibling();
3470
3545
 
3471
3546
  if (!$isTextNode(prevSibling) || $isTokenOrInertOrSegmented(prevSibling)) {
3472
3547
  prevSibling = $createTextNode();
3548
+ prevSibling.setFormat(format);
3473
3549
 
3474
3550
  if (!firstNodeParent.canInsertTextBefore()) {
3475
3551
  firstNodeParent.insertBefore(prevSibling);
@@ -3487,6 +3563,7 @@ class RangeSelection {
3487
3563
  }
3488
3564
  } else if (firstNode.isSegmented() && startOffset !== firstNodeTextLength) {
3489
3565
  const textNode = $createTextNode(firstNode.getTextContent());
3566
+ textNode.setFormat(format);
3490
3567
  firstNode.replace(textNode);
3491
3568
  firstNode = textNode;
3492
3569
  } else if (!this.isCollapsed() && text !== '') {
@@ -3544,10 +3621,14 @@ class RangeSelection {
3544
3621
 
3545
3622
  if (firstNode.getTextContent() === '') {
3546
3623
  firstNode.remove();
3547
- } else if (firstNode.isComposing() && this.anchor.type === 'text') {
3548
- // When composing, we need to adjust the anchor offset so that
3549
- // we correctly replace that right range.
3550
- this.anchor.offset -= text.length;
3624
+ } else if (this.anchor.type === 'text') {
3625
+ if (firstNode.isComposing()) {
3626
+ // When composing, we need to adjust the anchor offset so that
3627
+ // we correctly replace that right range.
3628
+ this.anchor.offset -= text.length;
3629
+ } else {
3630
+ this.format = firstNodeFormat;
3631
+ }
3551
3632
  }
3552
3633
  } else {
3553
3634
  const markedNodeKeysForKeep = new Set([...firstNode.getParentKeys(), ...lastNode.getParentKeys()]); // We have to get the parent elements before the next section,
@@ -3683,10 +3764,19 @@ class RangeSelection {
3683
3764
  formatText(formatType) {
3684
3765
  // TODO I wonder if this methods use selection.extract() instead?
3685
3766
  const selectedNodes = this.getNodes();
3686
- const selectedNodesLength = selectedNodes.length;
3687
- const lastIndex = selectedNodesLength - 1;
3688
- let firstNode = selectedNodes[0];
3689
- 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];
3690
3780
 
3691
3781
  if (this.isCollapsed()) {
3692
3782
  this.toggleFormat(formatType); // When changing format, we should stop composition
@@ -3697,42 +3787,22 @@ class RangeSelection {
3697
3787
 
3698
3788
  const anchor = this.anchor;
3699
3789
  const focus = this.focus;
3790
+ const anchorOffset = anchor.offset;
3700
3791
  const focusOffset = focus.offset;
3701
- let firstNextFormat = 0;
3792
+ let firstNextFormat = firstNode.getFormatFlags(formatType, null);
3702
3793
  let firstNodeTextLength = firstNode.getTextContent().length;
3703
-
3704
- for (let i = 0; i < selectedNodes.length; i++) {
3705
- const selectedNode = selectedNodes[i];
3706
-
3707
- if ($isTextNode(selectedNode)) {
3708
- firstNextFormat = selectedNode.getFormatFlags(formatType, null);
3709
- break;
3710
- }
3711
- }
3712
-
3713
- let anchorOffset = anchor.offset;
3714
- let startOffset;
3715
- let endOffset;
3716
3794
  const isBefore = anchor.isBefore(focus);
3717
- startOffset = isBefore ? anchorOffset : focusOffset;
3718
- 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
3719
3797
  // first node so we don't want to include it in the formatting change.
3720
3798
 
3721
- if (startOffset === firstNode.getTextContentSize()) {
3722
- let nextSibling = firstNode.getNextSibling();
3723
-
3724
- if ($isElementNode(nextSibling) && nextSibling.isInline()) {
3725
- nextSibling = nextSibling.getFirstChild();
3726
- }
3727
-
3728
- if ($isTextNode(nextSibling)) {
3729
- // we basically make the second node the firstNode, changing offsets accordingly
3730
- anchorOffset = 0;
3731
- startOffset = 0;
3732
- firstNode = nextSibling;
3733
- firstNodeTextLength = nextSibling.getTextContent().length;
3734
- firstNextFormat = firstNode.getFormatFlags(formatType, null);
3735
- }
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);
3736
3806
  } // This is the case where we only selected a single node
3737
3807
 
3738
3808
 
@@ -3743,10 +3813,8 @@ class RangeSelection {
3743
3813
  firstNode.select(startOffset, endOffset);
3744
3814
  this.format = firstNextFormat;
3745
3815
  return;
3746
- }
3816
+ } // No actual text is selected, so do nothing.
3747
3817
 
3748
- startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
3749
- endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset; // No actual text is selected, so do nothing.
3750
3818
 
3751
3819
  if (startOffset === endOffset) {
3752
3820
  return;
@@ -3769,7 +3837,9 @@ class RangeSelection {
3769
3837
  } // multiple nodes selected.
3770
3838
 
3771
3839
  } else {
3772
- 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) {
3773
3843
  if (startOffset !== 0) {
3774
3844
  // the entire first node isn't selected, so split it
3775
3845
  [, firstNode] = firstNode.splitText(startOffset);
@@ -3795,11 +3865,12 @@ class RangeSelection {
3795
3865
 
3796
3866
  lastNode.setFormat(lastNextFormat);
3797
3867
  }
3798
- } // deal with all the nodes in between
3868
+ }
3799
3869
 
3870
+ this.format = firstNextFormat | lastNextFormat; // deal with all the nodes in between
3800
3871
 
3801
- for (let i = 1; i < lastIndex; i++) {
3802
- const selectedNode = selectedNodes[i];
3872
+ for (let i = firstIndex + 1; i < lastIndex; i++) {
3873
+ const selectedNode = selectedTextNodes[i];
3803
3874
  const selectedNodeKey = selectedNode.__key;
3804
3875
 
3805
3876
  if ($isTextNode(selectedNode) && selectedNodeKey !== firstNode.__key && selectedNodeKey !== lastNode.__key && !selectedNode.isToken()) {
@@ -3871,7 +3942,7 @@ class RangeSelection {
3871
3942
  siblings.push(...nextSiblings);
3872
3943
  const firstNode = nodes[0];
3873
3944
  let didReplaceOrMerge = false;
3874
- let lastNodeInserted = null; // Time to insert the nodes!
3945
+ let lastNode = null; // Time to insert the nodes!
3875
3946
 
3876
3947
  for (let i = 0; i < nodes.length; i++) {
3877
3948
  const node = nodes[i];
@@ -3934,18 +4005,17 @@ class RangeSelection {
3934
4005
 
3935
4006
  if ($isElementNode(target)) {
3936
4007
  for (let s = 0; s < childrenLength; s++) {
3937
- lastNodeInserted = children[s];
3938
- target.append(lastNodeInserted);
4008
+ target.append(children[s]);
3939
4009
  }
3940
4010
  } else {
3941
4011
  for (let s = childrenLength - 1; s >= 0; s--) {
3942
- lastNodeInserted = children[s];
3943
- target.insertAfter(lastNodeInserted);
4012
+ target.insertAfter(children[s]);
3944
4013
  }
3945
4014
 
3946
4015
  target = target.getParentOrThrow();
3947
4016
  }
3948
4017
 
4018
+ lastNode = children[childrenLength - 1];
3949
4019
  element.remove();
3950
4020
  didReplaceOrMerge = true;
3951
4021
 
@@ -3973,7 +4043,7 @@ class RangeSelection {
3973
4043
  didReplaceOrMerge = false;
3974
4044
 
3975
4045
  if ($isElementNode(target) && !target.isInline()) {
3976
- lastNodeInserted = node;
4046
+ lastNode = node;
3977
4047
 
3978
4048
  if ($isDecoratorNode(node) && node.isTopLevel()) {
3979
4049
  target = target.insertAfter(node);
@@ -4006,8 +4076,8 @@ class RangeSelection {
4006
4076
  target = target.insertAfter(node);
4007
4077
  }
4008
4078
  }
4009
- } else if (!$isElementNode(node) || $isElementNode(node) && node.isInline() || $isDecoratorNode(target) && target.isTopLevel()) {
4010
- lastNodeInserted = node;
4079
+ } else if (!$isElementNode(node) || $isElementNode(node) && node.isInline() || $isDecoratorNode(target) && target.isTopLevel() || $isLineBreakNode(target)) {
4080
+ lastNode = node;
4011
4081
  target = target.insertAfter(node);
4012
4082
  } else {
4013
4083
  target = node.getParentOrThrow(); // Re-try again with the target being the parent
@@ -4036,7 +4106,7 @@ class RangeSelection {
4036
4106
  if ($isElementNode(target)) {
4037
4107
  // If the last node to be inserted was a text node,
4038
4108
  // then we should attempt to move selection to that.
4039
- const lastChild = $isTextNode(lastNodeInserted) ? lastNodeInserted : target.getLastDescendant();
4109
+ const lastChild = $isTextNode(lastNode) ? lastNode : target.getLastDescendant();
4040
4110
 
4041
4111
  if (!selectStart) {
4042
4112
  // Handle moving selection to end for elements
@@ -4050,18 +4120,26 @@ class RangeSelection {
4050
4120
  }
4051
4121
 
4052
4122
  if (siblings.length !== 0) {
4123
+ const originalTarget = target;
4124
+
4053
4125
  for (let i = siblings.length - 1; i >= 0; i--) {
4054
4126
  const sibling = siblings[i];
4055
4127
  const prevParent = sibling.getParentOrThrow();
4056
4128
 
4057
- if ($isElementNode(target) && !$isElementNode(sibling)) {
4058
- 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
+
4059
4136
  target = sibling;
4060
- } else if (!$isElementNode(target) && !$isElementNode(sibling)) {
4137
+ } else if (!$isElementNode(target) && !$isBlockElementNode(sibling)) {
4061
4138
  target.insertBefore(sibling);
4062
4139
  target = sibling;
4063
4140
  } else {
4064
4141
  if ($isElementNode(sibling) && !sibling.canInsertAfter(target)) {
4142
+ // @ts-ignore The clone method does exist on the constructor.
4065
4143
  const prevParentClone = prevParent.constructor.clone(prevParent);
4066
4144
 
4067
4145
  if (!$isElementNode(prevParentClone)) {
@@ -4344,13 +4422,18 @@ class RangeSelection {
4344
4422
  }
4345
4423
  }
4346
4424
 
4347
- 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
4348
4430
  // will be. We then use it to update the Lexical selection accordingly. This
4349
4431
  // is much more reliable than waiting for a beforeinput and using the ranges
4350
4432
  // from getTargetRanges(), and is also better than trying to do it ourselves
4351
4433
  // using Intl.Segmenter or other workarounds that struggle with word segments
4352
4434
  // and line segments (especially with word wrapping and non-Roman languages).
4353
4435
 
4436
+
4354
4437
  $moveNativeSelection(domSelection, alter, isBackward ? 'backward' : 'forward', granularity); // Guard against no ranges
4355
4438
 
4356
4439
  if (domSelection.rangeCount > 0) {
@@ -4374,7 +4457,7 @@ class RangeSelection {
4374
4457
  let anchorNode = anchor.getNode();
4375
4458
 
4376
4459
  if (!isBackward && ( // Delete forward handle case
4377
- 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())) {
4378
4461
  const nextSibling = anchorNode.getNextSibling() || anchorNode.getParentOrThrow().getNextSibling();
4379
4462
 
4380
4463
  if ($isElementNode(nextSibling) && !nextSibling.canExtractContents()) {
@@ -4475,6 +4558,8 @@ function $swapPoints(selection) {
4475
4558
  }
4476
4559
 
4477
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.
4478
4563
  domSelection.modify(alter, direction, granularity);
4479
4564
  }
4480
4565
 
@@ -4734,6 +4819,10 @@ function internalResolveSelectionPoints(anchorDOM, anchorOffset, focusDOM, focus
4734
4819
 
4735
4820
  normalizeSelectionPointsForBoundaries(resolvedAnchorPoint, resolvedFocusPoint, lastSelection);
4736
4821
  return [resolvedAnchorPoint, resolvedFocusPoint];
4822
+ }
4823
+
4824
+ function $isBlockElementNode(node) {
4825
+ return $isElementNode(node) && !node.isInline();
4737
4826
  } // This is used to make a selection when the existing
4738
4827
  // selection is null, i.e. forcing selection on the editor
4739
4828
  // when it current exists outside the editor.
@@ -4746,15 +4835,15 @@ function internalMakeRangeSelection(anchorKey, anchorOffset, focusKey, focusOffs
4746
4835
  editorState._selection = selection;
4747
4836
  return selection;
4748
4837
  }
4749
- function $createEmptyRangeSelection() {
4838
+ function $createRangeSelection() {
4750
4839
  const anchor = $createPoint('root', 0, 'element');
4751
4840
  const focus = $createPoint('root', 0, 'element');
4752
4841
  return new RangeSelection(anchor, focus, 0);
4753
4842
  }
4754
- function $createEmptyObjectSelection() {
4843
+ function $createNodeSelection() {
4755
4844
  return new NodeSelection(new Set());
4756
4845
  }
4757
- function $createEmptyGridSelection() {
4846
+ function $createGridSelection() {
4758
4847
  const anchor = $createPoint('root', 0, 'element');
4759
4848
  const focus = $createPoint('root', 0, 'element');
4760
4849
  return new GridSelection('root', anchor, focus);
@@ -5082,7 +5171,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5082
5171
  if (anchorOffset === nextAnchorOffset && focusOffset === nextFocusOffset && anchorDOMNode === nextAnchorNode && focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
5083
5172
  !(domSelection.type === 'Range' && isCollapsed)) {
5084
5173
  // If the root element does not have focus, ensure it has focus
5085
- if (activeElement === null || !rootElement.contains(activeElement)) {
5174
+ if (rootElement !== null && (activeElement === null || !rootElement.contains(activeElement))) {
5086
5175
  rootElement.focus({
5087
5176
  preventScroll: true
5088
5177
  });
@@ -5101,7 +5190,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5101
5190
  try {
5102
5191
  domSelection.setBaseAndExtent(nextAnchorNode, nextAnchorOffset, nextFocusNode, nextFocusOffset);
5103
5192
 
5104
- if (nextSelection.isCollapsed() && rootElement === activeElement) {
5193
+ if (nextSelection.isCollapsed() && rootElement !== null && rootElement === activeElement) {
5105
5194
  scrollIntoViewIfNeeded(editor, anchor, rootElement, tags);
5106
5195
  }
5107
5196
 
@@ -5283,7 +5372,6 @@ function $applyAllTransforms(editorState, editor) {
5283
5372
  }
5284
5373
 
5285
5374
  function $parseSerializedNode(serializedNode) {
5286
- // $FlowFixMe: intentional cast to our internal type
5287
5375
  const internalSerializedNode = serializedNode;
5288
5376
  return $parseSerializedNodeImpl(internalSerializedNode, getActiveEditor()._nodes);
5289
5377
  }
@@ -5298,14 +5386,13 @@ function $parseSerializedNodeImpl(serializedNode, registeredNodes) {
5298
5386
  }
5299
5387
  }
5300
5388
 
5301
- const nodeClass = registeredNode.klass; // @ts-expect-error TODO Replace Class utility type with InstanceType
5389
+ const nodeClass = registeredNode.klass;
5302
5390
 
5303
5391
  if (serializedNode.type !== nodeClass.getType()) {
5304
5392
  {
5305
5393
  throw Error(`LexicalNode: Node ${nodeClass.name} does not implement .importJSON().`);
5306
5394
  }
5307
- } // @ts-expect-error TODO Replace Class utility type with InstanceType
5308
-
5395
+ }
5309
5396
 
5310
5397
  const node = nodeClass.importJSON(serializedNode);
5311
5398
  const children = serializedNode.children;
@@ -5348,7 +5435,7 @@ function parseEditorState(serializedEditorState, editor, updateFn) {
5348
5435
  } // Make the editorState immutable
5349
5436
 
5350
5437
 
5351
- editorState._readOnly = true; // @ts-ignore
5438
+ editorState._readOnly = true;
5352
5439
 
5353
5440
  {
5354
5441
  handleDEVOnlyPendingUpdateGuarantees(editorState);
@@ -5443,7 +5530,9 @@ function commitPendingUpdates(editor) {
5443
5530
  mutatedNodes = reconcileRoot(currentEditorState, pendingEditorState, editor, dirtyType, dirtyElements, dirtyLeaves);
5444
5531
  } catch (error) {
5445
5532
  // Report errors
5446
- 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
5447
5536
 
5448
5537
 
5449
5538
  if (!isAttemptingToRecoverFromReconcilerError) {
@@ -5472,7 +5561,7 @@ function commitPendingUpdates(editor) {
5472
5561
  }
5473
5562
  }
5474
5563
 
5475
- pendingEditorState._readOnly = true; // @ts-ignore
5564
+ pendingEditorState._readOnly = true;
5476
5565
 
5477
5566
  {
5478
5567
  handleDEVOnlyPendingUpdateGuarantees(pendingEditorState);
@@ -5576,6 +5665,7 @@ function triggerListeners(type, editor, isCurrentlyEnqueuingUpdates, ...payload)
5576
5665
  const listeners = Array.from(editor._listeners[type]);
5577
5666
 
5578
5667
  for (let i = 0; i < listeners.length; i++) {
5668
+ // @ts-ignore
5579
5669
  listeners[i].apply(null, payload);
5580
5670
  }
5581
5671
  } finally {
@@ -5620,8 +5710,12 @@ function triggerEnqueuedUpdates(editor) {
5620
5710
  const queuedUpdates = editor._updates;
5621
5711
 
5622
5712
  if (queuedUpdates.length !== 0) {
5623
- const [updateFn, options] = queuedUpdates.shift();
5624
- beginUpdate(editor, updateFn, options);
5713
+ const queuedUpdate = queuedUpdates.shift();
5714
+
5715
+ if (queuedUpdate) {
5716
+ const [updateFn, options] = queuedUpdate;
5717
+ beginUpdate(editor, updateFn, options);
5718
+ }
5625
5719
  }
5626
5720
  }
5627
5721
 
@@ -5649,28 +5743,32 @@ function processNestedUpdates(editor, initialSkipTransforms) {
5649
5743
  // empty.
5650
5744
 
5651
5745
  while (queuedUpdates.length !== 0) {
5652
- const [nextUpdateFn, options] = queuedUpdates.shift();
5653
- let onUpdate;
5654
- let tag;
5746
+ const queuedUpdate = queuedUpdates.shift();
5655
5747
 
5656
- if (options !== undefined) {
5657
- onUpdate = options.onUpdate;
5658
- tag = options.tag;
5748
+ if (queuedUpdate) {
5749
+ const [nextUpdateFn, options] = queuedUpdate;
5750
+ let onUpdate;
5751
+ let tag;
5659
5752
 
5660
- if (options.skipTransforms) {
5661
- skipTransforms = true;
5662
- }
5753
+ if (options !== undefined) {
5754
+ onUpdate = options.onUpdate;
5755
+ tag = options.tag;
5663
5756
 
5664
- if (onUpdate) {
5665
- editor._deferred.push(onUpdate);
5666
- }
5757
+ if (options.skipTransforms) {
5758
+ skipTransforms = true;
5759
+ }
5667
5760
 
5668
- if (tag) {
5669
- editor._updateTags.add(tag);
5761
+ if (onUpdate) {
5762
+ editor._deferred.push(onUpdate);
5763
+ }
5764
+
5765
+ if (tag) {
5766
+ editor._updateTags.add(tag);
5767
+ }
5670
5768
  }
5671
- }
5672
5769
 
5673
- nextUpdateFn();
5770
+ nextUpdateFn();
5771
+ }
5674
5772
  }
5675
5773
 
5676
5774
  return skipTransforms;
@@ -5690,7 +5788,7 @@ function beginUpdate(editor, updateFn, options) {
5690
5788
  updateTags.add(tag);
5691
5789
  }
5692
5790
 
5693
- skipTransforms = options.skipTransforms;
5791
+ skipTransforms = options.skipTransforms || false;
5694
5792
  }
5695
5793
 
5696
5794
  if (onUpdate) {
@@ -5762,7 +5860,9 @@ function beginUpdate(editor, updateFn, options) {
5762
5860
  }
5763
5861
  } catch (error) {
5764
5862
  // Report errors
5765
- 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
5766
5866
 
5767
5867
 
5768
5868
  editor._pendingEditorState = currentEditorState;
@@ -5817,7 +5917,13 @@ function internalGetActiveEditor() {
5817
5917
  return activeEditor;
5818
5918
  }
5819
5919
 
5820
- /* eslint-disable no-constant-condition */
5920
+ /**
5921
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
5922
+ *
5923
+ * This source code is licensed under the MIT license found in the
5924
+ * LICENSE file in the root directory of this source tree.
5925
+ *
5926
+ */
5821
5927
  function removeNode(nodeToRemove, restoreSelection, preserveEmptyParent) {
5822
5928
  errorOnReadOnly();
5823
5929
  const key = nodeToRemove.__key;
@@ -5906,7 +6012,7 @@ class LexicalNode {
5906
6012
  // @ts-expect-error
5907
6013
  this.__type = this.constructor.getType();
5908
6014
  this.__parent = null;
5909
- $setNodeKey(this, key); // @ts-ignore
6015
+ $setNodeKey(this, key);
5910
6016
 
5911
6017
  {
5912
6018
  if (this.__type !== 'root') {
@@ -6118,7 +6224,6 @@ class LexicalNode {
6118
6224
  const b = node.getParents();
6119
6225
 
6120
6226
  if ($isElementNode(this)) {
6121
- // @ts-expect-error
6122
6227
  a.unshift(this);
6123
6228
  }
6124
6229
 
@@ -6375,52 +6480,11 @@ class LexicalNode {
6375
6480
 
6376
6481
  exportDOM(editor) {
6377
6482
  const element = this.createDOM(editor._config, editor);
6378
- const serializedNode = this.exportJSON();
6379
- element.setAttribute('data-lexical-node-type', this.__type);
6380
- element.setAttribute('data-lexical-node-json', JSON.stringify(serializedNode));
6381
- element.setAttribute('data-lexical-editor-key', editor._key);
6382
6483
  return {
6383
6484
  element
6384
6485
  };
6385
6486
  }
6386
6487
 
6387
- static importDOM() {
6388
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
6389
- const proto = this.prototype.constructor;
6390
- return {
6391
- // Catch-all key because we don't know the nodeName of the element returned by exportDOM.
6392
- '*': domNode => {
6393
- if (!(domNode instanceof HTMLElement)) return null;
6394
- const editorKey = domNode.getAttribute('data-lexical-editor-key');
6395
- const nodeType = domNode.getAttribute('data-lexical-node-type');
6396
- if (editorKey == null || nodeType == null) return null;
6397
- const editor = getActiveEditor();
6398
-
6399
- if (editorKey === editor.getKey() && nodeType === proto.getType()) {
6400
- try {
6401
- const json = domNode.getAttribute('data-lexical-node-json');
6402
-
6403
- if (json != null) {
6404
- const serializedNode = JSON.parse(json);
6405
- const node = proto.importJSON(serializedNode);
6406
- return {
6407
- conversion: () => ({
6408
- node
6409
- }),
6410
- // Max priority because of the 'data-lexical-node-type' attribute
6411
- // matching the one on node klass guarantees a match.
6412
- priority: 4
6413
- }; // eslint-disable-next-line no-empty
6414
- } // eslint-disable-next-line no-empty
6415
-
6416
- } catch {}
6417
- }
6418
-
6419
- return null;
6420
- }
6421
- };
6422
- }
6423
-
6424
6488
  exportJSON() {
6425
6489
  {
6426
6490
  throw Error(`exportJSON: base method not extended`);
@@ -6639,7 +6703,7 @@ class DecoratorNode extends LexicalNode {
6639
6703
  super(key);
6640
6704
  }
6641
6705
 
6642
- decorate(editor) {
6706
+ decorate(editor, config) {
6643
6707
  {
6644
6708
  throw Error(`decorate: base method not extended`);
6645
6709
  }
@@ -6872,8 +6936,12 @@ class ElementNode extends LexicalNode {
6872
6936
  }
6873
6937
 
6874
6938
  hasFormat(type) {
6875
- const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
6876
- 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;
6877
6945
  } // Mutators
6878
6946
 
6879
6947
 
@@ -6958,7 +7026,7 @@ class ElementNode extends LexicalNode {
6958
7026
  setFormat(type) {
6959
7027
  errorOnReadOnly();
6960
7028
  const self = this.getWritable();
6961
- self.__format = ELEMENT_TYPE_TO_FORMAT[type] || 0;
7029
+ self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0;
6962
7030
  return this;
6963
7031
  }
6964
7032
 
@@ -7108,7 +7176,7 @@ class ElementNode extends LexicalNode {
7108
7176
  return false;
7109
7177
  }
7110
7178
 
7111
- excludeFromCopy() {
7179
+ excludeFromCopy(destination) {
7112
7180
  return false;
7113
7181
  }
7114
7182
 
@@ -7551,20 +7619,24 @@ function setTextContent(nextText, dom, node) {
7551
7619
  dom.textContent = text;
7552
7620
  } else {
7553
7621
  const nodeValue = firstChild.nodeValue;
7554
- if (nodeValue !== text) if (isComposing || IS_FIREFOX) {
7555
- // We also use the diff composed text for general text in FF to avoid
7556
- // the spellcheck red line from flickering.
7557
- const [index, remove, insert] = diffComposedText(nodeValue, text);
7558
7622
 
7559
- if (remove !== 0) {
7560
- // @ts-expect-error
7561
- firstChild.deleteData(index, remove);
7562
- } // @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);
7563
7629
 
7630
+ if (remove !== 0) {
7631
+ // @ts-expect-error
7632
+ firstChild.deleteData(index, remove);
7633
+ } // @ts-expect-error
7564
7634
 
7565
- firstChild.insertData(index, insert);
7566
- } else {
7567
- firstChild.nodeValue = text;
7635
+
7636
+ firstChild.insertData(index, insert);
7637
+ } else {
7638
+ firstChild.nodeValue = text;
7639
+ }
7568
7640
  }
7569
7641
  }
7570
7642
  }
@@ -7817,19 +7889,21 @@ class TextNode extends LexicalNode {
7817
7889
 
7818
7890
  selectionTransform(prevSelection, nextSelection) {
7819
7891
  return;
7820
- }
7892
+ } // TODO 0.4 This should just be a `string`.
7893
+
7821
7894
 
7822
7895
  setFormat(format) {
7823
7896
  errorOnReadOnly();
7824
7897
  const self = this.getWritable();
7825
- self.__format = format;
7898
+ self.__format = typeof format === 'string' ? TEXT_TYPE_TO_FORMAT[format] : format;
7826
7899
  return self;
7827
- }
7900
+ } // TODO 0.4 This should just be a `string`.
7901
+
7828
7902
 
7829
7903
  setDetail(detail) {
7830
7904
  errorOnReadOnly();
7831
7905
  const self = this.getWritable();
7832
- self.__detail = detail;
7906
+ self.__detail = typeof detail === 'string' ? DETAIL_TYPE_TO_DETAIL[detail] : detail;
7833
7907
  return self;
7834
7908
  }
7835
7909
 
@@ -8119,11 +8193,13 @@ function convertSpanElement(domNode) {
8119
8193
  // domNode is a <span> since we matched it by nodeName
8120
8194
  const span = domNode; // Google Docs uses span tags + font-weight for bold text
8121
8195
 
8122
- 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
8123
8197
 
8124
8198
  const hasLinethroughTextDecoration = span.style.textDecoration === 'line-through'; // Google Docs uses span tags + font-style for italic text
8125
8199
 
8126
- 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';
8127
8203
  return {
8128
8204
  forChild: lexicalNode => {
8129
8205
  if ($isTextNode(lexicalNode) && hasBoldFontWeight) {
@@ -8138,6 +8214,10 @@ function convertSpanElement(domNode) {
8138
8214
  lexicalNode.toggleFormat('italic');
8139
8215
  }
8140
8216
 
8217
+ if ($isTextNode(lexicalNode) && hasUnderlineTextDecoration) {
8218
+ lexicalNode.toggleFormat('underline');
8219
+ }
8220
+
8141
8221
  return lexicalNode;
8142
8222
  },
8143
8223
  node: null
@@ -8162,8 +8242,21 @@ function convertBringAttentionToElement(domNode) {
8162
8242
  }
8163
8243
 
8164
8244
  function convertTextDOMNode(domNode) {
8245
+ const {
8246
+ parentElement
8247
+ } = domNode;
8248
+ const textContent = domNode.textContent || '';
8249
+ const textContentTrim = textContent.trim();
8250
+ const isPre = parentElement != null && parentElement.tagName.toLowerCase() === 'pre';
8251
+
8252
+ if (!isPre && textContentTrim.length === 0 && textContent.includes('\n')) {
8253
+ return {
8254
+ node: null
8255
+ };
8256
+ }
8257
+
8165
8258
  return {
8166
- node: $createTextNode(domNode.textContent)
8259
+ node: $createTextNode(textContent)
8167
8260
  };
8168
8261
  }
8169
8262
 
@@ -8249,10 +8342,8 @@ class ParagraphNode extends ElementNode {
8249
8342
  element
8250
8343
  } = super.exportDOM(editor);
8251
8344
 
8252
- if (element) {
8253
- if (this.getTextContentSize() === 0) {
8254
- element.append(document.createElement('br'));
8255
- }
8345
+ if (element && this.isEmpty()) {
8346
+ element.append(document.createElement('br'));
8256
8347
  }
8257
8348
 
8258
8349
  return {
@@ -8376,10 +8467,9 @@ function initializeConversionCache(nodes) {
8376
8467
  const conversionCache = new Map();
8377
8468
  const handledConversions = new Set();
8378
8469
  nodes.forEach(node => {
8379
- // @ts-expect-error TODO Replace Class utility type with InstanceType
8380
- const importDOM = node.klass.importDOM.bind(node.klass); // debugger;
8470
+ const importDOM = node.klass.importDOM != null ? node.klass.importDOM.bind(node.klass) : null;
8381
8471
 
8382
- if (handledConversions.has(importDOM)) {
8472
+ if (importDOM == null || handledConversions.has(importDOM)) {
8383
8473
  return;
8384
8474
  }
8385
8475
 
@@ -8423,7 +8513,6 @@ function createEditor(editorConfig) {
8423
8513
 
8424
8514
  for (let i = 0; i < nodes.length; i++) {
8425
8515
  const klass = nodes[i]; // Ensure custom nodes implement required methods.
8426
- // @ts-ignore
8427
8516
 
8428
8517
  {
8429
8518
  const name = klass.name;
@@ -8446,7 +8535,7 @@ function createEditor(editorConfig) {
8446
8535
  if (proto instanceof DecoratorNode) {
8447
8536
  // eslint-disable-next-line no-prototype-builtins
8448
8537
  if (!proto.hasOwnProperty('decorate')) {
8449
- console.warn(`${this.constructor.name} must implement "decorate" method`);
8538
+ console.warn(`${proto.constructor.name} must implement "decorate" method`);
8450
8539
  }
8451
8540
  }
8452
8541
 
@@ -8460,8 +8549,7 @@ function createEditor(editorConfig) {
8460
8549
  console.warn(`${name} should implement "exportJSON" method to ensure JSON and default HTML serialization works as expected`);
8461
8550
  }
8462
8551
  }
8463
- } // @ts-expect-error TODO Replace Class utility type with InstanceType
8464
-
8552
+ }
8465
8553
 
8466
8554
  const type = klass.getType();
8467
8555
  registeredNodes.set(type, {
@@ -8475,7 +8563,7 @@ function createEditor(editorConfig) {
8475
8563
  disableEvents,
8476
8564
  namespace,
8477
8565
  theme
8478
- }, onError, initializeConversionCache(registeredNodes), isReadOnly);
8566
+ }, onError ? onError : console.error, initializeConversionCache(registeredNodes), isReadOnly);
8479
8567
 
8480
8568
  if (initialEditorState !== undefined) {
8481
8569
  editor._pendingEditorState = initialEditorState;
@@ -8614,7 +8702,6 @@ class LexicalEditor {
8614
8702
  }
8615
8703
 
8616
8704
  registerMutationListener(klass, listener) {
8617
- // @ts-expect-error TODO Replace Class utility type with InstanceType
8618
8705
  const registeredNode = this._nodes.get(klass.getType());
8619
8706
 
8620
8707
  if (registeredNode === undefined) {
@@ -8630,10 +8717,7 @@ class LexicalEditor {
8630
8717
  };
8631
8718
  }
8632
8719
 
8633
- registerNodeTransform( // There's no Flow-safe way to preserve the T in Transform<T>, but <T = LexicalNode> in the
8634
- // declaration below guarantees these are LexicalNodes.
8635
- klass, listener) {
8636
- // @ts-expect-error TODO Replace Class utility type with InstanceType
8720
+ registerNodeTransform(klass, listener) {
8637
8721
  const type = klass.getType();
8638
8722
 
8639
8723
  const registeredNode = this._nodes.get(type);
@@ -8654,8 +8738,7 @@ class LexicalEditor {
8654
8738
 
8655
8739
  hasNodes(nodes) {
8656
8740
  for (let i = 0; i < nodes.length; i++) {
8657
- const klass = nodes[i]; // @ts-expect-error TODO Replace Class utility type with InstanceType
8658
-
8741
+ const klass = nodes[i];
8659
8742
  const type = klass.getType();
8660
8743
 
8661
8744
  if (!this._nodes.has(type)) {
@@ -8817,13 +8900,15 @@ class LexicalEditor {
8817
8900
  }
8818
8901
 
8819
8902
  setReadOnly(readOnly) {
8820
- this._readOnly = readOnly;
8821
- triggerListeners('readonly', this, true, readOnly);
8903
+ if (this._readOnly !== readOnly) {
8904
+ this._readOnly = readOnly;
8905
+ triggerListeners('readonly', this, true, readOnly);
8906
+ }
8822
8907
  }
8823
8908
 
8824
8909
  toJSON() {
8825
8910
  return {
8826
- editorState: this._editorState
8911
+ editorState: this._editorState.toJSON()
8827
8912
  };
8828
8913
  }
8829
8914
 
@@ -8836,7 +8921,7 @@ class LexicalEditor {
8836
8921
  * LICENSE file in the root directory of this source tree.
8837
8922
  *
8838
8923
  */
8839
- const VERSION = '0.3.3';
8924
+ const VERSION = '0.3.6';
8840
8925
 
8841
8926
  /**
8842
8927
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -8886,11 +8971,11 @@ function $isGridRowNode(node) {
8886
8971
  return node instanceof GridRowNode;
8887
8972
  }
8888
8973
 
8889
- exports.$createGridSelection = $createEmptyGridSelection;
8974
+ exports.$createGridSelection = $createGridSelection;
8890
8975
  exports.$createLineBreakNode = $createLineBreakNode;
8891
- exports.$createNodeSelection = $createEmptyObjectSelection;
8976
+ exports.$createNodeSelection = $createNodeSelection;
8892
8977
  exports.$createParagraphNode = $createParagraphNode;
8893
- exports.$createRangeSelection = $createEmptyRangeSelection;
8978
+ exports.$createRangeSelection = $createRangeSelection;
8894
8979
  exports.$createTextNode = $createTextNode;
8895
8980
  exports.$getDecoratorNode = $getDecoratorNode;
8896
8981
  exports.$getNearestNodeFromDOMNode = $getNearestNodeFromDOMNode;