lexical 0.2.4 → 0.2.7

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
@@ -56,6 +56,7 @@ const IS_ALIGN_RIGHT = 3;
56
56
  const IS_ALIGN_JUSTIFY = 4; // Reconciliation
57
57
 
58
58
  const ZERO_WIDTH_CHAR = '\u200b';
59
+ const DOUBLE_LINE_BREAK = '\n\n';
59
60
  const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC';
60
61
  const LTR = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' + '\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' + '\uFE00-\uFE6F\uFEFD-\uFFFF';
61
62
  const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']');
@@ -181,6 +182,9 @@ function getTextDirection(text) {
181
182
 
182
183
  return null;
183
184
  }
185
+ function $isTokenOrInertOrSegmented(node) {
186
+ return $isTokenOrInert(node) || node.isSegmented();
187
+ }
184
188
  function $isTokenOrInert(node) {
185
189
  return node.isToken() || node.isInert();
186
190
  }
@@ -322,21 +326,24 @@ function $setCompositionKey(compositionKey) {
322
326
  errorOnReadOnly();
323
327
  const editor = getActiveEditor();
324
328
  const previousCompositionKey = editor._compositionKey;
325
- editor._compositionKey = compositionKey;
326
329
 
327
- if (previousCompositionKey !== null) {
328
- const node = $getNodeByKey(previousCompositionKey);
330
+ if (compositionKey !== previousCompositionKey) {
331
+ editor._compositionKey = compositionKey;
329
332
 
330
- if (node !== null) {
331
- node.getWritable();
333
+ if (previousCompositionKey !== null) {
334
+ const node = $getNodeByKey(previousCompositionKey);
335
+
336
+ if (node !== null) {
337
+ node.getWritable();
338
+ }
332
339
  }
333
- }
334
340
 
335
- if (compositionKey !== null) {
336
- const node = $getNodeByKey(compositionKey);
341
+ if (compositionKey !== null) {
342
+ const node = $getNodeByKey(compositionKey);
337
343
 
338
- if (node !== null) {
339
- node.getWritable();
344
+ if (node !== null) {
345
+ node.getWritable();
346
+ }
340
347
  }
341
348
  }
342
349
  }
@@ -423,8 +430,15 @@ function internalGetRoot(editorState) {
423
430
  function $setSelection(selection) {
424
431
  const editorState = getActiveEditorState();
425
432
 
426
- if (selection !== null && Object.isFrozen(selection)) {
427
- console.warn('$setSelection called on frozen selection object. Ensure selection is cloned before passing in.');
433
+ if (selection !== null) {
434
+ if (Object.isFrozen(selection)) {
435
+ {
436
+ throw Error(`$setSelection called on frozen selection object. Ensure selection is cloned before passing in.`);
437
+ }
438
+ }
439
+
440
+ selection.dirty = true;
441
+ selection._cachedNodes = null;
428
442
  }
429
443
 
430
444
  editorState._selection = selection;
@@ -489,7 +503,7 @@ function getEditorsToPropagate(editor) {
489
503
  function createUID() {
490
504
  return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
491
505
  }
492
- function $updateSelectedTextFromDOM(editor, compositionEndEvent) {
506
+ function $updateSelectedTextFromDOM(editor, isCompositionEnd, data) {
493
507
  // Update the text content with the latest composition text
494
508
  const domSelection = getDOMSelection();
495
509
 
@@ -507,8 +521,7 @@ function $updateSelectedTextFromDOM(editor, compositionEndEvent) {
507
521
  const node = $getNearestNodeFromDOMNode(anchorNode);
508
522
 
509
523
  if ($isTextNode(node)) {
510
- let textContent = anchorNode.nodeValue;
511
- const data = compositionEndEvent !== null && compositionEndEvent.data; // Data is intentionally truthy, as we check for boolean, null and empty string.
524
+ let textContent = anchorNode.nodeValue; // Data is intentionally truthy, as we check for boolean, null and empty string.
512
525
 
513
526
  if (textContent === ZERO_WIDTH_CHAR && data) {
514
527
  const offset = data.length;
@@ -517,7 +530,7 @@ function $updateSelectedTextFromDOM(editor, compositionEndEvent) {
517
530
  focusOffset = offset;
518
531
  }
519
532
 
520
- $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, compositionEndEvent !== null);
533
+ $updateTextNodeFromDOMContent(node, textContent, anchorOffset, focusOffset, isCompositionEnd);
521
534
  }
522
535
  }
523
536
  }
@@ -543,7 +556,9 @@ function $updateTextNodeFromDOMContent(textNode, textContent, anchorOffset, focu
543
556
  const editor = getActiveEditor();
544
557
  setTimeout(() => {
545
558
  editor.update(() => {
546
- node.remove();
559
+ if (node.isAttached()) {
560
+ node.remove();
561
+ }
547
562
  });
548
563
  }, 20);
549
564
  } else {
@@ -727,6 +742,9 @@ function isMoveDown(keyCode, ctrlKey, shiftKey, altKey, metaKey) {
727
742
  function isModifier(ctrlKey, shiftKey, altKey, metaKey) {
728
743
  return ctrlKey || shiftKey || altKey || metaKey;
729
744
  }
745
+ function isSpace(keyCode) {
746
+ return keyCode === 32;
747
+ }
730
748
  function controlOrMeta(metaKey, ctrlKey) {
731
749
  if (IS_APPLE) {
732
750
  return metaKey;
@@ -852,6 +870,9 @@ function isFirefoxClipboardEvents() {
852
870
  function dispatchCommand(editor, type, payload) {
853
871
  return triggerCommandListeners(editor, type, payload);
854
872
  }
873
+ function $textContentRequiresDoubleLinebreakAtEnd(node) {
874
+ return !$isRootNode(node) && !node.isLastChild() && !node.isInline();
875
+ }
855
876
 
856
877
  /**
857
878
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1141,8 +1162,8 @@ function internalCreateNodeFromParse(parsedNode, parsedNodeMap, editor, parentKe
1141
1162
  }
1142
1163
  } else if (originalSelection.type === 'grid') {
1143
1164
  const gridKey = originalSelection.gridKey;
1144
- const anchorCellKey = originalSelection.anchorCellKey;
1145
- const focusCellKey = originalSelection.focusCellKey;
1165
+ const anchorCellKey = originalSelection.anchor.key;
1166
+ const focusCellKey = originalSelection.focus.key;
1146
1167
 
1147
1168
  if (remappedSelection == null && (gridKey === parsedKey || gridKey === anchorCellKey || gridKey === focusCellKey)) {
1148
1169
  state.remappedSelection = remappedSelection = { ...originalSelection,
@@ -1156,11 +1177,11 @@ function internalCreateNodeFromParse(parsedNode, parsedNodeMap, editor, parentKe
1156
1177
  }
1157
1178
 
1158
1179
  if (anchorCellKey === parsedKey) {
1159
- remappedSelection.anchorCellKey = key;
1180
+ remappedSelection.anchor.key = key;
1160
1181
  }
1161
1182
 
1162
1183
  if (focusCellKey === parsedKey) {
1163
- remappedSelection.focusCellKey = key;
1184
+ remappedSelection.focus.key = key;
1164
1185
  }
1165
1186
  }
1166
1187
  }
@@ -1199,6 +1220,7 @@ const KEY_ARROW_LEFT_COMMAND = createCommand();
1199
1220
  const KEY_ARROW_UP_COMMAND = createCommand();
1200
1221
  const KEY_ARROW_DOWN_COMMAND = createCommand();
1201
1222
  const KEY_ENTER_COMMAND = createCommand();
1223
+ const KEY_SPACE_COMMAND = createCommand();
1202
1224
  const KEY_BACKSPACE_COMMAND = createCommand();
1203
1225
  const KEY_ESCAPE_COMMAND = createCommand();
1204
1226
  const KEY_DELETE_COMMAND = createCommand();
@@ -1208,6 +1230,7 @@ const OUTDENT_CONTENT_COMMAND = createCommand();
1208
1230
  const DROP_COMMAND = createCommand();
1209
1231
  const FORMAT_ELEMENT_COMMAND = createCommand();
1210
1232
  const DRAGSTART_COMMAND = createCommand();
1233
+ const DRAGEND_COMMAND = createCommand();
1211
1234
  const COPY_COMMAND = createCommand();
1212
1235
  const CUT_COMMAND = createCommand();
1213
1236
  const CLEAR_EDITOR_COMMAND = createCommand();
@@ -1245,20 +1268,32 @@ if (CAN_USE_BEFORE_INPUT) {
1245
1268
  let lastKeyDownTimeStamp = 0;
1246
1269
  let rootElementsRegistered = 0;
1247
1270
  let isSelectionChangeFromReconcile = false;
1271
+ let isInsertLineBreak = false;
1272
+ let isFirefoxEndingComposition = false;
1273
+ let collapsedSelectionFormat = [0, 0, 'root', 0];
1274
+
1275
+ function shouldSkipSelectionChange(domNode, offset) {
1276
+ return domNode !== null && domNode.nodeType === DOM_TEXT_TYPE && offset !== 0 && offset !== domNode.nodeValue.length;
1277
+ }
1248
1278
 
1249
1279
  function onSelectionChange(domSelection, editor, isActive) {
1250
1280
  if (isSelectionChangeFromReconcile) {
1251
1281
  isSelectionChangeFromReconcile = false;
1252
1282
  const {
1253
1283
  anchorNode,
1254
- focusNode
1284
+ anchorOffset,
1285
+ focusNode,
1286
+ focusOffset
1255
1287
  } = domSelection; // If native DOM selection is on a DOM element, then
1256
1288
  // we should continue as usual, as Lexical's selection
1257
1289
  // may have normalized to a better child. If the DOM
1258
1290
  // element is a text node, we can safely apply this
1259
1291
  // optimization and skip the selection change entirely.
1292
+ // We also need to check if the offset is at the boundary,
1293
+ // because in this case, we might need to normalize to a
1294
+ // sibling instead.
1260
1295
 
1261
- if (anchorNode !== null && focusNode !== null && anchorNode.nodeType === DOM_TEXT_TYPE && focusNode.nodeType === DOM_TEXT_TYPE) {
1296
+ if (shouldSkipSelectionChange(anchorNode, anchorOffset) && shouldSkipSelectionChange(focusNode, focusOffset)) {
1262
1297
  return;
1263
1298
  }
1264
1299
  }
@@ -1273,19 +1308,45 @@ function onSelectionChange(domSelection, editor, isActive) {
1273
1308
 
1274
1309
  const selection = $getSelection(); // Update the selection format
1275
1310
 
1276
- if ($isRangeSelection(selection) && selection.isCollapsed()) {
1277
- // Badly interpreted range selection when collapsed - #1482
1278
- if (domSelection.type === 'Range') {
1279
- selection.dirty = true;
1280
- }
1281
-
1311
+ if ($isRangeSelection(selection)) {
1282
1312
  const anchor = selection.anchor;
1313
+ const anchorNode = anchor.getNode();
1314
+
1315
+ if (selection.isCollapsed()) {
1316
+ // Badly interpreted range selection when collapsed - #1482
1317
+ if (domSelection.type === 'Range') {
1318
+ selection.dirty = true;
1319
+ } // If we have marked a collapsed selection format, and we're
1320
+ // within the given time range – then attempt to use that format
1321
+ // instead of getting the format from the anchor node.
1322
+
1323
+
1324
+ const currentTimeStamp = window.event.timeStamp;
1325
+ const [lastFormat, lastOffset, lastKey, timeStamp] = collapsedSelectionFormat;
1326
+
1327
+ if (currentTimeStamp < timeStamp + 200 && anchor.offset === lastOffset && anchor.key === lastKey) {
1328
+ selection.format = lastFormat;
1329
+ } else {
1330
+ if (anchor.type === 'text') {
1331
+ selection.format = anchorNode.getFormat();
1332
+ } else if (anchor.type === 'element') {
1333
+ selection.format = 0;
1334
+ }
1335
+ }
1336
+ } else {
1337
+ const focus = selection.focus;
1338
+ const focusNode = focus.getNode();
1339
+ let combinedFormat = 0;
1340
+
1341
+ if (anchor.type === 'text') {
1342
+ combinedFormat |= anchorNode.getFormat();
1343
+ }
1344
+
1345
+ if (focus.type === 'text' && !anchorNode.is(focusNode)) {
1346
+ combinedFormat |= focusNode.getFormat();
1347
+ }
1283
1348
 
1284
- if (anchor.type === 'text') {
1285
- const anchorNode = anchor.getNode();
1286
- selection.format = anchorNode.getFormat();
1287
- } else if (anchor.type === 'element') {
1288
- selection.format = 0;
1349
+ selection.format = combinedFormat;
1289
1350
  }
1290
1351
  }
1291
1352
 
@@ -1306,7 +1367,7 @@ function onClick(event, editor) {
1306
1367
  const anchor = selection.anchor;
1307
1368
 
1308
1369
  if (anchor.type === 'element' && anchor.offset === 0 && selection.isCollapsed() && $getRoot().getChildrenSize() === 1 && anchor.getNode().getTopLevelElementOrThrow().isEmpty()) {
1309
- const lastSelection = editor.getEditorState()._selection;
1370
+ const lastSelection = $getPreviousSelection();
1310
1371
 
1311
1372
  if (lastSelection !== null && selection.is(lastSelection)) {
1312
1373
  getDOMSelection().removeAllRanges();
@@ -1383,25 +1444,36 @@ function onBeforeInput(event, editor) {
1383
1444
  updateEditor(editor, () => {
1384
1445
  const selection = $getSelection();
1385
1446
 
1386
- if (!$isRangeSelection(selection)) {
1387
- return;
1388
- }
1389
-
1390
1447
  if (inputType === 'deleteContentBackward') {
1391
- // Used for Android
1448
+ if (selection === null) {
1449
+ // Use previous selection
1450
+ const prevSelection = $getPreviousSelection();
1451
+
1452
+ if (!$isRangeSelection(prevSelection)) {
1453
+ return;
1454
+ }
1455
+
1456
+ $setSelection(prevSelection.clone());
1457
+ } // Used for Android
1458
+
1459
+
1392
1460
  $setCompositionKey(null);
1393
1461
  event.preventDefault();
1394
1462
  lastKeyDownTimeStamp = 0;
1395
1463
  dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true); // Fixes an Android bug where selection flickers when backspacing
1396
1464
 
1397
1465
  setTimeout(() => {
1398
- editor.update(() => {
1466
+ updateEditor(editor, () => {
1399
1467
  $setCompositionKey(null);
1400
1468
  });
1401
1469
  }, ANDROID_COMPOSITION_LATENCY);
1402
1470
  return;
1403
1471
  }
1404
1472
 
1473
+ if (!$isRangeSelection(selection)) {
1474
+ return;
1475
+ }
1476
+
1405
1477
  const data = event.data;
1406
1478
 
1407
1479
  if (!selection.dirty && selection.isCollapsed() && !$isRootNode(selection.anchor.getNode())) {
@@ -1417,7 +1489,7 @@ function onBeforeInput(event, editor) {
1417
1489
  if (data === '\n') {
1418
1490
  event.preventDefault();
1419
1491
  dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND);
1420
- } else if (data === '\n\n') {
1492
+ } else if (data === DOUBLE_LINE_BREAK) {
1421
1493
  event.preventDefault();
1422
1494
  dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND);
1423
1495
  } else if (data == null && event.dataTransfer) {
@@ -1466,8 +1538,16 @@ function onBeforeInput(event, editor) {
1466
1538
  case 'insertParagraph':
1467
1539
  {
1468
1540
  // Used for Android
1469
- $setCompositionKey(null);
1470
- dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND);
1541
+ $setCompositionKey(null); // Some browsers do not provide the type "insertLineBreak".
1542
+ // So instead, we need to infer it from the keyboard event.
1543
+
1544
+ if (isInsertLineBreak) {
1545
+ isInsertLineBreak = false;
1546
+ dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND);
1547
+ } else {
1548
+ dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND);
1549
+ }
1550
+
1471
1551
  break;
1472
1552
  }
1473
1553
 
@@ -1575,14 +1655,27 @@ function onInput(event, editor) {
1575
1655
  const data = event.data;
1576
1656
 
1577
1657
  if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, data, false)) {
1578
- dispatchCommand(editor, INSERT_TEXT_COMMAND, data); // For Android
1658
+ // Given we're over-riding the default behavior, we will need
1659
+ // to ensure to disable composition before dispatching the
1660
+ // insertText command for when changing the sequence for FF.
1661
+ if (isFirefoxEndingComposition) {
1662
+ onCompositionEndImpl(editor, data);
1663
+ isFirefoxEndingComposition = false;
1664
+ }
1665
+
1666
+ dispatchCommand(editor, INSERT_TEXT_COMMAND, data); // This ensures consistency on Android.
1579
1667
 
1580
1668
  if (editor._compositionKey !== null) {
1581
1669
  lastKeyDownTimeStamp = 0;
1582
1670
  $setCompositionKey(null);
1583
1671
  }
1584
1672
  } else {
1585
- $updateSelectedTextFromDOM(editor, null);
1673
+ $updateSelectedTextFromDOM(editor, false); // onInput always fires after onCompositionEnd for FF.
1674
+
1675
+ if (isFirefoxEndingComposition) {
1676
+ onCompositionEndImpl(editor, data);
1677
+ isFirefoxEndingComposition = false;
1678
+ }
1586
1679
  } // Also flush any other mutations that might have occurred
1587
1680
  // since the change.
1588
1681
 
@@ -1601,7 +1694,9 @@ function onCompositionStart(event, editor) {
1601
1694
 
1602
1695
  if ( // If it has been 30ms since the last keydown, then we should
1603
1696
  // apply the empty space heuristic.
1604
- event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY || anchor.type === 'element' || !selection.isCollapsed() || selection.anchor.getNode().getFormat() !== selection.format) {
1697
+ event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY || // FF has issues around composing multibyte characters, so we also
1698
+ // need to invoke the empty space heuristic below.
1699
+ IS_FIREFOX && anchor.type === 'element' || !selection.isCollapsed() || selection.anchor.getNode().getFormat() !== selection.format) {
1605
1700
  // We insert an empty space, ready for the composition
1606
1701
  // to get inserted into the new node we create. If
1607
1702
  // we don't do this, Safari will fail on us because
@@ -1612,40 +1707,56 @@ function onCompositionStart(event, editor) {
1612
1707
  });
1613
1708
  }
1614
1709
 
1615
- function onCompositionEnd(event, editor) {
1616
- updateEditor(editor, () => {
1617
- const compositionKey = editor._compositionKey;
1618
- $setCompositionKey(null);
1619
- const data = event.data; // Handle termination of composition.
1620
-
1621
- if (compositionKey !== null && data != null) {
1622
- // It can sometimes move to an adjacent DOM node when backspacing.
1623
- // So check for the empty case.
1624
- if (data === '') {
1625
- const node = $getNodeByKey(compositionKey);
1626
- const textNode = getDOMTextNode(editor.getElementByKey(compositionKey));
1627
-
1628
- if (textNode !== null && $isTextNode(node)) {
1629
- $updateTextNodeFromDOMContent(node, textNode.nodeValue, null, null, true);
1630
- }
1710
+ function onCompositionEndImpl(editor, data) {
1711
+ const compositionKey = editor._compositionKey;
1712
+ $setCompositionKey(null); // Handle termination of composition.
1631
1713
 
1632
- return;
1633
- } else if (data[data.length - 1] === '\n') {
1634
- const selection = $getSelection();
1714
+ if (compositionKey !== null && data != null) {
1715
+ // Composition can sometimes move to an adjacent DOM node when backspacing.
1716
+ // So check for the empty case.
1717
+ if (data === '') {
1718
+ const node = $getNodeByKey(compositionKey);
1719
+ const textNode = getDOMTextNode(editor.getElementByKey(compositionKey));
1635
1720
 
1636
- if ($isRangeSelection(selection)) {
1637
- // If the last character is a line break, we also need to insert
1638
- // a line break.
1639
- const focus = selection.focus;
1640
- selection.anchor.set(focus.key, focus.offset, focus.type);
1641
- dispatchCommand(editor, KEY_ENTER_COMMAND, null);
1642
- return;
1643
- }
1721
+ if (textNode !== null && $isTextNode(node)) {
1722
+ $updateTextNodeFromDOMContent(node, textNode.nodeValue, null, null, true);
1723
+ }
1724
+
1725
+ return;
1726
+ } // Composition can sometimes be that of a new line. In which case, we need to
1727
+ // handle that accordingly.
1728
+
1729
+
1730
+ if (data[data.length - 1] === '\n') {
1731
+ const selection = $getSelection();
1732
+
1733
+ if ($isRangeSelection(selection)) {
1734
+ // If the last character is a line break, we also need to insert
1735
+ // a line break.
1736
+ const focus = selection.focus;
1737
+ selection.anchor.set(focus.key, focus.offset, focus.type);
1738
+ dispatchCommand(editor, KEY_ENTER_COMMAND, null);
1739
+ return;
1644
1740
  }
1645
1741
  }
1742
+ }
1646
1743
 
1647
- $updateSelectedTextFromDOM(editor, event);
1648
- });
1744
+ $updateSelectedTextFromDOM(editor, true, data);
1745
+ }
1746
+
1747
+ function onCompositionEnd(event, editor) {
1748
+ // Firefox fires onCompositionEnd before onInput, but Chrome/Webkit,
1749
+ // fire onInput before onCompositionEnd. To ensure the sequence works
1750
+ // like Chrome/Webkit we use the isFirefoxEndingComposition flag to
1751
+ // defer handling of onCompositionEnd in Firefox till we have processed
1752
+ // the logic in onInput.
1753
+ if (IS_FIREFOX) {
1754
+ isFirefoxEndingComposition = true;
1755
+ } else {
1756
+ updateEditor(editor, () => {
1757
+ onCompositionEndImpl(editor, event.data);
1758
+ });
1759
+ }
1649
1760
  }
1650
1761
 
1651
1762
  function onKeyDown(event, editor) {
@@ -1672,11 +1783,16 @@ function onKeyDown(event, editor) {
1672
1783
  } else if (isMoveDown(keyCode, ctrlKey, shiftKey, altKey, metaKey)) {
1673
1784
  dispatchCommand(editor, KEY_ARROW_DOWN_COMMAND, event);
1674
1785
  } else if (isLineBreak(keyCode, shiftKey)) {
1786
+ isInsertLineBreak = true;
1675
1787
  dispatchCommand(editor, KEY_ENTER_COMMAND, event);
1788
+ } else if (isSpace(keyCode)) {
1789
+ dispatchCommand(editor, KEY_SPACE_COMMAND, event);
1676
1790
  } else if (isOpenLineBreak(keyCode, ctrlKey)) {
1677
1791
  event.preventDefault();
1792
+ isInsertLineBreak = true;
1678
1793
  dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, true);
1679
1794
  } else if (isParagraph(keyCode, shiftKey)) {
1795
+ isInsertLineBreak = false;
1680
1796
  dispatchCommand(editor, KEY_ENTER_COMMAND, event);
1681
1797
  } else if (isDeleteBackward(keyCode, altKey, metaKey, ctrlKey)) {
1682
1798
  if (isBackspace(keyCode)) {
@@ -1811,6 +1927,9 @@ function addRootElementEvents(rootElement, editor) {
1811
1927
  case 'dragstart':
1812
1928
  return dispatchCommand(editor, DRAGSTART_COMMAND, event);
1813
1929
 
1930
+ case 'dragend':
1931
+ return dispatchCommand(editor, DRAGEND_COMMAND, event);
1932
+
1814
1933
  case 'focus':
1815
1934
  return dispatchCommand(editor, FOCUS_COMMAND, event);
1816
1935
 
@@ -1877,6 +1996,9 @@ function cleanActiveNestedEditorsMap(editor) {
1877
1996
  function markSelectionChangeFromReconcile() {
1878
1997
  isSelectionChangeFromReconcile = true;
1879
1998
  }
1999
+ function markCollapsedSelectionFormat(format, offset, key, timeStamp) {
2000
+ collapsedSelectionFormat = [format, offset, key, timeStamp];
2001
+ }
1880
2002
 
1881
2003
  /**
1882
2004
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1944,7 +2066,7 @@ function setTextAlign(domStyle, value) {
1944
2066
  }
1945
2067
 
1946
2068
  function setElementIndent(dom, indent) {
1947
- dom.style.setProperty('padding-inline-start', indent === 0 ? '' : indent * 40 + 'px');
2069
+ dom.style.setProperty('padding-inline-start', indent === 0 ? '' : indent * 20 + 'px');
1948
2070
  }
1949
2071
 
1950
2072
  function setElementFormat(dom, format) {
@@ -2005,6 +2127,11 @@ function createNode(key, parentDOM, insertDOM) {
2005
2127
  }
2006
2128
 
2007
2129
  reconcileElementTerminatingLineBreak(null, children, dom);
2130
+
2131
+ if ($textContentRequiresDoubleLinebreakAtEnd(node)) {
2132
+ subTreeTextContent += DOUBLE_LINE_BREAK;
2133
+ editorTextContent += DOUBLE_LINE_BREAK;
2134
+ }
2008
2135
  } else {
2009
2136
  const text = node.getTextContent();
2010
2137
 
@@ -2176,12 +2303,12 @@ function reconcileBlockDirection(element, dom) {
2176
2303
  function reconcileChildrenWithDirection(prevChildren, nextChildren, element, dom) {
2177
2304
  const previousSubTreeDirectionTextContent = subTreeDirectionedTextContent;
2178
2305
  subTreeDirectionedTextContent = '';
2179
- reconcileChildren(prevChildren, nextChildren, dom);
2306
+ reconcileChildren(element, prevChildren, nextChildren, dom);
2180
2307
  reconcileBlockDirection(element, dom);
2181
2308
  subTreeDirectionedTextContent = previousSubTreeDirectionTextContent;
2182
2309
  }
2183
2310
 
2184
- function reconcileChildren(prevChildren, nextChildren, dom) {
2311
+ function reconcileChildren(element, prevChildren, nextChildren, dom) {
2185
2312
  const previousSubTreeTextContent = subTreeTextContent;
2186
2313
  subTreeTextContent = '';
2187
2314
  const prevChildrenLength = prevChildren.length;
@@ -2216,7 +2343,11 @@ function reconcileChildren(prevChildren, nextChildren, dom) {
2216
2343
  }
2217
2344
  }
2218
2345
  } else {
2219
- reconcileNodeChildren(prevChildren, nextChildren, prevChildrenLength, nextChildrenLength, dom);
2346
+ reconcileNodeChildren(prevChildren, nextChildren, prevChildrenLength, nextChildrenLength, element, dom);
2347
+ }
2348
+
2349
+ if ($textContentRequiresDoubleLinebreakAtEnd(element)) {
2350
+ subTreeTextContent += DOUBLE_LINE_BREAK;
2220
2351
  } // $FlowFixMe: internal field
2221
2352
 
2222
2353
 
@@ -2311,6 +2442,11 @@ function reconcileNode(key, parentDOM) {
2311
2442
  reconcileElementTerminatingLineBreak(prevChildren, nextChildren, dom);
2312
2443
  }
2313
2444
  }
2445
+
2446
+ if ($textContentRequiresDoubleLinebreakAtEnd(nextNode)) {
2447
+ subTreeTextContent += DOUBLE_LINE_BREAK;
2448
+ editorTextContent += DOUBLE_LINE_BREAK;
2449
+ }
2314
2450
  } else {
2315
2451
  const text = nextNode.getTextContent();
2316
2452
 
@@ -2320,6 +2456,9 @@ function reconcileNode(key, parentDOM) {
2320
2456
  if (decorator !== null) {
2321
2457
  reconcileDecorator(key, decorator);
2322
2458
  }
2459
+
2460
+ subTreeTextContent += text;
2461
+ editorTextContent += text;
2323
2462
  } else if ($isTextNode(nextNode) && !nextNode.isDirectionless()) {
2324
2463
  // Handle text content, for LTR, LTR cases.
2325
2464
  subTreeDirectionedTextContent += text;
@@ -2368,7 +2507,7 @@ function getNextSibling(element) {
2368
2507
  return element.nextSibling;
2369
2508
  }
2370
2509
 
2371
- function reconcileNodeChildren(prevChildren, nextChildren, prevChildrenLength, nextChildrenLength, dom) {
2510
+ function reconcileNodeChildren(prevChildren, nextChildren, prevChildrenLength, nextChildrenLength, element, dom) {
2372
2511
  const prevEndIndex = prevChildrenLength - 1;
2373
2512
  const nextEndIndex = nextChildrenLength - 1;
2374
2513
  let prevChildrenSet;
@@ -2582,11 +2721,15 @@ function reconcileSelection(prevSelection, nextSelection, editor, domSelection)
2582
2721
  const focusDOM = getElementByKeyOrThrow(editor, focusKey);
2583
2722
  const nextAnchorOffset = anchor.offset;
2584
2723
  const nextFocusOffset = focus.offset;
2724
+ const nextFormat = nextSelection.format;
2725
+ const isCollapsed = nextSelection.isCollapsed();
2585
2726
  let nextAnchorNode = anchorDOM;
2586
2727
  let nextFocusNode = focusDOM;
2728
+ let anchorFormatChanged = false;
2587
2729
 
2588
2730
  if (anchor.type === 'text') {
2589
2731
  nextAnchorNode = getDOMTextNode(anchorDOM);
2732
+ anchorFormatChanged = anchor.getNode().getFormat() !== nextFormat;
2590
2733
  }
2591
2734
 
2592
2735
  if (focus.type === 'text') {
@@ -2597,20 +2740,31 @@ function reconcileSelection(prevSelection, nextSelection, editor, domSelection)
2597
2740
 
2598
2741
  if (nextAnchorNode === null || nextFocusNode === null) {
2599
2742
  return;
2743
+ }
2744
+
2745
+ if (isCollapsed && (prevSelection === null || anchorFormatChanged || prevSelection.format !== nextFormat)) {
2746
+ markCollapsedSelectionFormat(nextFormat, nextAnchorOffset, anchorKey, performance.now());
2600
2747
  } // Diff against the native DOM selection to ensure we don't do
2601
- // an unnecessary selection update.
2748
+ // an unnecessary selection update. We also skip this check if
2749
+ // we're moving selection to within an element, as this can
2750
+ // sometimes be problematic around scrolling.
2602
2751
 
2603
2752
 
2604
2753
  if (anchorOffset === nextAnchorOffset && focusOffset === nextFocusOffset && anchorDOMNode === nextAnchorNode && focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
2605
- !(domSelection.type === 'Range' && nextSelection.isCollapsed())) {
2754
+ !(domSelection.type === 'Range' && isCollapsed)) {
2606
2755
  // If the root element does not have focus, ensure it has focus
2607
2756
  if (rootElement !== null && (activeElement === null || !rootElement.contains(activeElement))) {
2608
2757
  rootElement.focus({
2609
2758
  preventScroll: true
2610
2759
  });
2611
- }
2760
+ } // In Safari/iOS if we have selection on an element, then we also
2761
+ // need to additionally set the DOM selection, otherwise a selectionchange
2762
+ // event will not fire.
2612
2763
 
2613
- return;
2764
+
2765
+ if (!(IS_IOS || IS_SAFARI) || anchor.type !== 'element') {
2766
+ return;
2767
+ }
2614
2768
  } // Apply the updated selection to the DOM. Note: this will trigger
2615
2769
  // a "selectionchange" event, although it will be asynchronous.
2616
2770
 
@@ -2741,7 +2895,7 @@ function $normalizeAllDirtyTextNodes(editorState, editor) {
2741
2895
  for (const nodeKey of dirtyLeaves) {
2742
2896
  const node = nodeMap.get(nodeKey);
2743
2897
 
2744
- if ($isTextNode(node) && node.isSimpleText() && !node.isUnmergeable()) {
2898
+ if ($isTextNode(node) && node.isAttached() && node.isSimpleText() && !node.isUnmergeable()) {
2745
2899
  $normalizeTextNode(node);
2746
2900
  }
2747
2901
  }
@@ -2777,7 +2931,7 @@ function $applyAllTransforms(editorState, editor) {
2777
2931
  for (const nodeKey of untransformedDirtyLeaves) {
2778
2932
  const node = nodeMap.get(nodeKey);
2779
2933
 
2780
- if ($isTextNode(node) && node.isSimpleText() && !node.isUnmergeable()) {
2934
+ if ($isTextNode(node) && node.isAttached() && node.isSimpleText() && !node.isUnmergeable()) {
2781
2935
  $normalizeTextNode(node);
2782
2936
  }
2783
2937
 
@@ -2903,8 +3057,9 @@ function handleDEVOnlyPendingUpdateGuarantees(pendingEditorState) {
2903
3057
  function commitPendingUpdates(editor) {
2904
3058
  const pendingEditorState = editor._pendingEditorState;
2905
3059
  const rootElement = editor._rootElement;
3060
+ const headless = editor._headless;
2906
3061
 
2907
- if (rootElement === null || pendingEditorState === null) {
3062
+ if (rootElement === null && !headless || pendingEditorState === null) {
2908
3063
  return;
2909
3064
  }
2910
3065
 
@@ -2925,10 +3080,12 @@ function commitPendingUpdates(editor) {
2925
3080
  editor._updating = true;
2926
3081
 
2927
3082
  try {
2928
- const mutatedNodes = updateEditorState(rootElement, currentEditorState, pendingEditorState, currentSelection, pendingSelection, needsUpdate, editor);
3083
+ if (!headless && rootElement !== null) {
3084
+ const mutatedNodes = updateEditorState(rootElement, currentEditorState, pendingEditorState, currentSelection, pendingSelection, needsUpdate, editor);
2929
3085
 
2930
- if (mutatedNodes !== null) {
2931
- triggerMutationListeners(editor, currentEditorState, pendingEditorState, mutatedNodes);
3086
+ if (mutatedNodes !== null) {
3087
+ triggerMutationListeners(editor, currentEditorState, pendingEditorState, mutatedNodes);
3088
+ }
2932
3089
  }
2933
3090
  } catch (error) {
2934
3091
  // Report errors
@@ -3095,9 +3252,9 @@ function triggerDeferredUpdateCallbacks(editor) {
3095
3252
  }
3096
3253
  }
3097
3254
 
3098
- function processNestedUpdates(editor) {
3255
+ function processNestedUpdates(editor, initialSkipTransforms) {
3099
3256
  const queuedUpdates = editor._updates;
3100
- let skipTransforms = false; // Updates might grow as we process them, we so we'll need
3257
+ let skipTransforms = initialSkipTransforms || false; // Updates might grow as we process them, we so we'll need
3101
3258
  // to handle each update as we go until the updates array is
3102
3259
  // empty.
3103
3260
 
@@ -3169,13 +3326,13 @@ function beginUpdate(editor, updateFn, options) {
3169
3326
  activeEditor = editor;
3170
3327
 
3171
3328
  try {
3172
- if (editorStateWasCloned) {
3329
+ if (editorStateWasCloned && !editor._headless) {
3173
3330
  pendingEditorState._selection = internalCreateSelection(editor);
3174
3331
  }
3175
3332
 
3176
3333
  const startingCompositionKey = editor._compositionKey;
3177
3334
  updateFn();
3178
- skipTransforms = processNestedUpdates(editor);
3335
+ skipTransforms = processNestedUpdates(editor, skipTransforms);
3179
3336
  applySelectionTransforms(pendingEditorState, editor);
3180
3337
 
3181
3338
  if (editor._dirtyType !== NO_DIRTY_NODES) {
@@ -3506,6 +3663,8 @@ function initMutationObserver(editor) {
3506
3663
 
3507
3664
  class Point {
3508
3665
  constructor(key, offset, type) {
3666
+ // $FlowFixMe: is temporarily null
3667
+ this._selection = null;
3509
3668
  this.key = key;
3510
3669
  this.offset = offset;
3511
3670
  this.type = type;
@@ -3536,10 +3695,6 @@ class Point {
3536
3695
  return aNode.isBefore(bNode);
3537
3696
  }
3538
3697
 
3539
- getCharacterOffset() {
3540
- return this.type === 'text' ? this.offset : 0;
3541
- }
3542
-
3543
3698
  getNode() {
3544
3699
  const key = this.key;
3545
3700
  const node = $getNodeByKey(key);
@@ -3554,7 +3709,7 @@ class Point {
3554
3709
  }
3555
3710
 
3556
3711
  set(key, offset, type) {
3557
- const selection = $getSelection();
3712
+ const selection = this._selection;
3558
3713
  const oldKey = this.key;
3559
3714
  this.key = key;
3560
3715
  this.offset = offset;
@@ -3566,6 +3721,7 @@ class Point {
3566
3721
  }
3567
3722
 
3568
3723
  if (selection !== null && (selection.anchor === this || selection.focus === this)) {
3724
+ selection._cachedNodes = null;
3569
3725
  selection.dirty = true;
3570
3726
  }
3571
3727
  }
@@ -3642,6 +3798,7 @@ class NodeSelection {
3642
3798
  constructor(objects) {
3643
3799
  this.dirty = false;
3644
3800
  this._nodes = objects;
3801
+ this._cachedNodes = null;
3645
3802
  }
3646
3803
 
3647
3804
  is(selection) {
@@ -3658,18 +3815,24 @@ class NodeSelection {
3658
3815
  this.dirty = true;
3659
3816
 
3660
3817
  this._nodes.add(key);
3818
+
3819
+ this._cachedNodes = null;
3661
3820
  }
3662
3821
 
3663
3822
  delete(key) {
3664
3823
  this.dirty = true;
3665
3824
 
3666
3825
  this._nodes.delete(key);
3826
+
3827
+ this._cachedNodes = null;
3667
3828
  }
3668
3829
 
3669
3830
  clear() {
3670
3831
  this.dirty = true;
3671
3832
 
3672
3833
  this._nodes.clear();
3834
+
3835
+ this._cachedNodes = null;
3673
3836
  }
3674
3837
 
3675
3838
  has(key) {
@@ -3691,6 +3854,12 @@ class NodeSelection {
3691
3854
  }
3692
3855
 
3693
3856
  getNodes() {
3857
+ const cachedNodes = this._cachedNodes;
3858
+
3859
+ if (cachedNodes !== null) {
3860
+ return cachedNodes;
3861
+ }
3862
+
3694
3863
  const objects = this._nodes;
3695
3864
  const nodes = [];
3696
3865
 
@@ -3702,6 +3871,10 @@ class NodeSelection {
3702
3871
  }
3703
3872
  }
3704
3873
 
3874
+ if (!isCurrentlyReadOnlyMode()) {
3875
+ this._cachedNodes = nodes;
3876
+ }
3877
+
3705
3878
  return nodes;
3706
3879
  }
3707
3880
 
@@ -3721,13 +3894,14 @@ function $isRangeSelection(x) {
3721
3894
  return x instanceof RangeSelection;
3722
3895
  }
3723
3896
  class GridSelection {
3724
- constructor(gridKey, anchorCellKey, focusCellKey) {
3897
+ constructor(gridKey, anchor, focus) {
3725
3898
  this.gridKey = gridKey;
3726
- this.anchorCellKey = anchorCellKey;
3727
- this.anchor = $createPoint(anchorCellKey, 0, 'element');
3728
- this.focusCellKey = focusCellKey;
3729
- this.focus = $createPoint(focusCellKey, 0, 'element');
3899
+ this.anchor = anchor;
3900
+ this.focus = focus;
3730
3901
  this.dirty = false;
3902
+ this._cachedNodes = null;
3903
+ anchor._selection = this;
3904
+ focus._selection = this;
3731
3905
  }
3732
3906
 
3733
3907
  is(selection) {
@@ -3735,18 +3909,19 @@ class GridSelection {
3735
3909
  return false;
3736
3910
  }
3737
3911
 
3738
- return this.gridKey === selection.gridKey && this.anchorCellKey === selection.anchorCellKey && this.focusCellKey === selection.focusCellKey;
3912
+ return this.gridKey === selection.gridKey && this.anchor.is(this.focus);
3739
3913
  }
3740
3914
 
3741
3915
  set(gridKey, anchorCellKey, focusCellKey) {
3742
3916
  this.dirty = true;
3743
3917
  this.gridKey = gridKey;
3744
- this.anchorCellKey = anchorCellKey;
3745
- this.focusCellKey = focusCellKey;
3918
+ this.anchor.key = anchorCellKey;
3919
+ this.focus.key = focusCellKey;
3920
+ this._cachedNodes = null;
3746
3921
  }
3747
3922
 
3748
3923
  clone() {
3749
- return new GridSelection(this.gridKey, this.anchorCellKey, this.focusCellKey);
3924
+ return new GridSelection(this.gridKey, this.anchor, this.focus);
3750
3925
  }
3751
3926
 
3752
3927
  isCollapsed() {
@@ -3757,6 +3932,10 @@ class GridSelection {
3757
3932
  return this.focus.isBefore(this.anchor);
3758
3933
  }
3759
3934
 
3935
+ getCharacterOffsets() {
3936
+ return getCharacterOffsets(this);
3937
+ }
3938
+
3760
3939
  extract() {
3761
3940
  return this.getNodes();
3762
3941
  }
@@ -3768,7 +3947,7 @@ class GridSelection {
3768
3947
  }
3769
3948
 
3770
3949
  getShape() {
3771
- const anchorCellNode = $getNodeByKey(this.anchorCellKey);
3950
+ const anchorCellNode = $getNodeByKey(this.anchor.key);
3772
3951
 
3773
3952
  if (!anchorCellNode) {
3774
3953
  throw Error(`getNodes: expected to find AnchorNode`);
@@ -3776,7 +3955,7 @@ class GridSelection {
3776
3955
 
3777
3956
  const anchorCellNodeIndex = anchorCellNode.getIndexWithinParent();
3778
3957
  const anchorCelRoweIndex = anchorCellNode.getParentOrThrow().getIndexWithinParent();
3779
- const focusCellNode = $getNodeByKey(this.focusCellKey);
3958
+ const focusCellNode = $getNodeByKey(this.focus.key);
3780
3959
 
3781
3960
  if (!focusCellNode) {
3782
3961
  throw Error(`getNodes: expected to find FocusNode`);
@@ -3797,7 +3976,13 @@ class GridSelection {
3797
3976
  }
3798
3977
 
3799
3978
  getNodes() {
3800
- const nodes = new Set();
3979
+ const cachedNodes = this._cachedNodes;
3980
+
3981
+ if (cachedNodes !== null) {
3982
+ return cachedNodes;
3983
+ }
3984
+
3985
+ const nodesSet = new Set();
3801
3986
  const {
3802
3987
  fromX,
3803
3988
  fromY,
@@ -3812,12 +3997,12 @@ class GridSelection {
3812
3997
  }
3813
3998
  }
3814
3999
 
3815
- nodes.add(gridNode);
4000
+ nodesSet.add(gridNode);
3816
4001
  const gridRowNodes = gridNode.getChildren();
3817
4002
 
3818
4003
  for (let r = fromY; r <= toY; r++) {
3819
4004
  const gridRowNode = gridRowNodes[r];
3820
- nodes.add(gridRowNode);
4005
+ nodesSet.add(gridRowNode);
3821
4006
 
3822
4007
  if (!$isGridRowNode(gridRowNode)) {
3823
4008
  {
@@ -3836,12 +4021,12 @@ class GridSelection {
3836
4021
  }
3837
4022
  }
3838
4023
 
3839
- nodes.add(gridCellNode);
4024
+ nodesSet.add(gridCellNode);
3840
4025
  const children = gridCellNode.getChildren();
3841
4026
 
3842
4027
  while (children.length > 0) {
3843
4028
  const child = children.shift();
3844
- nodes.add(child);
4029
+ nodesSet.add(child);
3845
4030
 
3846
4031
  if ($isElementNode(child)) {
3847
4032
  children.unshift(...child.getChildren());
@@ -3850,7 +4035,13 @@ class GridSelection {
3850
4035
  }
3851
4036
  }
3852
4037
 
3853
- return Array.from(nodes);
4038
+ const nodes = Array.from(nodesSet);
4039
+
4040
+ if (!isCurrentlyReadOnlyMode()) {
4041
+ this._cachedNodes = nodes;
4042
+ }
4043
+
4044
+ return nodes;
3854
4045
  }
3855
4046
 
3856
4047
  getTextContent() {
@@ -3874,6 +4065,9 @@ class RangeSelection {
3874
4065
  this.focus = focus;
3875
4066
  this.dirty = false;
3876
4067
  this.format = format;
4068
+ this._cachedNodes = null;
4069
+ anchor._selection = this;
4070
+ focus._selection = this;
3877
4071
  }
3878
4072
 
3879
4073
  is(selection) {
@@ -3893,6 +4087,12 @@ class RangeSelection {
3893
4087
  }
3894
4088
 
3895
4089
  getNodes() {
4090
+ const cachedNodes = this._cachedNodes;
4091
+
4092
+ if (cachedNodes !== null) {
4093
+ return cachedNodes;
4094
+ }
4095
+
3896
4096
  const anchor = this.anchor;
3897
4097
  const focus = this.focus;
3898
4098
  let firstNode = anchor.getNode();
@@ -3906,20 +4106,29 @@ class RangeSelection {
3906
4106
  lastNode = lastNode.getDescendantByIndex(focus.offset);
3907
4107
  }
3908
4108
 
4109
+ let nodes;
4110
+
3909
4111
  if (firstNode.is(lastNode)) {
3910
- if ($isElementNode(firstNode)) {
3911
- return [];
4112
+ if ($isElementNode(firstNode) && (firstNode.getChildrenSize() > 0 || firstNode.excludeFromCopy())) {
4113
+ nodes = [];
4114
+ } else {
4115
+ nodes = [firstNode];
3912
4116
  }
4117
+ } else {
4118
+ nodes = firstNode.getNodesBetween(lastNode);
4119
+ }
3913
4120
 
3914
- return [firstNode];
4121
+ if (!isCurrentlyReadOnlyMode()) {
4122
+ this._cachedNodes = nodes;
3915
4123
  }
3916
4124
 
3917
- return firstNode.getNodesBetween(lastNode);
4125
+ return nodes;
3918
4126
  }
3919
4127
 
3920
4128
  setTextNodeRange(anchorNode, anchorOffset, focusNode, focusOffset) {
3921
4129
  $setPointValues(this.anchor, anchorNode.__key, anchorOffset, 'text');
3922
4130
  $setPointValues(this.focus, focusNode.__key, focusOffset, 'text');
4131
+ this._cachedNodes = null;
3923
4132
  this.dirty = true;
3924
4133
  }
3925
4134
 
@@ -3935,8 +4144,7 @@ class RangeSelection {
3935
4144
  const anchor = this.anchor;
3936
4145
  const focus = this.focus;
3937
4146
  const isBefore = anchor.isBefore(focus);
3938
- const anchorOffset = anchor.getCharacterOffset();
3939
- const focusOffset = focus.getCharacterOffset();
4147
+ const [anchorOffset, focusOffset] = getCharacterOffsets(this);
3940
4148
  let textContent = '';
3941
4149
  let prevWasElement = true;
3942
4150
 
@@ -3992,12 +4200,14 @@ class RangeSelection {
3992
4200
  const [anchorPoint, focusPoint] = resolvedSelectionPoints;
3993
4201
  $setPointValues(this.anchor, anchorPoint.key, anchorPoint.offset, anchorPoint.type);
3994
4202
  $setPointValues(this.focus, focusPoint.key, focusPoint.offset, focusPoint.type);
4203
+ this._cachedNodes = null;
3995
4204
  }
3996
4205
 
3997
4206
  clone() {
3998
4207
  const anchor = this.anchor;
3999
4208
  const focus = this.focus;
4000
- return new RangeSelection($createPoint(anchor.key, anchor.offset, anchor.type), $createPoint(focus.key, focus.offset, focus.type), this.format);
4209
+ const selection = new RangeSelection($createPoint(anchor.key, anchor.offset, anchor.type), $createPoint(focus.key, focus.offset, focus.type), this.format);
4210
+ return selection;
4001
4211
  }
4002
4212
 
4003
4213
  toggleFormat(format) {
@@ -4064,11 +4274,13 @@ class RangeSelection {
4064
4274
  const firstNodeText = firstNode.getTextContent();
4065
4275
  const firstNodeTextLength = firstNodeText.length;
4066
4276
  const firstNodeParent = firstNode.getParentOrThrow();
4277
+ const lastIndex = selectedNodesLength - 1;
4278
+ let lastNode = selectedNodes[lastIndex];
4067
4279
 
4068
4280
  if (this.isCollapsed() && startOffset === firstNodeTextLength && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextAfter() || !firstNodeParent.canInsertTextAfter())) {
4069
4281
  let nextSibling = firstNode.getNextSibling();
4070
4282
 
4071
- if (!$isTextNode(nextSibling) || $isTokenOrInert(nextSibling) || nextSibling.isSegmented()) {
4283
+ if (!$isTextNode(nextSibling) || $isTokenOrInertOrSegmented(nextSibling)) {
4072
4284
  nextSibling = $createTextNode();
4073
4285
 
4074
4286
  if (!firstNodeParent.canInsertTextAfter()) {
@@ -4088,7 +4300,7 @@ class RangeSelection {
4088
4300
  } else if (this.isCollapsed() && startOffset === 0 && (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextBefore() || !firstNodeParent.canInsertTextBefore())) {
4089
4301
  let prevSibling = firstNode.getPreviousSibling();
4090
4302
 
4091
- if (!$isTextNode(prevSibling) || $isTokenOrInert(prevSibling) || prevSibling.isSegmented()) {
4303
+ if (!$isTextNode(prevSibling) || $isTokenOrInertOrSegmented(prevSibling)) {
4092
4304
  prevSibling = $createTextNode();
4093
4305
 
4094
4306
  if (!firstNodeParent.canInsertTextBefore()) {
@@ -4109,6 +4321,19 @@ class RangeSelection {
4109
4321
  const textNode = $createTextNode(firstNode.getTextContent());
4110
4322
  firstNode.replace(textNode);
4111
4323
  firstNode = textNode;
4324
+ } else if (!this.isCollapsed() && text !== '') {
4325
+ // When the firstNode or lastNode parents are elements that
4326
+ // do not allow text to be inserted before or after, we first
4327
+ // clear the content. Then we normalize selection, then insert
4328
+ // the new content.
4329
+ const lastNodeParent = lastNode.getParent();
4330
+
4331
+ if (!firstNodeParent.canInsertTextBefore() || !firstNodeParent.canInsertTextAfter() || $isElementNode(lastNodeParent) && (!lastNodeParent.canInsertTextBefore() || !lastNodeParent.canInsertTextAfter())) {
4332
+ this.insertText('');
4333
+ normalizeSelectionPointsForBoundaries(this.anchor, this.focus, null);
4334
+ this.insertText(text);
4335
+ return;
4336
+ }
4112
4337
  }
4113
4338
 
4114
4339
  if (selectedNodesLength === 1) {
@@ -4157,11 +4382,24 @@ class RangeSelection {
4157
4382
  this.anchor.offset -= text.length;
4158
4383
  }
4159
4384
  } else {
4160
- const lastIndex = selectedNodesLength - 1;
4161
- let lastNode = selectedNodes[lastIndex];
4162
- const markedNodeKeysForKeep = new Set([...firstNode.getParentKeys(), ...lastNode.getParentKeys()]);
4385
+ const markedNodeKeysForKeep = new Set([...firstNode.getParentKeys(), ...lastNode.getParentKeys()]); // We have to get the parent elements before the next section,
4386
+ // as in that section we might mutate the lastNode.
4387
+
4163
4388
  const firstElement = $isElementNode(firstNode) ? firstNode : firstNode.getParentOrThrow();
4164
- const lastElement = $isElementNode(lastNode) ? lastNode : lastNode.getParentOrThrow(); // Handle mutations to the last node.
4389
+ let lastElement = $isElementNode(lastNode) ? lastNode : lastNode.getParentOrThrow();
4390
+ let lastElementChild = lastNode; // If the last element is inline, we should instead look at getting
4391
+ // the nodes of its parent, rather than itself. This behavior will
4392
+ // then better match how text node insertions work. We will need to
4393
+ // also update the last element's child accordingly as we do this.
4394
+
4395
+ if (!firstElement.is(lastElement) && lastElement.isInline()) {
4396
+ // Keep traversing till we have a non-inline element parent.
4397
+ do {
4398
+ lastElementChild = lastElement;
4399
+ lastElement = lastElement.getParentOrThrow();
4400
+ } while (lastElement.isInline());
4401
+ } // Handle mutations to the last node.
4402
+
4165
4403
 
4166
4404
  if (endPoint.type === 'text' && (endOffset !== 0 || lastNode.getTextContent() === '') || endPoint.type === 'element' && lastNode.getIndexWithinParent() < endOffset) {
4167
4405
  if ($isTextNode(lastNode) && !$isTokenOrInert(lastNode) && endOffset !== lastNode.getTextContentSize()) {
@@ -4174,7 +4412,13 @@ class RangeSelection {
4174
4412
  lastNode = lastNode.spliceText(0, endOffset, '');
4175
4413
  markedNodeKeysForKeep.add(lastNode.__key);
4176
4414
  } else {
4177
- lastNode.remove();
4415
+ const lastNodeParent = lastNode.getParentOrThrow();
4416
+
4417
+ if (!lastNodeParent.canBeEmpty() && lastNodeParent.getChildrenSize() === 1) {
4418
+ lastNodeParent.remove();
4419
+ } else {
4420
+ lastNode.remove();
4421
+ }
4178
4422
  }
4179
4423
  } else {
4180
4424
  markedNodeKeysForKeep.add(lastNode.__key);
@@ -4185,48 +4429,50 @@ class RangeSelection {
4185
4429
 
4186
4430
  const lastNodeChildren = lastElement.getChildren();
4187
4431
  const selectedNodesSet = new Set(selectedNodes);
4188
- const firstAndLastElementsAreEqual = firstElement.is(lastElement); // If the last element is an "inline" element, don't move it's text nodes to the first node.
4189
- // Instead, preserve the "inline" element's children and append to the first element.
4432
+ const firstAndLastElementsAreEqual = firstElement.is(lastElement); // We choose a target to insert all nodes after. In the case of having
4433
+ // and inline starting parent element with a starting node that has no
4434
+ // siblings, we should insert after the starting parent element, otherwise
4435
+ // we will incorrectly merge into the starting parent element.
4436
+ // TODO: should we keep on traversing parents if we're inside another
4437
+ // nested inline element?
4190
4438
 
4191
- if (!lastElement.canBeEmpty() && firstElement !== lastElement) {
4192
- firstElement.append(lastElement);
4193
- } else {
4194
- for (let i = lastNodeChildren.length - 1; i >= 0; i--) {
4195
- const lastNodeChild = lastNodeChildren[i];
4439
+ const insertionTarget = firstElement.isInline() && firstNode.getNextSibling() === null ? firstElement : firstNode;
4196
4440
 
4197
- if (lastNodeChild.is(firstNode) || $isElementNode(lastNodeChild) && lastNodeChild.isParentOf(firstNode)) {
4198
- break;
4199
- }
4441
+ for (let i = lastNodeChildren.length - 1; i >= 0; i--) {
4442
+ const lastNodeChild = lastNodeChildren[i];
4200
4443
 
4201
- if (lastNodeChild.isAttached()) {
4202
- if (!selectedNodesSet.has(lastNodeChild) || lastNodeChild.is(lastNode)) {
4203
- if (!firstAndLastElementsAreEqual) {
4204
- firstNode.insertAfter(lastNodeChild);
4205
- }
4206
- } else {
4207
- lastNodeChild.remove();
4444
+ if (lastNodeChild.is(firstNode) || $isElementNode(lastNodeChild) && lastNodeChild.isParentOf(firstNode)) {
4445
+ break;
4446
+ }
4447
+
4448
+ if (lastNodeChild.isAttached()) {
4449
+ if (!selectedNodesSet.has(lastNodeChild) || lastNodeChild.is(lastElementChild)) {
4450
+ if (!firstAndLastElementsAreEqual) {
4451
+ insertionTarget.insertAfter(lastNodeChild);
4208
4452
  }
4453
+ } else {
4454
+ lastNodeChild.remove();
4209
4455
  }
4210
4456
  }
4457
+ }
4211
4458
 
4212
- if (!firstAndLastElementsAreEqual) {
4213
- // Check if we have already moved out all the nodes of the
4214
- // last parent, and if so, traverse the parent tree and mark
4215
- // them all as being able to deleted too.
4216
- let parent = lastElement;
4217
- let lastRemovedParent = null;
4459
+ if (!firstAndLastElementsAreEqual) {
4460
+ // Check if we have already moved out all the nodes of the
4461
+ // last parent, and if so, traverse the parent tree and mark
4462
+ // them all as being able to deleted too.
4463
+ let parent = lastElement;
4464
+ let lastRemovedParent = null;
4218
4465
 
4219
- while (parent !== null) {
4220
- const children = parent.getChildren();
4221
- const childrenLength = children.length;
4466
+ while (parent !== null) {
4467
+ const children = parent.getChildren();
4468
+ const childrenLength = children.length;
4222
4469
 
4223
- if (childrenLength === 0 || children[childrenLength - 1].is(lastRemovedParent)) {
4224
- markedNodeKeysForKeep.delete(parent.__key);
4225
- lastRemovedParent = parent;
4226
- }
4227
-
4228
- parent = parent.getParent();
4470
+ if (childrenLength === 0 || children[childrenLength - 1].is(lastRemovedParent)) {
4471
+ markedNodeKeysForKeep.delete(parent.__key);
4472
+ lastRemovedParent = parent;
4229
4473
  }
4474
+
4475
+ parent = parent.getParent();
4230
4476
  }
4231
4477
  } // Ensure we do splicing after moving of nodes, as splicing
4232
4478
  // can have side-effects (in the case of hashtags).
@@ -4283,10 +4529,9 @@ class RangeSelection {
4283
4529
 
4284
4530
  const anchor = this.anchor;
4285
4531
  const focus = this.focus;
4286
- const firstNodeText = firstNode.getTextContent();
4287
- const firstNodeTextLength = firstNodeText.length;
4288
4532
  const focusOffset = focus.offset;
4289
4533
  let firstNextFormat = 0;
4534
+ let firstNodeTextLength = firstNode.getTextContent().length;
4290
4535
 
4291
4536
  for (let i = 0; i < selectedNodes.length; i++) {
4292
4537
  const selectedNode = selectedNodes[i];
@@ -4306,13 +4551,18 @@ class RangeSelection {
4306
4551
  // first node so we don't want to include it in the formatting change.
4307
4552
 
4308
4553
  if (startOffset === firstNode.getTextContentSize()) {
4309
- const nextSibling = firstNode.getNextSibling();
4554
+ let nextSibling = firstNode.getNextSibling();
4555
+
4556
+ if ($isElementNode(nextSibling) && nextSibling.isInline()) {
4557
+ nextSibling = nextSibling.getFirstChild();
4558
+ }
4310
4559
 
4311
4560
  if ($isTextNode(nextSibling)) {
4312
4561
  // we basically make the second node the firstNode, changing offsets accordingly
4313
4562
  anchorOffset = 0;
4314
4563
  startOffset = 0;
4315
4564
  firstNode = nextSibling;
4565
+ firstNodeTextLength = nextSibling.getTextContent().length;
4316
4566
  firstNextFormat = firstNode.getFormatFlags(formatType, null);
4317
4567
  }
4318
4568
  } // This is the case where we only selected a single node
@@ -4458,7 +4708,7 @@ class RangeSelection {
4458
4708
  for (let i = 0; i < nodes.length; i++) {
4459
4709
  const node = nodes[i];
4460
4710
 
4461
- if ($isElementNode(node)) {
4711
+ if ($isElementNode(node) && !node.isInline()) {
4462
4712
  // -----
4463
4713
  // Heuristics for the replacment or merging of elements
4464
4714
  // -----
@@ -4554,7 +4804,7 @@ class RangeSelection {
4554
4804
 
4555
4805
  didReplaceOrMerge = false;
4556
4806
 
4557
- if ($isElementNode(target)) {
4807
+ if ($isElementNode(target) && !target.isInline()) {
4558
4808
  lastNodeInserted = node;
4559
4809
 
4560
4810
  if ($isDecoratorNode(node) && node.isTopLevel()) {
@@ -4588,7 +4838,7 @@ class RangeSelection {
4588
4838
  target = target.insertAfter(node);
4589
4839
  }
4590
4840
  }
4591
- } else if (!$isElementNode(node) || $isDecoratorNode(target) && target.isTopLevel()) {
4841
+ } else if (!$isElementNode(node) || $isElementNode(node) && node.isInline() || $isDecoratorNode(target) && target.isTopLevel()) {
4592
4842
  lastNodeInserted = node;
4593
4843
  target = target.insertAfter(node);
4594
4844
  } else {
@@ -4738,7 +4988,17 @@ class RangeSelection {
4738
4988
  const nodesToMoveLength = nodesToMove.length;
4739
4989
 
4740
4990
  if (anchorOffset === 0 && nodesToMoveLength > 0 && currentElement.isInline()) {
4741
- currentElement.getParentOrThrow().insertBefore($createParagraphNode());
4991
+ const parent = currentElement.getParentOrThrow();
4992
+ const newElement = parent.insertNewAfter(this);
4993
+
4994
+ if ($isElementNode(newElement)) {
4995
+ const children = parent.getChildren();
4996
+
4997
+ for (let i = 0; i < children.length; i++) {
4998
+ newElement.append(children[i]);
4999
+ }
5000
+ }
5001
+
4742
5002
  return;
4743
5003
  }
4744
5004
 
@@ -4812,6 +5072,10 @@ class RangeSelection {
4812
5072
  }
4813
5073
  }
4814
5074
 
5075
+ getCharacterOffsets() {
5076
+ return getCharacterOffsets(this);
5077
+ }
5078
+
4815
5079
  extract() {
4816
5080
  const selectedNodes = this.getNodes();
4817
5081
  const selectedNodesLength = selectedNodes.length;
@@ -4820,8 +5084,7 @@ class RangeSelection {
4820
5084
  const focus = this.focus;
4821
5085
  let firstNode = selectedNodes[0];
4822
5086
  let lastNode = selectedNodes[lastIndex];
4823
- const anchorOffset = anchor.getCharacterOffset();
4824
- const focusOffset = focus.getCharacterOffset();
5087
+ const [anchorOffset, focusOffset] = getCharacterOffsets(this);
4825
5088
 
4826
5089
  if (selectedNodesLength === 0) {
4827
5090
  return [];
@@ -4899,6 +5162,16 @@ class RangeSelection {
4899
5162
  anchor.set(elementKey, offset, 'element');
4900
5163
  }
4901
5164
 
5165
+ return;
5166
+ } else {
5167
+ const siblingKey = sibling.__key;
5168
+ const offset = isBackward ? sibling.getTextContent().length : 0;
5169
+ focus.set(siblingKey, offset, 'text');
5170
+
5171
+ if (collapse) {
5172
+ anchor.set(siblingKey, offset, 'text');
5173
+ }
5174
+
4902
5175
  return;
4903
5176
  }
4904
5177
  }
@@ -4916,7 +5189,8 @@ class RangeSelection {
4916
5189
  const range = domSelection.getRangeAt(0); // Apply the DOM selection to our Lexical selection.
4917
5190
  // $FlowFixMe[incompatible-call]
4918
5191
 
4919
- this.applyDOMRange(range); // Because a range works on start and end, we might need to flip
5192
+ this.applyDOMRange(range);
5193
+ this.dirty = true; // Because a range works on start and end, we might need to flip
4920
5194
  // the anchor and focus points to match what the DOM has, not what
4921
5195
  // the range has specifically.
4922
5196
 
@@ -5001,6 +5275,29 @@ function $isNodeSelection(x) {
5001
5275
  return x instanceof NodeSelection;
5002
5276
  }
5003
5277
 
5278
+ function getCharacterOffset(point) {
5279
+ const offset = point.offset;
5280
+
5281
+ if (point.type === 'text') {
5282
+ return offset;
5283
+ } // $FlowFixMe: cast
5284
+
5285
+
5286
+ const parent = point.getNode();
5287
+ return offset === parent.getChildrenSize() ? parent.getTextContent().length : 0;
5288
+ }
5289
+
5290
+ function getCharacterOffsets(selection) {
5291
+ const anchor = selection.anchor;
5292
+ const focus = selection.focus;
5293
+
5294
+ if (anchor.type === 'element' && focus.type === 'element' && anchor.key === focus.key && anchor.offset === focus.offset) {
5295
+ return [0, 0];
5296
+ }
5297
+
5298
+ return [getCharacterOffset(anchor), getCharacterOffset(focus)];
5299
+ }
5300
+
5004
5301
  function $swapPoints(selection) {
5005
5302
  const focus = selection.focus;
5006
5303
  const anchor = selection.anchor;
@@ -5009,6 +5306,7 @@ function $swapPoints(selection) {
5009
5306
  const anchorType = anchor.type;
5010
5307
  $setPointValues(anchor, focus.key, focus.offset, focus.type);
5011
5308
  $setPointValues(focus, anchorKey, anchorOffset, anchorType);
5309
+ selection._cachedNodes = null;
5012
5310
  }
5013
5311
 
5014
5312
  function $moveNativeSelection(domSelection, alter, direction, granularity) {
@@ -5048,7 +5346,7 @@ function $updateCaretSelectionForUnicodeCharacter(selection, isBackward) {
5048
5346
  function $removeSegment(node, isBackward, offset) {
5049
5347
  const textNode = node;
5050
5348
  const textContent = textNode.getTextContent();
5051
- const split = textContent.split(/\s/g);
5349
+ const split = textContent.split(/(?=\s)/g);
5052
5350
  const splitLength = split.length;
5053
5351
  let segmentOffset = 0;
5054
5352
  let restoreOffset = 0;
@@ -5070,7 +5368,7 @@ function $removeSegment(node, isBackward, offset) {
5070
5368
  }
5071
5369
  }
5072
5370
 
5073
- const nextTextContent = split.join(' ');
5371
+ const nextTextContent = split.join('').trim();
5074
5372
 
5075
5373
  if (nextTextContent === '') {
5076
5374
  textNode.remove();
@@ -5169,6 +5467,78 @@ function internalResolveSelectionPoint(dom, offset, lastPoint) {
5169
5467
  return $createPoint(resolvedNode.__key, resolvedOffset, 'text');
5170
5468
  }
5171
5469
 
5470
+ function resolveSelectionPointOnBoundary(point, isBackward, isCollapsed) {
5471
+ const offset = point.offset;
5472
+ const node = point.getNode();
5473
+
5474
+ if (offset === 0) {
5475
+ const prevSibling = node.getPreviousSibling();
5476
+ const parent = node.getParent();
5477
+
5478
+ if (!isBackward) {
5479
+ if ($isElementNode(prevSibling) && !isCollapsed && prevSibling.isInline()) {
5480
+ point.key = prevSibling.__key;
5481
+ point.offset = prevSibling.getChildrenSize(); // $FlowFixMe: intentional
5482
+
5483
+ point.type = 'element';
5484
+ } else if ($isTextNode(prevSibling) && !prevSibling.isInert()) {
5485
+ point.key = prevSibling.__key;
5486
+ point.offset = prevSibling.getTextContent().length;
5487
+ }
5488
+ } else if ((isCollapsed || !isBackward) && prevSibling === null && $isElementNode(parent) && parent.isInline()) {
5489
+ const parentSibling = parent.getPreviousSibling();
5490
+
5491
+ if ($isTextNode(parentSibling)) {
5492
+ point.key = parentSibling.__key;
5493
+ point.offset = parentSibling.getTextContent().length;
5494
+ }
5495
+ }
5496
+ } else if (offset === node.getTextContent().length) {
5497
+ const nextSibling = node.getNextSibling();
5498
+ const parent = node.getParent();
5499
+
5500
+ if (isBackward && $isElementNode(nextSibling) && nextSibling.isInline()) {
5501
+ point.key = nextSibling.__key;
5502
+ point.offset = 0; // $FlowFixMe: intentional
5503
+
5504
+ point.type = 'element';
5505
+ } else if ((isCollapsed || isBackward) && nextSibling === null && $isElementNode(parent) && parent.isInline() && !parent.canInsertTextAfter()) {
5506
+ const parentSibling = parent.getNextSibling();
5507
+
5508
+ if ($isTextNode(parentSibling)) {
5509
+ point.key = parentSibling.__key;
5510
+ point.offset = 0;
5511
+ }
5512
+ }
5513
+ }
5514
+ }
5515
+
5516
+ function normalizeSelectionPointsForBoundaries(anchor, focus, lastSelection) {
5517
+ if (anchor.type === 'text' && focus.type === 'text') {
5518
+ const isBackward = anchor.isBefore(focus);
5519
+ const isCollapsed = anchor.is(focus); // Attempt to normalize the offset to the previous sibling if we're at the
5520
+ // start of a text node and the sibling is a text node or inline element.
5521
+
5522
+ resolveSelectionPointOnBoundary(anchor, isBackward, isCollapsed);
5523
+ resolveSelectionPointOnBoundary(focus, !isBackward, isCollapsed);
5524
+
5525
+ if (isCollapsed) {
5526
+ focus.key = anchor.key;
5527
+ focus.offset = anchor.offset;
5528
+ focus.type = anchor.type;
5529
+ }
5530
+
5531
+ const editor = getActiveEditor();
5532
+
5533
+ if (editor.isComposing() && editor._compositionKey !== anchor.key && $isRangeSelection(lastSelection)) {
5534
+ const lastAnchor = lastSelection.anchor;
5535
+ const lastFocus = lastSelection.focus;
5536
+ $setPointValues(anchor, lastAnchor.key, lastAnchor.offset, lastAnchor.type);
5537
+ $setPointValues(focus, lastFocus.key, lastFocus.offset, lastFocus.type);
5538
+ }
5539
+ }
5540
+ }
5541
+
5172
5542
  function internalResolveSelectionPoints(anchorDOM, anchorOffset, focusDOM, focusOffset, editor, lastSelection) {
5173
5543
  if (anchorDOM === null || focusDOM === null || !isSelectionWithinEditor(editor, anchorDOM, focusDOM)) {
5174
5544
  return null;
@@ -5186,46 +5556,19 @@ function internalResolveSelectionPoints(anchorDOM, anchorOffset, focusDOM, focus
5186
5556
  return null;
5187
5557
  }
5188
5558
 
5189
- if (resolvedAnchorPoint.type === 'text' && resolvedFocusPoint.type === 'text') {
5190
- const resolvedAnchorNode = resolvedAnchorPoint.getNode();
5191
- const resolvedFocusNode = resolvedFocusPoint.getNode(); // Handle normalization of selection when it is at the boundaries.
5192
-
5193
- const textContentSize = resolvedAnchorNode.getTextContentSize();
5194
- const resolvedAnchorOffset = resolvedAnchorPoint.offset;
5195
- const resolvedFocusOffset = resolvedFocusPoint.offset;
5196
-
5197
- if (resolvedAnchorNode === resolvedFocusNode && resolvedAnchorOffset === resolvedFocusOffset) {
5198
- if (anchorOffset === 0) {
5199
- const prevSibling = resolvedAnchorNode.getPreviousSibling();
5200
-
5201
- if ($isTextNode(prevSibling) && !prevSibling.isInert()) {
5202
- const offset = prevSibling.getTextContentSize();
5203
- const key = prevSibling.__key;
5204
- resolvedAnchorPoint.key = key;
5205
- resolvedFocusPoint.key = key;
5206
- resolvedAnchorPoint.offset = offset;
5207
- resolvedFocusPoint.offset = offset;
5208
- }
5209
- }
5210
- } else {
5211
- if (resolvedAnchorOffset === textContentSize) {
5212
- const nextSibling = resolvedAnchorNode.getNextSibling();
5559
+ if (resolvedAnchorPoint.type === 'element' && resolvedFocusPoint.type === 'element') {
5560
+ const anchorNode = getNodeFromDOM(anchorDOM);
5561
+ const focusNode = getNodeFromDOM(focusDOM); // Ensure if we're selecting the content of a decorator that we
5562
+ // return null for this point, as it's not in the controlled scope
5563
+ // of Lexical.
5213
5564
 
5214
- if ($isTextNode(nextSibling) && !nextSibling.isInert()) {
5215
- resolvedAnchorPoint.key = nextSibling.__key;
5216
- resolvedAnchorPoint.offset = 0;
5217
- }
5218
- }
5565
+ if ($isDecoratorNode(anchorNode) && $isDecoratorNode(focusNode)) {
5566
+ return null;
5219
5567
  }
5568
+ } // Handle normalization of selection when it is at the boundaries.
5220
5569
 
5221
- if (editor.isComposing() && editor._compositionKey !== resolvedAnchorPoint.key && $isRangeSelection(lastSelection)) {
5222
- const lastAnchor = lastSelection.anchor;
5223
- const lastFocus = lastSelection.focus;
5224
- $setPointValues(resolvedAnchorPoint, lastAnchor.key, lastAnchor.offset, lastAnchor.type);
5225
- $setPointValues(resolvedFocusPoint, lastFocus.key, lastFocus.offset, lastFocus.type);
5226
- }
5227
- }
5228
5570
 
5571
+ normalizeSelectionPointsForBoundaries(resolvedAnchorPoint, resolvedFocusPoint, lastSelection);
5229
5572
  return [resolvedAnchorPoint, resolvedFocusPoint];
5230
5573
  } // This is used to make a selection when the existing
5231
5574
  // selection is null, i.e. forcing selection on the editor
@@ -5248,7 +5591,9 @@ function $createEmptyObjectSelection() {
5248
5591
  return new NodeSelection(new Set());
5249
5592
  }
5250
5593
  function $createEmptyGridSelection() {
5251
- return new GridSelection('root', 'root', 'root');
5594
+ const anchor = $createPoint('root', 0, 'element');
5595
+ const focus = $createPoint('root', 0, 'element');
5596
+ return new GridSelection('root', anchor, focus);
5252
5597
  }
5253
5598
 
5254
5599
  function getActiveEventType() {
@@ -5327,7 +5672,7 @@ function internalCreateSelectionFromParse(parsedSelection) {
5327
5672
  } else if (parsedSelection.type === 'node') {
5328
5673
  return new NodeSelection(new Set(parsedSelection.nodes));
5329
5674
  } else if (parsedSelection.type === 'grid') {
5330
- return new GridSelection(parsedSelection.gridKey, parsedSelection.anchorCellKey, parsedSelection.focusCellKey);
5675
+ return new GridSelection(parsedSelection.gridKey, $createPoint(parsedSelection.anchor.key, parsedSelection.anchor.offset, parsedSelection.anchor.type), $createPoint(parsedSelection.focus.key, parsedSelection.focus.offset, parsedSelection.focus.type));
5331
5676
  }
5332
5677
  }
5333
5678
 
@@ -5669,8 +6014,7 @@ class LexicalNode {
5669
6014
  return false;
5670
6015
  }
5671
6016
 
5672
- const selectedNodeKeys = new Set(selection.getNodes().map(n => n.__key));
5673
- const isSelected = selectedNodeKeys.has(this.__key);
6017
+ const isSelected = selection.getNodes().some(n => n.__key === this.__key);
5674
6018
 
5675
6019
  if ($isTextNode(this)) {
5676
6020
  return isSelected;
@@ -6024,11 +6368,6 @@ class LexicalNode {
6024
6368
  const editor = getActiveEditor();
6025
6369
  const dirtyLeaves = editor._dirtyLeaves;
6026
6370
  return dirtyLeaves !== null && dirtyLeaves.has(this.__key);
6027
- } // TODO remove this and move to TextNode
6028
-
6029
-
6030
- isComposing() {
6031
- return this.__key === $getCompositionKey();
6032
6371
  }
6033
6372
 
6034
6373
  getLatest() {
@@ -6054,6 +6393,11 @@ class LexicalNode {
6054
6393
  const latestNode = this.getLatest();
6055
6394
  const parent = latestNode.__parent;
6056
6395
  const cloneNotNeeded = editor._cloneNotNeeded;
6396
+ const selection = $getSelection();
6397
+
6398
+ if (selection !== null) {
6399
+ selection._cachedNodes = null;
6400
+ }
6057
6401
 
6058
6402
  if (cloneNotNeeded.has(key)) {
6059
6403
  // Transforms clear the dirty node set on each iteration to keep track on newly dirty nodes
@@ -6084,34 +6428,29 @@ class LexicalNode {
6084
6428
  nodeMap.set(key, mutableNode); // $FlowFixMe this is LexicalNode
6085
6429
 
6086
6430
  return mutableNode;
6087
- } // TODO remove this completely
6088
-
6431
+ }
6089
6432
 
6090
6433
  getTextContent(includeInert, includeDirectionless) {
6091
6434
  return '';
6092
- } // TODO remove this completely
6093
-
6435
+ }
6094
6436
 
6095
6437
  getTextContentSize(includeInert, includeDirectionless) {
6096
6438
  return this.getTextContent(includeInert, includeDirectionless).length;
6097
6439
  } // View
6098
- // $FlowFixMe: Revise typings for EditorContext
6099
6440
 
6100
6441
 
6101
6442
  createDOM(config, editor) {
6102
6443
  {
6103
6444
  throw Error(`createDOM: base method not extended`);
6104
6445
  }
6105
- } // $FlowFixMe: Revise typings for EditorContext
6106
-
6446
+ }
6107
6447
 
6108
6448
  updateDOM( // $FlowFixMe: TODO
6109
6449
  prevNode, dom, config) {
6110
6450
  {
6111
6451
  throw Error(`updateDOM: base method not extended`);
6112
6452
  }
6113
- } // $FlowFixMe: Revise typings for EditorContext
6114
-
6453
+ }
6115
6454
 
6116
6455
  exportDOM(editor) {
6117
6456
  if ($isDecoratorNode(this)) {
@@ -6428,6 +6767,12 @@ class ElementNode extends LexicalNode {
6428
6767
  return dirtyElements !== null && dirtyElements.has(this.__key);
6429
6768
  }
6430
6769
 
6770
+ isLastChild() {
6771
+ const self = this.getLatest();
6772
+ const parent = self.getParentOrThrow();
6773
+ return parent.getLastChild() === self;
6774
+ }
6775
+
6431
6776
  getAllTextNodes(includeInert) {
6432
6777
  const textNodes = [];
6433
6778
  const self = this.getLatest();
@@ -6562,7 +6907,7 @@ class ElementNode extends LexicalNode {
6562
6907
  textContent += child.getTextContent(includeInert, includeDirectionless);
6563
6908
 
6564
6909
  if ($isElementNode(child) && i !== childrenLength - 1 && !child.isInline()) {
6565
- textContent += '\n\n';
6910
+ textContent += DOUBLE_LINE_BREAK;
6566
6911
  }
6567
6912
  }
6568
6913
 
@@ -6791,6 +7136,10 @@ class ElementNode extends LexicalNode {
6791
7136
  return false;
6792
7137
  }
6793
7138
 
7139
+ canIndent() {
7140
+ return true;
7141
+ }
7142
+
6794
7143
  collapseAtStart(selection) {
6795
7144
  return false;
6796
7145
  }
@@ -6831,6 +7180,10 @@ class ElementNode extends LexicalNode {
6831
7180
  return false;
6832
7181
  }
6833
7182
 
7183
+ extractWithChild(child, selection, destination) {
7184
+ return false;
7185
+ }
7186
+
6834
7187
  }
6835
7188
  function $isElementNode(node) {
6836
7189
  return node instanceof ElementNode;
@@ -7011,8 +7364,16 @@ class EditorState {
7011
7364
  nodes: Array.from(selection._nodes),
7012
7365
  type: 'node'
7013
7366
  } : $isGridSelection(selection) ? {
7014
- anchorCellKey: selection.anchorCellKey,
7015
- focusCellKey: selection.focusCellKey,
7367
+ anchor: {
7368
+ key: selection.anchor.key,
7369
+ offset: selection.anchor.offset,
7370
+ type: selection.anchor.type
7371
+ },
7372
+ focus: {
7373
+ key: selection.focus.key,
7374
+ offset: selection.focus.offset,
7375
+ type: selection.focus.type
7376
+ },
7016
7377
  gridKey: selection.gridKey,
7017
7378
  type: 'grid'
7018
7379
  } : null
@@ -7259,6 +7620,10 @@ class TextNode extends LexicalNode {
7259
7620
  return self.__mode === IS_TOKEN;
7260
7621
  }
7261
7622
 
7623
+ isComposing() {
7624
+ return this.__key === $getCompositionKey();
7625
+ }
7626
+
7262
7627
  isSegmented() {
7263
7628
  const self = this.getLatest();
7264
7629
  return self.__mode === IS_SEGMENTED;
@@ -7302,7 +7667,6 @@ class TextNode extends LexicalNode {
7302
7667
  const format = self.__format;
7303
7668
  return toggleTextFormatType(format, type, alignWithFormat);
7304
7669
  } // View
7305
- // $FlowFixMe: Revise typings for EditorContext
7306
7670
 
7307
7671
 
7308
7672
  createDOM(config) {
@@ -7327,8 +7691,7 @@ class TextNode extends LexicalNode {
7327
7691
  }
7328
7692
 
7329
7693
  return dom;
7330
- } // $FlowFixMe: Revise typings for EditorContext
7331
-
7694
+ }
7332
7695
 
7333
7696
  updateDOM(prevNode, dom, config) {
7334
7697
  const nextText = this.__text;
@@ -7540,7 +7903,8 @@ class TextNode extends LexicalNode {
7540
7903
  }
7541
7904
 
7542
7905
  const updatedText = text.slice(0, index) + newText + text.slice(index + delCount);
7543
- return writableSelf.setTextContent(updatedText);
7906
+ writableSelf.__text = updatedText;
7907
+ return writableSelf;
7544
7908
  }
7545
7909
 
7546
7910
  canInsertTextBefore() {
@@ -7705,10 +8069,12 @@ class TextNode extends LexicalNode {
7705
8069
  }
7706
8070
  }
7707
8071
 
7708
- const newText = isBefore ? target.__text + text : text + target.__text;
8072
+ const targetText = target.__text;
8073
+ const newText = isBefore ? targetText + text : text + targetText;
7709
8074
  this.setTextContent(newText);
8075
+ const writableSelf = this.getWritable();
7710
8076
  target.remove();
7711
- return this.getLatest();
8077
+ return writableSelf;
7712
8078
  }
7713
8079
 
7714
8080
  isTextEntity() {
@@ -7982,7 +8348,6 @@ function createEditor(editorConfig) {
7982
8348
  const config = editorConfig || {};
7983
8349
  const namespace = config.namespace || createUID();
7984
8350
  const theme = config.theme || {};
7985
- const context = config.context || {};
7986
8351
  const parentEditor = config.parentEditor || null;
7987
8352
  const disableEvents = config.disableEvents || false;
7988
8353
  const editorState = createEmptyEditorState();
@@ -8004,8 +8369,6 @@ function createEditor(editorConfig) {
8004
8369
 
8005
8370
 
8006
8371
  const editor = new LexicalEditor(editorState, parentEditor, registeredNodes, {
8007
- // $FlowFixMe: we use our internal type to simpify the generics
8008
- context,
8009
8372
  disableEvents,
8010
8373
  namespace,
8011
8374
  theme
@@ -8066,6 +8429,7 @@ class LexicalEditor {
8066
8429
  this._onError = onError;
8067
8430
  this._htmlConversions = htmlConversions;
8068
8431
  this._readOnly = false;
8432
+ this._headless = false;
8069
8433
  }
8070
8434
 
8071
8435
  isComposing() {
@@ -8288,8 +8652,8 @@ class LexicalEditor {
8288
8652
  commitPendingUpdates(this);
8289
8653
  }
8290
8654
 
8291
- parseEditorState(stringifiedEditorState) {
8292
- const parsedEditorState = JSON.parse(stringifiedEditorState);
8655
+ parseEditorState(maybeStringifiedEditorState) {
8656
+ const parsedEditorState = typeof maybeStringifiedEditorState === 'string' ? JSON.parse(maybeStringifiedEditorState) : maybeStringifiedEditorState;
8293
8657
  return parseEditorState(parsedEditorState, this);
8294
8658
  }
8295
8659
 
@@ -8364,7 +8728,7 @@ class LexicalEditor {
8364
8728
  *
8365
8729
  *
8366
8730
  */
8367
- const VERSION = '0.2.4';
8731
+ const VERSION = '0.2.7';
8368
8732
 
8369
8733
  /**
8370
8734
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -8455,6 +8819,7 @@ exports.CUT_COMMAND = CUT_COMMAND;
8455
8819
  exports.DELETE_CHARACTER_COMMAND = DELETE_CHARACTER_COMMAND;
8456
8820
  exports.DELETE_LINE_COMMAND = DELETE_LINE_COMMAND;
8457
8821
  exports.DELETE_WORD_COMMAND = DELETE_WORD_COMMAND;
8822
+ exports.DRAGEND_COMMAND = DRAGEND_COMMAND;
8458
8823
  exports.DRAGSTART_COMMAND = DRAGSTART_COMMAND;
8459
8824
  exports.DROP_COMMAND = DROP_COMMAND;
8460
8825
  exports.DecoratorNode = DecoratorNode;
@@ -8478,7 +8843,9 @@ exports.KEY_DELETE_COMMAND = KEY_DELETE_COMMAND;
8478
8843
  exports.KEY_ENTER_COMMAND = KEY_ENTER_COMMAND;
8479
8844
  exports.KEY_ESCAPE_COMMAND = KEY_ESCAPE_COMMAND;
8480
8845
  exports.KEY_MODIFIER_COMMAND = KEY_MODIFIER_COMMAND;
8846
+ exports.KEY_SPACE_COMMAND = KEY_SPACE_COMMAND;
8481
8847
  exports.KEY_TAB_COMMAND = KEY_TAB_COMMAND;
8848
+ exports.LineBreakNode = LineBreakNode;
8482
8849
  exports.OUTDENT_CONTENT_COMMAND = OUTDENT_CONTENT_COMMAND;
8483
8850
  exports.PASTE_COMMAND = PASTE_COMMAND;
8484
8851
  exports.ParagraphNode = ParagraphNode;