lexical 0.6.4 → 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().
@@ -1156,8 +1162,15 @@ function setMutatedNode(mutatedNodes, registeredNodes, mutationListeners, node,
1156
1162
  mutatedNodes.set(klass, mutatedNodesByType);
1157
1163
  }
1158
1164
 
1159
- if (!mutatedNodesByType.has(nodeKey)) {
1160
- 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);
1161
1174
  }
1162
1175
  }
1163
1176
  function $nodesOfType(klass) {
@@ -1424,6 +1437,17 @@ function errorOnInsertTextNodeOnRoot(node, insertNode) {
1424
1437
  }
1425
1438
  }
1426
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
+ }
1427
1451
 
1428
1452
  /**
1429
1453
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1689,6 +1713,10 @@ function setElementFormat(dom, format) {
1689
1713
  setTextAlign(domStyle, 'right');
1690
1714
  } else if (format === IS_ALIGN_JUSTIFY) {
1691
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');
1692
1720
  }
1693
1721
  }
1694
1722
 
@@ -5523,7 +5551,7 @@ function adjustPointOffsetForMergedSibling(point, isBefore, key, target, textLen
5523
5551
  point.offset -= 1;
5524
5552
  }
5525
5553
  }
5526
- function updateDOMSelection(prevSelection, nextSelection, editor, domSelection, tags, rootElement) {
5554
+ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection, tags, rootElement, dirtyLeavesCount) {
5527
5555
  const anchorDOMNode = domSelection.anchorNode;
5528
5556
  const focusDOMNode = domSelection.focusNode;
5529
5557
  const anchorOffset = domSelection.anchorOffset;
@@ -5531,7 +5559,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5531
5559
  const activeElement = document.activeElement; // TODO: make this not hard-coded, and add another config option
5532
5560
  // that makes this configurable.
5533
5561
 
5534
- if (tags.has('collaboration') && activeElement !== rootElement) {
5562
+ if (tags.has('collaboration') && activeElement !== rootElement || activeElement !== null && isSelectionCapturedInDecoratorInput(activeElement)) {
5535
5563
  return;
5536
5564
  }
5537
5565
 
@@ -5587,7 +5615,7 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5587
5615
  if (anchorOffset === nextAnchorOffset && focusOffset === nextFocusOffset && anchorDOMNode === nextAnchorNode && focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
5588
5616
  !(domSelection.type === 'Range' && isCollapsed)) {
5589
5617
  // If the root element does not have focus, ensure it has focus
5590
- if (rootElement !== null && (activeElement === null || !rootElement.contains(activeElement))) {
5618
+ if (activeElement === null || !rootElement.contains(activeElement)) {
5591
5619
  rootElement.focus({
5592
5620
  preventScroll: true
5593
5621
  });
@@ -5596,28 +5624,39 @@ function updateDOMSelection(prevSelection, nextSelection, editor, domSelection,
5596
5624
  if (anchor.type !== 'element') {
5597
5625
  return;
5598
5626
  }
5599
- } // Apply the updated selection to the DOM. Note: this will trigger
5600
- // a "selectionchange" event, although it will be asynchronous.
5601
-
5602
-
5603
- try {
5604
- domSelection.setBaseAndExtent(nextAnchorNode, nextAnchorOffset, nextFocusNode, nextFocusOffset);
5605
-
5606
- if (!tags.has('skip-scroll-into-view') && nextSelection.isCollapsed() && rootElement !== null && rootElement === activeElement) {
5607
- const selectionTarget = nextSelection instanceof RangeSelection && nextSelection.anchor.type === 'element' ? nextAnchorNode.childNodes[nextAnchorOffset] || null : domSelection.rangeCount > 0 ? domSelection.getRangeAt(0) : null;
5627
+ }
5608
5628
 
5609
- if (selectionTarget !== null) {
5610
- // @ts-ignore Text nodes do have getBoundingClientRect
5611
- const selectionRect = selectionTarget.getBoundingClientRect();
5612
- 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);
5613
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.
5614
5647
  }
5615
5648
 
5616
- markSelectionChangeFromDOMUpdate();
5617
- } catch (error) {// If we encounter an error, continue. This can sometimes
5618
- // occur with FF and there's no good reason as to why it
5619
- // 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
+ }
5620
5657
  }
5658
+
5659
+ markSelectionChangeFromDOMUpdate();
5621
5660
  }
5622
5661
  function $insertNodes(nodes, selectStart) {
5623
5662
  let selection = $getSelection();
@@ -6018,6 +6057,7 @@ function commitPendingUpdates(editor) {
6018
6057
  const normalizedNodes = editor._normalizedNodes;
6019
6058
  const tags = editor._updateTags;
6020
6059
  const deferred = editor._deferred;
6060
+ const dirtyLeavesCount = dirtyLeaves.size;
6021
6061
 
6022
6062
  if (needsUpdate) {
6023
6063
  editor._dirtyType = NO_DIRTY_NODES;
@@ -6043,7 +6083,7 @@ function commitPendingUpdates(editor) {
6043
6083
  activeEditorState = pendingEditorState;
6044
6084
 
6045
6085
  try {
6046
- updateDOMSelection(currentSelection, pendingSelection, editor, domSelection, tags, rootElement);
6086
+ updateDOMSelection(currentSelection, pendingSelection, editor, domSelection, tags, rootElement, dirtyLeavesCount);
6047
6087
  } finally {
6048
6088
  activeEditor = previousActiveEditor;
6049
6089
  activeEditorState = previousActiveEditorState;
@@ -6421,10 +6461,9 @@ function removeNode(nodeToRemove, restoreSelection, preserveEmptyParent) {
6421
6461
  }
6422
6462
  }
6423
6463
 
6424
- internalMarkSiblingsAsDirty(nodeToRemove);
6425
- parentChildren.splice(index, 1);
6426
6464
  const writableNodeToRemove = nodeToRemove.getWritable();
6427
- writableNodeToRemove.__parent = null;
6465
+ internalMarkSiblingsAsDirty(nodeToRemove);
6466
+ removeFromParent(writableNodeToRemove);
6428
6467
 
6429
6468
  if ($isRangeSelection(selection) && restoreSelection && !selectionMoved) {
6430
6469
  $updateElementSelectionOnCreateDeleteNode(selection, parent, index, -1);
@@ -6438,17 +6477,6 @@ function removeNode(nodeToRemove, restoreSelection, preserveEmptyParent) {
6438
6477
  parent.selectEnd();
6439
6478
  }
6440
6479
  }
6441
- function $getNodeByKeyOrThrow(key) {
6442
- const node = $getNodeByKey(key);
6443
-
6444
- if (node === null) {
6445
- {
6446
- throw Error(`Expected node with key ${key} to exist but it's not in the nodeMap.`);
6447
- }
6448
- }
6449
-
6450
- return node;
6451
- }
6452
6480
  class LexicalNode {
6453
6481
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6454
6482
  // Flow doesn't support abstract classes unfortunately, so we can't _force_
@@ -6898,9 +6926,14 @@ class LexicalNode {
6898
6926
 
6899
6927
  const mutableNode = constructor.clone(latestNode);
6900
6928
  mutableNode.__parent = parent;
6929
+ mutableNode.__next = latestNode.__next;
6930
+ mutableNode.__prev = latestNode.__prev;
6901
6931
 
6902
6932
  if ($isElementNode(latestNode) && $isElementNode(mutableNode)) {
6903
6933
  mutableNode.__children = Array.from(latestNode.__children);
6934
+ mutableNode.__first = latestNode.__first;
6935
+ mutableNode.__last = latestNode.__last;
6936
+ mutableNode.__size = latestNode.__size;
6904
6937
  mutableNode.__indent = latestNode.__indent;
6905
6938
  mutableNode.__format = latestNode.__format;
6906
6939
  mutableNode.__dir = latestNode.__dir;
@@ -7601,33 +7634,16 @@ class ElementNode extends LexicalNode {
7601
7634
  if ($isRangeSelection(selection)) {
7602
7635
  const nodesToRemoveKeySet = new Set(nodesToRemoveKeys);
7603
7636
  const nodesToInsertKeySet = new Set(nodesToInsertKeys);
7604
-
7605
- const isPointRemoved = point => {
7606
- let node = point.getNode();
7607
-
7608
- while (node) {
7609
- const nodeKey = node.__key;
7610
-
7611
- if (nodesToRemoveKeySet.has(nodeKey) && !nodesToInsertKeySet.has(nodeKey)) {
7612
- return true;
7613
- }
7614
-
7615
- node = node.getParent();
7616
- }
7617
-
7618
- return false;
7619
- };
7620
-
7621
7637
  const {
7622
7638
  anchor,
7623
7639
  focus
7624
7640
  } = selection;
7625
7641
 
7626
- if (isPointRemoved(anchor)) {
7642
+ if (isPointRemoved(anchor, nodesToRemoveKeySet, nodesToInsertKeySet)) {
7627
7643
  moveSelectionPointToSibling(anchor, anchor.getNode(), this, nodeBeforeRange, nodeAfterRange);
7628
7644
  }
7629
7645
 
7630
- if (isPointRemoved(focus)) {
7646
+ if (isPointRemoved(focus, nodesToRemoveKeySet, nodesToInsertKeySet)) {
7631
7647
  moveSelectionPointToSibling(focus, focus.getNode(), this, nodeBeforeRange, nodeAfterRange);
7632
7648
  } // Unlink removed nodes from current parent
7633
7649
 
@@ -7741,6 +7757,22 @@ function $isElementNode(node) {
7741
7757
  return node instanceof ElementNode;
7742
7758
  }
7743
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
+
7744
7776
  /**
7745
7777
  * Copyright (c) Meta Platforms, Inc. and affiliates.
7746
7778
  *
@@ -8383,6 +8415,14 @@ class TextNode extends LexicalNode {
8383
8415
  conversion: convertTextFormatElement,
8384
8416
  priority: 0
8385
8417
  }),
8418
+ sub: node => ({
8419
+ conversion: convertTextFormatElement,
8420
+ priority: 0
8421
+ }),
8422
+ sup: node => ({
8423
+ conversion: convertTextFormatElement,
8424
+ priority: 0
8425
+ }),
8386
8426
  u: node => ({
8387
8427
  conversion: convertTextFormatElement,
8388
8428
  priority: 0
@@ -8804,6 +8844,8 @@ const nodeNameToTextFormat = {
8804
8844
  em: 'italic',
8805
8845
  i: 'italic',
8806
8846
  strong: 'bold',
8847
+ sub: 'subscript',
8848
+ sup: 'superscript',
8807
8849
  u: 'underline'
8808
8850
  };
8809
8851
 
@@ -9191,7 +9233,7 @@ class LexicalEditor {
9191
9233
  // Doing so, causes e2e tests around the lock to fail.
9192
9234
 
9193
9235
  this._editable = true;
9194
- this._headless = false;
9236
+ this._headless = parentEditor !== null && parentEditor._headless;
9195
9237
  this._window = null;
9196
9238
  }
9197
9239
 
@@ -9515,7 +9557,7 @@ class LexicalEditor {
9515
9557
  * LICENSE file in the root directory of this source tree.
9516
9558
  *
9517
9559
  */
9518
- const VERSION = '0.6.4';
9560
+ const VERSION = '0.6.5';
9519
9561
 
9520
9562
  /**
9521
9563
  * Copyright (c) Meta Platforms, Inc. and affiliates.
package/Lexical.js.flow CHANGED
@@ -661,7 +661,14 @@ declare export function $isRootNode(
661
661
  /**
662
662
  * LexicalElementNode
663
663
  */
664
- export type ElementFormatType = 'left' | 'center' | 'right' | 'justify' | '';
664
+ export type ElementFormatType =
665
+ | 'left'
666
+ | 'start'
667
+ | 'center'
668
+ | 'right'
669
+ | 'end'
670
+ | 'justify'
671
+ | '';
665
672
  declare export class ElementNode extends LexicalNode {
666
673
  __children: Array<NodeKey>;
667
674
  __format: number;