lexical 0.6.3 → 0.6.5

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
@@ -95,8 +95,8 @@ const CAN_USE_BEFORE_INPUT = CAN_USE_DOM && 'InputEvent' in window && !documentM
95
95
  const IS_SAFARI = CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
96
96
  const IS_IOS = CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; // Keep these in case we need to use them in the future.
97
97
  // export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
98
- // export const IS_CHROME: boolean = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
99
- // export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
98
+
99
+ const IS_CHROME = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent); // export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
100
100
 
101
101
  /**
102
102
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -133,7 +133,9 @@ const IS_UNMERGEABLE = 1 << 1; // Element node formatting
133
133
  const IS_ALIGN_LEFT = 1;
134
134
  const IS_ALIGN_CENTER = 2;
135
135
  const IS_ALIGN_RIGHT = 3;
136
- const IS_ALIGN_JUSTIFY = 4; // Reconciliation
136
+ const IS_ALIGN_JUSTIFY = 4;
137
+ const IS_ALIGN_START = 5;
138
+ const IS_ALIGN_END = 6; // Reconciliation
137
139
 
138
140
  const NON_BREAKING_SPACE = '\u00A0';
139
141
  const ZERO_WIDTH_SPACE = '\u200b'; // For iOS/Safari we use a non breaking space, otherwise the cursor appears
@@ -165,15 +167,19 @@ const DETAIL_TYPE_TO_DETAIL = {
165
167
  };
166
168
  const ELEMENT_TYPE_TO_FORMAT = {
167
169
  center: IS_ALIGN_CENTER,
170
+ end: IS_ALIGN_END,
168
171
  justify: IS_ALIGN_JUSTIFY,
169
172
  left: IS_ALIGN_LEFT,
170
- right: IS_ALIGN_RIGHT
173
+ right: IS_ALIGN_RIGHT,
174
+ start: IS_ALIGN_START
171
175
  };
172
176
  const ELEMENT_FORMAT_TO_TYPE = {
173
177
  [IS_ALIGN_CENTER]: 'center',
178
+ [IS_ALIGN_END]: 'end',
174
179
  [IS_ALIGN_JUSTIFY]: 'justify',
175
180
  [IS_ALIGN_LEFT]: 'left',
176
- [IS_ALIGN_RIGHT]: 'right'
181
+ [IS_ALIGN_RIGHT]: 'right',
182
+ [IS_ALIGN_START]: 'start'
177
183
  };
178
184
  const TEXT_MODE_TO_TYPE = {
179
185
  normal: IS_NORMAL,
@@ -455,19 +461,18 @@ const scheduleMicroTask = typeof queueMicrotask === 'function' ? queueMicrotask
455
461
  };
456
462
  function $isSelectionCapturedInDecorator(node) {
457
463
  return $isDecoratorNode($getNearestNodeFromDOMNode(node));
458
- } // TODO change to $ function
459
-
464
+ }
460
465
  function isSelectionCapturedInDecoratorInput(anchorDOM) {
461
466
  const activeElement = document.activeElement;
462
467
  const nodeName = activeElement !== null ? activeElement.nodeName : null;
463
- return !$isDecoratorNode($getNearestNodeFromDOMNode(anchorDOM)) || nodeName !== 'INPUT' && nodeName !== 'TEXTAREA';
468
+ return $isDecoratorNode($getNearestNodeFromDOMNode(anchorDOM)) && (nodeName === 'INPUT' || nodeName === 'TEXTAREA');
464
469
  }
465
470
  function isSelectionWithinEditor(editor, anchorDOM, focusDOM) {
466
471
  const rootElement = editor.getRootElement();
467
472
 
468
473
  try {
469
474
  return rootElement !== null && rootElement.contains(anchorDOM) && rootElement.contains(focusDOM) && // Ignore if selection is within nested editor
470
- anchorDOM !== null && isSelectionCapturedInDecoratorInput(anchorDOM) && getNearestEditorFromDOMNode(anchorDOM) === editor;
475
+ anchorDOM !== null && !isSelectionCapturedInDecoratorInput(anchorDOM) && getNearestEditorFromDOMNode(anchorDOM) === editor;
471
476
  } catch (error) {
472
477
  return false;
473
478
  }
@@ -601,6 +606,7 @@ function removeFromParent(writableNode) {
601
606
 
602
607
  internalMarkSiblingsAsDirty(writableNode);
603
608
  children.splice(index, 1);
609
+ writableNode.__parent = null;
604
610
  }
605
611
  } // Never use this function directly! It will break
606
612
  // the cloning heuristic. Instead use node.getWritable().
@@ -826,6 +832,13 @@ function getEditorsToPropagate(editor) {
826
832
  function createUID() {
827
833
  return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
828
834
  }
835
+ function getAnchorTextFromDOM(anchorNode) {
836
+ if (anchorNode.nodeType === DOM_TEXT_TYPE) {
837
+ return anchorNode.nodeValue;
838
+ }
839
+
840
+ return null;
841
+ }
829
842
  function $updateSelectedTextFromDOM(isCompositionEnd, data) {
830
843
  // Update the text content with the latest composition text
831
844
  const domSelection = getDOMSelection();
@@ -840,12 +853,12 @@ function $updateSelectedTextFromDOM(isCompositionEnd, data) {
840
853
  focusOffset
841
854
  } = domSelection;
842
855
 
843
- if (anchorNode !== null && anchorNode.nodeType === DOM_TEXT_TYPE) {
856
+ if (anchorNode !== null) {
857
+ let textContent = getAnchorTextFromDOM(anchorNode);
844
858
  const node = $getNearestNodeFromDOMNode(anchorNode);
845
859
 
846
- if ($isTextNode(node)) {
847
- let textContent = anchorNode.nodeValue; // Data is intentionally truthy, as we check for boolean, null and empty string.
848
-
860
+ if (textContent !== null && $isTextNode(node)) {
861
+ // Data is intentionally truthy, as we check for boolean, null and empty string.
849
862
  if (textContent === COMPOSITION_SUFFIX && data) {
850
863
  const offset = data.length;
851
864
  textContent = data;
@@ -953,33 +966,6 @@ function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
953
966
  } else {
954
967
  return false;
955
968
  }
956
- } // This function is used to determine if Lexical should attempt to override
957
- // the default browser behavior for insertion of text and use its own internal
958
- // heuristics. This is an extremely important function, and makes much of Lexical
959
- // work as intended between different browsers and across word, line and character
960
- // boundary/formats. It also is important for text replacement, node schemas and
961
- // composition mechanics.
962
-
963
-
964
- function $shouldPreventDefaultAndInsertText(selection, text) {
965
- const anchor = selection.anchor;
966
- const focus = selection.focus;
967
- const anchorNode = anchor.getNode();
968
- const domSelection = getDOMSelection();
969
- const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;
970
- const anchorKey = anchor.key;
971
- const backingAnchorElement = getActiveEditor().getElementByKey(anchorKey);
972
- const textLength = text.length;
973
- return anchorKey !== focus.key || // If we're working with a non-text node.
974
- !$isTextNode(anchorNode) || // If we are replacing a range with a single character or grapheme, and not composing.
975
- (textLength < 2 || doesContainGrapheme(text)) && anchor.offset !== focus.offset && !anchorNode.isComposing() || // Any non standard text node.
976
- $isTokenOrSegmented(anchorNode) || // If the text length is more than a single character and we're either
977
- // dealing with this in "beforeinput" or where the node has already recently
978
- // been changed (thus is dirty).
979
- anchorNode.isDirty() && textLength > 1 || // If the DOM selection element is not the same as the backing node
980
- backingAnchorElement !== null && !anchorNode.isComposing() && domAnchorNode !== getDOMTextNode(backingAnchorElement) || // Check if we're changing from bold to italics, or some other format.
981
- anchorNode.getFormat() !== selection.format || // One last set of heuristics to check against.
982
- $shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode);
983
969
  }
984
970
  function isTab(keyCode, altKey, ctrlKey, metaKey) {
985
971
  return keyCode === 9 && !altKey && !ctrlKey && !metaKey;
@@ -1176,8 +1162,15 @@ function setMutatedNode(mutatedNodes, registeredNodes, mutationListeners, node,
1176
1162
  mutatedNodes.set(klass, mutatedNodesByType);
1177
1163
  }
1178
1164
 
1179
- if (!mutatedNodesByType.has(nodeKey)) {
1180
- mutatedNodesByType.set(nodeKey, mutation);
1165
+ const prevMutation = mutatedNodesByType.get(nodeKey); // If the node has already been "destroyed", yet we are
1166
+ // re-making it, then this means a move likely happened.
1167
+ // We should change the mutation to be that of "updated"
1168
+ // instead.
1169
+
1170
+ const isMove = prevMutation === 'destroyed' && mutation === 'created';
1171
+
1172
+ if (prevMutation === undefined || isMove) {
1173
+ mutatedNodesByType.set(nodeKey, isMove ? 'updated' : mutation);
1181
1174
  }
1182
1175
  }
1183
1176
  function $nodesOfType(klass) {
@@ -1444,6 +1437,17 @@ function errorOnInsertTextNodeOnRoot(node, insertNode) {
1444
1437
  }
1445
1438
  }
1446
1439
  }
1440
+ function $getNodeByKeyOrThrow(key) {
1441
+ const node = $getNodeByKey(key);
1442
+
1443
+ if (node === null) {
1444
+ {
1445
+ throw Error(`Expected node with key ${key} to exist but it's not in the nodeMap.`);
1446
+ }
1447
+ }
1448
+
1449
+ return node;
1450
+ }
1447
1451
 
1448
1452
  /**
1449
1453
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1709,6 +1713,10 @@ function setElementFormat(dom, format) {
1709
1713
  setTextAlign(domStyle, 'right');
1710
1714
  } else if (format === IS_ALIGN_JUSTIFY) {
1711
1715
  setTextAlign(domStyle, 'justify');
1716
+ } else if (format === IS_ALIGN_START) {
1717
+ setTextAlign(domStyle, 'start');
1718
+ } else if (format === IS_ALIGN_END) {
1719
+ setTextAlign(domStyle, 'end');
1712
1720
  }
1713
1721
  }
1714
1722
 
@@ -2284,12 +2292,43 @@ if (CAN_USE_BEFORE_INPUT) {
2284
2292
 
2285
2293
  let lastKeyDownTimeStamp = 0;
2286
2294
  let lastKeyCode = 0;
2295
+ let lastBeforeInputInsertTextTimeStamp = 0;
2287
2296
  let rootElementsRegistered = 0;
2288
2297
  let isSelectionChangeFromDOMUpdate = false;
2289
2298
  let isSelectionChangeFromMouseDown = false;
2290
2299
  let isInsertLineBreak = false;
2291
2300
  let isFirefoxEndingComposition = false;
2292
- let collapsedSelectionFormat = [0, 0, 'root', 0];
2301
+ let collapsedSelectionFormat = [0, 0, 'root', 0]; // This function is used to determine if Lexical should attempt to override
2302
+ // the default browser behavior for insertion of text and use its own internal
2303
+ // heuristics. This is an extremely important function, and makes much of Lexical
2304
+ // work as intended between different browsers and across word, line and character
2305
+ // boundary/formats. It also is important for text replacement, node schemas and
2306
+ // composition mechanics.
2307
+
2308
+ function $shouldPreventDefaultAndInsertText(selection, text, timeStamp, isBeforeInput) {
2309
+ const anchor = selection.anchor;
2310
+ const focus = selection.focus;
2311
+ const anchorNode = anchor.getNode();
2312
+ const domSelection = getDOMSelection();
2313
+ const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;
2314
+ const anchorKey = anchor.key;
2315
+ const backingAnchorElement = getActiveEditor().getElementByKey(anchorKey);
2316
+ const textLength = text.length;
2317
+ return anchorKey !== focus.key || // If we're working with a non-text node.
2318
+ !$isTextNode(anchorNode) || // If we are replacing a range with a single character or grapheme, and not composing.
2319
+ (!isBeforeInput && (!CAN_USE_BEFORE_INPUT || // We check to see if there has been
2320
+ // a recent beforeinput event for "textInput". If there has been one in the last
2321
+ // 50ms then we proceed as normal. However, if there is not, then this is likely
2322
+ // a dangling `input` event caused by execCommand('insertText').
2323
+ lastBeforeInputInsertTextTimeStamp < timeStamp + 50) || textLength < 2 || doesContainGrapheme(text)) && anchor.offset !== focus.offset && !anchorNode.isComposing() || // Any non standard text node.
2324
+ $isTokenOrSegmented(anchorNode) || // If the text length is more than a single character and we're either
2325
+ // dealing with this in "beforeinput" or where the node has already recently
2326
+ // been changed (thus is dirty).
2327
+ anchorNode.isDirty() && textLength > 1 || // If the DOM selection element is not the same as the backing node during beforeinput.
2328
+ (isBeforeInput || !CAN_USE_BEFORE_INPUT) && backingAnchorElement !== null && !anchorNode.isComposing() && domAnchorNode !== getDOMTextNode(backingAnchorElement) || // Check if we're changing from bold to italics, or some other format.
2329
+ anchorNode.getFormat() !== selection.format || // One last set of heuristics to check against.
2330
+ $shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode);
2331
+ }
2293
2332
 
2294
2333
  function shouldSkipSelectionChange(domNode, offset) {
2295
2334
  return domNode !== null && domNode.nodeValue !== null && domNode.nodeType === DOM_TEXT_TYPE && offset !== 0 && offset !== domNode.nodeValue.length;
@@ -2525,11 +2564,12 @@ function onBeforeInput(event, editor) {
2525
2564
  const text = event.dataTransfer.getData('text/plain');
2526
2565
  event.preventDefault();
2527
2566
  selection.insertRawText(text);
2528
- } else if (data != null && $shouldPreventDefaultAndInsertText(selection, data)) {
2567
+ } else if (data != null && $shouldPreventDefaultAndInsertText(selection, data, event.timeStamp, true)) {
2529
2568
  event.preventDefault();
2530
2569
  dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2531
2570
  }
2532
2571
 
2572
+ lastBeforeInputInsertTextTimeStamp = event.timeStamp;
2533
2573
  return;
2534
2574
  } // Prevent the browser from carrying out
2535
2575
  // the input event, so we can control the
@@ -2682,7 +2722,7 @@ function onInput(event, editor) {
2682
2722
  const selection = $getSelection();
2683
2723
  const data = event.data;
2684
2724
 
2685
- if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, data)) {
2725
+ if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, data, event.timeStamp, false)) {
2686
2726
  // Given we're over-riding the default behavior, we will need
2687
2727
  // to ensure to disable composition before dispatching the
2688
2728
  // insertText command for when changing the sequence for FF.
@@ -2691,7 +2731,22 @@ function onInput(event, editor) {
2691
2731
  isFirefoxEndingComposition = false;
2692
2732
  }
2693
2733
 
2694
- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2734
+ const anchor = selection.anchor;
2735
+ const anchorNode = anchor.getNode();
2736
+ const domSelection = getDOMSelection();
2737
+
2738
+ if (domSelection === null) {
2739
+ return;
2740
+ }
2741
+
2742
+ const offset = anchor.offset; // If the content is the same as inserted, then don't dispatch an insertion.
2743
+ // Given onInput doesn't take the current selection (it uses the previous)
2744
+ // we can compare that against what the DOM currently says.
2745
+
2746
+ if (!CAN_USE_BEFORE_INPUT || selection.isCollapsed() || !$isTextNode(anchorNode) || domSelection.anchorNode === null || anchorNode.getTextContent().slice(0, offset) + data + anchorNode.getTextContent().slice(offset + selection.focus.offset) !== getAnchorTextFromDOM(domSelection.anchorNode)) {
2747
+ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2748
+ }
2749
+
2695
2750
  const textLength = data.length; // Another hack for FF, as it's possible that the IME is still
2696
2751
  // open, even though compositionend has already fired (sigh).
2697
2752
 
@@ -3209,6 +3264,7 @@ function selectPointOnNode(point, node) {
3209
3264
  if ($isTextNode(nextSibling)) {
3210
3265
  key = nextSibling.__key;
3211
3266
  offset = 0;
3267
+ type = 'text';
3212
3268
  } else {
3213
3269
  const parentNode = node.getParent();
3214
3270
 
@@ -4284,8 +4340,18 @@ class RangeSelection {
4284
4340
  const childrenLength = children.length;
4285
4341
 
4286
4342
  if ($isElementNode(target)) {
4343
+ let firstChild = target.getFirstChild();
4344
+
4287
4345
  for (let s = 0; s < childrenLength; s++) {
4288
- target.append(children[s]);
4346
+ const child = children[s];
4347
+
4348
+ if (firstChild === null) {
4349
+ target.append(child);
4350
+ } else {
4351
+ firstChild.insertAfter(child);
4352
+ }
4353
+
4354
+ firstChild = child;
4289
4355
  }
4290
4356
  } else {
4291
4357
  for (let s = childrenLength - 1; s >= 0; s--) {
@@ -4400,7 +4466,11 @@ class RangeSelection {
4400
4466
  if (lastChild === null) {
4401
4467
  target.select();
4402
4468
  } else if ($isTextNode(lastChild)) {
4403
- lastChild.select();
4469
+ if (lastChild.getTextContent() === '') {
4470
+ lastChild.selectPrevious();
4471
+ } else {
4472
+ lastChild.select();
4473
+ }
4404
4474
  } else {
4405
4475
  lastChild.selectNext();
4406
4476
  }
@@ -5481,7 +5551,7 @@ function adjustPointOffsetForMergedSibling(point, isBefore, key, target, textLen
5481
5551
  point.offset -= 1;
5482
5552
  }
5483
5553
  }
5484
- function updateDOMSelection(prevSelection, nextSelection, editor, domSelection, tags, rootElement) {
5554
+ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection, tags, rootElement, dirtyLeavesCount) {
5485
5555
  const anchorDOMNode = domSelection.anchorNode;
5486
5556
  const focusDOMNode = domSelection.focusNode;
5487
5557
  const anchorOffset = domSelection.anchorOffset;
@@ -5489,7 +5559,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5489
5559
  const activeElement = document.activeElement; // TODO: make this not hard-coded, and add another config option
5490
5560
  // that makes this configurable.
5491
5561
 
5492
- if (tags.has('collaboration') && activeElement !== rootElement) {
5562
+ if (tags.has('collaboration') && activeElement !== rootElement || activeElement !== null && isSelectionCapturedInDecoratorInput(activeElement)) {
5493
5563
  return;
5494
5564
  }
5495
5565
 
@@ -5545,7 +5615,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5545
5615
  if (anchorOffset === nextAnchorOffset && focusOffset === nextFocusOffset && anchorDOMNode === nextAnchorNode && focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
5546
5616
  !(domSelection.type === 'Range' && isCollapsed)) {
5547
5617
  // If the root element does not have focus, ensure it has focus
5548
- if (rootElement !== null && (activeElement === null || !rootElement.contains(activeElement))) {
5618
+ if (activeElement === null || !rootElement.contains(activeElement)) {
5549
5619
  rootElement.focus({
5550
5620
  preventScroll: true
5551
5621
  });
@@ -5554,28 +5624,39 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5554
5624
  if (anchor.type !== 'element') {
5555
5625
  return;
5556
5626
  }
5557
- } // Apply the updated selection to the DOM. Note: this will trigger
5558
- // a "selectionchange" event, although it will be asynchronous.
5559
-
5560
-
5561
- try {
5562
- domSelection.setBaseAndExtent(nextAnchorNode, nextAnchorOffset, nextFocusNode, nextFocusOffset);
5563
-
5564
- if (!tags.has('skip-scroll-into-view') && nextSelection.isCollapsed() && rootElement !== null && rootElement === activeElement) {
5565
- const selectionTarget = nextSelection instanceof RangeSelection && nextSelection.anchor.type === 'element' ? nextAnchorNode.childNodes[nextAnchorOffset] || null : domSelection.rangeCount > 0 ? domSelection.getRangeAt(0) : null;
5627
+ }
5566
5628
 
5567
- if (selectionTarget !== null) {
5568
- // @ts-ignore Text nodes do have getBoundingClientRect
5569
- const selectionRect = selectionTarget.getBoundingClientRect();
5570
- scrollIntoViewIfNeeded(editor, selectionRect, rootElement);
5629
+ if (!tags.has('skip-scroll-into-view')) // Apply the updated selection to the DOM. Note: this will trigger
5630
+ // a "selectionchange" event, although it will be asynchronous.
5631
+ try {
5632
+ // When updating more than 1000 nodes on Chrome, it's actually better to defer
5633
+ // updating the selection till the next frame. This is because Chrome's
5634
+ // Blink engine has hard limit on how many DOM nodes it can redraw in
5635
+ // a single cycle, so keeping it to the next frame improves performance.
5636
+ // The downside is that is makes the computation within Lexical more
5637
+ // complex, as now, we've sync update the DOM, but selection no longer
5638
+ // matches.
5639
+ if (IS_CHROME && dirtyLeavesCount > 1000) {
5640
+ window.requestAnimationFrame(() => domSelection.setBaseAndExtent(nextAnchorNode, nextAnchorOffset, nextFocusNode, nextFocusOffset));
5641
+ } else {
5642
+ domSelection.setBaseAndExtent(nextAnchorNode, nextAnchorOffset, nextFocusNode, nextFocusOffset);
5571
5643
  }
5644
+ } catch (error) {// If we encounter an error, continue. This can sometimes
5645
+ // occur with FF and there's no good reason as to why it
5646
+ // should happen.
5572
5647
  }
5573
5648
 
5574
- markSelectionChangeFromDOMUpdate();
5575
- } catch (error) {// If we encounter an error, continue. This can sometimes
5576
- // occur with FF and there's no good reason as to why it
5577
- // should happen.
5649
+ if (!tags.has('skip-scroll-into-view') && nextSelection.isCollapsed() && rootElement !== null && rootElement === document.activeElement) {
5650
+ const selectionTarget = nextSelection instanceof RangeSelection && nextSelection.anchor.type === 'element' ? nextAnchorNode.childNodes[nextAnchorOffset] || null : domSelection.rangeCount > 0 ? domSelection.getRangeAt(0) : null;
5651
+
5652
+ if (selectionTarget !== null) {
5653
+ // @ts-ignore Text nodes do have getBoundingClientRect
5654
+ const selectionRect = selectionTarget.getBoundingClientRect();
5655
+ scrollIntoViewIfNeeded(editor, selectionRect, rootElement);
5656
+ }
5578
5657
  }
5658
+
5659
+ markSelectionChangeFromDOMUpdate();
5579
5660
  }
5580
5661
  function $insertNodes(nodes, selectStart) {
5581
5662
  let selection = $getSelection();
@@ -5976,6 +6057,7 @@ function commitPendingUpdates(editor) {
5976
6057
  const normalizedNodes = editor._normalizedNodes;
5977
6058
  const tags = editor._updateTags;
5978
6059
  const deferred = editor._deferred;
6060
+ const dirtyLeavesCount = dirtyLeaves.size;
5979
6061
 
5980
6062
  if (needsUpdate) {
5981
6063
  editor._dirtyType = NO_DIRTY_NODES;
@@ -6001,7 +6083,7 @@ function commitPendingUpdates(editor) {
6001
6083
  activeEditorState = pendingEditorState;
6002
6084
 
6003
6085
  try {
6004
- updateDOMSelection(currentSelection, pendingSelection, editor, domSelection, tags, rootElement);
6086
+ updateDOMSelection(currentSelection, pendingSelection, editor, domSelection, tags, rootElement, dirtyLeavesCount);
6005
6087
  } finally {
6006
6088
  activeEditor = previousActiveEditor;
6007
6089
  activeEditorState = previousActiveEditorState;
@@ -6379,10 +6461,9 @@ function removeNode(nodeToRemove, restoreSelection, preserveEmptyParent) {
6379
6461
  }
6380
6462
  }
6381
6463
 
6382
- internalMarkSiblingsAsDirty(nodeToRemove);
6383
- parentChildren.splice(index, 1);
6384
6464
  const writableNodeToRemove = nodeToRemove.getWritable();
6385
- writableNodeToRemove.__parent = null;
6465
+ internalMarkSiblingsAsDirty(nodeToRemove);
6466
+ removeFromParent(writableNodeToRemove);
6386
6467
 
6387
6468
  if ($isRangeSelection(selection) && restoreSelection && !selectionMoved) {
6388
6469
  $updateElementSelectionOnCreateDeleteNode(selection, parent, index, -1);
@@ -6396,17 +6477,6 @@ function removeNode(nodeToRemove, restoreSelection, preserveEmptyParent) {
6396
6477
  parent.selectEnd();
6397
6478
  }
6398
6479
  }
6399
- function $getNodeByKeyOrThrow(key) {
6400
- const node = $getNodeByKey(key);
6401
-
6402
- if (node === null) {
6403
- {
6404
- throw Error(`Expected node with key ${key} to exist but it's not in the nodeMap.`);
6405
- }
6406
- }
6407
-
6408
- return node;
6409
- }
6410
6480
  class LexicalNode {
6411
6481
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6412
6482
  // Flow doesn't support abstract classes unfortunately, so we can't _force_
@@ -6430,6 +6500,8 @@ class LexicalNode {
6430
6500
  // @ts-expect-error
6431
6501
  this.__type = this.constructor.getType();
6432
6502
  this.__parent = null;
6503
+ this.__prev = null;
6504
+ this.__next = null;
6433
6505
  $setNodeKey(this, key);
6434
6506
 
6435
6507
  {
@@ -6854,9 +6926,14 @@ class LexicalNode {
6854
6926
 
6855
6927
  const mutableNode = constructor.clone(latestNode);
6856
6928
  mutableNode.__parent = parent;
6929
+ mutableNode.__next = latestNode.__next;
6930
+ mutableNode.__prev = latestNode.__prev;
6857
6931
 
6858
6932
  if ($isElementNode(latestNode) && $isElementNode(mutableNode)) {
6859
6933
  mutableNode.__children = Array.from(latestNode.__children);
6934
+ mutableNode.__first = latestNode.__first;
6935
+ mutableNode.__last = latestNode.__last;
6936
+ mutableNode.__size = latestNode.__size;
6860
6937
  mutableNode.__indent = latestNode.__indent;
6861
6938
  mutableNode.__format = latestNode.__format;
6862
6939
  mutableNode.__dir = latestNode.__dir;
@@ -7175,10 +7252,20 @@ class ElementNode extends LexicalNode {
7175
7252
 
7176
7253
  /** @internal */
7177
7254
 
7255
+ /** @internal */
7256
+
7257
+ /** @internal */
7258
+
7259
+ /** @internal */
7260
+
7178
7261
  /** @internal */
7179
7262
  constructor(key) {
7180
- super(key);
7263
+ super(key); // TODO: remove children and switch to using first/last as part of linked list work
7264
+
7181
7265
  this.__children = [];
7266
+ this.__first = null;
7267
+ this.__last = null;
7268
+ this.__size = 0;
7182
7269
  this.__format = 0;
7183
7270
  this.__indent = 0;
7184
7271
  this.__dir = null;
@@ -7547,33 +7634,16 @@ class ElementNode extends LexicalNode {
7547
7634
  if ($isRangeSelection(selection)) {
7548
7635
  const nodesToRemoveKeySet = new Set(nodesToRemoveKeys);
7549
7636
  const nodesToInsertKeySet = new Set(nodesToInsertKeys);
7550
-
7551
- const isPointRemoved = point => {
7552
- let node = point.getNode();
7553
-
7554
- while (node) {
7555
- const nodeKey = node.__key;
7556
-
7557
- if (nodesToRemoveKeySet.has(nodeKey) && !nodesToInsertKeySet.has(nodeKey)) {
7558
- return true;
7559
- }
7560
-
7561
- node = node.getParent();
7562
- }
7563
-
7564
- return false;
7565
- };
7566
-
7567
7637
  const {
7568
7638
  anchor,
7569
7639
  focus
7570
7640
  } = selection;
7571
7641
 
7572
- if (isPointRemoved(anchor)) {
7642
+ if (isPointRemoved(anchor, nodesToRemoveKeySet, nodesToInsertKeySet)) {
7573
7643
  moveSelectionPointToSibling(anchor, anchor.getNode(), this, nodeBeforeRange, nodeAfterRange);
7574
7644
  }
7575
7645
 
7576
- if (isPointRemoved(focus)) {
7646
+ if (isPointRemoved(focus, nodesToRemoveKeySet, nodesToInsertKeySet)) {
7577
7647
  moveSelectionPointToSibling(focus, focus.getNode(), this, nodeBeforeRange, nodeAfterRange);
7578
7648
  } // Unlink removed nodes from current parent
7579
7649
 
@@ -7687,6 +7757,22 @@ function $isElementNode(node) {
7687
7757
  return node instanceof ElementNode;
7688
7758
  }
7689
7759
 
7760
+ function isPointRemoved(point, nodesToRemoveKeySet, nodesToInsertKeySet) {
7761
+ let node = point.getNode();
7762
+
7763
+ while (node) {
7764
+ const nodeKey = node.__key;
7765
+
7766
+ if (nodesToRemoveKeySet.has(nodeKey) && !nodesToInsertKeySet.has(nodeKey)) {
7767
+ return true;
7768
+ }
7769
+
7770
+ node = node.getParent();
7771
+ }
7772
+
7773
+ return false;
7774
+ }
7775
+
7690
7776
  /**
7691
7777
  * Copyright (c) Meta Platforms, Inc. and affiliates.
7692
7778
  *
@@ -8329,6 +8415,14 @@ class TextNode extends LexicalNode {
8329
8415
  conversion: convertTextFormatElement,
8330
8416
  priority: 0
8331
8417
  }),
8418
+ sub: node => ({
8419
+ conversion: convertTextFormatElement,
8420
+ priority: 0
8421
+ }),
8422
+ sup: node => ({
8423
+ conversion: convertTextFormatElement,
8424
+ priority: 0
8425
+ }),
8332
8426
  u: node => ({
8333
8427
  conversion: convertTextFormatElement,
8334
8428
  priority: 0
@@ -8401,15 +8495,24 @@ class TextNode extends LexicalNode {
8401
8495
 
8402
8496
  setMode(type) {
8403
8497
  const mode = TEXT_MODE_TO_TYPE[type];
8498
+
8499
+ if (this.__mode === mode) {
8500
+ return this;
8501
+ }
8502
+
8404
8503
  const self = this.getWritable();
8405
8504
  self.__mode = mode;
8406
8505
  return self;
8407
8506
  }
8408
8507
 
8409
8508
  setTextContent(text) {
8410
- const writableSelf = this.getWritable();
8411
- writableSelf.__text = text;
8412
- return writableSelf;
8509
+ if (this.__text === text) {
8510
+ return this;
8511
+ }
8512
+
8513
+ const self = this.getWritable();
8514
+ self.__text = text;
8515
+ return self;
8413
8516
  }
8414
8517
 
8415
8518
  select(_anchorOffset, _focusOffset) {
@@ -8741,6 +8844,8 @@ const nodeNameToTextFormat = {
8741
8844
  em: 'italic',
8742
8845
  i: 'italic',
8743
8846
  strong: 'bold',
8847
+ sub: 'subscript',
8848
+ sup: 'superscript',
8744
8849
  u: 'underline'
8745
8850
  };
8746
8851
 
@@ -9128,7 +9233,7 @@ class LexicalEditor {
9128
9233
  // Doing so, causes e2e tests around the lock to fail.
9129
9234
 
9130
9235
  this._editable = true;
9131
- this._headless = false;
9236
+ this._headless = parentEditor !== null && parentEditor._headless;
9132
9237
  this._window = null;
9133
9238
  }
9134
9239
 
@@ -9452,7 +9557,7 @@ class LexicalEditor {
9452
9557
  * LICENSE file in the root directory of this source tree.
9453
9558
  *
9454
9559
  */
9455
- const VERSION = '0.6.3';
9560
+ const VERSION = '0.6.5';
9456
9561
 
9457
9562
  /**
9458
9563
  * Copyright (c) Meta Platforms, Inc. and affiliates.
package/Lexical.js.flow CHANGED
@@ -494,7 +494,6 @@ type TextPointType = {
494
494
  getNode: () => TextNode,
495
495
  set: (key: NodeKey, offset: number, type: 'text' | 'element') => void,
496
496
  getCharacterOffset: () => number,
497
- isAtNodeEnd: () => boolean,
498
497
  };
499
498
  export type ElementPoint = ElementPointType;
500
499
  type ElementPointType = {
@@ -505,7 +504,6 @@ type ElementPointType = {
505
504
  isBefore: (PointType) => boolean,
506
505
  getNode: () => ElementNode,
507
506
  set: (key: NodeKey, offset: number, type: 'text' | 'element') => void,
508
- isAtNodeEnd: () => boolean,
509
507
  };
510
508
  export type Point = PointType;
511
509
  type PointType = TextPointType | ElementPointType;
@@ -629,7 +627,8 @@ declare export class LineBreakNode extends LexicalNode {
629
627
  static importJSON(
630
628
  serializedLineBreakNode: SerializedLineBreakNode,
631
629
  ): LineBreakNode;
632
- exportJSON(): SerializedLexicalNode;
630
+ // $FlowExpectedError[incompatible-extend] 'linebreak' is a literal string
631
+ exportJSON(): SerializedLineBreakNode;
633
632
  }
634
633
  declare export function $createLineBreakNode(): LineBreakNode;
635
634
  declare export function $isLineBreakNode(
@@ -662,7 +661,14 @@ declare export function $isRootNode(
662
661
  /**
663
662
  * LexicalElementNode
664
663
  */
665
- export type ElementFormatType = 'left' | 'center' | 'right' | 'justify' | '';
664
+ export type ElementFormatType =
665
+ | 'left'
666
+ | 'start'
667
+ | 'center'
668
+ | 'right'
669
+ | 'end'
670
+ | 'justify'
671
+ | '';
666
672
  declare export class ElementNode extends LexicalNode {
667
673
  __children: Array<NodeKey>;
668
674
  __format: number;
@@ -757,7 +763,8 @@ declare export class ParagraphNode extends ElementNode {
757
763
  static importJSON(
758
764
  serializedParagraphNode: SerializedParagraphNode,
759
765
  ): ParagraphNode;
760
- exportJSON(): SerializedElementNode;
766
+ // $FlowExpectedError[incompatible-extend] 'paragraph' is a literal string
767
+ exportJSON(): SerializedParagraphNode;
761
768
  }
762
769
  declare export function $createParagraphNode(): ParagraphNode;
763
770
  declare export function $isParagraphNode(