lexical
Version:
Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.
1,291 lines (1,243 loc) • 433 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const documentMode = CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
const IS_APPLE = CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
const IS_FIREFOX = CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
const CAN_USE_BEFORE_INPUT = CAN_USE_DOM && 'InputEvent' in window && !documentMode ? 'getTargetRanges' in new window.InputEvent('input') : false;
const IS_SAFARI = CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
const IS_IOS = CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const IS_ANDROID = CAN_USE_DOM && /Android/.test(navigator.userAgent);
// Keep these in case we need to use them in the future.
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
const IS_CHROME = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
const IS_ANDROID_CHROME = CAN_USE_DOM && IS_ANDROID && IS_CHROME;
const IS_APPLE_WEBKIT = CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
function normalizeClassNames(...classNames) {
const rval = [];
for (const className of classNames) {
if (className && typeof className === 'string') {
for (const [s] of className.matchAll(/\S+/g)) {
rval.push(s);
}
}
}
return rval;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// DOM
const DOM_ELEMENT_TYPE = 1;
const DOM_TEXT_TYPE = 3;
const DOM_DOCUMENT_TYPE = 9;
const DOM_DOCUMENT_FRAGMENT_TYPE = 11;
// Reconciling
const NO_DIRTY_NODES = 0;
const HAS_DIRTY_NODES = 1;
const FULL_RECONCILE = 2;
// Text node modes
const IS_NORMAL = 0;
const IS_TOKEN = 1;
const IS_SEGMENTED = 2;
// IS_INERT = 3
// Text node formatting
const IS_BOLD = 1;
const IS_ITALIC = 1 << 1;
const IS_STRIKETHROUGH = 1 << 2;
const IS_UNDERLINE = 1 << 3;
const IS_CODE = 1 << 4;
const IS_SUBSCRIPT = 1 << 5;
const IS_SUPERSCRIPT = 1 << 6;
const IS_HIGHLIGHT = 1 << 7;
const IS_LOWERCASE = 1 << 8;
const IS_UPPERCASE = 1 << 9;
const IS_CAPITALIZE = 1 << 10;
const IS_ALL_FORMATTING = IS_BOLD | IS_ITALIC | IS_STRIKETHROUGH | IS_UNDERLINE | IS_CODE | IS_SUBSCRIPT | IS_SUPERSCRIPT | IS_HIGHLIGHT | IS_LOWERCASE | IS_UPPERCASE | IS_CAPITALIZE;
// Text node details
const IS_DIRECTIONLESS = 1;
const IS_UNMERGEABLE = 1 << 1;
// Element node formatting
const IS_ALIGN_LEFT = 1;
const IS_ALIGN_CENTER = 2;
const IS_ALIGN_RIGHT = 3;
const IS_ALIGN_JUSTIFY = 4;
const IS_ALIGN_START = 5;
const IS_ALIGN_END = 6;
// Reconciliation
const NON_BREAKING_SPACE = '\u00A0';
const ZERO_WIDTH_SPACE = '\u200b';
// For iOS/Safari we use a non breaking space, otherwise the cursor appears
// overlapping the composed text.
const COMPOSITION_SUFFIX = IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT ? NON_BREAKING_SPACE : ZERO_WIDTH_SPACE;
const DOUBLE_LINE_BREAK = '\n\n';
// For FF, we need to use a non-breaking space, or it gets composition
// in a stuck state.
const COMPOSITION_START_CHAR = IS_FIREFOX ? NON_BREAKING_SPACE : COMPOSITION_SUFFIX;
const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC';
const LTR = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' + '\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' + '\uFE00-\uFE6F\uFEFD-\uFFFF';
// eslint-disable-next-line no-misleading-character-class
const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']');
// eslint-disable-next-line no-misleading-character-class
const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']');
const TEXT_TYPE_TO_FORMAT = {
bold: IS_BOLD,
capitalize: IS_CAPITALIZE,
code: IS_CODE,
highlight: IS_HIGHLIGHT,
italic: IS_ITALIC,
lowercase: IS_LOWERCASE,
strikethrough: IS_STRIKETHROUGH,
subscript: IS_SUBSCRIPT,
superscript: IS_SUPERSCRIPT,
underline: IS_UNDERLINE,
uppercase: IS_UPPERCASE
};
const DETAIL_TYPE_TO_DETAIL = {
directionless: IS_DIRECTIONLESS,
unmergeable: IS_UNMERGEABLE
};
const ELEMENT_TYPE_TO_FORMAT = {
center: IS_ALIGN_CENTER,
end: IS_ALIGN_END,
justify: IS_ALIGN_JUSTIFY,
left: IS_ALIGN_LEFT,
right: IS_ALIGN_RIGHT,
start: IS_ALIGN_START
};
const ELEMENT_FORMAT_TO_TYPE = {
[IS_ALIGN_CENTER]: 'center',
[IS_ALIGN_END]: 'end',
[IS_ALIGN_JUSTIFY]: 'justify',
[IS_ALIGN_LEFT]: 'left',
[IS_ALIGN_RIGHT]: 'right',
[IS_ALIGN_START]: 'start'
};
const TEXT_MODE_TO_TYPE = {
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
token: IS_TOKEN
};
const TEXT_TYPE_TO_MODE = {
[IS_NORMAL]: 'normal',
[IS_SEGMENTED]: 'segmented',
[IS_TOKEN]: 'token'
};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
function $garbageCollectDetachedDecorators(editor, pendingEditorState) {
const currentDecorators = editor._decorators;
const pendingDecorators = editor._pendingDecorators;
let decorators = pendingDecorators || currentDecorators;
const nodeMap = pendingEditorState._nodeMap;
let key;
for (key in decorators) {
if (!nodeMap.has(key)) {
if (decorators === currentDecorators) {
decorators = cloneDecorators(editor);
}
delete decorators[key];
}
}
}
function $garbageCollectDetachedDeepChildNodes(node, parentKey, prevNodeMap, nodeMap, nodeMapDelete, dirtyNodes) {
let child = node.getFirstChild();
while (child !== null) {
const childKey = child.__key;
// TODO Revise condition below, redundant? LexicalNode already cleans up children when moving Nodes
if (child.__parent === parentKey) {
if ($isElementNode(child)) {
$garbageCollectDetachedDeepChildNodes(child, childKey, prevNodeMap, nodeMap, nodeMapDelete, dirtyNodes);
}
// If we have created a node and it was dereferenced, then also
// remove it from out dirty nodes Set.
if (!prevNodeMap.has(childKey)) {
dirtyNodes.delete(childKey);
}
nodeMapDelete.push(childKey);
}
child = child.getNextSibling();
}
}
function $garbageCollectDetachedNodes(prevEditorState, editorState, dirtyLeaves, dirtyElements) {
const prevNodeMap = prevEditorState._nodeMap;
const nodeMap = editorState._nodeMap;
// Store dirtyElements in a queue for later deletion; deleting dirty subtrees too early will
// hinder accessing .__next on child nodes
const nodeMapDelete = [];
for (const [nodeKey] of dirtyElements) {
const node = nodeMap.get(nodeKey);
if (node !== undefined) {
// Garbage collect node and its children if they exist
if (!node.isAttached()) {
if ($isElementNode(node)) {
$garbageCollectDetachedDeepChildNodes(node, nodeKey, prevNodeMap, nodeMap, nodeMapDelete, dirtyElements);
}
// If we have created a node and it was dereferenced, then also
// remove it from out dirty nodes Set.
if (!prevNodeMap.has(nodeKey)) {
dirtyElements.delete(nodeKey);
}
nodeMapDelete.push(nodeKey);
}
}
}
for (const nodeKey of nodeMapDelete) {
nodeMap.delete(nodeKey);
}
for (const nodeKey of dirtyLeaves) {
const node = nodeMap.get(nodeKey);
if (node !== undefined && !node.isAttached()) {
if (!prevNodeMap.has(nodeKey)) {
dirtyLeaves.delete(nodeKey);
}
nodeMap.delete(nodeKey);
}
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// The time between a text entry event and the mutation observer firing.
const TEXT_MUTATION_VARIANCE = 100;
let isProcessingMutations = false;
let lastTextEntryTimeStamp = 0;
function getIsProcessingMutations() {
return isProcessingMutations;
}
function updateTimeStamp(event) {
lastTextEntryTimeStamp = event.timeStamp;
}
function initTextEntryListener(editor) {
if (lastTextEntryTimeStamp === 0) {
getWindow(editor).addEventListener('textInput', updateTimeStamp, true);
}
}
function isManagedLineBreak(dom, target, editor) {
const isBR = dom.nodeName === 'BR';
const lexicalLineBreak = target.__lexicalLineBreak;
return lexicalLineBreak && (dom === lexicalLineBreak || isBR && dom.previousSibling === lexicalLineBreak) || isBR && getNodeKeyFromDOMNode(dom, editor) !== undefined;
}
function getLastSelection(editor) {
return editor.getEditorState().read(() => {
const selection = $getSelection();
return selection !== null ? selection.clone() : null;
});
}
function $handleTextMutation(target, node, editor) {
const domSelection = getDOMSelection(getWindow(editor));
let anchorOffset = null;
let focusOffset = null;
if (domSelection !== null && domSelection.anchorNode === target) {
anchorOffset = domSelection.anchorOffset;
focusOffset = domSelection.focusOffset;
}
const text = target.nodeValue;
if (text !== null) {
$updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
}
}
function shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode) {
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
if (anchorNode.is(targetNode) && selection.format !== anchorNode.getFormat()) {
return false;
}
}
return isDOMTextNode(targetDOM) && targetNode.isAttached();
}
function $getNearestManagedNodePairFromDOMNode(startingDOM, editor, editorState, rootElement) {
for (let dom = startingDOM; dom && !isDOMUnmanaged(dom); dom = getParentElement(dom)) {
const key = getNodeKeyFromDOMNode(dom, editor);
if (key !== undefined) {
const node = $getNodeByKey(key, editorState);
if (node) {
// All decorator nodes are unmanaged
return $isDecoratorNode(node) || !isHTMLElement(dom) ? undefined : [dom, node];
}
} else if (dom === rootElement) {
return [rootElement, internalGetRoot(editorState)];
}
}
}
function flushMutations(editor, mutations, observer) {
isProcessingMutations = true;
const shouldFlushTextMutations = performance.now() - lastTextEntryTimeStamp > TEXT_MUTATION_VARIANCE;
try {
updateEditorSync(editor, () => {
const selection = $getSelection() || getLastSelection(editor);
const badDOMTargets = new Map();
const rootElement = editor.getRootElement();
// We use the current editor state, as that reflects what is
// actually "on screen".
const currentEditorState = editor._editorState;
const blockCursorElement = editor._blockCursorElement;
let shouldRevertSelection = false;
let possibleTextForFirefoxPaste = '';
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i];
const type = mutation.type;
const targetDOM = mutation.target;
const pair = $getNearestManagedNodePairFromDOMNode(targetDOM, editor, currentEditorState, rootElement);
if (!pair) {
continue;
}
const [nodeDOM, targetNode] = pair;
if (type === 'characterData') {
// Text mutations are deferred and passed to mutation listeners to be
// processed outside of the Lexical engine.
if (shouldFlushTextMutations && $isTextNode(targetNode) && isDOMTextNode(targetDOM) && shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode)) {
$handleTextMutation(targetDOM, targetNode, editor);
}
} else if (type === 'childList') {
shouldRevertSelection = true;
// We attempt to "undo" any changes that have occurred outside
// of Lexical. We want Lexical's editor state to be source of truth.
// To the user, these will look like no-ops.
const addedDOMs = mutation.addedNodes;
for (let s = 0; s < addedDOMs.length; s++) {
const addedDOM = addedDOMs[s];
const node = $getNodeFromDOMNode(addedDOM);
const parentDOM = addedDOM.parentNode;
if (parentDOM != null && addedDOM !== blockCursorElement && node === null && !isManagedLineBreak(addedDOM, parentDOM, editor)) {
if (IS_FIREFOX) {
const possibleText = (isHTMLElement(addedDOM) ? addedDOM.innerText : null) || addedDOM.nodeValue;
if (possibleText) {
possibleTextForFirefoxPaste += possibleText;
}
}
parentDOM.removeChild(addedDOM);
}
}
const removedDOMs = mutation.removedNodes;
const removedDOMsLength = removedDOMs.length;
if (removedDOMsLength > 0) {
let unremovedBRs = 0;
for (let s = 0; s < removedDOMsLength; s++) {
const removedDOM = removedDOMs[s];
if (isManagedLineBreak(removedDOM, targetDOM, editor) || blockCursorElement === removedDOM) {
targetDOM.appendChild(removedDOM);
unremovedBRs++;
}
}
if (removedDOMsLength !== unremovedBRs) {
badDOMTargets.set(nodeDOM, targetNode);
}
}
}
}
// Now we process each of the unique target nodes, attempting
// to restore their contents back to the source of truth, which
// is Lexical's "current" editor state. This is basically like
// an internal revert on the DOM.
if (badDOMTargets.size > 0) {
for (const [nodeDOM, targetNode] of badDOMTargets) {
targetNode.reconcileObservedMutation(nodeDOM, editor);
}
}
// Capture all the mutations made during this function. This
// also prevents us having to process them on the next cycle
// of onMutation, as these mutations were made by us.
const records = observer.takeRecords();
// Check for any random auto-added <br> elements, and remove them.
// These get added by the browser when we undo the above mutations
// and this can lead to a broken UI.
if (records.length > 0) {
for (let i = 0; i < records.length; i++) {
const record = records[i];
const addedNodes = record.addedNodes;
const target = record.target;
for (let s = 0; s < addedNodes.length; s++) {
const addedDOM = addedNodes[s];
const parentDOM = addedDOM.parentNode;
if (parentDOM != null && addedDOM.nodeName === 'BR' && !isManagedLineBreak(addedDOM, target, editor)) {
parentDOM.removeChild(addedDOM);
}
}
}
// Clear any of those removal mutations
observer.takeRecords();
}
if (selection !== null) {
if (shouldRevertSelection) {
$setSelection(selection);
}
if (IS_FIREFOX && isFirefoxClipboardEvents(editor)) {
selection.insertRawText(possibleTextForFirefoxPaste);
}
}
});
} finally {
isProcessingMutations = false;
}
}
function flushRootMutations(editor) {
const observer = editor._observer;
if (observer !== null) {
const mutations = observer.takeRecords();
flushMutations(editor, mutations, observer);
}
}
function initMutationObserver(editor) {
initTextEntryListener(editor);
editor._observer = new MutationObserver((mutations, observer) => {
flushMutations(editor, mutations, observer);
});
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
function $canSimpleTextNodesBeMerged(node1, node2) {
const node1Mode = node1.__mode;
const node1Format = node1.__format;
const node1Style = node1.__style;
const node2Mode = node2.__mode;
const node2Format = node2.__format;
const node2Style = node2.__style;
return (node1Mode === null || node1Mode === node2Mode) && (node1Format === null || node1Format === node2Format) && (node1Style === null || node1Style === node2Style);
}
function $mergeTextNodes(node1, node2) {
const writableNode1 = node1.mergeWithSibling(node2);
const normalizedNodes = getActiveEditor()._normalizedNodes;
normalizedNodes.add(node1.__key);
normalizedNodes.add(node2.__key);
return writableNode1;
}
function $normalizeTextNode(textNode) {
let node = textNode;
if (node.__text === '' && node.isSimpleText() && !node.isUnmergeable()) {
node.remove();
return;
}
// Backward
let previousNode;
while ((previousNode = node.getPreviousSibling()) !== null && $isTextNode(previousNode) && previousNode.isSimpleText() && !previousNode.isUnmergeable()) {
if (previousNode.__text === '') {
previousNode.remove();
} else if ($canSimpleTextNodesBeMerged(previousNode, node)) {
node = $mergeTextNodes(previousNode, node);
break;
} else {
break;
}
}
// Forward
let nextNode;
while ((nextNode = node.getNextSibling()) !== null && $isTextNode(nextNode) && nextNode.isSimpleText() && !nextNode.isUnmergeable()) {
if (nextNode.__text === '') {
nextNode.remove();
} else if ($canSimpleTextNodesBeMerged(node, nextNode)) {
node = $mergeTextNodes(node, nextNode);
break;
} else {
break;
}
}
}
function $normalizeSelection(selection) {
$normalizePoint(selection.anchor);
$normalizePoint(selection.focus);
return selection;
}
function $normalizePoint(point) {
while (point.type === 'element') {
const node = point.getNode();
const offset = point.offset;
let nextNode;
let nextOffsetAtEnd;
if (offset === node.getChildrenSize()) {
nextNode = node.getChildAtIndex(offset - 1);
nextOffsetAtEnd = true;
} else {
nextNode = node.getChildAtIndex(offset);
nextOffsetAtEnd = false;
}
if ($isTextNode(nextNode)) {
point.set(nextNode.__key, nextOffsetAtEnd ? nextNode.getTextContentSize() : 0, 'text', true);
break;
} else if (!$isElementNode(nextNode)) {
break;
}
point.set(nextNode.__key, nextOffsetAtEnd ? nextNode.getChildrenSize() : 0, 'element', true);
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
let subTreeTextContent = '';
let subTreeDirectionedTextContent = '';
let subTreeTextFormat = null;
let subTreeTextStyle = '';
let editorTextContent = '';
let activeEditorConfig;
let activeEditor$1;
let activeEditorNodes;
let treatAllNodesAsDirty = false;
let activeEditorStateReadOnly = false;
let activeMutationListeners;
let activeTextDirection = null;
let activeDirtyElements;
let activeDirtyLeaves;
let activePrevNodeMap;
let activeNextNodeMap;
let activePrevKeyToDOMMap;
let mutatedNodes;
function destroyNode(key, parentDOM) {
const node = activePrevNodeMap.get(key);
if (parentDOM !== null) {
const dom = getPrevElementByKeyOrThrow(key);
if (dom.parentNode === parentDOM) {
parentDOM.removeChild(dom);
}
}
// This logic is really important, otherwise we will leak DOM nodes
// when their corresponding LexicalNodes are removed from the editor state.
if (!activeNextNodeMap.has(key)) {
activeEditor$1._keyToDOMMap.delete(key);
}
if ($isElementNode(node)) {
const children = createChildrenArray(node, activePrevNodeMap);
destroyChildren(children, 0, children.length - 1, null);
}
if (node !== undefined) {
setMutatedNode(mutatedNodes, activeEditorNodes, activeMutationListeners, node, 'destroyed');
}
}
function destroyChildren(children, _startIndex, endIndex, dom) {
let startIndex = _startIndex;
for (; startIndex <= endIndex; ++startIndex) {
const child = children[startIndex];
if (child !== undefined) {
destroyNode(child, dom);
}
}
}
function setTextAlign(domStyle, value) {
domStyle.setProperty('text-align', value);
}
const DEFAULT_INDENT_VALUE = '40px';
function setElementIndent(dom, indent) {
const indentClassName = activeEditorConfig.theme.indent;
if (typeof indentClassName === 'string') {
const elementHasClassName = dom.classList.contains(indentClassName);
if (indent > 0 && !elementHasClassName) {
dom.classList.add(indentClassName);
} else if (indent < 1 && elementHasClassName) {
dom.classList.remove(indentClassName);
}
}
const indentationBaseValue = getComputedStyle(dom).getPropertyValue('--lexical-indent-base-value') || DEFAULT_INDENT_VALUE;
dom.style.setProperty('padding-inline-start', indent === 0 ? '' : `calc(${indent} * ${indentationBaseValue})`);
}
function setElementFormat(dom, format) {
const domStyle = dom.style;
if (format === 0) {
setTextAlign(domStyle, '');
} else if (format === IS_ALIGN_LEFT) {
setTextAlign(domStyle, 'left');
} else if (format === IS_ALIGN_CENTER) {
setTextAlign(domStyle, 'center');
} else if (format === IS_ALIGN_RIGHT) {
setTextAlign(domStyle, 'right');
} else if (format === IS_ALIGN_JUSTIFY) {
setTextAlign(domStyle, 'justify');
} else if (format === IS_ALIGN_START) {
setTextAlign(domStyle, 'start');
} else if (format === IS_ALIGN_END) {
setTextAlign(domStyle, 'end');
}
}
function $createNode(key, slot) {
const node = activeNextNodeMap.get(key);
if (node === undefined) {
{
throw Error(`createNode: node does not exist in nodeMap`);
}
}
const dom = node.createDOM(activeEditorConfig, activeEditor$1);
storeDOMWithKey(key, dom, activeEditor$1);
// This helps preserve the text, and stops spell check tools from
// merging or break the spans (which happens if they are missing
// this attribute).
if ($isTextNode(node)) {
dom.setAttribute('data-lexical-text', 'true');
} else if ($isDecoratorNode(node)) {
dom.setAttribute('data-lexical-decorator', 'true');
}
if ($isElementNode(node)) {
const indent = node.__indent;
const childrenSize = node.__size;
if (indent !== 0) {
setElementIndent(dom, indent);
}
if (childrenSize !== 0) {
const endIndex = childrenSize - 1;
const children = createChildrenArray(node, activeNextNodeMap);
$createChildrenWithDirection(children, endIndex, node, dom);
}
const format = node.__format;
if (format !== 0) {
setElementFormat(dom, format);
}
if (!node.isInline()) {
reconcileElementTerminatingLineBreak(null, node, dom);
}
if ($textContentRequiresDoubleLinebreakAtEnd(node)) {
subTreeTextContent += DOUBLE_LINE_BREAK;
editorTextContent += DOUBLE_LINE_BREAK;
}
} else {
const text = node.getTextContent();
if ($isDecoratorNode(node)) {
const decorator = node.decorate(activeEditor$1, activeEditorConfig);
if (decorator !== null) {
reconcileDecorator(key, decorator);
}
// Decorators are always non editable
dom.contentEditable = 'false';
} else if ($isTextNode(node)) {
if (!node.isDirectionless()) {
subTreeDirectionedTextContent += text;
}
}
subTreeTextContent += text;
editorTextContent += text;
}
if (slot !== null) {
slot.insertChild(dom);
}
{
// Freeze the node in DEV to prevent accidental mutations
Object.freeze(node);
}
setMutatedNode(mutatedNodes, activeEditorNodes, activeMutationListeners, node, 'created');
return dom;
}
function $createChildrenWithDirection(children, endIndex, element, dom) {
const previousSubTreeDirectionedTextContent = subTreeDirectionedTextContent;
subTreeDirectionedTextContent = '';
$createChildren(children, element, 0, endIndex, element.getDOMSlot(dom));
reconcileBlockDirection(element, dom);
subTreeDirectionedTextContent = previousSubTreeDirectionedTextContent;
}
function $createChildren(children, element, _startIndex, endIndex, slot) {
const previousSubTreeTextContent = subTreeTextContent;
subTreeTextContent = '';
let startIndex = _startIndex;
for (; startIndex <= endIndex; ++startIndex) {
$createNode(children[startIndex], slot);
const node = activeNextNodeMap.get(children[startIndex]);
if (node !== null && $isTextNode(node)) {
if (subTreeTextFormat === null) {
subTreeTextFormat = node.getFormat();
}
if (subTreeTextStyle === '') {
subTreeTextStyle = node.getStyle();
}
}
}
if ($textContentRequiresDoubleLinebreakAtEnd(element)) {
subTreeTextContent += DOUBLE_LINE_BREAK;
}
const dom = slot.element;
dom.__lexicalTextContent = subTreeTextContent;
subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
}
function isLastChildLineBreakOrDecorator(element, nodeMap) {
if (element) {
const lastKey = element.__last;
if (lastKey) {
const node = nodeMap.get(lastKey);
if (node) {
return $isLineBreakNode(node) ? 'line-break' : $isDecoratorNode(node) && node.isInline() ? 'decorator' : null;
}
}
return 'empty';
}
return null;
}
// If we end an element with a LineBreakNode, then we need to add an additional <br>
function reconcileElementTerminatingLineBreak(prevElement, nextElement, dom) {
const prevLineBreak = isLastChildLineBreakOrDecorator(prevElement, activePrevNodeMap);
const nextLineBreak = isLastChildLineBreakOrDecorator(nextElement, activeNextNodeMap);
if (prevLineBreak !== nextLineBreak) {
nextElement.getDOMSlot(dom).setManagedLineBreak(nextLineBreak);
}
}
function reconcileTextFormat(element) {
if (subTreeTextFormat != null && subTreeTextFormat !== element.__textFormat && !activeEditorStateReadOnly) {
element.setTextFormat(subTreeTextFormat);
element.setTextStyle(subTreeTextStyle);
}
}
function reconcileTextStyle(element) {
if (subTreeTextStyle !== '' && subTreeTextStyle !== element.__textStyle && !activeEditorStateReadOnly) {
element.setTextStyle(subTreeTextStyle);
}
}
function reconcileBlockDirection(element, dom) {
const previousSubTreeDirectionTextContent = dom.__lexicalDirTextContent || '';
const previousDirection = dom.__lexicalDir || '';
if (previousSubTreeDirectionTextContent !== subTreeDirectionedTextContent || previousDirection !== activeTextDirection) {
const hasEmptyDirectionedTextContent = subTreeDirectionedTextContent === '';
const direction = hasEmptyDirectionedTextContent ? activeTextDirection : getTextDirection(subTreeDirectionedTextContent);
if (direction !== previousDirection) {
const classList = dom.classList;
const theme = activeEditorConfig.theme;
let previousDirectionTheme = previousDirection !== null ? theme[previousDirection] : undefined;
let nextDirectionTheme = direction !== null ? theme[direction] : undefined;
// Remove the old theme classes if they exist
if (previousDirectionTheme !== undefined) {
if (typeof previousDirectionTheme === 'string') {
const classNamesArr = normalizeClassNames(previousDirectionTheme);
previousDirectionTheme = theme[previousDirection] = classNamesArr;
}
// @ts-ignore: intentional
classList.remove(...previousDirectionTheme);
}
if (direction === null || hasEmptyDirectionedTextContent && direction === 'ltr') {
// Remove direction
dom.removeAttribute('dir');
} else {
// Apply the new theme classes if they exist
if (nextDirectionTheme !== undefined) {
if (typeof nextDirectionTheme === 'string') {
const classNamesArr = normalizeClassNames(nextDirectionTheme);
// @ts-expect-error: intentional
nextDirectionTheme = theme[direction] = classNamesArr;
}
if (nextDirectionTheme !== undefined) {
classList.add(...nextDirectionTheme);
}
}
// Update direction
dom.dir = direction;
}
if (!activeEditorStateReadOnly) {
const writableNode = element.getWritable();
writableNode.__dir = direction;
}
}
activeTextDirection = direction;
dom.__lexicalDirTextContent = subTreeDirectionedTextContent;
dom.__lexicalDir = direction;
}
}
function $reconcileChildrenWithDirection(prevElement, nextElement, dom) {
const previousSubTreeDirectionTextContent = subTreeDirectionedTextContent;
subTreeDirectionedTextContent = '';
subTreeTextFormat = null;
subTreeTextStyle = '';
$reconcileChildren(prevElement, nextElement, nextElement.getDOMSlot(dom));
reconcileBlockDirection(nextElement, dom);
reconcileTextFormat(nextElement);
reconcileTextStyle(nextElement);
subTreeDirectionedTextContent = previousSubTreeDirectionTextContent;
}
function createChildrenArray(element, nodeMap) {
const children = [];
let nodeKey = element.__first;
while (nodeKey !== null) {
const node = nodeMap.get(nodeKey);
if (node === undefined) {
{
throw Error(`createChildrenArray: node does not exist in nodeMap`);
}
}
children.push(nodeKey);
nodeKey = node.__next;
}
return children;
}
function $reconcileChildren(prevElement, nextElement, slot) {
const previousSubTreeTextContent = subTreeTextContent;
const prevChildrenSize = prevElement.__size;
const nextChildrenSize = nextElement.__size;
subTreeTextContent = '';
const dom = slot.element;
if (prevChildrenSize === 1 && nextChildrenSize === 1) {
const prevFirstChildKey = prevElement.__first;
const nextFirstChildKey = nextElement.__first;
if (prevFirstChildKey === nextFirstChildKey) {
$reconcileNode(prevFirstChildKey, dom);
} else {
const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey);
const replacementDOM = $createNode(nextFirstChildKey, null);
try {
dom.replaceChild(replacementDOM, lastDOM);
} catch (error) {
if (typeof error === 'object' && error != null) {
const msg = `${error.toString()} Parent: ${dom.tagName}, new child: {tag: ${replacementDOM.tagName} key: ${nextFirstChildKey}}, old child: {tag: ${lastDOM.tagName}, key: ${prevFirstChildKey}}.`;
throw new Error(msg);
} else {
throw error;
}
}
destroyNode(prevFirstChildKey, null);
}
const nextChildNode = activeNextNodeMap.get(nextFirstChildKey);
if ($isTextNode(nextChildNode)) {
if (subTreeTextFormat === null) {
subTreeTextFormat = nextChildNode.getFormat();
}
if (subTreeTextStyle === '') {
subTreeTextStyle = nextChildNode.getStyle();
}
}
} else {
const prevChildren = createChildrenArray(prevElement, activePrevNodeMap);
const nextChildren = createChildrenArray(nextElement, activeNextNodeMap);
if (!(prevChildren.length === prevChildrenSize)) {
throw Error(`$reconcileChildren: prevChildren.length !== prevChildrenSize`);
}
if (!(nextChildren.length === nextChildrenSize)) {
throw Error(`$reconcileChildren: nextChildren.length !== nextChildrenSize`);
}
if (prevChildrenSize === 0) {
if (nextChildrenSize !== 0) {
$createChildren(nextChildren, nextElement, 0, nextChildrenSize - 1, slot);
}
} else if (nextChildrenSize === 0) {
if (prevChildrenSize !== 0) {
const canUseFastPath = slot.after == null && slot.before == null && slot.element.__lexicalLineBreak == null;
destroyChildren(prevChildren, 0, prevChildrenSize - 1, canUseFastPath ? null : dom);
if (canUseFastPath) {
// Fast path for removing DOM nodes
dom.textContent = '';
}
}
} else {
$reconcileNodeChildren(nextElement, prevChildren, nextChildren, prevChildrenSize, nextChildrenSize, slot);
}
}
if ($textContentRequiresDoubleLinebreakAtEnd(nextElement)) {
subTreeTextContent += DOUBLE_LINE_BREAK;
}
dom.__lexicalTextContent = subTreeTextContent;
subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
}
function $reconcileNode(key, parentDOM) {
const prevNode = activePrevNodeMap.get(key);
let nextNode = activeNextNodeMap.get(key);
if (prevNode === undefined || nextNode === undefined) {
{
throw Error(`reconcileNode: prevNode or nextNode does not exist in nodeMap`);
}
}
const isDirty = treatAllNodesAsDirty || activeDirtyLeaves.has(key) || activeDirtyElements.has(key);
const dom = getElementByKeyOrThrow(activeEditor$1, key);
// If the node key points to the same instance in both states
// and isn't dirty, we just update the text content cache
// and return the existing DOM Node.
if (prevNode === nextNode && !isDirty) {
if ($isElementNode(prevNode)) {
const previousSubTreeTextContent = dom.__lexicalTextContent;
if (previousSubTreeTextContent !== undefined) {
subTreeTextContent += previousSubTreeTextContent;
editorTextContent += previousSubTreeTextContent;
}
const previousSubTreeDirectionTextContent = dom.__lexicalDirTextContent;
if (previousSubTreeDirectionTextContent !== undefined) {
subTreeDirectionedTextContent += previousSubTreeDirectionTextContent;
}
} else {
const text = prevNode.getTextContent();
if ($isTextNode(prevNode) && !prevNode.isDirectionless()) {
subTreeDirectionedTextContent += text;
}
editorTextContent += text;
subTreeTextContent += text;
}
return dom;
}
// If the node key doesn't point to the same instance in both maps,
// it means it were cloned. If they're also dirty, we mark them as mutated.
if (prevNode !== nextNode && isDirty) {
setMutatedNode(mutatedNodes, activeEditorNodes, activeMutationListeners, nextNode, 'updated');
}
// Update node. If it returns true, we need to unmount and re-create the node
if (nextNode.updateDOM(prevNode, dom, activeEditorConfig)) {
const replacementDOM = $createNode(key, null);
if (parentDOM === null) {
{
throw Error(`reconcileNode: parentDOM is null`);
}
}
parentDOM.replaceChild(replacementDOM, dom);
destroyNode(key, null);
return replacementDOM;
}
if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
// Reconcile element children
const nextIndent = nextNode.__indent;
if (nextIndent !== prevNode.__indent) {
setElementIndent(dom, nextIndent);
}
const nextFormat = nextNode.__format;
if (nextFormat !== prevNode.__format) {
setElementFormat(dom, nextFormat);
}
if (isDirty) {
$reconcileChildrenWithDirection(prevNode, nextNode, dom);
if (!$isRootNode(nextNode) && !nextNode.isInline()) {
reconcileElementTerminatingLineBreak(prevNode, nextNode, dom);
}
}
if ($textContentRequiresDoubleLinebreakAtEnd(nextNode)) {
subTreeTextContent += DOUBLE_LINE_BREAK;
editorTextContent += DOUBLE_LINE_BREAK;
}
} else {
const text = nextNode.getTextContent();
if ($isDecoratorNode(nextNode)) {
const decorator = nextNode.decorate(activeEditor$1, activeEditorConfig);
if (decorator !== null) {
reconcileDecorator(key, decorator);
}
} else if ($isTextNode(nextNode) && !nextNode.isDirectionless()) {
// Handle text content, for LTR, LTR cases.
subTreeDirectionedTextContent += text;
}
subTreeTextContent += text;
editorTextContent += text;
}
if (!activeEditorStateReadOnly && $isRootNode(nextNode) && nextNode.__cachedText !== editorTextContent) {
// Cache the latest text content.
const nextRootNode = nextNode.getWritable();
nextRootNode.__cachedText = editorTextContent;
nextNode = nextRootNode;
}
{
// Freeze the node in DEV to prevent accidental mutations
Object.freeze(nextNode);
}
return dom;
}
function reconcileDecorator(key, decorator) {
let pendingDecorators = activeEditor$1._pendingDecorators;
const currentDecorators = activeEditor$1._decorators;
if (pendingDecorators === null) {
if (currentDecorators[key] === decorator) {
return;
}
pendingDecorators = cloneDecorators(activeEditor$1);
}
pendingDecorators[key] = decorator;
}
function getNextSibling(element) {
let nextSibling = element.nextSibling;
if (nextSibling !== null && nextSibling === activeEditor$1._blockCursorElement) {
nextSibling = nextSibling.nextSibling;
}
return nextSibling;
}
function $reconcileNodeChildren(nextElement, prevChildren, nextChildren, prevChildrenLength, nextChildrenLength, slot) {
const prevEndIndex = prevChildrenLength - 1;
const nextEndIndex = nextChildrenLength - 1;
let prevChildrenSet;
let nextChildrenSet;
let siblingDOM = slot.getFirstChild();
let prevIndex = 0;
let nextIndex = 0;
while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
const prevKey = prevChildren[prevIndex];
const nextKey = nextChildren[nextIndex];
if (prevKey === nextKey) {
siblingDOM = getNextSibling($reconcileNode(nextKey, slot.element));
prevIndex++;
nextIndex++;
} else {
if (prevChildrenSet === undefined) {
prevChildrenSet = new Set(prevChildren);
}
if (nextChildrenSet === undefined) {
nextChildrenSet = new Set(nextChildren);
}
const nextHasPrevKey = nextChildrenSet.has(prevKey);
const prevHasNextKey = prevChildrenSet.has(nextKey);
if (!nextHasPrevKey) {
// Remove prev
siblingDOM = getNextSibling(getPrevElementByKeyOrThrow(prevKey));
destroyNode(prevKey, slot.element);
prevIndex++;
} else if (!prevHasNextKey) {
// Create next
$createNode(nextKey, slot.withBefore(siblingDOM));
nextIndex++;
} else {
// Move next
const childDOM = getElementByKeyOrThrow(activeEditor$1, nextKey);
if (childDOM === siblingDOM) {
siblingDOM = getNextSibling($reconcileNode(nextKey, slot.element));
} else {
slot.withBefore(siblingDOM).insertChild(childDOM);
$reconcileNode(nextKey, slot.element);
}
prevIndex++;
nextIndex++;
}
}
const node = activeNextNodeMap.get(nextKey);
if (node !== null && $isTextNode(node)) {
if (subTreeTextFormat === null) {
subTreeTextFormat = node.getFormat();
}
if (subTreeTextStyle === '') {
subTreeTextStyle = node.getStyle();
}
}
}
const appendNewChildren = prevIndex > prevEndIndex;
const removeOldChildren = nextIndex > nextEndIndex;
if (appendNewChildren && !removeOldChildren) {
const previousNode = nextChildren[nextEndIndex + 1];
const insertDOM = previousNode === undefined ? null : activeEditor$1.getElementByKey(previousNode);
$createChildren(nextChildren, nextElement, nextIndex, nextEndIndex, slot.withBefore(insertDOM));
} else if (removeOldChildren && !appendNewChildren) {
destroyChildren(prevChildren, prevIndex, prevEndIndex, slot.element);
}
}
function $reconcileRoot(prevEditorState, nextEditorState, editor, dirtyType, dirtyElements, dirtyLeaves) {
// We cache text content to make retrieval more efficient.
// The cache must be rebuilt during reconciliation to account for any changes.
subTreeTextContent = '';
editorTextContent = '';
subTreeDirectionedTextContent = '';
// Rather than pass around a load of arguments through the stack recursively
// we instead set them as bindings within the scope of the module.
treatAllNodesAsDirty = dirtyType === FULL_RECONCILE;
activeTextDirection = null;
activeEditor$1 = editor;
activeEditorConfig = editor._config;
activeEditorNodes = editor._nodes;
activeMutationListeners = activeEditor$1._listeners.mutation;
activeDirtyElements = dirtyElements;
activeDirtyLeaves = dirtyLeaves;
activePrevNodeMap = prevEditorState._nodeMap;
activeNextNodeMap = nextEditorState._nodeMap;
activeEditorStateReadOnly = nextEditorState._readOnly;
activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);
// We keep track of mutated nodes so we can trigger mutation
// listeners later in the update cycle.
const currentMutatedNodes = new Map();
mutatedNodes = currentMutatedNodes;
$reconcileNode('root', null);
// We don't want a bunch of void checks throughout the scope
// so instead we make it seem that these values are always set.
// We also want to make sure we clear them down, otherwise we
// can leak memory.
// @ts-ignore
activeEditor$1 = undefined;
// @ts-ignore
activeEditorNodes = undefined;
// @ts-ignore
activeDirtyElements = undefined;
// @ts-ignore
activeDirtyLeaves = undefined;
// @ts-ignore
activePrevNodeMap = undefined;
// @ts-ignore
activeNextNodeMap = undefined;
// @ts-ignore
activeEditorConfig = undefined;
// @ts-ignore
activePrevKeyToDOMMap = undefined;
// @ts-ignore
mutatedNodes = undefined;
return currentMutatedNodes;
}
function storeDOMWithKey(key, dom, editor) {
const keyToDOMMap = editor._keyToDOMMap;
setNodeKeyOnDOMNode(dom, editor, key);
keyToDOMMap.set(key, dom);
}
function getPrevElementByKeyOrThrow(key) {
const element = activePrevKeyToDOMMap.get(key);
if (element === undefined) {
{
throw Error(`Reconciliation: could not find DOM element for node key ${key}`);
}
}
return element;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
function createCommand(type) {
return {
type
} ;
}
const SELECTION_CHANGE_COMMAND = createCommand('SELECTION_CHANGE_COMMAND');
const SELECTION_INSERT_CLIPBOARD_NODES_COMMAND = createCommand('SELECTION_INSERT_CLIPBOARD_NODES_COMMAND');
const CLICK_COMMAND = createCommand('CLICK_COMMAND');
const DELETE_CHARACTER_COMMAND = createCommand('DELETE_CHARACTER_COMMAND');
const INSERT_LINE_BREAK_COMMAND = createCommand('INSERT_LINE_BREAK_COMMAND');
const INSERT_PARAGRAPH_COMMAND = createCommand('INSERT_PARAGRAPH_COMMAND');
const CONTROLLED_TEXT_INSERTION_COMMAND = createCommand('CONTROLLED_TEXT_INSERTION_COMMAND');
const PASTE_COMMAND = createCommand('PASTE_COMMAND');
const REMOVE_TEXT_COMMAND = createCommand('REMOVE_TEXT_COMMAND');
const DELETE_WORD_COMMAND = createCommand('DELETE_WORD_COMMAND');
const DELETE_LINE_COMMAND = createCommand('DELETE_LINE_COMMAND');
const FORMAT_TEXT_COMMAND = createCommand('FORMAT_TEXT_COMMAND');
const UNDO_COMMAND = createCommand('UNDO_COMMAND');
const REDO_COMMAND = createCommand('REDO_COMMAND');
const KEY_DOWN_COMMAND = createCommand('KEYDOWN_COMMAND');
const KEY_ARROW_RIGHT_COMMAND = createCommand('KEY_ARROW_RIGHT_COMMAND');
const MOVE_TO_END = createCommand('MOVE_TO_END');
const KEY_ARROW_LEFT_COMMAND = createCommand('KEY_ARROW_LEFT_COMMAND');
const MOVE_TO_START = createCommand('MOVE_TO_START');
const KEY_ARROW_UP_COMMAND = createCommand('KEY_ARROW_UP_COMMAND');
const KEY_ARROW_DOWN_COMMAND = createCommand('KEY_ARROW_DOWN_COMMAND');
const KEY_ENTER_COMMAND = createCommand('KEY_ENTER_COMMAND');
const KEY_SPACE_COMMAND = createCommand('KEY_SPACE_COMMAND');
const KEY_BACKSPACE_COMMAND = createCommand('KEY_BACKSPACE_COMMAND');
const KEY_ESCAPE_COMMAND = createCommand('KEY_ESCAPE_COMMAND');
const KEY_DELETE_COMMAND = createCommand('KEY_DELETE_COMMAND');
const KEY_TAB_COMMAND = createCommand('KEY_TAB_COMMAND');
const INSERT_TAB_COMMAND = createCommand('INSERT_TAB_COMMAND');
const INDENT_CONTENT_COMMAND = createCommand('INDENT_CONTENT_COMMAND');
const OUTDENT_CONTENT_COMMAND = createCommand('OUTDENT_CONTENT_COMMAND');
const DROP_COMMAND = createCommand('DROP_COMMAND');
const FORMAT_ELEMENT_COMMAND = createCommand('FORMAT_ELEMENT_COMMAND');
const DRAGSTART_COMMAND = createCommand('DRAGSTART_COMMAND');
const DRAGOVER_COMMAND = createCommand('DRAGOVER_COMMAND');
const DRAGEND_COMMAND = createCommand('DRAGEND_COMMAND');
const COPY_COMMAND = createCommand('COPY_COMMAND');
const CUT_COMMAND = createCommand('CUT_COMMAND');
const SELECT_ALL_COMMAND = createCommand('SELECT_ALL_COMMAND');
const CLEAR_EDITOR_COMMAND = createCommand('CLEAR_EDITOR_COMMAND');
const CLEAR_HISTORY_COMMAND = createCommand('CLEAR_HISTORY_COMMAND');
const CAN_REDO_COMMAND = createCommand('CAN_REDO_COMMAND');
const CAN_UNDO_COMMAND = createCommand('CAN_UNDO_COMMAND');
const FOCUS_COMMAND = createCommand('FOCUS_COMMAND');
const BLUR_COMMAND = createCommand('BLUR_COMMAND');
const KEY_MODIFIER_COMMAND = createCommand('KEY_MODIFIER_COMMAND');
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const PASS_THROUGH_COMMAND = Object.freeze({});
const ANDROID_COMPOSITION_LATENCY = 30;
const rootElementEvents = [['keydown', onKeyDown], ['pointerdown', onPointerDown], ['compositionstart', onCompositionStart], ['compositionend', onCompositionEnd], ['input', onInput], ['click', onClick], ['cut', PASS_THROUGH_COMMAND], ['copy', PASS_THROUGH_COMMAND], ['dragstart', PASS_THROUGH_COMMAND], ['dragover', PASS_THROUGH_COMMAND], ['dragend', PASS_THROUGH_COMMAND], ['paste', PASS_THROUGH_COMMAND], ['focus', PASS_THROUGH_COMMAND], ['blur', PASS_THROUGH_COMMAND], ['drop', PASS_THROUGH_COMMAND]];
if (CAN_USE_BEFORE_INPUT) {
rootElementEvents.push(['beforeinput', (event, editor) => onBeforeInput(event, editor)]);
}
let lastKeyDownTimeStamp = 0;
let lastKeyCode = null;
let lastBeforeInputInsertTextTimeStamp = 0;
let unprocessedBeforeInputData = null;
const rootElementsRegistered = new WeakMap();
let isSelectionChangeFromDOMUpdate = false;
let isSelectionChangeFromMouseDown = false;
let isInsertLineBreak = false;
let isFirefoxEndingComposition = false;
let isSafariEndingComposition = false;
let safariEndCompositionEventData = '';
let collapsedSelectionFormat = [0, '', 0, 'root', 0];
// This function is used to determine if Lexical should attempt to override
// the default browser behavior for insertion of text and use its own internal
// heuristics. This is an extremely important function, and makes much of Lexical
// work as intended between different browsers and across word, line and character
// boundary/formats. It also is important for text replacement, node schemas and
// composition mechanics.
function $shouldPreventDefaultAndInsertText(selection, domTargetRange, text, timeStamp, isBeforeInput) {
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = anchor.getNode();
const editor = getActiveEditor();
const domSelection = getDOMSelection(getWindow(editor));
const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;
const anchorKey = anchor.key;
const backingAnchorElement = editor.getElementByKey(anchorKey);
const textLength = text.length;
return anchorKey !== focus.key ||
// If we're working with a non-text node.
!$isTextNode(anchorNode) ||
// If we are replacing a range with a single character or grapheme, and not composing.
(!isBeforeInput && (!CAN_USE_BEFORE_INPUT ||
// We check to see if there has been
// a recent beforeinput event for "textInput". If there has been one in the last
// 50ms then we proceed as normal. However, if there is not, then this is likely
// a dangling `input` event caused by execCommand('insertText').
lastBeforeInputInsertTextTimeStamp < timeStamp + 50) || anchorNode.isDirty() && textLength < 2 ||
// TODO consider if there are other scenarios when multiple code units
// should be addressed here
doesContainSurrogatePair(text)) && anchor.offset !== focus.offset && !anchorNode.isComposing() ||
// Any non standard text node.
$isTokenOrSegmented(anchorNode) ||
// If the text length is more than a single character and we're either
// dealing with this in "beforeinput" or where the node has already recently
// been changed (thus is dirty).
anchorNode.isDirty() && textLength > 1 ||
// If the DOM selection element is not the same as the backing node during beforeinput.
(isBeforeInput || !CAN_USE_BEFORE_INPUT) && backingAnchorElement !== null && !anchorNode.isComposing() && domAnchorNode !== getDOMTextNode(backingAnchorElement) ||
// If TargetRange is not the same as the DOM selection; browser trying to edit random parts
// of the editor.
domSelection !== null && domTargetRange !== null && (!domTargetRange.collapsed || domTargetRange.startContainer !== domSelection.anchorNode || domTargetRange.startOffset !== domSelection.anchorOffset) ||
// Check if we're changing from bold to italics, or some other format.
anchorNode.getFormat() !== selection.format || anchorNode.getStyle() !== selection.style ||
// One last set of heuristics to check against.
$shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode);
}
function shouldSkipSelectionChange(domNode, offset) {
return isDOMTextNode(domNode) && domNode.nodeValue !== null && offset !== 0 && offset !== domNode.nodeValue.length;
}
function onSelectionChange(domSelection, editor, isActive) {
const {
anchorNode: anchor