lexical 0.6.2 → 0.6.4

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
@@ -826,6 +826,13 @@ function getEditorsToPropagate(editor) {
826
826
  function createUID() {
827
827
  return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
828
828
  }
829
+ function getAnchorTextFromDOM(anchorNode) {
830
+ if (anchorNode.nodeType === DOM_TEXT_TYPE) {
831
+ return anchorNode.nodeValue;
832
+ }
833
+
834
+ return null;
835
+ }
829
836
  function $updateSelectedTextFromDOM(isCompositionEnd, data) {
830
837
  // Update the text content with the latest composition text
831
838
  const domSelection = getDOMSelection();
@@ -840,12 +847,12 @@ function $updateSelectedTextFromDOM(isCompositionEnd, data) {
840
847
  focusOffset
841
848
  } = domSelection;
842
849
 
843
- if (anchorNode !== null && anchorNode.nodeType === DOM_TEXT_TYPE) {
850
+ if (anchorNode !== null) {
851
+ let textContent = getAnchorTextFromDOM(anchorNode);
844
852
  const node = $getNearestNodeFromDOMNode(anchorNode);
845
853
 
846
- if ($isTextNode(node)) {
847
- let textContent = anchorNode.nodeValue; // Data is intentionally truthy, as we check for boolean, null and empty string.
848
-
854
+ if (textContent !== null && $isTextNode(node)) {
855
+ // Data is intentionally truthy, as we check for boolean, null and empty string.
849
856
  if (textContent === COMPOSITION_SUFFIX && data) {
850
857
  const offset = data.length;
851
858
  textContent = data;
@@ -953,33 +960,6 @@ function $shouldInsertTextAfterOrBeforeTextNode(selection, node) {
953
960
  } else {
954
961
  return false;
955
962
  }
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
963
  }
984
964
  function isTab(keyCode, altKey, ctrlKey, metaKey) {
985
965
  return keyCode === 9 && !altKey && !ctrlKey && !metaKey;
@@ -1258,7 +1238,11 @@ function getElementByKeyOrThrow(editor, key) {
1258
1238
 
1259
1239
  return element;
1260
1240
  }
1261
- function scrollIntoViewIfNeeded(editor, selectionRect, rootElement, tags) {
1241
+ function getParentElement(element) {
1242
+ const parentElement = element.assignedSlot || element.parentElement;
1243
+ return parentElement !== null && parentElement.nodeType === 11 ? parentElement.host : parentElement;
1244
+ }
1245
+ function scrollIntoViewIfNeeded(editor, selectionRect, rootElement) {
1262
1246
  const doc = rootElement.ownerDocument;
1263
1247
  const defaultView = doc.defaultView;
1264
1248
 
@@ -1307,7 +1291,11 @@ function scrollIntoViewIfNeeded(editor, selectionRect, rootElement, tags) {
1307
1291
  }
1308
1292
  }
1309
1293
 
1310
- element = element.parentElement;
1294
+ if (isBodyElement) {
1295
+ break;
1296
+ }
1297
+
1298
+ element = getParentElement(element);
1311
1299
  }
1312
1300
  }
1313
1301
  function $addUpdateTag(tag) {
@@ -1427,6 +1415,15 @@ function $applyNodeReplacement(node) {
1427
1415
 
1428
1416
  return node;
1429
1417
  }
1418
+ function errorOnInsertTextNodeOnRoot(node, insertNode) {
1419
+ const parentNode = node.getParent();
1420
+
1421
+ if ($isRootNode(parentNode) && !$isElementNode(insertNode) && !$isDecoratorNode(insertNode)) {
1422
+ {
1423
+ throw Error(`Only element or decorator nodes can be inserted in to the root node`);
1424
+ }
1425
+ }
1426
+ }
1430
1427
 
1431
1428
  /**
1432
1429
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1815,7 +1812,7 @@ function createChildren(children, _startIndex, endIndex, dom, insertDOM) {
1815
1812
  function isLastChildLineBreakOrDecorator(children, nodeMap) {
1816
1813
  const childKey = children[children.length - 1];
1817
1814
  const node = nodeMap.get(childKey);
1818
- return $isLineBreakNode(node) || $isDecoratorNode(node);
1815
+ return $isLineBreakNode(node) || $isDecoratorNode(node) && node.isInline();
1819
1816
  } // If we end an element with a LineBreakNode, then we need to add an additional <br>
1820
1817
 
1821
1818
 
@@ -2267,12 +2264,43 @@ if (CAN_USE_BEFORE_INPUT) {
2267
2264
 
2268
2265
  let lastKeyDownTimeStamp = 0;
2269
2266
  let lastKeyCode = 0;
2267
+ let lastBeforeInputInsertTextTimeStamp = 0;
2270
2268
  let rootElementsRegistered = 0;
2271
2269
  let isSelectionChangeFromDOMUpdate = false;
2272
2270
  let isSelectionChangeFromMouseDown = false;
2273
2271
  let isInsertLineBreak = false;
2274
2272
  let isFirefoxEndingComposition = false;
2275
- let collapsedSelectionFormat = [0, 0, 'root', 0];
2273
+ let collapsedSelectionFormat = [0, 0, 'root', 0]; // This function is used to determine if Lexical should attempt to override
2274
+ // the default browser behavior for insertion of text and use its own internal
2275
+ // heuristics. This is an extremely important function, and makes much of Lexical
2276
+ // work as intended between different browsers and across word, line and character
2277
+ // boundary/formats. It also is important for text replacement, node schemas and
2278
+ // composition mechanics.
2279
+
2280
+ function $shouldPreventDefaultAndInsertText(selection, text, timeStamp, isBeforeInput) {
2281
+ const anchor = selection.anchor;
2282
+ const focus = selection.focus;
2283
+ const anchorNode = anchor.getNode();
2284
+ const domSelection = getDOMSelection();
2285
+ const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;
2286
+ const anchorKey = anchor.key;
2287
+ const backingAnchorElement = getActiveEditor().getElementByKey(anchorKey);
2288
+ const textLength = text.length;
2289
+ return anchorKey !== focus.key || // If we're working with a non-text node.
2290
+ !$isTextNode(anchorNode) || // If we are replacing a range with a single character or grapheme, and not composing.
2291
+ (!isBeforeInput && (!CAN_USE_BEFORE_INPUT || // We check to see if there has been
2292
+ // a recent beforeinput event for "textInput". If there has been one in the last
2293
+ // 50ms then we proceed as normal. However, if there is not, then this is likely
2294
+ // a dangling `input` event caused by execCommand('insertText').
2295
+ lastBeforeInputInsertTextTimeStamp < timeStamp + 50) || textLength < 2 || doesContainGrapheme(text)) && anchor.offset !== focus.offset && !anchorNode.isComposing() || // Any non standard text node.
2296
+ $isTokenOrSegmented(anchorNode) || // If the text length is more than a single character and we're either
2297
+ // dealing with this in "beforeinput" or where the node has already recently
2298
+ // been changed (thus is dirty).
2299
+ anchorNode.isDirty() && textLength > 1 || // If the DOM selection element is not the same as the backing node during beforeinput.
2300
+ (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.
2301
+ anchorNode.getFormat() !== selection.format || // One last set of heuristics to check against.
2302
+ $shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode);
2303
+ }
2276
2304
 
2277
2305
  function shouldSkipSelectionChange(domNode, offset) {
2278
2306
  return domNode !== null && domNode.nodeValue !== null && domNode.nodeType === DOM_TEXT_TYPE && offset !== 0 && offset !== domNode.nodeValue.length;
@@ -2508,11 +2536,12 @@ function onBeforeInput(event, editor) {
2508
2536
  const text = event.dataTransfer.getData('text/plain');
2509
2537
  event.preventDefault();
2510
2538
  selection.insertRawText(text);
2511
- } else if (data != null && $shouldPreventDefaultAndInsertText(selection, data)) {
2539
+ } else if (data != null && $shouldPreventDefaultAndInsertText(selection, data, event.timeStamp, true)) {
2512
2540
  event.preventDefault();
2513
2541
  dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2514
2542
  }
2515
2543
 
2544
+ lastBeforeInputInsertTextTimeStamp = event.timeStamp;
2516
2545
  return;
2517
2546
  } // Prevent the browser from carrying out
2518
2547
  // the input event, so we can control the
@@ -2665,7 +2694,7 @@ function onInput(event, editor) {
2665
2694
  const selection = $getSelection();
2666
2695
  const data = event.data;
2667
2696
 
2668
- if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, data)) {
2697
+ if (data != null && $isRangeSelection(selection) && $shouldPreventDefaultAndInsertText(selection, data, event.timeStamp, false)) {
2669
2698
  // Given we're over-riding the default behavior, we will need
2670
2699
  // to ensure to disable composition before dispatching the
2671
2700
  // insertText command for when changing the sequence for FF.
@@ -2674,7 +2703,22 @@ function onInput(event, editor) {
2674
2703
  isFirefoxEndingComposition = false;
2675
2704
  }
2676
2705
 
2677
- dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2706
+ const anchor = selection.anchor;
2707
+ const anchorNode = anchor.getNode();
2708
+ const domSelection = getDOMSelection();
2709
+
2710
+ if (domSelection === null) {
2711
+ return;
2712
+ }
2713
+
2714
+ const offset = anchor.offset; // If the content is the same as inserted, then don't dispatch an insertion.
2715
+ // Given onInput doesn't take the current selection (it uses the previous)
2716
+ // we can compare that against what the DOM currently says.
2717
+
2718
+ 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)) {
2719
+ dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
2720
+ }
2721
+
2678
2722
  const textLength = data.length; // Another hack for FF, as it's possible that the IME is still
2679
2723
  // open, even though compositionend has already fired (sigh).
2680
2724
 
@@ -2779,11 +2823,6 @@ function onCompositionEnd(event, editor) {
2779
2823
  }
2780
2824
 
2781
2825
  function onKeyDown(event, editor) {
2782
- if (hasStoppedLexicalPropagation(event)) {
2783
- return;
2784
- }
2785
-
2786
- stopLexicalPropagation(event);
2787
2826
  lastKeyDownTimeStamp = event.timeStamp;
2788
2827
  lastKeyCode = event.keyCode;
2789
2828
 
@@ -2992,10 +3031,22 @@ function addRootElementEvents(rootElement, editor) {
2992
3031
  for (let i = 0; i < rootElementEvents.length; i++) {
2993
3032
  const [eventName, onEvent] = rootElementEvents[i];
2994
3033
  const eventHandler = typeof onEvent === 'function' ? event => {
3034
+ if (hasStoppedLexicalPropagation(event)) {
3035
+ return;
3036
+ }
3037
+
3038
+ stopLexicalPropagation(event);
3039
+
2995
3040
  if (editor.isEditable()) {
2996
3041
  onEvent(event, editor);
2997
3042
  }
2998
3043
  } : event => {
3044
+ if (hasStoppedLexicalPropagation(event)) {
3045
+ return;
3046
+ }
3047
+
3048
+ stopLexicalPropagation(event);
3049
+
2999
3050
  if (editor.isEditable()) {
3000
3051
  switch (eventName) {
3001
3052
  case 'cut':
@@ -3185,6 +3236,7 @@ function selectPointOnNode(point, node) {
3185
3236
  if ($isTextNode(nextSibling)) {
3186
3237
  key = nextSibling.__key;
3187
3238
  offset = 0;
3239
+ type = 'text';
3188
3240
  } else {
3189
3241
  const parentNode = node.getParent();
3190
3242
 
@@ -4260,8 +4312,18 @@ class RangeSelection {
4260
4312
  const childrenLength = children.length;
4261
4313
 
4262
4314
  if ($isElementNode(target)) {
4315
+ let firstChild = target.getFirstChild();
4316
+
4263
4317
  for (let s = 0; s < childrenLength; s++) {
4264
- target.append(children[s]);
4318
+ const child = children[s];
4319
+
4320
+ if (firstChild === null) {
4321
+ target.append(child);
4322
+ } else {
4323
+ firstChild.insertAfter(child);
4324
+ }
4325
+
4326
+ firstChild = child;
4265
4327
  }
4266
4328
  } else {
4267
4329
  for (let s = childrenLength - 1; s >= 0; s--) {
@@ -4376,7 +4438,11 @@ class RangeSelection {
4376
4438
  if (lastChild === null) {
4377
4439
  target.select();
4378
4440
  } else if ($isTextNode(lastChild)) {
4379
- lastChild.select();
4441
+ if (lastChild.getTextContent() === '') {
4442
+ lastChild.selectPrevious();
4443
+ } else {
4444
+ lastChild.select();
4445
+ }
4380
4446
  } else {
4381
4447
  lastChild.selectNext();
4382
4448
  }
@@ -4389,7 +4455,8 @@ class RangeSelection {
4389
4455
  const sibling = siblings[i];
4390
4456
  const prevParent = sibling.getParentOrThrow();
4391
4457
 
4392
- if ($isElementNode(target) && !$isBlockElementNode(sibling) && !($isDecoratorNode(sibling) && !sibling.isInline())) {
4458
+ if ($isElementNode(target) && !$isBlockElementNode(sibling) && !($isDecoratorNode(sibling) && ( // Note: We are only looking for decorators that are inline and not isolated.
4459
+ !sibling.isInline() || sibling.isIsolated()))) {
4393
4460
  if (originalTarget === target) {
4394
4461
  target.append(sibling);
4395
4462
  } else {
@@ -4711,7 +4778,8 @@ class RangeSelection {
4711
4778
  if (domSelection.rangeCount > 0) {
4712
4779
  const range = domSelection.getRangeAt(0); // Apply the DOM selection to our Lexical selection.
4713
4780
 
4714
- const root = $getNearestRootOrShadowRoot(this.anchor.getNode());
4781
+ const anchorNode = this.anchor.getNode();
4782
+ const root = $isRootNode(anchorNode) ? anchorNode : $getNearestRootOrShadowRoot(anchorNode);
4715
4783
  this.applyDOMRange(range);
4716
4784
  this.dirty = true;
4717
4785
 
@@ -4776,6 +4844,24 @@ class RangeSelection {
4776
4844
  if ($isElementNode(nextSibling) && !nextSibling.canExtractContents()) {
4777
4845
  return;
4778
4846
  }
4847
+ } // Handle the deletion around decorators.
4848
+
4849
+
4850
+ const possibleNode = $getDecoratorNode(focus, isBackward);
4851
+
4852
+ if ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) {
4853
+ // Make it possible to move selection from range selection to
4854
+ // node selection on the node.
4855
+ if (possibleNode.isKeyboardSelectable() && $isElementNode(anchorNode) && anchorNode.getChildrenSize() === 0) {
4856
+ anchorNode.remove();
4857
+ const nodeSelection = $createNodeSelection();
4858
+ nodeSelection.add(possibleNode.__key);
4859
+ $setSelection(nodeSelection);
4860
+ } else {
4861
+ possibleNode.remove();
4862
+ }
4863
+
4864
+ return;
4779
4865
  }
4780
4866
 
4781
4867
  this.modify('extend', isBackward, 'character');
@@ -6386,6 +6472,8 @@ class LexicalNode {
6386
6472
  // @ts-expect-error
6387
6473
  this.__type = this.constructor.getType();
6388
6474
  this.__parent = null;
6475
+ this.__prev = null;
6476
+ this.__next = null;
6389
6477
  $setNodeKey(this, key);
6390
6478
 
6391
6479
  {
@@ -6889,6 +6977,7 @@ class LexicalNode {
6889
6977
 
6890
6978
  replace(replaceWith) {
6891
6979
  errorOnReadOnly();
6980
+ errorOnInsertTextNodeOnRoot(this, replaceWith);
6892
6981
  const toReplaceKey = this.__key;
6893
6982
  const writableReplaceWith = replaceWith.getWritable();
6894
6983
  removeFromParent(writableReplaceWith);
@@ -6932,6 +7021,7 @@ class LexicalNode {
6932
7021
 
6933
7022
  insertAfter(nodeToInsert) {
6934
7023
  errorOnReadOnly();
7024
+ errorOnInsertTextNodeOnRoot(this, nodeToInsert);
6935
7025
  const writableSelf = this.getWritable();
6936
7026
  const writableNodeToInsert = nodeToInsert.getWritable();
6937
7027
  const oldParent = writableNodeToInsert.getParent();
@@ -6984,6 +7074,8 @@ class LexicalNode {
6984
7074
  }
6985
7075
 
6986
7076
  insertBefore(nodeToInsert) {
7077
+ errorOnReadOnly();
7078
+ errorOnInsertTextNodeOnRoot(this, nodeToInsert);
6987
7079
  const writableSelf = this.getWritable();
6988
7080
  const writableNodeToInsert = nodeToInsert.getWritable();
6989
7081
  removeFromParent(writableNodeToInsert);
@@ -7127,10 +7219,20 @@ class ElementNode extends LexicalNode {
7127
7219
 
7128
7220
  /** @internal */
7129
7221
 
7222
+ /** @internal */
7223
+
7224
+ /** @internal */
7225
+
7226
+ /** @internal */
7227
+
7130
7228
  /** @internal */
7131
7229
  constructor(key) {
7132
- super(key);
7230
+ super(key); // TODO: remove children and switch to using first/last as part of linked list work
7231
+
7133
7232
  this.__children = [];
7233
+ this.__first = null;
7234
+ this.__last = null;
7235
+ this.__size = 0;
7134
7236
  this.__format = 0;
7135
7237
  this.__indent = 0;
7136
7238
  this.__dir = null;
@@ -7745,6 +7847,10 @@ class RootNode extends ElementNode {
7745
7847
  };
7746
7848
  }
7747
7849
 
7850
+ collapseAtStart() {
7851
+ return true;
7852
+ }
7853
+
7748
7854
  }
7749
7855
  function $createRootNode() {
7750
7856
  return new RootNode();
@@ -8349,15 +8455,24 @@ class TextNode extends LexicalNode {
8349
8455
 
8350
8456
  setMode(type) {
8351
8457
  const mode = TEXT_MODE_TO_TYPE[type];
8458
+
8459
+ if (this.__mode === mode) {
8460
+ return this;
8461
+ }
8462
+
8352
8463
  const self = this.getWritable();
8353
8464
  self.__mode = mode;
8354
8465
  return self;
8355
8466
  }
8356
8467
 
8357
8468
  setTextContent(text) {
8358
- const writableSelf = this.getWritable();
8359
- writableSelf.__text = text;
8360
- return writableSelf;
8469
+ if (this.__text === text) {
8470
+ return this;
8471
+ }
8472
+
8473
+ const self = this.getWritable();
8474
+ self.__text = text;
8475
+ return self;
8361
8476
  }
8362
8477
 
8363
8478
  select(_anchorOffset, _focusOffset) {
@@ -9400,7 +9515,7 @@ class LexicalEditor {
9400
9515
  * LICENSE file in the root directory of this source tree.
9401
9516
  *
9402
9517
  */
9403
- const VERSION = '0.6.2';
9518
+ const VERSION = '0.6.4';
9404
9519
 
9405
9520
  /**
9406
9521
  * 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(
@@ -757,7 +756,8 @@ declare export class ParagraphNode extends ElementNode {
757
756
  static importJSON(
758
757
  serializedParagraphNode: SerializedParagraphNode,
759
758
  ): ParagraphNode;
760
- exportJSON(): SerializedElementNode;
759
+ // $FlowExpectedError[incompatible-extend] 'paragraph' is a literal string
760
+ exportJSON(): SerializedParagraphNode;
761
761
  }
762
762
  declare export function $createParagraphNode(): ParagraphNode;
763
763
  declare export function $isParagraphNode(