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 +213 -108
- package/Lexical.js.flow +12 -5
- package/Lexical.prod.js +183 -182
- package/LexicalConstants.d.ts +2 -0
- package/LexicalNode.d.ts +4 -1
- package/LexicalSelection.d.ts +1 -3
- package/LexicalUtils.d.ts +3 -1
- package/LexicalVersion.d.ts +1 -1
- package/nodes/LexicalElementNode.d.ts +7 -1
- package/nodes/LexicalLineBreakNode.d.ts +1 -1
- package/nodes/LexicalParagraphNode.d.ts +1 -1
- package/package.json +1 -1
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
|
-
|
|
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;
|
|
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
|
-
}
|
|
459
|
-
|
|
464
|
+
}
|
|
460
465
|
function isSelectionCapturedInDecoratorInput(anchorDOM) {
|
|
461
466
|
const activeElement = document.activeElement;
|
|
462
467
|
const nodeName = activeElement !== null ? activeElement.nodeName : null;
|
|
463
|
-
return
|
|
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
|
|
856
|
+
if (anchorNode !== null) {
|
|
857
|
+
let textContent = getAnchorTextFromDOM(anchorNode);
|
|
844
858
|
const node = $getNearestNodeFromDOMNode(anchorNode);
|
|
845
859
|
|
|
846
|
-
if ($isTextNode(node)) {
|
|
847
|
-
|
|
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
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
-
}
|
|
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
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
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
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|