Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@lexical/selection

Package Overview
Dependencies
Maintainers
6
Versions
603
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lexical/selection - npm Package Compare versions

Comparing version
0.44.1-nightly.20260519.0
to
0.45.0
+19
dist/index.d.ts
/**
* 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.
*
*/
import { getStyleObjectFromCSS as getStyleObjectFromCSS_ } from 'lexical';
import { $trimTextContentFromAnchor } from './lexical-node';
export { $addNodeStyle, $ensureForwardRangeSelection, $forEachSelectedTextNode, $isAtNodeEnd, $patchStyleText, $sliceSelectedTextNodeContent, $trimTextContentFromAnchor, } from './lexical-node';
export { $copyBlockFormatIndent, $getSelectionStyleValueForProperty, $isParentElementRTL, $moveCaretSelection, $moveCharacter, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $wrapNodes, } from './range-selection';
export { $getComputedStyleForElement, $getComputedStyleForParent, $isParentRTL, createDOMRange, createRectsFromDOMRange, getCSSFromStyleObject, } from './utils';
/** @deprecated moved to the `lexical` package */
export declare const getStyleObjectFromCSS: typeof getStyleObjectFromCSS_;
/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */
export declare const trimTextContentFromAnchor: typeof $trimTextContentFromAnchor;
export {
/** @deprecated moved to the lexical package */ $cloneWithProperties,
/** @deprecated moved to the lexical package */ $selectAll, } from 'lexical';
import { BaseSelection, ElementNode, LexicalEditor, Point, RangeSelection, TextNode } from 'lexical';
/**
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
* it to be generated into the new TextNode.
* @param selection - The selection containing the node whose TextNode is to be edited.
* @param textNode - The TextNode to be edited.
* @param mutates - 'clone' to return a clone before mutating, 'self' to update in-place
* @returns The updated TextNode or clone.
*/
export declare function $sliceSelectedTextNodeContent<T extends TextNode>(selection: BaseSelection, textNode: T, mutates?: 'clone' | 'self'): T;
/**
* Determines if the current selection is at the end of the node.
* @param point - The point of the selection to test.
* @returns true if the provided point offset is in the last possible position, false otherwise.
*/
export declare function $isAtNodeEnd(point: Point): boolean;
/**
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
* @param editor - The lexical editor.
* @param anchor - The anchor of the current selection, where the selection should be pointing.
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
*/
export declare function $trimTextContentFromAnchor(editor: LexicalEditor, anchor: Point, delCount: number): void;
/**
* @deprecated node styles are parsed on demand and not cached eternally
*/
export declare const $addNodeStyle: (_node: TextNode) => void;
/**
* Applies the provided styles to the given TextNode, ElementNode, or
* collapsed RangeSelection.
*
* @param target - The TextNode, ElementNode, or collapsed RangeSelection to apply the styles to
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
export declare function $patchStyle(target: TextNode | RangeSelection | ElementNode, patch: Record<string, string | null | ((currentStyleValue: string | null, _target: typeof target) => string)>): void;
/**
* Applies the provided styles to the TextNodes in the provided Selection.
* Will update partially selected TextNodes by splitting the TextNode and applying
* the styles to the appropriate one.
* @param selection - The selected node(s) to update.
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
export declare function $patchStyleText(selection: BaseSelection, patch: Record<string, string | null | ((currentStyleValue: string | null, target: TextNode | RangeSelection | ElementNode) => string)>): void;
export declare function $forEachSelectedTextNode(fn: (textNode: TextNode) => void): void;
/**
* Ensure that the given RangeSelection is not backwards. If it
* is backwards, then the anchor and focus points will be swapped
* in-place. Ensuring that the selection is a writable RangeSelection
* is the responsibility of the caller (e.g. in a read-only context
* you will want to clone $getSelection() before using this).
*
* @param selection a writable RangeSelection
*/
export declare function $ensureForwardRangeSelection(selection: RangeSelection): void;
/**
* 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.
*
*/
'use strict';
var lexical = require('lexical');
/**
* 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.
*
*/
// Do not require this module directly! Use normal `invariant` calls.
function formatDevErrorMessage(message) {
throw new Error(message);
}
/**
* 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.
*
*/
/*@__INLINE__*/
function warnOnlyOnce(message) {
{
let run = false;
return () => {
if (!run) {
console.warn(message);
}
run = 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.
*
*/
function getDOMTextNode(element) {
let node = element;
while (node != null) {
if (node.nodeType === Node.TEXT_NODE) {
return node;
}
node = node.firstChild;
}
return null;
}
function getDOMIndexWithinParent(node) {
const parent = node.parentNode;
if (parent == null) {
throw new Error('Should never happen');
}
return [parent, Array.from(parent.childNodes).indexOf(node)];
}
/**
* Creates a selection range for the DOM.
* @param editor - The lexical editor.
* @param anchorNode - The anchor node of a selection.
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
* @param focusNode - The current focus.
* @param _focusOffset - The amount of space offset from the focus to the anchor.
* @returns The range of selection for the DOM that was created.
*/
function createDOMRange(editor, anchorNode, _anchorOffset, focusNode, _focusOffset) {
const anchorKey = anchorNode.getKey();
const focusKey = focusNode.getKey();
const range = document.createRange();
let anchorDOM = editor.getElementByKey(anchorKey);
let focusDOM = editor.getElementByKey(focusKey);
let anchorOffset = _anchorOffset;
let focusOffset = _focusOffset;
if (lexical.$isTextNode(anchorNode)) {
anchorDOM = getDOMTextNode(anchorDOM);
}
if (lexical.$isTextNode(focusNode)) {
focusDOM = getDOMTextNode(focusDOM);
}
if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
return null;
}
if (anchorDOM.nodeName === 'BR') {
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
}
if (focusDOM.nodeName === 'BR') {
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
}
const firstChild = anchorDOM.firstChild;
if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
focusOffset = 1;
}
try {
range.setStart(anchorDOM, anchorOffset);
range.setEnd(focusDOM, focusOffset);
} catch (_e) {
return null;
}
if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
// Range is backwards, we need to reverse it
range.setStart(focusDOM, focusOffset);
range.setEnd(anchorDOM, anchorOffset);
}
return range;
}
/**
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
* @param editor - The lexical editor
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
* @returns The selectionRects as an array.
*/
function createRectsFromDOMRange(editor, range) {
const rootElement = editor.getRootElement();
if (rootElement === null) {
return [];
}
const rootRect = rootElement.getBoundingClientRect();
const computedStyle = getComputedStyle(rootElement);
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
const selectionRects = Array.from(range.getClientRects());
let selectionRectsLength = selectionRects.length;
//sort rects from top left to bottom right.
selectionRects.sort((a, b) => {
const top = a.top - b.top;
// Some rects match position closely, but not perfectly,
// so we give a 3px tolerance.
if (Math.abs(top) <= 3) {
return a.left - b.left;
}
return top;
});
let prevRect;
for (let i = 0; i < selectionRectsLength; i++) {
const selectionRect = selectionRects[i];
// Exclude rects that overlap preceding Rects in the sorted list.
const isOverlappingRect = prevRect && prevRect.top <= selectionRect.top && prevRect.top + prevRect.height > selectionRect.top && prevRect.left + prevRect.width > selectionRect.left;
// Exclude selections that span the entire element
const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
if (isOverlappingRect || selectionSpansElement) {
selectionRects.splice(i--, 1);
selectionRectsLength--;
continue;
}
prevRect = selectionRect;
}
return selectionRects;
}
/**
* Serializes a style object into a CSS declaration string, the inverse of
* {@link getStyleObjectFromCSS}.
* @param styles - An object mapping CSS property names to their values.
* @returns A CSS string of the form `prop: value;` for each entry, concatenated together.
*/
function getCSSFromStyleObject(styles) {
let css = '';
for (const style in styles) {
if (style) {
css += `${style}: ${styles[style]};`;
}
}
return css;
}
/**
* Gets the computed DOM styles of the element.
* @param element - The node to check the styles for.
* @returns the computed styles of the element or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForElement(element) {
const editor = lexical.$getEditor();
const domElement = editor.getElementByKey(element.getKey());
if (domElement === null) {
return null;
}
const view = domElement.ownerDocument.defaultView;
if (view === null) {
return null;
}
return view.getComputedStyle(domElement);
}
/**
* Gets the computed DOM styles of the parent of the node.
* @param node - The node to check its parent's styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForParent(node) {
const parent = lexical.$isRootNode(node) ? node : node.getParentOrThrow();
return $getComputedStyleForElement(parent);
}
/**
* Determines whether a node's parent is RTL.
* @param node - The node to check whether it is RTL.
* @returns whether the node is RTL.
*/
function $isParentRTL(node) {
const styles = $getComputedStyleForParent(node);
return styles !== null && styles.direction === 'rtl';
}
/**
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
* it to be generated into the new TextNode.
* @param selection - The selection containing the node whose TextNode is to be edited.
* @param textNode - The TextNode to be edited.
* @param mutates - 'clone' to return a clone before mutating, 'self' to update in-place
* @returns The updated TextNode or clone.
*/
function $sliceSelectedTextNodeContent(selection, textNode, mutates = 'self') {
const anchorAndFocus = selection.getStartEndPoints();
if (textNode.isSelected(selection) && !lexical.$isTokenOrSegmented(textNode) && anchorAndFocus !== null) {
const [anchor, focus] = anchorAndFocus;
const isBackward = selection.isBackward();
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const isAnchor = textNode.is(anchorNode);
const isFocus = textNode.is(focusNode);
if (isAnchor || isFocus) {
const [anchorOffset, focusOffset] = lexical.$getCharacterOffsets(selection);
const isSame = anchorNode.is(focusNode);
const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
let startOffset = 0;
let endOffset = undefined;
if (isSame) {
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
} else if (isFirst) {
const offset = isBackward ? focusOffset : anchorOffset;
startOffset = offset;
endOffset = undefined;
} else if (isLast) {
const offset = isBackward ? anchorOffset : focusOffset;
startOffset = 0;
endOffset = offset;
}
// NOTE: This mutates __text directly because the primary use case is to
// modify a $cloneWithProperties node that should never be added
// to the EditorState so we must not call getWritable via setTextContent
const text = textNode.__text.slice(startOffset, endOffset);
if (text !== textNode.__text) {
if (mutates === 'clone') {
textNode = lexical.$cloneWithPropertiesEphemeral(textNode);
}
textNode.__text = text;
}
}
}
return textNode;
}
/**
* Determines if the current selection is at the end of the node.
* @param point - The point of the selection to test.
* @returns true if the provided point offset is in the last possible position, false otherwise.
*/
function $isAtNodeEnd(point) {
if (point.type === 'text') {
return point.offset === point.getNode().getTextContentSize();
}
const node = point.getNode();
if (!lexical.$isElementNode(node)) {
formatDevErrorMessage(`isAtNodeEnd: node must be a TextNode or ElementNode`);
}
return point.offset === node.getChildrenSize();
}
/**
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
* @param editor - The lexical editor.
* @param anchor - The anchor of the current selection, where the selection should be pointing.
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
*/
function $trimTextContentFromAnchor(editor, anchor, delCount) {
// Work from the current selection anchor point
let currentNode = anchor.getNode();
let remaining = delCount;
if (lexical.$isElementNode(currentNode)) {
const descendantNode = currentNode.getDescendantByIndex(anchor.offset);
if (descendantNode !== null) {
currentNode = descendantNode;
}
}
while (remaining > 0 && currentNode !== null) {
if (lexical.$isElementNode(currentNode)) {
const lastDescendant = currentNode.getLastDescendant();
if (lastDescendant !== null) {
currentNode = lastDescendant;
}
}
let nextNode = currentNode.getPreviousSibling();
let additionalElementWhitespace = 0;
if (nextNode === null) {
let parent = currentNode.getParentOrThrow();
let parentSibling = parent.getPreviousSibling();
while (parentSibling === null) {
parent = parent.getParent();
if (parent === null) {
nextNode = null;
break;
}
parentSibling = parent.getPreviousSibling();
}
if (parent !== null) {
additionalElementWhitespace = parent.isInline() ? 0 : 2;
nextNode = parentSibling;
}
}
let text = currentNode.getTextContent();
// If the text is empty, we need to consider adding in two line breaks to match
// the content if we were to get it from its parent.
if (text === '' && lexical.$isElementNode(currentNode) && !currentNode.isInline()) {
// TODO: should this be handled in core?
text = '\n\n';
}
const currentNodeSize = text.length;
if (!lexical.$isTextNode(currentNode) || remaining >= currentNodeSize) {
const parent = currentNode.getParent();
currentNode.remove();
if (parent != null && parent.getChildrenSize() === 0 && !lexical.$isRootNode(parent)) {
parent.remove();
}
remaining -= currentNodeSize + additionalElementWhitespace;
currentNode = nextNode;
} else {
const key = currentNode.getKey();
// See if we can just revert it to what was in the last editor state
const prevTextContent = editor.getEditorState().read(() => {
const prevNode = lexical.$getNodeByKey(key);
if (lexical.$isTextNode(prevNode) && prevNode.isSimpleText()) {
return prevNode.getTextContent();
}
return null;
});
const offset = currentNodeSize - remaining;
const slicedText = text.slice(0, offset);
if (prevTextContent !== null && prevTextContent !== text) {
const prevSelection = lexical.$getPreviousSelection();
let target = currentNode;
if (!currentNode.isSimpleText()) {
const textNode = lexical.$createTextNode(prevTextContent);
currentNode.replace(textNode);
target = textNode;
} else {
currentNode.setTextContent(prevTextContent);
}
if (lexical.$isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
const prevOffset = prevSelection.anchor.offset;
target.select(prevOffset, prevOffset);
}
} else if (currentNode.isSimpleText()) {
// Split text
const isSelected = anchor.key === key;
let anchorOffset = anchor.offset;
// Move offset to end if it's less than the remaining number, otherwise
// we'll have a negative splitStart.
if (anchorOffset < remaining) {
anchorOffset = currentNodeSize;
}
const splitStart = isSelected ? anchorOffset - remaining : 0;
const splitEnd = isSelected ? anchorOffset : offset;
if (isSelected && splitStart === 0) {
const [excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
} else {
const [, excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
}
} else {
const textNode = lexical.$createTextNode(slicedText);
currentNode.replace(textNode);
}
remaining = 0;
}
}
}
/**
* @deprecated node styles are parsed on demand and not cached eternally
*/
const $addNodeStyle = warnOnlyOnce('$addNodeStyle is a deprecated no-op and calls should be removed');
/**
* Applies the provided styles to the given TextNode, ElementNode, or
* collapsed RangeSelection.
*
* @param target - The TextNode, ElementNode, or collapsed RangeSelection to apply the styles to
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyle(target, patch) {
if (!(lexical.$isRangeSelection(target) ? target.isCollapsed() : lexical.$isTextNode(target) || lexical.$isElementNode(target))) {
formatDevErrorMessage(`$patchStyle must only be called with a TextNode, ElementNode, or collapsed RangeSelection`);
}
const prevStyles = lexical.getStyleObjectFromCSS(lexical.$isRangeSelection(target) ? target.style : lexical.$isTextNode(target) ? target.getStyle() : target.getTextStyle());
const newStyles = Object.entries(patch).reduce((styles, [key, value]) => {
if (typeof value === 'function') {
styles[key] = value(prevStyles[key], target);
} else if (value === null) {
delete styles[key];
} else {
styles[key] = value;
}
return styles;
}, {
...prevStyles
});
const newCSSText = getCSSFromStyleObject(newStyles);
if (lexical.$isRangeSelection(target) || lexical.$isTextNode(target)) {
target.setStyle(newCSSText);
} else {
target.setTextStyle(newCSSText);
}
}
/**
* Applies the provided styles to the TextNodes in the provided Selection.
* Will update partially selected TextNodes by splitting the TextNode and applying
* the styles to the appropriate one.
* @param selection - The selected node(s) to update.
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyleText(selection, patch) {
if (lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
$patchStyle(selection, patch);
const emptyNode = selection.anchor.getNode();
if (lexical.$isElementNode(emptyNode) && emptyNode.isEmpty()) {
$patchStyle(emptyNode, patch);
}
}
$forEachSelectedTextNode(textNode => {
$patchStyle(textNode, patch);
});
const nodes = selection.getNodes();
if (nodes.length > 0) {
const patchedElementKeys = new Set();
for (const node of nodes) {
if (!lexical.$isElementNode(node) || !node.canBeEmpty() || node.getChildrenSize() !== 0) {
continue;
}
const key = node.getKey();
if (patchedElementKeys.has(key)) {
continue;
}
patchedElementKeys.add(key);
$patchStyle(node, patch);
}
}
}
function $forEachSelectedTextNode(fn) {
const selection = lexical.$getSelection();
if (!selection) {
return;
}
const slicedTextNodes = new Map();
const getSliceIndices = node => slicedTextNodes.get(node.getKey()) || [0, node.getTextContentSize()];
if (lexical.$isRangeSelection(selection)) {
for (const slice of lexical.$caretRangeFromSelection(selection).getTextSlices()) {
if (slice) {
slicedTextNodes.set(slice.caret.origin.getKey(), slice.getSliceIndices());
}
}
}
const selectedNodes = selection.getNodes();
for (const selectedNode of selectedNodes) {
if (!(lexical.$isTextNode(selectedNode) && selectedNode.canHaveFormat())) {
continue;
}
const [startOffset, endOffset] = getSliceIndices(selectedNode);
// No actual text is selected, so do nothing.
if (endOffset === startOffset) {
continue;
}
// The entire node is selected or a token/segment, so just format it
if (lexical.$isTokenOrSegmented(selectedNode) || startOffset === 0 && endOffset === selectedNode.getTextContentSize()) {
fn(selectedNode);
} else {
// The node is partially selected, so split it into two or three nodes
// and style the selected one.
const splitNodes = selectedNode.splitText(startOffset, endOffset);
const replacement = splitNodes[startOffset === 0 ? 0 : 1];
fn(replacement);
}
}
// Prior to NodeCaret #7046 this would have been a side-effect
// so we do this for test compatibility.
// TODO: we may want to consider simplifying by removing this
if (lexical.$isRangeSelection(selection) && selection.anchor.type === 'text' && selection.focus.type === 'text' && selection.anchor.key === selection.focus.key) {
$ensureForwardRangeSelection(selection);
}
}
/**
* Ensure that the given RangeSelection is not backwards. If it
* is backwards, then the anchor and focus points will be swapped
* in-place. Ensuring that the selection is a writable RangeSelection
* is the responsibility of the caller (e.g. in a read-only context
* you will want to clone $getSelection() before using this).
*
* @param selection a writable RangeSelection
*/
function $ensureForwardRangeSelection(selection) {
if (selection.isBackward()) {
const {
anchor,
focus
} = selection;
// stash for the in-place swap
const {
key,
offset,
type
} = anchor;
anchor.set(focus.key, focus.offset, focus.type);
focus.set(key, offset, type);
}
}
function $copyBlockFormatIndent(srcNode, destNode) {
const format = srcNode.getFormatType();
const indent = srcNode.getIndent();
if (format !== destNode.getFormatType()) {
destNode.setFormat(format);
}
if (indent !== destNode.getIndent()) {
destNode.setIndent(indent);
}
}
function $isPointAtBlockStart(point, block) {
if (point.offset !== 0) {
return false;
}
let node = point.getNode();
// When an ElementNode is empty it's not possible to distinguish if
// the selection's intent is the entire block or the edge so we consider
// it to be the entire block
if (lexical.$isElementNode(node) && node.isEmpty()) {
return false;
}
while (!node.is(block)) {
if (node.getPreviousSibling() !== null) {
return false;
}
const parent = node.getParent();
if (parent === null) {
return false;
}
node = parent;
}
return true;
}
/**
* Converts all nodes in the selection that are of one block type to another.
* @param selection - The selected blocks to be converted.
* @param $createElement - The function that creates the node. eg. $createParagraphNode.
* @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
*/
function $setBlocksType(selection, $createElement, $afterCreateElement = $copyBlockFormatIndent) {
if (!selection) {
return;
}
// Selections tend to not include their containing blocks so we effectively
// expand it here
const anchorAndFocus = selection.getStartEndPoints();
let skipFocusAtBlockStart = false;
let focusBlock = null;
const blockMap = new Map();
if (anchorAndFocus) {
const [anchor, focus] = anchorAndFocus;
const anchorBlock = lexical.$findMatchingParent(anchor.getNode(), lexical.INTERNAL_$isBlock);
focusBlock = lexical.$findMatchingParent(focus.getNode(), lexical.INTERNAL_$isBlock);
skipFocusAtBlockStart = lexical.$isElementNode(focusBlock) && !focusBlock.is(anchorBlock) && $isPointAtBlockStart(focus, focusBlock);
if (lexical.$isElementNode(anchorBlock)) {
blockMap.set(anchorBlock.getKey(), anchorBlock);
}
if (lexical.$isElementNode(focusBlock) && !skipFocusAtBlockStart) {
blockMap.set(focusBlock.getKey(), focusBlock);
}
}
for (const node of selection.getNodes()) {
if (lexical.$isElementNode(node) && lexical.INTERNAL_$isBlock(node)) {
if (skipFocusAtBlockStart && node.is(focusBlock)) {
continue;
}
blockMap.set(node.getKey(), node);
} else if (!anchorAndFocus) {
const ancestorBlock = lexical.$findMatchingParent(node, lexical.INTERNAL_$isBlock);
if (lexical.$isElementNode(ancestorBlock)) {
blockMap.set(ancestorBlock.getKey(), ancestorBlock);
}
}
}
// Selection remapping is delegated to LexicalNode.replace (and the
// ListItemNode.replace override): both remap an element-anchored point
// on the replaced block to {key: replacement, offset: prevSize + offset}.
for (const prevNode of blockMap.values()) {
const element = $createElement();
$afterCreateElement(prevNode, element);
prevNode.replace(element, true);
}
}
function isPointAttached(point) {
return point.getNode().isAttached();
}
function $removeParentEmptyElements(startingNode) {
let node = startingNode;
while (node !== null && !lexical.$isRootOrShadowRoot(node)) {
const latest = node.getLatest();
const parentNode = node.getParent();
if (latest.getChildrenSize() === 0) {
node.remove(true);
}
node = parentNode;
}
}
/**
* @deprecated In favor of $setBlockTypes
* Wraps all nodes in the selection into another node of the type returned by createElement.
* @param selection - The selection of nodes to be wrapped.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to append the wrapped selection and its children to.
*/
function $wrapNodes(selection, createElement, wrappingElement = null) {
const anchorAndFocus = selection.getStartEndPoints();
const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
const nodes = selection.getNodes();
const nodesLength = nodes.length;
if (anchor !== null && (nodesLength === 0 || nodesLength === 1 && anchor.type === 'element' && anchor.getNode().getChildrenSize() === 0)) {
const target = anchor.type === 'text' ? anchor.getNode().getParentOrThrow() : anchor.getNode();
const children = target.getChildren();
let element = createElement();
element.setFormat(target.getFormatType());
element.setIndent(target.getIndent());
children.forEach(child => element.append(child));
if (wrappingElement) {
element = wrappingElement.append(element);
}
target.replace(element);
return;
}
let topLevelNode = null;
let descendants = [];
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
// Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
// user selected multiple Root-like nodes that have to be treated separately as if they are
// their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
// of each of the cell nodes.
if (lexical.$isRootOrShadowRoot(node)) {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [];
topLevelNode = node;
} else if (topLevelNode === null || topLevelNode !== null && lexical.$hasAncestor(node, topLevelNode)) {
descendants.push(node);
} else {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [node];
}
}
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
}
/**
* Wraps each node into a new ElementNode.
* @param selection - The selection of nodes to wrap.
* @param nodes - An array of nodes, generally the descendants of the selection.
* @param nodesLength - The length of nodes.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to wrap all the nodes into.
* @returns
*/
function $wrapNodesImpl(selection, nodes, nodesLength, createElement, wrappingElement = null) {
if (nodes.length === 0) {
return;
}
const firstNode = nodes[0];
const elementMapping = new Map();
const elements = [];
// The below logic is to find the right target for us to
// either insertAfter/insertBefore/append the corresponding
// elements to. This is made more complicated due to nested
// structures.
let target = lexical.$isElementNode(firstNode) ? firstNode : firstNode.getParentOrThrow();
if (target.isInline()) {
target = target.getParentOrThrow();
}
let targetIsPrevSibling = false;
while (target !== null) {
const prevSibling = target.getPreviousSibling();
if (prevSibling !== null) {
target = prevSibling;
targetIsPrevSibling = true;
break;
}
target = target.getParentOrThrow();
if (lexical.$isRootOrShadowRoot(target)) {
break;
}
}
const emptyElements = new Set();
// Find any top level empty elements
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
if (lexical.$isElementNode(node) && node.getChildrenSize() === 0) {
emptyElements.add(node.getKey());
}
}
const movedNodes = new Set();
// Move out all leaf nodes into our elements array.
// If we find a top level empty element, also move make
// an element for that.
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
let parent = node.getParent();
if (parent !== null && parent.isInline()) {
parent = parent.getParent();
}
if (parent !== null && lexical.$isLeafNode(node) && !movedNodes.has(node.getKey())) {
const parentKey = parent.getKey();
if (elementMapping.get(parentKey) === undefined) {
const targetElement = createElement();
targetElement.setFormat(parent.getFormatType());
targetElement.setIndent(parent.getIndent());
elements.push(targetElement);
elementMapping.set(parentKey, targetElement);
// Move node and its siblings to the new
// element.
const children = parent.getChildren();
targetElement.splice(targetElement.getChildrenSize(), 0, children);
for (const child of children) {
movedNodes.add(child.getKey());
if (lexical.$isElementNode(child)) {
// Skip nested leaf nodes if the parent has already been moved
for (const key of child.getChildrenKeys()) {
movedNodes.add(key);
}
}
}
$removeParentEmptyElements(parent);
}
} else if (emptyElements.has(node.getKey())) {
if (!lexical.$isElementNode(node)) {
formatDevErrorMessage(`Expected node in emptyElements to be an ElementNode`);
}
const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
elements.push(targetElement);
node.remove(true);
}
}
if (wrappingElement !== null) {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
wrappingElement.append(element);
}
}
let lastElement = null;
// If our target is Root-like, let's see if we can re-adjust
// so that the target is the first child instead.
if (lexical.$isRootOrShadowRoot(target)) {
if (targetIsPrevSibling) {
if (wrappingElement !== null) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
}
}
} else {
const firstChild = target.getFirstChild();
if (lexical.$isElementNode(firstChild)) {
target = firstChild;
}
if (firstChild === null) {
if (wrappingElement) {
target.append(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
target.append(element);
lastElement = element;
}
}
} else {
if (wrappingElement !== null) {
firstChild.insertBefore(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
firstChild.insertBefore(element);
lastElement = element;
}
}
}
}
} else {
if (wrappingElement) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
lastElement = element;
}
}
}
const prevSelection = lexical.$getPreviousSelection();
if (lexical.$isRangeSelection(prevSelection) && isPointAttached(prevSelection.anchor) && isPointAttached(prevSelection.focus)) {
lexical.$setSelection(prevSelection.clone());
} else if (lastElement !== null) {
lastElement.selectEnd();
} else {
selection.dirty = true;
}
}
/**
* Tests if the selection's parent element has vertical writing mode.
* @param selection - The selection whose parent to test.
* @returns true if the selection's parent has vertical writing mode (writing-mode: vertical-rl), false otherwise.
*/
function $isEditorVerticalOrientation(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.writingMode === 'vertical-rl';
}
/**
* Gets the computed DOM styles of the parent of the selection's anchor node.
* @param selection - The selection to check the styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyle(selection) {
const anchorNode = selection.anchor.getNode();
if (lexical.$isElementNode(anchorNode)) {
return $getComputedStyleForElement(anchorNode);
}
return $getComputedStyleForParent(anchorNode);
}
/**
* Determines if the default character selection should be overridden. Used with DecoratorNodes
* @param selection - The selection whose default character selection may need to be overridden.
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
* @returns true if it should be overridden, false if not.
*/
function $shouldOverrideDefaultCharacterSelection(selection, isBackward) {
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical writing mode, we adjust the direction for correct caret movement
let adjustedIsBackward = isVertical ? !isBackward : isBackward;
// In right-to-left writing mode, we invert the direction for correct caret movement
if ($isParentElementRTL(selection)) {
adjustedIsBackward = !adjustedIsBackward;
}
const focusCaret = lexical.$caretFromPoint(selection.focus, adjustedIsBackward ? 'previous' : 'next');
if (lexical.$isExtendableTextPointCaret(focusCaret)) {
return false;
}
for (const nextCaret of lexical.$extendCaretToRange(focusCaret)) {
if (lexical.$isChildCaret(nextCaret)) {
return !nextCaret.origin.isInline();
} else if (lexical.$isElementNode(nextCaret.origin)) {
continue;
} else if (lexical.$isDecoratorNode(nextCaret.origin)) {
return true;
}
break;
}
return false;
}
/**
* Moves the selection according to the arguments.
* @param selection - The selected text or nodes.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
* @param granularity - The distance to adjust the current selection.
*/
function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
}
/**
* Tests a parent element for right to left direction.
* @param selection - The selection whose parent is to be tested.
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
*/
function $isParentElementRTL(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.direction === 'rtl';
}
/**
* Moves selection by character according to arguments.
* @param selection - The selection of the characters to move.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
*/
function $moveCharacter(selection, isHoldingShift, isBackward) {
const isRTL = $isParentElementRTL(selection);
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical-rl writing mode, arrow key directions need to be flipped
// to match the visual flow of text (top to bottom, right to left)
let adjustedIsBackward;
if (isVertical) {
// In vertical-rl mode, we need to completely invert the direction
// Left arrow (backward) should move down (forward)
// Right arrow (forward) should move up (backward)
adjustedIsBackward = !isBackward;
} else if (isRTL) {
// In horizontal RTL mode, use the standard RTL behavior
adjustedIsBackward = !isBackward;
} else {
// Standard LTR horizontal text
adjustedIsBackward = isBackward;
}
// Apply the direction adjustment to move the caret
$moveCaretSelection(selection, isHoldingShift, adjustedIsBackward, 'character');
}
/**
* Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.
* @param node - The node whose style value to get.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property.
* @returns The value of the property for node.
*/
function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
const css = node.getStyle();
const styleObject = lexical.getStyleObjectFromCSS(css);
if (styleObject !== null) {
return styleObject[styleProperty] || defaultValue;
}
return defaultValue;
}
/**
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
* If all TextNodes do not have the same value, it returns an empty string.
* @param selection - The selection of TextNodes whose value to find.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property, defaults to an empty string.
* @returns The value of the property for the selected TextNodes.
*/
function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
let styleValue = null;
const nodes = selection.getNodes();
// The anchor/focus boundary handling below is specific to RangeSelection;
// other selection types (e.g. table) style every node they contain.
let startNode;
let endNode;
if (lexical.$isRangeSelection(selection)) {
if (selection.isCollapsed() && selection.style !== '') {
const styleObject = lexical.getStyleObjectFromCSS(selection.style);
if (styleObject !== null && styleProperty in styleObject) {
return styleObject[styleProperty];
}
}
const {
anchor,
focus
} = selection;
const isBackward = selection.isBackward();
const firstNode = isBackward ? focus.getNode() : anchor.getNode();
const lastNode = isBackward ? anchor.getNode() : focus.getNode();
const startOffset = isBackward ? focus.offset : anchor.offset;
const endOffset = isBackward ? anchor.offset : focus.offset;
// A boundary node contributes no styled text when the selection merely
// touches its edge: the first node when the start offset is at its very
// end, and the last node when the end offset is at its very beginning.
if (lexical.$isTextNode(firstNode) && startOffset === firstNode.getTextContentSize()) {
startNode = firstNode;
}
if (endOffset === 0) {
endNode = lastNode;
}
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// Skip the excluded boundary node for this position (startNode at the
// head, endNode elsewhere); both are undefined when nothing is excluded.
if (lexical.$isTextNode(node) && !node.is(i === 0 ? startNode : endNode)) {
const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
if (styleValue === null) {
styleValue = nodeStyleValue;
} else if (styleValue !== nodeStyleValue) {
// multiple text nodes are in the selection and they don't all
// have the same style.
styleValue = '';
break;
}
}
}
return styleValue === null ? defaultValue : styleValue;
}
/**
* 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.
*
*/
/** @deprecated moved to the `lexical` package */
const getStyleObjectFromCSS = lexical.getStyleObjectFromCSS;
/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */
const trimTextContentFromAnchor = $trimTextContentFromAnchor;
exports.$cloneWithProperties = lexical.$cloneWithProperties;
exports.$selectAll = lexical.$selectAll;
exports.$addNodeStyle = $addNodeStyle;
exports.$copyBlockFormatIndent = $copyBlockFormatIndent;
exports.$ensureForwardRangeSelection = $ensureForwardRangeSelection;
exports.$forEachSelectedTextNode = $forEachSelectedTextNode;
exports.$getComputedStyleForElement = $getComputedStyleForElement;
exports.$getComputedStyleForParent = $getComputedStyleForParent;
exports.$getSelectionStyleValueForProperty = $getSelectionStyleValueForProperty;
exports.$isAtNodeEnd = $isAtNodeEnd;
exports.$isParentElementRTL = $isParentElementRTL;
exports.$isParentRTL = $isParentRTL;
exports.$moveCaretSelection = $moveCaretSelection;
exports.$moveCharacter = $moveCharacter;
exports.$patchStyleText = $patchStyleText;
exports.$setBlocksType = $setBlocksType;
exports.$shouldOverrideDefaultCharacterSelection = $shouldOverrideDefaultCharacterSelection;
exports.$sliceSelectedTextNodeContent = $sliceSelectedTextNodeContent;
exports.$trimTextContentFromAnchor = $trimTextContentFromAnchor;
exports.$wrapNodes = $wrapNodes;
exports.createDOMRange = createDOMRange;
exports.createRectsFromDOMRange = createRectsFromDOMRange;
exports.getCSSFromStyleObject = getCSSFromStyleObject;
exports.getStyleObjectFromCSS = getStyleObjectFromCSS;
exports.trimTextContentFromAnchor = trimTextContentFromAnchor;
/**
* 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.
*
*/
import { $isTextNode, $getEditor, $isRootNode, $getSelection, $isRangeSelection, $caretRangeFromSelection, $isTokenOrSegmented, $isElementNode, $getCharacterOffsets, $cloneWithPropertiesEphemeral, $getNodeByKey, $getPreviousSelection, $createTextNode, getStyleObjectFromCSS as getStyleObjectFromCSS$1, $findMatchingParent, INTERNAL_$isBlock, $caretFromPoint, $isExtendableTextPointCaret, $extendCaretToRange, $isChildCaret, $isDecoratorNode, $isRootOrShadowRoot, $hasAncestor, $isLeafNode, $setSelection } from 'lexical';
export { $cloneWithProperties, $selectAll } from 'lexical';
/**
* 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.
*
*/
// Do not require this module directly! Use normal `invariant` calls.
function formatDevErrorMessage(message) {
throw new Error(message);
}
/**
* 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.
*
*/
/*@__INLINE__*/
function warnOnlyOnce(message) {
{
let run = false;
return () => {
if (!run) {
console.warn(message);
}
run = 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.
*
*/
function getDOMTextNode(element) {
let node = element;
while (node != null) {
if (node.nodeType === Node.TEXT_NODE) {
return node;
}
node = node.firstChild;
}
return null;
}
function getDOMIndexWithinParent(node) {
const parent = node.parentNode;
if (parent == null) {
throw new Error('Should never happen');
}
return [parent, Array.from(parent.childNodes).indexOf(node)];
}
/**
* Creates a selection range for the DOM.
* @param editor - The lexical editor.
* @param anchorNode - The anchor node of a selection.
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
* @param focusNode - The current focus.
* @param _focusOffset - The amount of space offset from the focus to the anchor.
* @returns The range of selection for the DOM that was created.
*/
function createDOMRange(editor, anchorNode, _anchorOffset, focusNode, _focusOffset) {
const anchorKey = anchorNode.getKey();
const focusKey = focusNode.getKey();
const range = document.createRange();
let anchorDOM = editor.getElementByKey(anchorKey);
let focusDOM = editor.getElementByKey(focusKey);
let anchorOffset = _anchorOffset;
let focusOffset = _focusOffset;
if ($isTextNode(anchorNode)) {
anchorDOM = getDOMTextNode(anchorDOM);
}
if ($isTextNode(focusNode)) {
focusDOM = getDOMTextNode(focusDOM);
}
if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
return null;
}
if (anchorDOM.nodeName === 'BR') {
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
}
if (focusDOM.nodeName === 'BR') {
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
}
const firstChild = anchorDOM.firstChild;
if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
focusOffset = 1;
}
try {
range.setStart(anchorDOM, anchorOffset);
range.setEnd(focusDOM, focusOffset);
} catch (_e) {
return null;
}
if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
// Range is backwards, we need to reverse it
range.setStart(focusDOM, focusOffset);
range.setEnd(anchorDOM, anchorOffset);
}
return range;
}
/**
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
* @param editor - The lexical editor
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
* @returns The selectionRects as an array.
*/
function createRectsFromDOMRange(editor, range) {
const rootElement = editor.getRootElement();
if (rootElement === null) {
return [];
}
const rootRect = rootElement.getBoundingClientRect();
const computedStyle = getComputedStyle(rootElement);
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
const selectionRects = Array.from(range.getClientRects());
let selectionRectsLength = selectionRects.length;
//sort rects from top left to bottom right.
selectionRects.sort((a, b) => {
const top = a.top - b.top;
// Some rects match position closely, but not perfectly,
// so we give a 3px tolerance.
if (Math.abs(top) <= 3) {
return a.left - b.left;
}
return top;
});
let prevRect;
for (let i = 0; i < selectionRectsLength; i++) {
const selectionRect = selectionRects[i];
// Exclude rects that overlap preceding Rects in the sorted list.
const isOverlappingRect = prevRect && prevRect.top <= selectionRect.top && prevRect.top + prevRect.height > selectionRect.top && prevRect.left + prevRect.width > selectionRect.left;
// Exclude selections that span the entire element
const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
if (isOverlappingRect || selectionSpansElement) {
selectionRects.splice(i--, 1);
selectionRectsLength--;
continue;
}
prevRect = selectionRect;
}
return selectionRects;
}
/**
* Serializes a style object into a CSS declaration string, the inverse of
* {@link getStyleObjectFromCSS}.
* @param styles - An object mapping CSS property names to their values.
* @returns A CSS string of the form `prop: value;` for each entry, concatenated together.
*/
function getCSSFromStyleObject(styles) {
let css = '';
for (const style in styles) {
if (style) {
css += `${style}: ${styles[style]};`;
}
}
return css;
}
/**
* Gets the computed DOM styles of the element.
* @param element - The node to check the styles for.
* @returns the computed styles of the element or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForElement(element) {
const editor = $getEditor();
const domElement = editor.getElementByKey(element.getKey());
if (domElement === null) {
return null;
}
const view = domElement.ownerDocument.defaultView;
if (view === null) {
return null;
}
return view.getComputedStyle(domElement);
}
/**
* Gets the computed DOM styles of the parent of the node.
* @param node - The node to check its parent's styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForParent(node) {
const parent = $isRootNode(node) ? node : node.getParentOrThrow();
return $getComputedStyleForElement(parent);
}
/**
* Determines whether a node's parent is RTL.
* @param node - The node to check whether it is RTL.
* @returns whether the node is RTL.
*/
function $isParentRTL(node) {
const styles = $getComputedStyleForParent(node);
return styles !== null && styles.direction === 'rtl';
}
/**
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
* it to be generated into the new TextNode.
* @param selection - The selection containing the node whose TextNode is to be edited.
* @param textNode - The TextNode to be edited.
* @param mutates - 'clone' to return a clone before mutating, 'self' to update in-place
* @returns The updated TextNode or clone.
*/
function $sliceSelectedTextNodeContent(selection, textNode, mutates = 'self') {
const anchorAndFocus = selection.getStartEndPoints();
if (textNode.isSelected(selection) && !$isTokenOrSegmented(textNode) && anchorAndFocus !== null) {
const [anchor, focus] = anchorAndFocus;
const isBackward = selection.isBackward();
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const isAnchor = textNode.is(anchorNode);
const isFocus = textNode.is(focusNode);
if (isAnchor || isFocus) {
const [anchorOffset, focusOffset] = $getCharacterOffsets(selection);
const isSame = anchorNode.is(focusNode);
const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
let startOffset = 0;
let endOffset = undefined;
if (isSame) {
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
} else if (isFirst) {
const offset = isBackward ? focusOffset : anchorOffset;
startOffset = offset;
endOffset = undefined;
} else if (isLast) {
const offset = isBackward ? anchorOffset : focusOffset;
startOffset = 0;
endOffset = offset;
}
// NOTE: This mutates __text directly because the primary use case is to
// modify a $cloneWithProperties node that should never be added
// to the EditorState so we must not call getWritable via setTextContent
const text = textNode.__text.slice(startOffset, endOffset);
if (text !== textNode.__text) {
if (mutates === 'clone') {
textNode = $cloneWithPropertiesEphemeral(textNode);
}
textNode.__text = text;
}
}
}
return textNode;
}
/**
* Determines if the current selection is at the end of the node.
* @param point - The point of the selection to test.
* @returns true if the provided point offset is in the last possible position, false otherwise.
*/
function $isAtNodeEnd(point) {
if (point.type === 'text') {
return point.offset === point.getNode().getTextContentSize();
}
const node = point.getNode();
if (!$isElementNode(node)) {
formatDevErrorMessage(`isAtNodeEnd: node must be a TextNode or ElementNode`);
}
return point.offset === node.getChildrenSize();
}
/**
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
* @param editor - The lexical editor.
* @param anchor - The anchor of the current selection, where the selection should be pointing.
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
*/
function $trimTextContentFromAnchor(editor, anchor, delCount) {
// Work from the current selection anchor point
let currentNode = anchor.getNode();
let remaining = delCount;
if ($isElementNode(currentNode)) {
const descendantNode = currentNode.getDescendantByIndex(anchor.offset);
if (descendantNode !== null) {
currentNode = descendantNode;
}
}
while (remaining > 0 && currentNode !== null) {
if ($isElementNode(currentNode)) {
const lastDescendant = currentNode.getLastDescendant();
if (lastDescendant !== null) {
currentNode = lastDescendant;
}
}
let nextNode = currentNode.getPreviousSibling();
let additionalElementWhitespace = 0;
if (nextNode === null) {
let parent = currentNode.getParentOrThrow();
let parentSibling = parent.getPreviousSibling();
while (parentSibling === null) {
parent = parent.getParent();
if (parent === null) {
nextNode = null;
break;
}
parentSibling = parent.getPreviousSibling();
}
if (parent !== null) {
additionalElementWhitespace = parent.isInline() ? 0 : 2;
nextNode = parentSibling;
}
}
let text = currentNode.getTextContent();
// If the text is empty, we need to consider adding in two line breaks to match
// the content if we were to get it from its parent.
if (text === '' && $isElementNode(currentNode) && !currentNode.isInline()) {
// TODO: should this be handled in core?
text = '\n\n';
}
const currentNodeSize = text.length;
if (!$isTextNode(currentNode) || remaining >= currentNodeSize) {
const parent = currentNode.getParent();
currentNode.remove();
if (parent != null && parent.getChildrenSize() === 0 && !$isRootNode(parent)) {
parent.remove();
}
remaining -= currentNodeSize + additionalElementWhitespace;
currentNode = nextNode;
} else {
const key = currentNode.getKey();
// See if we can just revert it to what was in the last editor state
const prevTextContent = editor.getEditorState().read(() => {
const prevNode = $getNodeByKey(key);
if ($isTextNode(prevNode) && prevNode.isSimpleText()) {
return prevNode.getTextContent();
}
return null;
});
const offset = currentNodeSize - remaining;
const slicedText = text.slice(0, offset);
if (prevTextContent !== null && prevTextContent !== text) {
const prevSelection = $getPreviousSelection();
let target = currentNode;
if (!currentNode.isSimpleText()) {
const textNode = $createTextNode(prevTextContent);
currentNode.replace(textNode);
target = textNode;
} else {
currentNode.setTextContent(prevTextContent);
}
if ($isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
const prevOffset = prevSelection.anchor.offset;
target.select(prevOffset, prevOffset);
}
} else if (currentNode.isSimpleText()) {
// Split text
const isSelected = anchor.key === key;
let anchorOffset = anchor.offset;
// Move offset to end if it's less than the remaining number, otherwise
// we'll have a negative splitStart.
if (anchorOffset < remaining) {
anchorOffset = currentNodeSize;
}
const splitStart = isSelected ? anchorOffset - remaining : 0;
const splitEnd = isSelected ? anchorOffset : offset;
if (isSelected && splitStart === 0) {
const [excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
} else {
const [, excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
}
} else {
const textNode = $createTextNode(slicedText);
currentNode.replace(textNode);
}
remaining = 0;
}
}
}
/**
* @deprecated node styles are parsed on demand and not cached eternally
*/
const $addNodeStyle = warnOnlyOnce('$addNodeStyle is a deprecated no-op and calls should be removed');
/**
* Applies the provided styles to the given TextNode, ElementNode, or
* collapsed RangeSelection.
*
* @param target - The TextNode, ElementNode, or collapsed RangeSelection to apply the styles to
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyle(target, patch) {
if (!($isRangeSelection(target) ? target.isCollapsed() : $isTextNode(target) || $isElementNode(target))) {
formatDevErrorMessage(`$patchStyle must only be called with a TextNode, ElementNode, or collapsed RangeSelection`);
}
const prevStyles = getStyleObjectFromCSS$1($isRangeSelection(target) ? target.style : $isTextNode(target) ? target.getStyle() : target.getTextStyle());
const newStyles = Object.entries(patch).reduce((styles, [key, value]) => {
if (typeof value === 'function') {
styles[key] = value(prevStyles[key], target);
} else if (value === null) {
delete styles[key];
} else {
styles[key] = value;
}
return styles;
}, {
...prevStyles
});
const newCSSText = getCSSFromStyleObject(newStyles);
if ($isRangeSelection(target) || $isTextNode(target)) {
target.setStyle(newCSSText);
} else {
target.setTextStyle(newCSSText);
}
}
/**
* Applies the provided styles to the TextNodes in the provided Selection.
* Will update partially selected TextNodes by splitting the TextNode and applying
* the styles to the appropriate one.
* @param selection - The selected node(s) to update.
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyleText(selection, patch) {
if ($isRangeSelection(selection) && selection.isCollapsed()) {
$patchStyle(selection, patch);
const emptyNode = selection.anchor.getNode();
if ($isElementNode(emptyNode) && emptyNode.isEmpty()) {
$patchStyle(emptyNode, patch);
}
}
$forEachSelectedTextNode(textNode => {
$patchStyle(textNode, patch);
});
const nodes = selection.getNodes();
if (nodes.length > 0) {
const patchedElementKeys = new Set();
for (const node of nodes) {
if (!$isElementNode(node) || !node.canBeEmpty() || node.getChildrenSize() !== 0) {
continue;
}
const key = node.getKey();
if (patchedElementKeys.has(key)) {
continue;
}
patchedElementKeys.add(key);
$patchStyle(node, patch);
}
}
}
function $forEachSelectedTextNode(fn) {
const selection = $getSelection();
if (!selection) {
return;
}
const slicedTextNodes = new Map();
const getSliceIndices = node => slicedTextNodes.get(node.getKey()) || [0, node.getTextContentSize()];
if ($isRangeSelection(selection)) {
for (const slice of $caretRangeFromSelection(selection).getTextSlices()) {
if (slice) {
slicedTextNodes.set(slice.caret.origin.getKey(), slice.getSliceIndices());
}
}
}
const selectedNodes = selection.getNodes();
for (const selectedNode of selectedNodes) {
if (!($isTextNode(selectedNode) && selectedNode.canHaveFormat())) {
continue;
}
const [startOffset, endOffset] = getSliceIndices(selectedNode);
// No actual text is selected, so do nothing.
if (endOffset === startOffset) {
continue;
}
// The entire node is selected or a token/segment, so just format it
if ($isTokenOrSegmented(selectedNode) || startOffset === 0 && endOffset === selectedNode.getTextContentSize()) {
fn(selectedNode);
} else {
// The node is partially selected, so split it into two or three nodes
// and style the selected one.
const splitNodes = selectedNode.splitText(startOffset, endOffset);
const replacement = splitNodes[startOffset === 0 ? 0 : 1];
fn(replacement);
}
}
// Prior to NodeCaret #7046 this would have been a side-effect
// so we do this for test compatibility.
// TODO: we may want to consider simplifying by removing this
if ($isRangeSelection(selection) && selection.anchor.type === 'text' && selection.focus.type === 'text' && selection.anchor.key === selection.focus.key) {
$ensureForwardRangeSelection(selection);
}
}
/**
* Ensure that the given RangeSelection is not backwards. If it
* is backwards, then the anchor and focus points will be swapped
* in-place. Ensuring that the selection is a writable RangeSelection
* is the responsibility of the caller (e.g. in a read-only context
* you will want to clone $getSelection() before using this).
*
* @param selection a writable RangeSelection
*/
function $ensureForwardRangeSelection(selection) {
if (selection.isBackward()) {
const {
anchor,
focus
} = selection;
// stash for the in-place swap
const {
key,
offset,
type
} = anchor;
anchor.set(focus.key, focus.offset, focus.type);
focus.set(key, offset, type);
}
}
function $copyBlockFormatIndent(srcNode, destNode) {
const format = srcNode.getFormatType();
const indent = srcNode.getIndent();
if (format !== destNode.getFormatType()) {
destNode.setFormat(format);
}
if (indent !== destNode.getIndent()) {
destNode.setIndent(indent);
}
}
function $isPointAtBlockStart(point, block) {
if (point.offset !== 0) {
return false;
}
let node = point.getNode();
// When an ElementNode is empty it's not possible to distinguish if
// the selection's intent is the entire block or the edge so we consider
// it to be the entire block
if ($isElementNode(node) && node.isEmpty()) {
return false;
}
while (!node.is(block)) {
if (node.getPreviousSibling() !== null) {
return false;
}
const parent = node.getParent();
if (parent === null) {
return false;
}
node = parent;
}
return true;
}
/**
* Converts all nodes in the selection that are of one block type to another.
* @param selection - The selected blocks to be converted.
* @param $createElement - The function that creates the node. eg. $createParagraphNode.
* @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
*/
function $setBlocksType(selection, $createElement, $afterCreateElement = $copyBlockFormatIndent) {
if (!selection) {
return;
}
// Selections tend to not include their containing blocks so we effectively
// expand it here
const anchorAndFocus = selection.getStartEndPoints();
let skipFocusAtBlockStart = false;
let focusBlock = null;
const blockMap = new Map();
if (anchorAndFocus) {
const [anchor, focus] = anchorAndFocus;
const anchorBlock = $findMatchingParent(anchor.getNode(), INTERNAL_$isBlock);
focusBlock = $findMatchingParent(focus.getNode(), INTERNAL_$isBlock);
skipFocusAtBlockStart = $isElementNode(focusBlock) && !focusBlock.is(anchorBlock) && $isPointAtBlockStart(focus, focusBlock);
if ($isElementNode(anchorBlock)) {
blockMap.set(anchorBlock.getKey(), anchorBlock);
}
if ($isElementNode(focusBlock) && !skipFocusAtBlockStart) {
blockMap.set(focusBlock.getKey(), focusBlock);
}
}
for (const node of selection.getNodes()) {
if ($isElementNode(node) && INTERNAL_$isBlock(node)) {
if (skipFocusAtBlockStart && node.is(focusBlock)) {
continue;
}
blockMap.set(node.getKey(), node);
} else if (!anchorAndFocus) {
const ancestorBlock = $findMatchingParent(node, INTERNAL_$isBlock);
if ($isElementNode(ancestorBlock)) {
blockMap.set(ancestorBlock.getKey(), ancestorBlock);
}
}
}
// Selection remapping is delegated to LexicalNode.replace (and the
// ListItemNode.replace override): both remap an element-anchored point
// on the replaced block to {key: replacement, offset: prevSize + offset}.
for (const prevNode of blockMap.values()) {
const element = $createElement();
$afterCreateElement(prevNode, element);
prevNode.replace(element, true);
}
}
function isPointAttached(point) {
return point.getNode().isAttached();
}
function $removeParentEmptyElements(startingNode) {
let node = startingNode;
while (node !== null && !$isRootOrShadowRoot(node)) {
const latest = node.getLatest();
const parentNode = node.getParent();
if (latest.getChildrenSize() === 0) {
node.remove(true);
}
node = parentNode;
}
}
/**
* @deprecated In favor of $setBlockTypes
* Wraps all nodes in the selection into another node of the type returned by createElement.
* @param selection - The selection of nodes to be wrapped.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to append the wrapped selection and its children to.
*/
function $wrapNodes(selection, createElement, wrappingElement = null) {
const anchorAndFocus = selection.getStartEndPoints();
const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
const nodes = selection.getNodes();
const nodesLength = nodes.length;
if (anchor !== null && (nodesLength === 0 || nodesLength === 1 && anchor.type === 'element' && anchor.getNode().getChildrenSize() === 0)) {
const target = anchor.type === 'text' ? anchor.getNode().getParentOrThrow() : anchor.getNode();
const children = target.getChildren();
let element = createElement();
element.setFormat(target.getFormatType());
element.setIndent(target.getIndent());
children.forEach(child => element.append(child));
if (wrappingElement) {
element = wrappingElement.append(element);
}
target.replace(element);
return;
}
let topLevelNode = null;
let descendants = [];
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
// Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
// user selected multiple Root-like nodes that have to be treated separately as if they are
// their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
// of each of the cell nodes.
if ($isRootOrShadowRoot(node)) {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [];
topLevelNode = node;
} else if (topLevelNode === null || topLevelNode !== null && $hasAncestor(node, topLevelNode)) {
descendants.push(node);
} else {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [node];
}
}
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
}
/**
* Wraps each node into a new ElementNode.
* @param selection - The selection of nodes to wrap.
* @param nodes - An array of nodes, generally the descendants of the selection.
* @param nodesLength - The length of nodes.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to wrap all the nodes into.
* @returns
*/
function $wrapNodesImpl(selection, nodes, nodesLength, createElement, wrappingElement = null) {
if (nodes.length === 0) {
return;
}
const firstNode = nodes[0];
const elementMapping = new Map();
const elements = [];
// The below logic is to find the right target for us to
// either insertAfter/insertBefore/append the corresponding
// elements to. This is made more complicated due to nested
// structures.
let target = $isElementNode(firstNode) ? firstNode : firstNode.getParentOrThrow();
if (target.isInline()) {
target = target.getParentOrThrow();
}
let targetIsPrevSibling = false;
while (target !== null) {
const prevSibling = target.getPreviousSibling();
if (prevSibling !== null) {
target = prevSibling;
targetIsPrevSibling = true;
break;
}
target = target.getParentOrThrow();
if ($isRootOrShadowRoot(target)) {
break;
}
}
const emptyElements = new Set();
// Find any top level empty elements
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
if ($isElementNode(node) && node.getChildrenSize() === 0) {
emptyElements.add(node.getKey());
}
}
const movedNodes = new Set();
// Move out all leaf nodes into our elements array.
// If we find a top level empty element, also move make
// an element for that.
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
let parent = node.getParent();
if (parent !== null && parent.isInline()) {
parent = parent.getParent();
}
if (parent !== null && $isLeafNode(node) && !movedNodes.has(node.getKey())) {
const parentKey = parent.getKey();
if (elementMapping.get(parentKey) === undefined) {
const targetElement = createElement();
targetElement.setFormat(parent.getFormatType());
targetElement.setIndent(parent.getIndent());
elements.push(targetElement);
elementMapping.set(parentKey, targetElement);
// Move node and its siblings to the new
// element.
const children = parent.getChildren();
targetElement.splice(targetElement.getChildrenSize(), 0, children);
for (const child of children) {
movedNodes.add(child.getKey());
if ($isElementNode(child)) {
// Skip nested leaf nodes if the parent has already been moved
for (const key of child.getChildrenKeys()) {
movedNodes.add(key);
}
}
}
$removeParentEmptyElements(parent);
}
} else if (emptyElements.has(node.getKey())) {
if (!$isElementNode(node)) {
formatDevErrorMessage(`Expected node in emptyElements to be an ElementNode`);
}
const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
elements.push(targetElement);
node.remove(true);
}
}
if (wrappingElement !== null) {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
wrappingElement.append(element);
}
}
let lastElement = null;
// If our target is Root-like, let's see if we can re-adjust
// so that the target is the first child instead.
if ($isRootOrShadowRoot(target)) {
if (targetIsPrevSibling) {
if (wrappingElement !== null) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
}
}
} else {
const firstChild = target.getFirstChild();
if ($isElementNode(firstChild)) {
target = firstChild;
}
if (firstChild === null) {
if (wrappingElement) {
target.append(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
target.append(element);
lastElement = element;
}
}
} else {
if (wrappingElement !== null) {
firstChild.insertBefore(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
firstChild.insertBefore(element);
lastElement = element;
}
}
}
}
} else {
if (wrappingElement) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
lastElement = element;
}
}
}
const prevSelection = $getPreviousSelection();
if ($isRangeSelection(prevSelection) && isPointAttached(prevSelection.anchor) && isPointAttached(prevSelection.focus)) {
$setSelection(prevSelection.clone());
} else if (lastElement !== null) {
lastElement.selectEnd();
} else {
selection.dirty = true;
}
}
/**
* Tests if the selection's parent element has vertical writing mode.
* @param selection - The selection whose parent to test.
* @returns true if the selection's parent has vertical writing mode (writing-mode: vertical-rl), false otherwise.
*/
function $isEditorVerticalOrientation(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.writingMode === 'vertical-rl';
}
/**
* Gets the computed DOM styles of the parent of the selection's anchor node.
* @param selection - The selection to check the styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyle(selection) {
const anchorNode = selection.anchor.getNode();
if ($isElementNode(anchorNode)) {
return $getComputedStyleForElement(anchorNode);
}
return $getComputedStyleForParent(anchorNode);
}
/**
* Determines if the default character selection should be overridden. Used with DecoratorNodes
* @param selection - The selection whose default character selection may need to be overridden.
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
* @returns true if it should be overridden, false if not.
*/
function $shouldOverrideDefaultCharacterSelection(selection, isBackward) {
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical writing mode, we adjust the direction for correct caret movement
let adjustedIsBackward = isVertical ? !isBackward : isBackward;
// In right-to-left writing mode, we invert the direction for correct caret movement
if ($isParentElementRTL(selection)) {
adjustedIsBackward = !adjustedIsBackward;
}
const focusCaret = $caretFromPoint(selection.focus, adjustedIsBackward ? 'previous' : 'next');
if ($isExtendableTextPointCaret(focusCaret)) {
return false;
}
for (const nextCaret of $extendCaretToRange(focusCaret)) {
if ($isChildCaret(nextCaret)) {
return !nextCaret.origin.isInline();
} else if ($isElementNode(nextCaret.origin)) {
continue;
} else if ($isDecoratorNode(nextCaret.origin)) {
return true;
}
break;
}
return false;
}
/**
* Moves the selection according to the arguments.
* @param selection - The selected text or nodes.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
* @param granularity - The distance to adjust the current selection.
*/
function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
}
/**
* Tests a parent element for right to left direction.
* @param selection - The selection whose parent is to be tested.
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
*/
function $isParentElementRTL(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.direction === 'rtl';
}
/**
* Moves selection by character according to arguments.
* @param selection - The selection of the characters to move.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
*/
function $moveCharacter(selection, isHoldingShift, isBackward) {
const isRTL = $isParentElementRTL(selection);
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical-rl writing mode, arrow key directions need to be flipped
// to match the visual flow of text (top to bottom, right to left)
let adjustedIsBackward;
if (isVertical) {
// In vertical-rl mode, we need to completely invert the direction
// Left arrow (backward) should move down (forward)
// Right arrow (forward) should move up (backward)
adjustedIsBackward = !isBackward;
} else if (isRTL) {
// In horizontal RTL mode, use the standard RTL behavior
adjustedIsBackward = !isBackward;
} else {
// Standard LTR horizontal text
adjustedIsBackward = isBackward;
}
// Apply the direction adjustment to move the caret
$moveCaretSelection(selection, isHoldingShift, adjustedIsBackward, 'character');
}
/**
* Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.
* @param node - The node whose style value to get.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property.
* @returns The value of the property for node.
*/
function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
const css = node.getStyle();
const styleObject = getStyleObjectFromCSS$1(css);
if (styleObject !== null) {
return styleObject[styleProperty] || defaultValue;
}
return defaultValue;
}
/**
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
* If all TextNodes do not have the same value, it returns an empty string.
* @param selection - The selection of TextNodes whose value to find.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property, defaults to an empty string.
* @returns The value of the property for the selected TextNodes.
*/
function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
let styleValue = null;
const nodes = selection.getNodes();
// The anchor/focus boundary handling below is specific to RangeSelection;
// other selection types (e.g. table) style every node they contain.
let startNode;
let endNode;
if ($isRangeSelection(selection)) {
if (selection.isCollapsed() && selection.style !== '') {
const styleObject = getStyleObjectFromCSS$1(selection.style);
if (styleObject !== null && styleProperty in styleObject) {
return styleObject[styleProperty];
}
}
const {
anchor,
focus
} = selection;
const isBackward = selection.isBackward();
const firstNode = isBackward ? focus.getNode() : anchor.getNode();
const lastNode = isBackward ? anchor.getNode() : focus.getNode();
const startOffset = isBackward ? focus.offset : anchor.offset;
const endOffset = isBackward ? anchor.offset : focus.offset;
// A boundary node contributes no styled text when the selection merely
// touches its edge: the first node when the start offset is at its very
// end, and the last node when the end offset is at its very beginning.
if ($isTextNode(firstNode) && startOffset === firstNode.getTextContentSize()) {
startNode = firstNode;
}
if (endOffset === 0) {
endNode = lastNode;
}
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// Skip the excluded boundary node for this position (startNode at the
// head, endNode elsewhere); both are undefined when nothing is excluded.
if ($isTextNode(node) && !node.is(i === 0 ? startNode : endNode)) {
const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
if (styleValue === null) {
styleValue = nodeStyleValue;
} else if (styleValue !== nodeStyleValue) {
// multiple text nodes are in the selection and they don't all
// have the same style.
styleValue = '';
break;
}
}
}
return styleValue === null ? defaultValue : styleValue;
}
/**
* 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.
*
*/
/** @deprecated moved to the `lexical` package */
const getStyleObjectFromCSS = getStyleObjectFromCSS$1;
/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */
const trimTextContentFromAnchor = $trimTextContentFromAnchor;
export { $addNodeStyle, $copyBlockFormatIndent, $ensureForwardRangeSelection, $forEachSelectedTextNode, $getComputedStyleForElement, $getComputedStyleForParent, $getSelectionStyleValueForProperty, $isAtNodeEnd, $isParentElementRTL, $isParentRTL, $moveCaretSelection, $moveCharacter, $patchStyleText, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $sliceSelectedTextNodeContent, $trimTextContentFromAnchor, $wrapNodes, createDOMRange, createRectsFromDOMRange, getCSSFromStyleObject, getStyleObjectFromCSS, trimTextContentFromAnchor };
/**
* 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.
*
*/
'use strict'
const LexicalSelection = process.env.NODE_ENV !== 'production' ? require('./LexicalSelection.dev.js') : require('./LexicalSelection.prod.js');
module.exports = LexicalSelection;

Sorry, the diff of this file is not supported yet

/**
* 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.
*
*/
import * as modDev from './LexicalSelection.dev.mjs';
import * as modProd from './LexicalSelection.prod.mjs';
const mod = process.env.NODE_ENV !== 'production' ? modDev : modProd;
export const $addNodeStyle = mod.$addNodeStyle;
export const $cloneWithProperties = mod.$cloneWithProperties;
export const $copyBlockFormatIndent = mod.$copyBlockFormatIndent;
export const $ensureForwardRangeSelection = mod.$ensureForwardRangeSelection;
export const $forEachSelectedTextNode = mod.$forEachSelectedTextNode;
export const $getComputedStyleForElement = mod.$getComputedStyleForElement;
export const $getComputedStyleForParent = mod.$getComputedStyleForParent;
export const $getSelectionStyleValueForProperty = mod.$getSelectionStyleValueForProperty;
export const $isAtNodeEnd = mod.$isAtNodeEnd;
export const $isParentElementRTL = mod.$isParentElementRTL;
export const $isParentRTL = mod.$isParentRTL;
export const $moveCaretSelection = mod.$moveCaretSelection;
export const $moveCharacter = mod.$moveCharacter;
export const $patchStyleText = mod.$patchStyleText;
export const $selectAll = mod.$selectAll;
export const $setBlocksType = mod.$setBlocksType;
export const $shouldOverrideDefaultCharacterSelection = mod.$shouldOverrideDefaultCharacterSelection;
export const $sliceSelectedTextNodeContent = mod.$sliceSelectedTextNodeContent;
export const $trimTextContentFromAnchor = mod.$trimTextContentFromAnchor;
export const $wrapNodes = mod.$wrapNodes;
export const createDOMRange = mod.createDOMRange;
export const createRectsFromDOMRange = mod.createRectsFromDOMRange;
export const getCSSFromStyleObject = mod.getCSSFromStyleObject;
export const getStyleObjectFromCSS = mod.getStyleObjectFromCSS;
export const trimTextContentFromAnchor = mod.trimTextContentFromAnchor;
/**
* 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 mod = await (process.env.NODE_ENV !== 'production' ? import('./LexicalSelection.dev.mjs') : import('./LexicalSelection.prod.mjs'));
export const $addNodeStyle = mod.$addNodeStyle;
export const $cloneWithProperties = mod.$cloneWithProperties;
export const $copyBlockFormatIndent = mod.$copyBlockFormatIndent;
export const $ensureForwardRangeSelection = mod.$ensureForwardRangeSelection;
export const $forEachSelectedTextNode = mod.$forEachSelectedTextNode;
export const $getComputedStyleForElement = mod.$getComputedStyleForElement;
export const $getComputedStyleForParent = mod.$getComputedStyleForParent;
export const $getSelectionStyleValueForProperty = mod.$getSelectionStyleValueForProperty;
export const $isAtNodeEnd = mod.$isAtNodeEnd;
export const $isParentElementRTL = mod.$isParentElementRTL;
export const $isParentRTL = mod.$isParentRTL;
export const $moveCaretSelection = mod.$moveCaretSelection;
export const $moveCharacter = mod.$moveCharacter;
export const $patchStyleText = mod.$patchStyleText;
export const $selectAll = mod.$selectAll;
export const $setBlocksType = mod.$setBlocksType;
export const $shouldOverrideDefaultCharacterSelection = mod.$shouldOverrideDefaultCharacterSelection;
export const $sliceSelectedTextNodeContent = mod.$sliceSelectedTextNodeContent;
export const $trimTextContentFromAnchor = mod.$trimTextContentFromAnchor;
export const $wrapNodes = mod.$wrapNodes;
export const createDOMRange = mod.createDOMRange;
export const createRectsFromDOMRange = mod.createRectsFromDOMRange;
export const getCSSFromStyleObject = mod.getCSSFromStyleObject;
export const getStyleObjectFromCSS = mod.getStyleObjectFromCSS;
export const trimTextContentFromAnchor = mod.trimTextContentFromAnchor;
/**
* 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.
*
*/
"use strict";var e=require("lexical");function t(e,...t){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",e);for(const e of t)o.append("v",e);throw n.search=o.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}function n(e){let t=e;for(;null!=t;){if(t.nodeType===Node.TEXT_NODE)return t;t=t.firstChild}return null}function o(e){const t=e.parentNode;if(null==t)throw new Error("Should never happen");return[t,Array.from(t.childNodes).indexOf(e)]}function r(e){let t="";for(const n in e)n&&(t+=`${n}: ${e[n]};`);return t}function i(t){const n=e.$getEditor().getElementByKey(t.getKey());if(null===n)return null;const o=n.ownerDocument.defaultView;return null===o?null:o.getComputedStyle(n)}function s(t){return i(e.$isRootNode(t)?t:t.getParentOrThrow())}function l(t,n,o){let r=n.getNode(),i=o;if(e.$isElementNode(r)){const e=r.getDescendantByIndex(n.offset);null!==e&&(r=e)}for(;i>0&&null!==r;){if(e.$isElementNode(r)){const e=r.getLastDescendant();null!==e&&(r=e)}let o=r.getPreviousSibling(),s=0;if(null===o){let e=r.getParentOrThrow(),t=e.getPreviousSibling();for(;null===t;){if(e=e.getParent(),null===e){o=null;break}t=e.getPreviousSibling()}null!==e&&(s=e.isInline()?0:2,o=t)}let l=r.getTextContent();""===l&&e.$isElementNode(r)&&!r.isInline()&&(l="\n\n");const c=l.length;if(!e.$isTextNode(r)||i>=c){const t=r.getParent();r.remove(),null==t||0!==t.getChildrenSize()||e.$isRootNode(t)||t.remove(),i-=c+s,r=o}else{const o=r.getKey(),s=t.getEditorState().read(()=>{const t=e.$getNodeByKey(o);return e.$isTextNode(t)&&t.isSimpleText()?t.getTextContent():null}),f=c-i,d=l.slice(0,f);if(null!==s&&s!==l){const t=e.$getPreviousSelection();let n=r;if(r.isSimpleText())r.setTextContent(s);else{const t=e.$createTextNode(s);r.replace(t),n=t}if(e.$isRangeSelection(t)&&t.isCollapsed()){const e=t.anchor.offset;n.select(e,e)}}else if(r.isSimpleText()){const e=n.key===o;let t=n.offset;t<i&&(t=c);const s=e?t-i:0,l=e?t:f;if(e&&0===s){const[e]=r.splitText(s,l);e.remove()}else{const[,e]=r.splitText(s,l);e.remove()}}else{const t=e.$createTextNode(d);r.replace(t)}i=0}}}const c=()=>{};function f(n,o){(e.$isRangeSelection(n)?n.isCollapsed():e.$isTextNode(n)||e.$isElementNode(n))||t(280);const i=e.getStyleObjectFromCSS(e.$isRangeSelection(n)?n.style:e.$isTextNode(n)?n.getStyle():n.getTextStyle()),s=r(Object.entries(o).reduce((e,[t,o])=>("function"==typeof o?e[t]=o(i[t],n):null===o?delete e[t]:e[t]=o,e),{...i}));e.$isRangeSelection(n)||e.$isTextNode(n)?n.setStyle(s):n.setTextStyle(s)}function d(t){const n=e.$getSelection();if(!n)return;const o=new Map,r=e=>o.get(e.getKey())||[0,e.getTextContentSize()];if(e.$isRangeSelection(n))for(const t of e.$caretRangeFromSelection(n).getTextSlices())t&&o.set(t.caret.origin.getKey(),t.getSliceIndices());const i=n.getNodes();for(const n of i){if(!e.$isTextNode(n)||!n.canHaveFormat())continue;const[o,i]=r(n);if(i!==o)if(e.$isTokenOrSegmented(n)||0===o&&i===n.getTextContentSize())t(n);else{t(n.splitText(o,i)[0===o?0:1])}}e.$isRangeSelection(n)&&"text"===n.anchor.type&&"text"===n.focus.type&&n.anchor.key===n.focus.key&&a(n)}function a(e){if(e.isBackward()){const{anchor:t,focus:n}=e,{key:o,offset:r,type:i}=t;t.set(n.key,n.offset,n.type),n.set(o,r,i)}}function g(e,t){const n=e.getFormatType(),o=e.getIndent();n!==t.getFormatType()&&t.setFormat(n),o!==t.getIndent()&&t.setIndent(o)}function u(e){return e.getNode().isAttached()}function p(t){let n=t;for(;null!==n&&!e.$isRootOrShadowRoot(n);){const e=n.getLatest(),t=n.getParent();0===e.getChildrenSize()&&n.remove(!0),n=t}}function $(n,o,r,i,s=null){if(0===o.length)return;const l=o[0],c=new Map,f=[];let d=e.$isElementNode(l)?l:l.getParentOrThrow();d.isInline()&&(d=d.getParentOrThrow());let a=!1;for(;null!==d;){const t=d.getPreviousSibling();if(null!==t){d=t,a=!0;break}if(d=d.getParentOrThrow(),e.$isRootOrShadowRoot(d))break}const g=new Set;for(let t=0;t<r;t++){const n=o[t];e.$isElementNode(n)&&0===n.getChildrenSize()&&g.add(n.getKey())}const $=new Set;for(let n=0;n<r;n++){const r=o[n];let s=r.getParent();if(null!==s&&s.isInline()&&(s=s.getParent()),null!==s&&e.$isLeafNode(r)&&!$.has(r.getKey())){const t=s.getKey();if(void 0===c.get(t)){const n=i();n.setFormat(s.getFormatType()),n.setIndent(s.getIndent()),f.push(n),c.set(t,n);const o=s.getChildren();n.splice(n.getChildrenSize(),0,o);for(const t of o)if($.add(t.getKey()),e.$isElementNode(t))for(const e of t.getChildrenKeys())$.add(e);p(s)}}else if(g.has(r.getKey())){e.$isElementNode(r)||t(179);const n=i();n.setFormat(r.getFormatType()),n.setIndent(r.getIndent()),f.push(n),r.remove(!0)}}if(null!==s)for(let e=0;e<f.length;e++){const t=f[e];s.append(t)}let S=null;if(e.$isRootOrShadowRoot(d))if(a)if(null!==s)d.insertAfter(s);else for(let e=f.length-1;e>=0;e--){const t=f[e];d.insertAfter(t)}else{const t=d.getFirstChild();if(e.$isElementNode(t)&&(d=t),null===t)if(s)d.append(s);else for(let e=0;e<f.length;e++){const t=f[e];d.append(t),S=t}else if(null!==s)t.insertBefore(s);else for(let e=0;e<f.length;e++){const n=f[e];t.insertBefore(n),S=n}}else if(s)d.insertAfter(s);else for(let e=f.length-1;e>=0;e--){const t=f[e];d.insertAfter(t),S=t}const h=e.$getPreviousSelection();e.$isRangeSelection(h)&&u(h.anchor)&&u(h.focus)?e.$setSelection(h.clone()):null!==S?S.selectEnd():n.dirty=!0}function S(e){const t=h(e);return null!==t&&"vertical-rl"===t.writingMode}function h(t){const n=t.anchor.getNode();return e.$isElementNode(n)?i(n):s(n)}function m(e,t,n,o){e.modify(t?"extend":"move",n,o)}function N(e){const t=h(e);return null!==t&&"rtl"===t.direction}function y(t,n,o){const r=t.getStyle(),i=e.getStyleObjectFromCSS(r);return null!==i&&i[n]||o}const x=e.getStyleObjectFromCSS,T=l;exports.$cloneWithProperties=e.$cloneWithProperties,exports.$selectAll=e.$selectAll,exports.$addNodeStyle=c,exports.$copyBlockFormatIndent=g,exports.$ensureForwardRangeSelection=a,exports.$forEachSelectedTextNode=d,exports.$getComputedStyleForElement=i,exports.$getComputedStyleForParent=s,exports.$getSelectionStyleValueForProperty=function(t,n,o=""){let r=null;const i=t.getNodes();let s,l;if(e.$isRangeSelection(t)){if(t.isCollapsed()&&""!==t.style){const o=e.getStyleObjectFromCSS(t.style);if(null!==o&&n in o)return o[n]}const{anchor:o,focus:r}=t,i=t.isBackward(),c=i?r.getNode():o.getNode(),f=i?o.getNode():r.getNode(),d=i?r.offset:o.offset,a=i?o.offset:r.offset;e.$isTextNode(c)&&d===c.getTextContentSize()&&(s=c),0===a&&(l=f)}for(let t=0;t<i.length;t++){const c=i[t];if(e.$isTextNode(c)&&!c.is(0===t?s:l)){const e=y(c,n,o);if(null===r)r=e;else if(r!==e){r="";break}}}return null===r?o:r},exports.$isAtNodeEnd=function(n){if("text"===n.type)return n.offset===n.getNode().getTextContentSize();const o=n.getNode();return e.$isElementNode(o)||t(177),n.offset===o.getChildrenSize()},exports.$isParentElementRTL=N,exports.$isParentRTL=function(e){const t=s(e);return null!==t&&"rtl"===t.direction},exports.$moveCaretSelection=m,exports.$moveCharacter=function(e,t,n){const o=N(e);let r;r=S(e)||o?!n:n,m(e,t,r,"character")},exports.$patchStyleText=function(t,n){if(e.$isRangeSelection(t)&&t.isCollapsed()){f(t,n);const o=t.anchor.getNode();e.$isElementNode(o)&&o.isEmpty()&&f(o,n)}d(e=>{f(e,n)});const o=t.getNodes();if(o.length>0){const t=new Set;for(const r of o){if(!e.$isElementNode(r)||!r.canBeEmpty()||0!==r.getChildrenSize())continue;const o=r.getKey();t.has(o)||(t.add(o),f(r,n))}}},exports.$setBlocksType=function(t,n,o=g){if(!t)return;const r=t.getStartEndPoints();let i=!1,s=null;const l=new Map;if(r){const[t,n]=r,o=e.$findMatchingParent(t.getNode(),e.INTERNAL_$isBlock);s=e.$findMatchingParent(n.getNode(),e.INTERNAL_$isBlock),i=e.$isElementNode(s)&&!s.is(o)&&function(t,n){if(0!==t.offset)return!1;let o=t.getNode();if(e.$isElementNode(o)&&o.isEmpty())return!1;for(;!o.is(n);){if(null!==o.getPreviousSibling())return!1;const e=o.getParent();if(null===e)return!1;o=e}return!0}(n,s),e.$isElementNode(o)&&l.set(o.getKey(),o),e.$isElementNode(s)&&!i&&l.set(s.getKey(),s)}for(const n of t.getNodes())if(e.$isElementNode(n)&&e.INTERNAL_$isBlock(n)){if(i&&n.is(s))continue;l.set(n.getKey(),n)}else if(!r){const t=e.$findMatchingParent(n,e.INTERNAL_$isBlock);e.$isElementNode(t)&&l.set(t.getKey(),t)}for(const e of l.values()){const t=n();o(e,t),e.replace(t,!0)}},exports.$shouldOverrideDefaultCharacterSelection=function(t,n){let o=S(t)?!n:n;N(t)&&(o=!o);const r=e.$caretFromPoint(t.focus,o?"previous":"next");if(e.$isExtendableTextPointCaret(r))return!1;for(const t of e.$extendCaretToRange(r)){if(e.$isChildCaret(t))return!t.origin.isInline();if(!e.$isElementNode(t.origin)){if(e.$isDecoratorNode(t.origin))return!0;break}}return!1},exports.$sliceSelectedTextNodeContent=function(t,n,o="self"){const r=t.getStartEndPoints();if(n.isSelected(t)&&!e.$isTokenOrSegmented(n)&&null!==r){const[i,s]=r,l=t.isBackward(),c=i.getNode(),f=s.getNode(),d=n.is(c),a=n.is(f);if(d||a){const[r,i]=e.$getCharacterOffsets(t),s=c.is(f),d=n.is(l?f:c),a=n.is(l?c:f);let g,u=0;if(s)u=r>i?i:r,g=r>i?r:i;else if(d){u=l?i:r,g=void 0}else if(a){u=0,g=l?r:i}const p=n.__text.slice(u,g);p!==n.__text&&("clone"===o&&(n=e.$cloneWithPropertiesEphemeral(n)),n.__text=p)}}return n},exports.$trimTextContentFromAnchor=l,exports.$wrapNodes=function(t,n,o=null){const r=t.getStartEndPoints(),i=r?r[0]:null,s=t.getNodes(),l=s.length;if(null!==i&&(0===l||1===l&&"element"===i.type&&0===i.getNode().getChildrenSize())){const e="text"===i.type?i.getNode().getParentOrThrow():i.getNode(),t=e.getChildren();let r=n();return r.setFormat(e.getFormatType()),r.setIndent(e.getIndent()),t.forEach(e=>r.append(e)),o&&(r=o.append(r)),void e.replace(r)}let c=null,f=[];for(let r=0;r<l;r++){const i=s[r];e.$isRootOrShadowRoot(i)?($(t,f,f.length,n,o),f=[],c=i):null===c||null!==c&&e.$hasAncestor(i,c)?f.push(i):($(t,f,f.length,n,o),f=[i])}$(t,f,f.length,n,o)},exports.createDOMRange=function(t,r,i,s,l){const c=r.getKey(),f=s.getKey(),d=document.createRange();let a=t.getElementByKey(c),g=t.getElementByKey(f),u=i,p=l;if(e.$isTextNode(r)&&(a=n(a)),e.$isTextNode(s)&&(g=n(g)),void 0===r||void 0===s||null===a||null===g)return null;"BR"===a.nodeName&&([a,u]=o(a)),"BR"===g.nodeName&&([g,p]=o(g));const $=a.firstChild;a===g&&null!=$&&"BR"===$.nodeName&&0===u&&0===p&&(p=1);try{d.setStart(a,u),d.setEnd(g,p)}catch(e){return null}return!d.collapsed||u===p&&c===f||(d.setStart(g,p),d.setEnd(a,u)),d},exports.createRectsFromDOMRange=function(e,t){const n=e.getRootElement();if(null===n)return[];const o=n.getBoundingClientRect(),r=getComputedStyle(n),i=parseFloat(r.paddingLeft)+parseFloat(r.paddingRight),s=Array.from(t.getClientRects());let l,c=s.length;s.sort((e,t)=>{const n=e.top-t.top;return Math.abs(n)<=3?e.left-t.left:n});for(let e=0;e<c;e++){const t=s[e],n=l&&l.top<=t.top&&l.top+l.height>t.top&&l.left+l.width>t.left,r=t.width+i===o.width;n||r?(s.splice(e--,1),c--):l=t}return s},exports.getCSSFromStyleObject=r,exports.getStyleObjectFromCSS=x,exports.trimTextContentFromAnchor=T;
/**
* 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.
*
*/
import{$isTextNode as e,$getEditor as t,$isRootNode as n,$getSelection as o,$isRangeSelection as l,$caretRangeFromSelection as r,$isTokenOrSegmented as i,$isElementNode as s,$getCharacterOffsets as c,$cloneWithPropertiesEphemeral as f,$getNodeByKey as u,$getPreviousSelection as g,$createTextNode as d,getStyleObjectFromCSS as a,$findMatchingParent as p,INTERNAL_$isBlock as h,$caretFromPoint as y,$isExtendableTextPointCaret as m,$extendCaretToRange as S,$isChildCaret as x,$isDecoratorNode as N,$isRootOrShadowRoot as T,$hasAncestor as C,$isLeafNode as v,$setSelection as w}from"lexical";export{$cloneWithProperties,$selectAll}from"lexical";function P(e,...t){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",e);for(const e of t)o.append("v",e);throw n.search=o.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}function K(e){let t=e;for(;null!=t;){if(t.nodeType===Node.TEXT_NODE)return t;t=t.firstChild}return null}function E(e){const t=e.parentNode;if(null==t)throw new Error("Should never happen");return[t,Array.from(t.childNodes).indexOf(e)]}function I(t,n,o,l,r){const i=n.getKey(),s=l.getKey(),c=document.createRange();let f=t.getElementByKey(i),u=t.getElementByKey(s),g=o,d=r;if(e(n)&&(f=K(f)),e(l)&&(u=K(u)),void 0===n||void 0===l||null===f||null===u)return null;"BR"===f.nodeName&&([f,g]=E(f)),"BR"===u.nodeName&&([u,d]=E(u));const a=f.firstChild;f===u&&null!=a&&"BR"===a.nodeName&&0===g&&0===d&&(d=1);try{c.setStart(f,g),c.setEnd(u,d)}catch(e){return null}return!c.collapsed||g===d&&i===s||(c.setStart(u,d),c.setEnd(f,g)),c}function B(e,t){const n=e.getRootElement();if(null===n)return[];const o=n.getBoundingClientRect(),l=getComputedStyle(n),r=parseFloat(l.paddingLeft)+parseFloat(l.paddingRight),i=Array.from(t.getClientRects());let s,c=i.length;i.sort((e,t)=>{const n=e.top-t.top;return Math.abs(n)<=3?e.left-t.left:n});for(let e=0;e<c;e++){const t=i[e],n=s&&s.top<=t.top&&s.top+s.height>t.top&&s.left+s.width>t.left,l=t.width+r===o.width;n||l?(i.splice(e--,1),c--):s=t}return i}function F(e){let t="";for(const n in e)n&&(t+=`${n}: ${e[n]};`);return t}function b(e){const n=t().getElementByKey(e.getKey());if(null===n)return null;const o=n.ownerDocument.defaultView;return null===o?null:o.getComputedStyle(n)}function k(e){return b(n(e)?e:e.getParentOrThrow())}function z(e){const t=k(e);return null!==t&&"rtl"===t.direction}function O(e,t,n="self"){const o=e.getStartEndPoints();if(t.isSelected(e)&&!i(t)&&null!==o){const[l,r]=o,i=e.isBackward(),s=l.getNode(),u=r.getNode(),g=t.is(s),d=t.is(u);if(g||d){const[o,l]=c(e),r=s.is(u),g=t.is(i?u:s),d=t.is(i?s:u);let a,p=0;if(r)p=o>l?l:o,a=o>l?o:l;else if(g){p=i?l:o,a=void 0}else if(d){p=0,a=i?o:l}const h=t.__text.slice(p,a);h!==t.__text&&("clone"===n&&(t=f(t)),t.__text=h)}}return t}function R(e){if("text"===e.type)return e.offset===e.getNode().getTextContentSize();const t=e.getNode();return s(t)||P(177),e.offset===t.getChildrenSize()}function A(t,o,r){let i=o.getNode(),c=r;if(s(i)){const e=i.getDescendantByIndex(o.offset);null!==e&&(i=e)}for(;c>0&&null!==i;){if(s(i)){const e=i.getLastDescendant();null!==e&&(i=e)}let r=i.getPreviousSibling(),f=0;if(null===r){let e=i.getParentOrThrow(),t=e.getPreviousSibling();for(;null===t;){if(e=e.getParent(),null===e){r=null;break}t=e.getPreviousSibling()}null!==e&&(f=e.isInline()?0:2,r=t)}let a=i.getTextContent();""===a&&s(i)&&!i.isInline()&&(a="\n\n");const p=a.length;if(!e(i)||c>=p){const e=i.getParent();i.remove(),null==e||0!==e.getChildrenSize()||n(e)||e.remove(),c-=p+f,i=r}else{const n=i.getKey(),r=t.getEditorState().read(()=>{const t=u(n);return e(t)&&t.isSimpleText()?t.getTextContent():null}),s=p-c,f=a.slice(0,s);if(null!==r&&r!==a){const e=g();let t=i;if(i.isSimpleText())i.setTextContent(r);else{const e=d(r);i.replace(e),t=e}if(l(e)&&e.isCollapsed()){const n=e.anchor.offset;t.select(n,n)}}else if(i.isSimpleText()){const e=o.key===n;let t=o.offset;t<c&&(t=p);const l=e?t-c:0,r=e?t:s;if(e&&0===l){const[e]=i.splitText(l,r);e.remove()}else{const[,e]=i.splitText(l,r);e.remove()}}else{const e=d(f);i.replace(e)}c=0}}}const _=()=>{};function L(t,n){(l(t)?t.isCollapsed():e(t)||s(t))||P(280);const o=a(l(t)?t.style:e(t)?t.getStyle():t.getTextStyle()),r=F(Object.entries(n).reduce((e,[n,l])=>("function"==typeof l?e[n]=l(o[n],t):null===l?delete e[n]:e[n]=l,e),{...o}));l(t)||e(t)?t.setStyle(r):t.setTextStyle(r)}function M(e,t){if(l(e)&&e.isCollapsed()){L(e,t);const n=e.anchor.getNode();s(n)&&n.isEmpty()&&L(n,t)}$(e=>{L(e,t)});const n=e.getNodes();if(n.length>0){const e=new Set;for(const o of n){if(!s(o)||!o.canBeEmpty()||0!==o.getChildrenSize())continue;const n=o.getKey();e.has(n)||(e.add(n),L(o,t))}}}function $(t){const n=o();if(!n)return;const s=new Map,c=e=>s.get(e.getKey())||[0,e.getTextContentSize()];if(l(n))for(const e of r(n).getTextSlices())e&&s.set(e.caret.origin.getKey(),e.getSliceIndices());const f=n.getNodes();for(const n of f){if(!e(n)||!n.canHaveFormat())continue;const[o,l]=c(n);if(l!==o)if(i(n)||0===o&&l===n.getTextContentSize())t(n);else{t(n.splitText(o,l)[0===o?0:1])}}l(n)&&"text"===n.anchor.type&&"text"===n.focus.type&&n.anchor.key===n.focus.key&&D(n)}function D(e){if(e.isBackward()){const{anchor:t,focus:n}=e,{key:o,offset:l,type:r}=t;t.set(n.key,n.offset,n.type),n.set(o,l,r)}}function j(e,t){const n=e.getFormatType(),o=e.getIndent();n!==t.getFormatType()&&t.setFormat(n),o!==t.getIndent()&&t.setIndent(o)}function U(e,t,n=j){if(!e)return;const o=e.getStartEndPoints();let l=!1,r=null;const i=new Map;if(o){const[e,t]=o,n=p(e.getNode(),h);r=p(t.getNode(),h),l=s(r)&&!r.is(n)&&function(e,t){if(0!==e.offset)return!1;let n=e.getNode();if(s(n)&&n.isEmpty())return!1;for(;!n.is(t);){if(null!==n.getPreviousSibling())return!1;const e=n.getParent();if(null===e)return!1;n=e}return!0}(t,r),s(n)&&i.set(n.getKey(),n),s(r)&&!l&&i.set(r.getKey(),r)}for(const t of e.getNodes())if(s(t)&&h(t)){if(l&&t.is(r))continue;i.set(t.getKey(),t)}else if(!o){const e=p(t,h);s(e)&&i.set(e.getKey(),e)}for(const e of i.values()){const o=t();n(e,o),e.replace(o,!0)}}function H(e){return e.getNode().isAttached()}function V(e){let t=e;for(;null!==t&&!T(t);){const e=t.getLatest(),n=t.getParent();0===e.getChildrenSize()&&t.remove(!0),t=n}}function W(e,t,n=null){const o=e.getStartEndPoints(),l=o?o[0]:null,r=e.getNodes(),i=r.length;if(null!==l&&(0===i||1===i&&"element"===l.type&&0===l.getNode().getChildrenSize())){const e="text"===l.type?l.getNode().getParentOrThrow():l.getNode(),o=e.getChildren();let r=t();return r.setFormat(e.getFormatType()),r.setIndent(e.getIndent()),o.forEach(e=>r.append(e)),n&&(r=n.append(r)),void e.replace(r)}let s=null,c=[];for(let o=0;o<i;o++){const l=r[o];T(l)?(X(e,c,c.length,t,n),c=[],s=l):null===s||null!==s&&C(l,s)?c.push(l):(X(e,c,c.length,t,n),c=[l])}X(e,c,c.length,t,n)}function X(e,t,n,o,r=null){if(0===t.length)return;const i=t[0],c=new Map,f=[];let u=s(i)?i:i.getParentOrThrow();u.isInline()&&(u=u.getParentOrThrow());let d=!1;for(;null!==u;){const e=u.getPreviousSibling();if(null!==e){u=e,d=!0;break}if(u=u.getParentOrThrow(),T(u))break}const a=new Set;for(let e=0;e<n;e++){const n=t[e];s(n)&&0===n.getChildrenSize()&&a.add(n.getKey())}const p=new Set;for(let e=0;e<n;e++){const n=t[e];let l=n.getParent();if(null!==l&&l.isInline()&&(l=l.getParent()),null!==l&&v(n)&&!p.has(n.getKey())){const e=l.getKey();if(void 0===c.get(e)){const t=o();t.setFormat(l.getFormatType()),t.setIndent(l.getIndent()),f.push(t),c.set(e,t);const n=l.getChildren();t.splice(t.getChildrenSize(),0,n);for(const e of n)if(p.add(e.getKey()),s(e))for(const t of e.getChildrenKeys())p.add(t);V(l)}}else if(a.has(n.getKey())){s(n)||P(179);const e=o();e.setFormat(n.getFormatType()),e.setIndent(n.getIndent()),f.push(e),n.remove(!0)}}if(null!==r)for(let e=0;e<f.length;e++){const t=f[e];r.append(t)}let h=null;if(T(u))if(d)if(null!==r)u.insertAfter(r);else for(let e=f.length-1;e>=0;e--){const t=f[e];u.insertAfter(t)}else{const e=u.getFirstChild();if(s(e)&&(u=e),null===e)if(r)u.append(r);else for(let e=0;e<f.length;e++){const t=f[e];u.append(t),h=t}else if(null!==r)e.insertBefore(r);else for(let t=0;t<f.length;t++){const n=f[t];e.insertBefore(n),h=n}}else if(r)u.insertAfter(r);else for(let e=f.length-1;e>=0;e--){const t=f[e];u.insertAfter(t),h=t}const y=g();l(y)&&H(y.anchor)&&H(y.focus)?w(y.clone()):null!==h?h.selectEnd():e.dirty=!0}function q(e){const t=G(e);return null!==t&&"vertical-rl"===t.writingMode}function G(e){const t=e.anchor.getNode();return s(t)?b(t):k(t)}function J(e,t){let n=q(e)?!t:t;Y(e)&&(n=!n);const o=y(e.focus,n?"previous":"next");if(m(o))return!1;for(const e of S(o)){if(x(e))return!e.origin.isInline();if(!s(e.origin)){if(N(e.origin))return!0;break}}return!1}function Q(e,t,n,o){e.modify(t?"extend":"move",n,o)}function Y(e){const t=G(e);return null!==t&&"rtl"===t.direction}function Z(e,t,n){const o=Y(e);let l;l=q(e)||o?!n:n,Q(e,t,l,"character")}function ee(e,t,n){const o=e.getStyle(),l=a(o);return null!==l&&l[t]||n}function te(t,n,o=""){let r=null;const i=t.getNodes();let s,c;if(l(t)){if(t.isCollapsed()&&""!==t.style){const e=a(t.style);if(null!==e&&n in e)return e[n]}const{anchor:o,focus:l}=t,r=t.isBackward(),i=r?l.getNode():o.getNode(),f=r?o.getNode():l.getNode(),u=r?l.offset:o.offset,g=r?o.offset:l.offset;e(i)&&u===i.getTextContentSize()&&(s=i),0===g&&(c=f)}for(let t=0;t<i.length;t++){const l=i[t];if(e(l)&&!l.is(0===t?s:c)){const e=ee(l,n,o);if(null===r)r=e;else if(r!==e){r="";break}}}return null===r?o:r}const ne=a,oe=A;export{_ as $addNodeStyle,j as $copyBlockFormatIndent,D as $ensureForwardRangeSelection,$ as $forEachSelectedTextNode,b as $getComputedStyleForElement,k as $getComputedStyleForParent,te as $getSelectionStyleValueForProperty,R as $isAtNodeEnd,Y as $isParentElementRTL,z as $isParentRTL,Q as $moveCaretSelection,Z as $moveCharacter,M as $patchStyleText,U as $setBlocksType,J as $shouldOverrideDefaultCharacterSelection,O as $sliceSelectedTextNodeContent,A as $trimTextContentFromAnchor,W as $wrapNodes,I as createDOMRange,B as createRectsFromDOMRange,F as getCSSFromStyleObject,ne as getStyleObjectFromCSS,oe as trimTextContentFromAnchor};
/**
* 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.
*
*/
import type { BaseSelection, ElementNode, LexicalNode, RangeSelection } from 'lexical';
export declare function $copyBlockFormatIndent(srcNode: ElementNode, destNode: ElementNode): void;
/**
* Converts all nodes in the selection that are of one block type to another.
* @param selection - The selected blocks to be converted.
* @param $createElement - The function that creates the node. eg. $createParagraphNode.
* @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
*/
export declare function $setBlocksType<T extends ElementNode>(selection: BaseSelection | null, $createElement: () => T, $afterCreateElement?: (prevNodeSrc: ElementNode, newNodeDest: T) => void): void;
/**
* @deprecated In favor of $setBlockTypes
* Wraps all nodes in the selection into another node of the type returned by createElement.
* @param selection - The selection of nodes to be wrapped.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to append the wrapped selection and its children to.
*/
export declare function $wrapNodes(selection: BaseSelection, createElement: () => ElementNode, wrappingElement?: null | ElementNode): void;
/**
* Wraps each node into a new ElementNode.
* @param selection - The selection of nodes to wrap.
* @param nodes - An array of nodes, generally the descendants of the selection.
* @param nodesLength - The length of nodes.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to wrap all the nodes into.
* @returns
*/
export declare function $wrapNodesImpl(selection: BaseSelection, nodes: LexicalNode[], nodesLength: number, createElement: () => ElementNode, wrappingElement?: null | ElementNode): void;
/**
* Determines if the default character selection should be overridden. Used with DecoratorNodes
* @param selection - The selection whose default character selection may need to be overridden.
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
* @returns true if it should be overridden, false if not.
*/
export declare function $shouldOverrideDefaultCharacterSelection(selection: RangeSelection, isBackward: boolean): boolean;
/**
* Moves the selection according to the arguments.
* @param selection - The selected text or nodes.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
* @param granularity - The distance to adjust the current selection.
*/
export declare function $moveCaretSelection(selection: RangeSelection, isHoldingShift: boolean, isBackward: boolean, granularity: 'character' | 'word' | 'lineboundary'): void;
/**
* Tests a parent element for right to left direction.
* @param selection - The selection whose parent is to be tested.
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
*/
export declare function $isParentElementRTL(selection: RangeSelection): boolean;
/**
* Moves selection by character according to arguments.
* @param selection - The selection of the characters to move.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
*/
export declare function $moveCharacter(selection: RangeSelection, isHoldingShift: boolean, isBackward: boolean): void;
/**
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
* If all TextNodes do not have the same value, it returns an empty string.
* @param selection - The selection of TextNodes whose value to find.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property, defaults to an empty string.
* @returns The value of the property for the selected TextNodes.
*/
export declare function $getSelectionStyleValueForProperty(selection: BaseSelection, styleProperty: string, defaultValue?: string): string;
/**
* 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.
*
*/
import type { ElementNode, LexicalEditor, LexicalNode } from 'lexical';
import { getStyleObjectFromCSS } from 'lexical';
/**
* Creates a selection range for the DOM.
* @param editor - The lexical editor.
* @param anchorNode - The anchor node of a selection.
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
* @param focusNode - The current focus.
* @param _focusOffset - The amount of space offset from the focus to the anchor.
* @returns The range of selection for the DOM that was created.
*/
export declare function createDOMRange(editor: LexicalEditor, anchorNode: LexicalNode, _anchorOffset: number, focusNode: LexicalNode, _focusOffset: number): Range | null;
/**
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
* @param editor - The lexical editor
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
* @returns The selectionRects as an array.
*/
export declare function createRectsFromDOMRange(editor: LexicalEditor, range: Range): Array<ClientRect>;
/**
* @deprecated Use {@link getStyleObjectFromCSS}, this is just an alias for backwards compatibility.
*/
export declare const getStyleObjectFromRawCSS: typeof getStyleObjectFromCSS;
/**
* Serializes a style object into a CSS declaration string, the inverse of
* {@link getStyleObjectFromCSS}.
* @param styles - An object mapping CSS property names to their values.
* @returns A CSS string of the form `prop: value;` for each entry, concatenated together.
*/
export declare function getCSSFromStyleObject(styles: Record<string, string>): string;
/**
* Gets the computed DOM styles of the element.
* @param element - The node to check the styles for.
* @returns the computed styles of the element or null if there is no DOM element or no default view for the document.
*/
export declare function $getComputedStyleForElement(element: ElementNode): CSSStyleDeclaration | null;
/**
* Gets the computed DOM styles of the parent of the node.
* @param node - The node to check its parent's styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
export declare function $getComputedStyleForParent(node: LexicalNode): CSSStyleDeclaration | null;
/**
* Determines whether a node's parent is RTL.
* @param node - The node to check whether it is RTL.
* @returns whether the node is RTL.
*/
export declare function $isParentRTL(node: LexicalNode): boolean;
/**
* 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.
*
*/
import {getStyleObjectFromCSS as getStyleObjectFromCSS_} from 'lexical';
import {$trimTextContentFromAnchor} from './lexical-node';
export {
$addNodeStyle,
$ensureForwardRangeSelection,
$forEachSelectedTextNode,
$isAtNodeEnd,
$patchStyleText,
$sliceSelectedTextNodeContent,
$trimTextContentFromAnchor,
} from './lexical-node';
export {
$copyBlockFormatIndent,
$getSelectionStyleValueForProperty,
$isParentElementRTL,
$moveCaretSelection,
$moveCharacter,
$setBlocksType,
$shouldOverrideDefaultCharacterSelection,
$wrapNodes,
} from './range-selection';
export {
$getComputedStyleForElement,
$getComputedStyleForParent,
$isParentRTL,
createDOMRange,
createRectsFromDOMRange,
getCSSFromStyleObject,
} from './utils';
/** @deprecated moved to the `lexical` package */
export const getStyleObjectFromCSS = getStyleObjectFromCSS_;
/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */
export const trimTextContentFromAnchor = $trimTextContentFromAnchor;
export {
/** @deprecated moved to the lexical package */ $cloneWithProperties,
/** @deprecated moved to the lexical package */ $selectAll,
} from 'lexical';
/**
* 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.
*
*/
import invariant from '@lexical/internal/invariant';
import warnOnlyOnce from '@lexical/internal/warnOnlyOnce';
import {
$caretRangeFromSelection,
$cloneWithPropertiesEphemeral,
$createTextNode,
$getCharacterOffsets,
$getNodeByKey,
$getPreviousSelection,
$getSelection,
$isElementNode,
$isRangeSelection,
$isRootNode,
$isTextNode,
$isTokenOrSegmented,
BaseSelection,
ElementNode,
getStyleObjectFromCSS,
LexicalEditor,
LexicalNode,
NodeKey,
Point,
RangeSelection,
TextNode,
} from 'lexical';
import {getCSSFromStyleObject} from './utils';
/**
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
* it to be generated into the new TextNode.
* @param selection - The selection containing the node whose TextNode is to be edited.
* @param textNode - The TextNode to be edited.
* @param mutates - 'clone' to return a clone before mutating, 'self' to update in-place
* @returns The updated TextNode or clone.
*/
export function $sliceSelectedTextNodeContent<T extends TextNode>(
selection: BaseSelection,
textNode: T,
mutates: 'clone' | 'self' = 'self',
): T {
const anchorAndFocus = selection.getStartEndPoints();
if (
textNode.isSelected(selection) &&
!$isTokenOrSegmented(textNode) &&
anchorAndFocus !== null
) {
const [anchor, focus] = anchorAndFocus;
const isBackward = selection.isBackward();
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const isAnchor = textNode.is(anchorNode);
const isFocus = textNode.is(focusNode);
if (isAnchor || isFocus) {
const [anchorOffset, focusOffset] = $getCharacterOffsets(selection);
const isSame = anchorNode.is(focusNode);
const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
let startOffset = 0;
let endOffset = undefined;
if (isSame) {
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
} else if (isFirst) {
const offset = isBackward ? focusOffset : anchorOffset;
startOffset = offset;
endOffset = undefined;
} else if (isLast) {
const offset = isBackward ? anchorOffset : focusOffset;
startOffset = 0;
endOffset = offset;
}
// NOTE: This mutates __text directly because the primary use case is to
// modify a $cloneWithProperties node that should never be added
// to the EditorState so we must not call getWritable via setTextContent
const text = textNode.__text.slice(startOffset, endOffset);
if (text !== textNode.__text) {
if (mutates === 'clone') {
textNode = $cloneWithPropertiesEphemeral(textNode);
}
textNode.__text = text;
}
}
}
return textNode;
}
/**
* Determines if the current selection is at the end of the node.
* @param point - The point of the selection to test.
* @returns true if the provided point offset is in the last possible position, false otherwise.
*/
export function $isAtNodeEnd(point: Point): boolean {
if (point.type === 'text') {
return point.offset === point.getNode().getTextContentSize();
}
const node = point.getNode();
invariant(
$isElementNode(node),
'isAtNodeEnd: node must be a TextNode or ElementNode',
);
return point.offset === node.getChildrenSize();
}
/**
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
* @param editor - The lexical editor.
* @param anchor - The anchor of the current selection, where the selection should be pointing.
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
*/
export function $trimTextContentFromAnchor(
editor: LexicalEditor,
anchor: Point,
delCount: number,
): void {
// Work from the current selection anchor point
let currentNode: LexicalNode | null = anchor.getNode();
let remaining: number = delCount;
if ($isElementNode(currentNode)) {
const descendantNode = currentNode.getDescendantByIndex(anchor.offset);
if (descendantNode !== null) {
currentNode = descendantNode;
}
}
while (remaining > 0 && currentNode !== null) {
if ($isElementNode(currentNode)) {
const lastDescendant: null | LexicalNode =
currentNode.getLastDescendant<LexicalNode>();
if (lastDescendant !== null) {
currentNode = lastDescendant;
}
}
let nextNode: LexicalNode | null = currentNode.getPreviousSibling();
let additionalElementWhitespace = 0;
if (nextNode === null) {
let parent: LexicalNode | null = currentNode.getParentOrThrow();
let parentSibling: LexicalNode | null = parent.getPreviousSibling();
while (parentSibling === null) {
parent = parent.getParent();
if (parent === null) {
nextNode = null;
break;
}
parentSibling = parent.getPreviousSibling();
}
if (parent !== null) {
additionalElementWhitespace = parent.isInline() ? 0 : 2;
nextNode = parentSibling;
}
}
let text = currentNode.getTextContent();
// If the text is empty, we need to consider adding in two line breaks to match
// the content if we were to get it from its parent.
if (text === '' && $isElementNode(currentNode) && !currentNode.isInline()) {
// TODO: should this be handled in core?
text = '\n\n';
}
const currentNodeSize = text.length;
if (!$isTextNode(currentNode) || remaining >= currentNodeSize) {
const parent = currentNode.getParent();
currentNode.remove();
if (
parent != null &&
parent.getChildrenSize() === 0 &&
!$isRootNode(parent)
) {
parent.remove();
}
remaining -= currentNodeSize + additionalElementWhitespace;
currentNode = nextNode;
} else {
const key = currentNode.getKey();
// See if we can just revert it to what was in the last editor state
const prevTextContent: string | null = editor
.getEditorState()
.read(() => {
const prevNode = $getNodeByKey(key);
if ($isTextNode(prevNode) && prevNode.isSimpleText()) {
return prevNode.getTextContent();
}
return null;
});
const offset = currentNodeSize - remaining;
const slicedText = text.slice(0, offset);
if (prevTextContent !== null && prevTextContent !== text) {
const prevSelection = $getPreviousSelection();
let target = currentNode;
if (!currentNode.isSimpleText()) {
const textNode = $createTextNode(prevTextContent);
currentNode.replace(textNode);
target = textNode;
} else {
currentNode.setTextContent(prevTextContent);
}
if ($isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
const prevOffset = prevSelection.anchor.offset;
target.select(prevOffset, prevOffset);
}
} else if (currentNode.isSimpleText()) {
// Split text
const isSelected = anchor.key === key;
let anchorOffset = anchor.offset;
// Move offset to end if it's less than the remaining number, otherwise
// we'll have a negative splitStart.
if (anchorOffset < remaining) {
anchorOffset = currentNodeSize;
}
const splitStart = isSelected ? anchorOffset - remaining : 0;
const splitEnd = isSelected ? anchorOffset : offset;
if (isSelected && splitStart === 0) {
const [excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
} else {
const [, excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
}
} else {
const textNode = $createTextNode(slicedText);
currentNode.replace(textNode);
}
remaining = 0;
}
}
}
/**
* @deprecated node styles are parsed on demand and not cached eternally
*/
export const $addNodeStyle: (_node: TextNode) => void = warnOnlyOnce(
'$addNodeStyle is a deprecated no-op and calls should be removed',
);
/**
* Applies the provided styles to the given TextNode, ElementNode, or
* collapsed RangeSelection.
*
* @param target - The TextNode, ElementNode, or collapsed RangeSelection to apply the styles to
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
export function $patchStyle(
target: TextNode | RangeSelection | ElementNode,
patch: Record<
string,
| string
| null
| ((currentStyleValue: string | null, _target: typeof target) => string)
>,
): void {
invariant(
$isRangeSelection(target)
? target.isCollapsed()
: $isTextNode(target) || $isElementNode(target),
'$patchStyle must only be called with a TextNode, ElementNode, or collapsed RangeSelection',
);
const prevStyles = getStyleObjectFromCSS(
$isRangeSelection(target)
? target.style
: $isTextNode(target)
? target.getStyle()
: target.getTextStyle(),
);
const newStyles = Object.entries(patch).reduce<Record<string, string>>(
(styles, [key, value]) => {
if (typeof value === 'function') {
styles[key] = value(prevStyles[key], target);
} else if (value === null) {
delete styles[key];
} else {
styles[key] = value;
}
return styles;
},
{...prevStyles},
);
const newCSSText = getCSSFromStyleObject(newStyles);
if ($isRangeSelection(target) || $isTextNode(target)) {
target.setStyle(newCSSText);
} else {
target.setTextStyle(newCSSText);
}
}
/**
* Applies the provided styles to the TextNodes in the provided Selection.
* Will update partially selected TextNodes by splitting the TextNode and applying
* the styles to the appropriate one.
* @param selection - The selected node(s) to update.
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
export function $patchStyleText(
selection: BaseSelection,
patch: Record<
string,
| string
| null
| ((
currentStyleValue: string | null,
target: TextNode | RangeSelection | ElementNode,
) => string)
>,
): void {
if ($isRangeSelection(selection) && selection.isCollapsed()) {
$patchStyle(selection, patch);
const emptyNode = selection.anchor.getNode();
if ($isElementNode(emptyNode) && emptyNode.isEmpty()) {
$patchStyle(emptyNode, patch);
}
}
$forEachSelectedTextNode(textNode => {
$patchStyle(textNode, patch);
});
const nodes = selection.getNodes();
if (nodes.length > 0) {
const patchedElementKeys = new Set<NodeKey>();
for (const node of nodes) {
if (
!$isElementNode(node) ||
!node.canBeEmpty() ||
node.getChildrenSize() !== 0
) {
continue;
}
const key = node.getKey();
if (patchedElementKeys.has(key)) {
continue;
}
patchedElementKeys.add(key);
$patchStyle(node, patch);
}
}
}
export function $forEachSelectedTextNode(
fn: (textNode: TextNode) => void,
): void {
const selection = $getSelection();
if (!selection) {
return;
}
const slicedTextNodes = new Map<
NodeKey,
[startIndex: number, endIndex: number]
>();
const getSliceIndices = (
node: TextNode,
): [startIndex: number, endIndex: number] =>
slicedTextNodes.get(node.getKey()) || [0, node.getTextContentSize()];
if ($isRangeSelection(selection)) {
for (const slice of $caretRangeFromSelection(selection).getTextSlices()) {
if (slice) {
slicedTextNodes.set(
slice.caret.origin.getKey(),
slice.getSliceIndices(),
);
}
}
}
const selectedNodes = selection.getNodes();
for (const selectedNode of selectedNodes) {
if (!($isTextNode(selectedNode) && selectedNode.canHaveFormat())) {
continue;
}
const [startOffset, endOffset] = getSliceIndices(selectedNode);
// No actual text is selected, so do nothing.
if (endOffset === startOffset) {
continue;
}
// The entire node is selected or a token/segment, so just format it
if (
$isTokenOrSegmented(selectedNode) ||
(startOffset === 0 && endOffset === selectedNode.getTextContentSize())
) {
fn(selectedNode);
} else {
// The node is partially selected, so split it into two or three nodes
// and style the selected one.
const splitNodes = selectedNode.splitText(startOffset, endOffset);
const replacement = splitNodes[startOffset === 0 ? 0 : 1];
fn(replacement);
}
}
// Prior to NodeCaret #7046 this would have been a side-effect
// so we do this for test compatibility.
// TODO: we may want to consider simplifying by removing this
if (
$isRangeSelection(selection) &&
selection.anchor.type === 'text' &&
selection.focus.type === 'text' &&
selection.anchor.key === selection.focus.key
) {
$ensureForwardRangeSelection(selection);
}
}
/**
* Ensure that the given RangeSelection is not backwards. If it
* is backwards, then the anchor and focus points will be swapped
* in-place. Ensuring that the selection is a writable RangeSelection
* is the responsibility of the caller (e.g. in a read-only context
* you will want to clone $getSelection() before using this).
*
* @param selection a writable RangeSelection
*/
export function $ensureForwardRangeSelection(selection: RangeSelection): void {
if (selection.isBackward()) {
const {anchor, focus} = selection;
// stash for the in-place swap
const {key, offset, type} = anchor;
anchor.set(focus.key, focus.offset, focus.type);
focus.set(key, offset, type);
}
}
/**
* 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.
*
*/
import type {
BaseSelection,
DecoratorNode,
ElementNode,
LexicalNode,
NodeKey,
Point,
RangeSelection,
TextNode,
} from 'lexical';
import invariant from '@lexical/internal/invariant';
import {
$caretFromPoint,
$extendCaretToRange,
$findMatchingParent,
$getPreviousSelection,
$hasAncestor,
$isChildCaret,
$isDecoratorNode,
$isElementNode,
$isExtendableTextPointCaret,
$isLeafNode,
$isRangeSelection,
$isRootOrShadowRoot,
$isTextNode,
$setSelection,
getStyleObjectFromCSS,
INTERNAL_$isBlock,
} from 'lexical';
import {$getComputedStyleForElement, $getComputedStyleForParent} from './utils';
export function $copyBlockFormatIndent(
srcNode: ElementNode,
destNode: ElementNode,
): void {
const format = srcNode.getFormatType();
const indent = srcNode.getIndent();
if (format !== destNode.getFormatType()) {
destNode.setFormat(format);
}
if (indent !== destNode.getIndent()) {
destNode.setIndent(indent);
}
}
function $isPointAtBlockStart(point: Point, block: ElementNode): boolean {
if (point.offset !== 0) {
return false;
}
let node: LexicalNode = point.getNode();
// When an ElementNode is empty it's not possible to distinguish if
// the selection's intent is the entire block or the edge so we consider
// it to be the entire block
if ($isElementNode(node) && node.isEmpty()) {
return false;
}
while (!node.is(block)) {
if (node.getPreviousSibling() !== null) {
return false;
}
const parent = node.getParent();
if (parent === null) {
return false;
}
node = parent;
}
return true;
}
/**
* Converts all nodes in the selection that are of one block type to another.
* @param selection - The selected blocks to be converted.
* @param $createElement - The function that creates the node. eg. $createParagraphNode.
* @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
*/
export function $setBlocksType<T extends ElementNode>(
selection: BaseSelection | null,
$createElement: () => T,
$afterCreateElement: (
prevNodeSrc: ElementNode,
newNodeDest: T,
) => void = $copyBlockFormatIndent,
): void {
if (!selection) {
return;
}
// Selections tend to not include their containing blocks so we effectively
// expand it here
const anchorAndFocus = selection.getStartEndPoints();
let skipFocusAtBlockStart = false;
let focusBlock: ElementNode | DecoratorNode<unknown> | null = null;
const blockMap = new Map<NodeKey, ElementNode>();
if (anchorAndFocus) {
const [anchor, focus] = anchorAndFocus;
const anchorBlock = $findMatchingParent(
anchor.getNode(),
INTERNAL_$isBlock,
);
focusBlock = $findMatchingParent(focus.getNode(), INTERNAL_$isBlock);
skipFocusAtBlockStart =
$isElementNode(focusBlock) &&
!focusBlock.is(anchorBlock) &&
$isPointAtBlockStart(focus, focusBlock);
if ($isElementNode(anchorBlock)) {
blockMap.set(anchorBlock.getKey(), anchorBlock);
}
if ($isElementNode(focusBlock) && !skipFocusAtBlockStart) {
blockMap.set(focusBlock.getKey(), focusBlock);
}
}
for (const node of selection.getNodes()) {
if ($isElementNode(node) && INTERNAL_$isBlock(node)) {
if (skipFocusAtBlockStart && node.is(focusBlock)) {
continue;
}
blockMap.set(node.getKey(), node);
} else if (!anchorAndFocus) {
const ancestorBlock = $findMatchingParent(node, INTERNAL_$isBlock);
if ($isElementNode(ancestorBlock)) {
blockMap.set(ancestorBlock.getKey(), ancestorBlock);
}
}
}
// Selection remapping is delegated to LexicalNode.replace (and the
// ListItemNode.replace override): both remap an element-anchored point
// on the replaced block to {key: replacement, offset: prevSize + offset}.
for (const prevNode of blockMap.values()) {
const element = $createElement();
$afterCreateElement(prevNode, element);
prevNode.replace(element, true);
}
}
function isPointAttached(point: Point): boolean {
return point.getNode().isAttached();
}
function $removeParentEmptyElements(startingNode: ElementNode): void {
let node: ElementNode | null = startingNode;
while (node !== null && !$isRootOrShadowRoot(node)) {
const latest = node.getLatest();
const parentNode: ElementNode | null = node.getParent<ElementNode>();
if (latest.getChildrenSize() === 0) {
node.remove(true);
}
node = parentNode;
}
}
/**
* @deprecated In favor of $setBlockTypes
* Wraps all nodes in the selection into another node of the type returned by createElement.
* @param selection - The selection of nodes to be wrapped.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to append the wrapped selection and its children to.
*/
export function $wrapNodes(
selection: BaseSelection,
createElement: () => ElementNode,
wrappingElement: null | ElementNode = null,
): void {
const anchorAndFocus = selection.getStartEndPoints();
const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
const nodes = selection.getNodes();
const nodesLength = nodes.length;
if (
anchor !== null &&
(nodesLength === 0 ||
(nodesLength === 1 &&
anchor.type === 'element' &&
anchor.getNode().getChildrenSize() === 0))
) {
const target =
anchor.type === 'text'
? anchor.getNode().getParentOrThrow()
: anchor.getNode();
const children = target.getChildren();
let element = createElement();
element.setFormat(target.getFormatType());
element.setIndent(target.getIndent());
children.forEach(child => element.append(child));
if (wrappingElement) {
element = wrappingElement.append(element);
}
target.replace(element);
return;
}
let topLevelNode = null;
let descendants: LexicalNode[] = [];
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
// Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
// user selected multiple Root-like nodes that have to be treated separately as if they are
// their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
// of each of the cell nodes.
if ($isRootOrShadowRoot(node)) {
$wrapNodesImpl(
selection,
descendants,
descendants.length,
createElement,
wrappingElement,
);
descendants = [];
topLevelNode = node;
} else if (
topLevelNode === null ||
(topLevelNode !== null && $hasAncestor(node, topLevelNode))
) {
descendants.push(node);
} else {
$wrapNodesImpl(
selection,
descendants,
descendants.length,
createElement,
wrappingElement,
);
descendants = [node];
}
}
$wrapNodesImpl(
selection,
descendants,
descendants.length,
createElement,
wrappingElement,
);
}
/**
* Wraps each node into a new ElementNode.
* @param selection - The selection of nodes to wrap.
* @param nodes - An array of nodes, generally the descendants of the selection.
* @param nodesLength - The length of nodes.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to wrap all the nodes into.
* @returns
*/
export function $wrapNodesImpl(
selection: BaseSelection,
nodes: LexicalNode[],
nodesLength: number,
createElement: () => ElementNode,
wrappingElement: null | ElementNode = null,
): void {
if (nodes.length === 0) {
return;
}
const firstNode = nodes[0];
const elementMapping: Map<NodeKey, ElementNode> = new Map();
const elements = [];
// The below logic is to find the right target for us to
// either insertAfter/insertBefore/append the corresponding
// elements to. This is made more complicated due to nested
// structures.
let target = $isElementNode(firstNode)
? firstNode
: firstNode.getParentOrThrow();
if (target.isInline()) {
target = target.getParentOrThrow();
}
let targetIsPrevSibling = false;
while (target !== null) {
const prevSibling = target.getPreviousSibling<ElementNode>();
if (prevSibling !== null) {
target = prevSibling;
targetIsPrevSibling = true;
break;
}
target = target.getParentOrThrow();
if ($isRootOrShadowRoot(target)) {
break;
}
}
const emptyElements = new Set();
// Find any top level empty elements
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
if ($isElementNode(node) && node.getChildrenSize() === 0) {
emptyElements.add(node.getKey());
}
}
const movedNodes: Set<NodeKey> = new Set();
// Move out all leaf nodes into our elements array.
// If we find a top level empty element, also move make
// an element for that.
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
let parent = node.getParent();
if (parent !== null && parent.isInline()) {
parent = parent.getParent();
}
if (
parent !== null &&
$isLeafNode(node) &&
!movedNodes.has(node.getKey())
) {
const parentKey = parent.getKey();
if (elementMapping.get(parentKey) === undefined) {
const targetElement = createElement();
targetElement.setFormat(parent.getFormatType());
targetElement.setIndent(parent.getIndent());
elements.push(targetElement);
elementMapping.set(parentKey, targetElement);
// Move node and its siblings to the new
// element.
const children = parent.getChildren();
targetElement.splice(targetElement.getChildrenSize(), 0, children);
for (const child of children) {
movedNodes.add(child.getKey());
if ($isElementNode(child)) {
// Skip nested leaf nodes if the parent has already been moved
for (const key of child.getChildrenKeys()) {
movedNodes.add(key);
}
}
}
$removeParentEmptyElements(parent);
}
} else if (emptyElements.has(node.getKey())) {
invariant(
$isElementNode(node),
'Expected node in emptyElements to be an ElementNode',
);
const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
elements.push(targetElement);
node.remove(true);
}
}
if (wrappingElement !== null) {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
wrappingElement.append(element);
}
}
let lastElement = null;
// If our target is Root-like, let's see if we can re-adjust
// so that the target is the first child instead.
if ($isRootOrShadowRoot(target)) {
if (targetIsPrevSibling) {
if (wrappingElement !== null) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
}
}
} else {
const firstChild = target.getFirstChild();
if ($isElementNode(firstChild)) {
target = firstChild;
}
if (firstChild === null) {
if (wrappingElement) {
target.append(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
target.append(element);
lastElement = element;
}
}
} else {
if (wrappingElement !== null) {
firstChild.insertBefore(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
firstChild.insertBefore(element);
lastElement = element;
}
}
}
}
} else {
if (wrappingElement) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
lastElement = element;
}
}
}
const prevSelection = $getPreviousSelection();
if (
$isRangeSelection(prevSelection) &&
isPointAttached(prevSelection.anchor) &&
isPointAttached(prevSelection.focus)
) {
$setSelection(prevSelection.clone());
} else if (lastElement !== null) {
lastElement.selectEnd();
} else {
selection.dirty = true;
}
}
/**
* Tests if the selection's parent element has vertical writing mode.
* @param selection - The selection whose parent to test.
* @returns true if the selection's parent has vertical writing mode (writing-mode: vertical-rl), false otherwise.
*/
function $isEditorVerticalOrientation(selection: RangeSelection): boolean {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.writingMode === 'vertical-rl';
}
/**
* Gets the computed DOM styles of the parent of the selection's anchor node.
* @param selection - The selection to check the styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyle(
selection: RangeSelection,
): CSSStyleDeclaration | null {
const anchorNode = selection.anchor.getNode();
if ($isElementNode(anchorNode)) {
return $getComputedStyleForElement(anchorNode);
}
return $getComputedStyleForParent(anchorNode);
}
/**
* Determines if the default character selection should be overridden. Used with DecoratorNodes
* @param selection - The selection whose default character selection may need to be overridden.
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
* @returns true if it should be overridden, false if not.
*/
export function $shouldOverrideDefaultCharacterSelection(
selection: RangeSelection,
isBackward: boolean,
): boolean {
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical writing mode, we adjust the direction for correct caret movement
let adjustedIsBackward = isVertical ? !isBackward : isBackward;
// In right-to-left writing mode, we invert the direction for correct caret movement
if ($isParentElementRTL(selection)) {
adjustedIsBackward = !adjustedIsBackward;
}
const focusCaret = $caretFromPoint(
selection.focus,
adjustedIsBackward ? 'previous' : 'next',
);
if ($isExtendableTextPointCaret(focusCaret)) {
return false;
}
for (const nextCaret of $extendCaretToRange(focusCaret)) {
if ($isChildCaret(nextCaret)) {
return !nextCaret.origin.isInline();
} else if ($isElementNode(nextCaret.origin)) {
continue;
} else if ($isDecoratorNode(nextCaret.origin)) {
return true;
}
break;
}
return false;
}
/**
* Moves the selection according to the arguments.
* @param selection - The selected text or nodes.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
* @param granularity - The distance to adjust the current selection.
*/
export function $moveCaretSelection(
selection: RangeSelection,
isHoldingShift: boolean,
isBackward: boolean,
granularity: 'character' | 'word' | 'lineboundary',
): void {
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
}
/**
* Tests a parent element for right to left direction.
* @param selection - The selection whose parent is to be tested.
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
*/
export function $isParentElementRTL(selection: RangeSelection): boolean {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.direction === 'rtl';
}
/**
* Moves selection by character according to arguments.
* @param selection - The selection of the characters to move.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
*/
export function $moveCharacter(
selection: RangeSelection,
isHoldingShift: boolean,
isBackward: boolean,
): void {
const isRTL = $isParentElementRTL(selection);
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical-rl writing mode, arrow key directions need to be flipped
// to match the visual flow of text (top to bottom, right to left)
let adjustedIsBackward;
if (isVertical) {
// In vertical-rl mode, we need to completely invert the direction
// Left arrow (backward) should move down (forward)
// Right arrow (forward) should move up (backward)
adjustedIsBackward = !isBackward;
} else if (isRTL) {
// In horizontal RTL mode, use the standard RTL behavior
adjustedIsBackward = !isBackward;
} else {
// Standard LTR horizontal text
adjustedIsBackward = isBackward;
}
// Apply the direction adjustment to move the caret
$moveCaretSelection(
selection,
isHoldingShift,
adjustedIsBackward,
'character',
);
}
/**
* Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.
* @param node - The node whose style value to get.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property.
* @returns The value of the property for node.
*/
function $getNodeStyleValueForProperty(
node: TextNode,
styleProperty: string,
defaultValue: string,
): string {
const css = node.getStyle();
const styleObject = getStyleObjectFromCSS(css);
if (styleObject !== null) {
return styleObject[styleProperty] || defaultValue;
}
return defaultValue;
}
/**
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
* If all TextNodes do not have the same value, it returns an empty string.
* @param selection - The selection of TextNodes whose value to find.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property, defaults to an empty string.
* @returns The value of the property for the selected TextNodes.
*/
export function $getSelectionStyleValueForProperty(
selection: BaseSelection,
styleProperty: string,
defaultValue = '',
): string {
let styleValue: string | null = null;
const nodes = selection.getNodes();
// The anchor/focus boundary handling below is specific to RangeSelection;
// other selection types (e.g. table) style every node they contain.
let startNode: LexicalNode | undefined;
let endNode: LexicalNode | undefined;
if ($isRangeSelection(selection)) {
if (selection.isCollapsed() && selection.style !== '') {
const styleObject = getStyleObjectFromCSS(selection.style);
if (styleObject !== null && styleProperty in styleObject) {
return styleObject[styleProperty];
}
}
const {anchor, focus} = selection;
const isBackward = selection.isBackward();
const firstNode = isBackward ? focus.getNode() : anchor.getNode();
const lastNode = isBackward ? anchor.getNode() : focus.getNode();
const startOffset = isBackward ? focus.offset : anchor.offset;
const endOffset = isBackward ? anchor.offset : focus.offset;
// A boundary node contributes no styled text when the selection merely
// touches its edge: the first node when the start offset is at its very
// end, and the last node when the end offset is at its very beginning.
if (
$isTextNode(firstNode) &&
startOffset === firstNode.getTextContentSize()
) {
startNode = firstNode;
}
if (endOffset === 0) {
endNode = lastNode;
}
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// Skip the excluded boundary node for this position (startNode at the
// head, endNode elsewhere); both are undefined when nothing is excluded.
if ($isTextNode(node) && !node.is(i === 0 ? startNode : endNode)) {
const nodeStyleValue = $getNodeStyleValueForProperty(
node,
styleProperty,
defaultValue,
);
if (styleValue === null) {
styleValue = nodeStyleValue;
} else if (styleValue !== nodeStyleValue) {
// multiple text nodes are in the selection and they don't all
// have the same style.
styleValue = '';
break;
}
}
}
return styleValue === null ? defaultValue : styleValue;
}
/**
* 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.
*
*/
import type {ElementNode, LexicalEditor, LexicalNode} from 'lexical';
import {
$getEditor,
$isRootNode,
$isTextNode,
getStyleObjectFromCSS,
} from 'lexical';
function getDOMTextNode(element: Node | null): Text | null {
let node = element;
while (node != null) {
if (node.nodeType === Node.TEXT_NODE) {
return node as Text;
}
node = node.firstChild;
}
return null;
}
function getDOMIndexWithinParent(node: ChildNode): [ParentNode, number] {
const parent = node.parentNode;
if (parent == null) {
throw new Error('Should never happen');
}
return [parent, Array.from(parent.childNodes).indexOf(node)];
}
/**
* Creates a selection range for the DOM.
* @param editor - The lexical editor.
* @param anchorNode - The anchor node of a selection.
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
* @param focusNode - The current focus.
* @param _focusOffset - The amount of space offset from the focus to the anchor.
* @returns The range of selection for the DOM that was created.
*/
export function createDOMRange(
editor: LexicalEditor,
anchorNode: LexicalNode,
_anchorOffset: number,
focusNode: LexicalNode,
_focusOffset: number,
): Range | null {
const anchorKey = anchorNode.getKey();
const focusKey = focusNode.getKey();
const range = document.createRange();
let anchorDOM: Node | Text | null = editor.getElementByKey(anchorKey);
let focusDOM: Node | Text | null = editor.getElementByKey(focusKey);
let anchorOffset = _anchorOffset;
let focusOffset = _focusOffset;
if ($isTextNode(anchorNode)) {
anchorDOM = getDOMTextNode(anchorDOM);
}
if ($isTextNode(focusNode)) {
focusDOM = getDOMTextNode(focusDOM);
}
if (
anchorNode === undefined ||
focusNode === undefined ||
anchorDOM === null ||
focusDOM === null
) {
return null;
}
if (anchorDOM.nodeName === 'BR') {
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM as ChildNode);
}
if (focusDOM.nodeName === 'BR') {
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM as ChildNode);
}
const firstChild = anchorDOM.firstChild;
if (
anchorDOM === focusDOM &&
firstChild != null &&
firstChild.nodeName === 'BR' &&
anchorOffset === 0 &&
focusOffset === 0
) {
focusOffset = 1;
}
try {
range.setStart(anchorDOM, anchorOffset);
range.setEnd(focusDOM, focusOffset);
} catch (_e) {
return null;
}
if (
range.collapsed &&
(anchorOffset !== focusOffset || anchorKey !== focusKey)
) {
// Range is backwards, we need to reverse it
range.setStart(focusDOM, focusOffset);
range.setEnd(anchorDOM, anchorOffset);
}
return range;
}
/**
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
* @param editor - The lexical editor
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
* @returns The selectionRects as an array.
*/
export function createRectsFromDOMRange(
editor: LexicalEditor,
range: Range,
): Array<ClientRect> {
const rootElement = editor.getRootElement();
if (rootElement === null) {
return [];
}
const rootRect = rootElement.getBoundingClientRect();
const computedStyle = getComputedStyle(rootElement);
const rootPadding =
parseFloat(computedStyle.paddingLeft) +
parseFloat(computedStyle.paddingRight);
const selectionRects = Array.from(range.getClientRects());
let selectionRectsLength = selectionRects.length;
//sort rects from top left to bottom right.
selectionRects.sort((a, b) => {
const top = a.top - b.top;
// Some rects match position closely, but not perfectly,
// so we give a 3px tolerance.
if (Math.abs(top) <= 3) {
return a.left - b.left;
}
return top;
});
let prevRect;
for (let i = 0; i < selectionRectsLength; i++) {
const selectionRect = selectionRects[i];
// Exclude rects that overlap preceding Rects in the sorted list.
const isOverlappingRect =
prevRect &&
prevRect.top <= selectionRect.top &&
prevRect.top + prevRect.height > selectionRect.top &&
prevRect.left + prevRect.width > selectionRect.left;
// Exclude selections that span the entire element
const selectionSpansElement =
selectionRect.width + rootPadding === rootRect.width;
if (isOverlappingRect || selectionSpansElement) {
selectionRects.splice(i--, 1);
selectionRectsLength--;
continue;
}
prevRect = selectionRect;
}
return selectionRects;
}
/**
* @deprecated Use {@link getStyleObjectFromCSS}, this is just an alias for backwards compatibility.
*/
export const getStyleObjectFromRawCSS = getStyleObjectFromCSS;
/**
* Serializes a style object into a CSS declaration string, the inverse of
* {@link getStyleObjectFromCSS}.
* @param styles - An object mapping CSS property names to their values.
* @returns A CSS string of the form `prop: value;` for each entry, concatenated together.
*/
export function getCSSFromStyleObject(styles: Record<string, string>): string {
let css = '';
for (const style in styles) {
if (style) {
css += `${style}: ${styles[style]};`;
}
}
return css;
}
/**
* Gets the computed DOM styles of the element.
* @param element - The node to check the styles for.
* @returns the computed styles of the element or null if there is no DOM element or no default view for the document.
*/
export function $getComputedStyleForElement(
element: ElementNode,
): CSSStyleDeclaration | null {
const editor = $getEditor();
const domElement = editor.getElementByKey(element.getKey());
if (domElement === null) {
return null;
}
const view = domElement.ownerDocument.defaultView;
if (view === null) {
return null;
}
return view.getComputedStyle(domElement);
}
/**
* Gets the computed DOM styles of the parent of the node.
* @param node - The node to check its parent's styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
export function $getComputedStyleForParent(
node: LexicalNode,
): CSSStyleDeclaration | null {
const parent = $isRootNode(node) ? node : node.getParentOrThrow();
return $getComputedStyleForElement(parent);
}
/**
* Determines whether a node's parent is RTL.
* @param node - The node to check whether it is RTL.
* @returns whether the node is RTL.
*/
export function $isParentRTL(node: LexicalNode): boolean {
const styles = $getComputedStyleForParent(node);
return styles !== null && styles.direction === 'rtl';
}
+30
-15

@@ -12,5 +12,5 @@ {

"license": "MIT",
"version": "0.44.1-nightly.20260519.0",
"main": "LexicalSelection.js",
"types": "index.d.ts",
"version": "0.45.0",
"main": "./dist/LexicalSelection.js",
"types": "./dist/index.d.ts",
"repository": {

@@ -21,18 +21,19 @@ "type": "git",

},
"module": "LexicalSelection.mjs",
"module": "./dist/LexicalSelection.mjs",
"sideEffects": false,
"exports": {
".": {
"source": "./src/index.ts",
"import": {
"types": "./index.d.ts",
"development": "./LexicalSelection.dev.mjs",
"production": "./LexicalSelection.prod.mjs",
"node": "./LexicalSelection.node.mjs",
"default": "./LexicalSelection.mjs"
"types": "./dist/index.d.ts",
"development": "./dist/LexicalSelection.dev.mjs",
"production": "./dist/LexicalSelection.prod.mjs",
"node": "./dist/LexicalSelection.node.mjs",
"default": "./dist/LexicalSelection.mjs"
},
"require": {
"types": "./index.d.ts",
"development": "./LexicalSelection.dev.js",
"production": "./LexicalSelection.prod.js",
"default": "./LexicalSelection.js"
"types": "./dist/index.d.ts",
"development": "./dist/LexicalSelection.dev.js",
"production": "./dist/LexicalSelection.prod.js",
"default": "./dist/LexicalSelection.js"
}

@@ -42,4 +43,18 @@ }

"dependencies": {
"lexical": "0.44.1-nightly.20260519.0"
}
"@lexical/internal": "0.45.0",
"lexical": "0.45.0"
},
"files": [
"dist",
"src",
"!src/__tests__",
"!src/__bench__",
"!src/__mocks__",
"!src/**/*.test.ts",
"!src/**/*.test.tsx",
"!src/**/*.bench.ts",
"!src/**/*.bench.tsx",
"README.md",
"LICENSE"
]
}
/**
* 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.
*
*/
import { getStyleObjectFromCSS as getStyleObjectFromCSS_ } from 'lexical';
import { $trimTextContentFromAnchor } from './lexical-node';
export { $addNodeStyle, $ensureForwardRangeSelection, $forEachSelectedTextNode, $isAtNodeEnd, $patchStyleText, $sliceSelectedTextNodeContent, $trimTextContentFromAnchor, } from './lexical-node';
export { $copyBlockFormatIndent, $getSelectionStyleValueForProperty, $isParentElementRTL, $moveCaretSelection, $moveCharacter, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $wrapNodes, } from './range-selection';
export { $getComputedStyleForElement, $getComputedStyleForParent, $isParentRTL, createDOMRange, createRectsFromDOMRange, getCSSFromStyleObject, } from './utils';
/** @deprecated moved to the `lexical` package */
export declare const getStyleObjectFromCSS: typeof getStyleObjectFromCSS_;
/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */
export declare const trimTextContentFromAnchor: typeof $trimTextContentFromAnchor;
export {
/** @deprecated moved to the lexical package */ $cloneWithProperties,
/** @deprecated moved to the lexical package */ $selectAll, } from 'lexical';
/**
* 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.
*
*/
import { BaseSelection, ElementNode, LexicalEditor, Point, RangeSelection, TextNode } from 'lexical';
/**
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
* it to be generated into the new TextNode.
* @param selection - The selection containing the node whose TextNode is to be edited.
* @param textNode - The TextNode to be edited.
* @param mutates - 'clone' to return a clone before mutating, 'self' to update in-place
* @returns The updated TextNode or clone.
*/
export declare function $sliceSelectedTextNodeContent<T extends TextNode>(selection: BaseSelection, textNode: T, mutates?: 'clone' | 'self'): T;
/**
* Determines if the current selection is at the end of the node.
* @param point - The point of the selection to test.
* @returns true if the provided point offset is in the last possible position, false otherwise.
*/
export declare function $isAtNodeEnd(point: Point): boolean;
/**
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
* @param editor - The lexical editor.
* @param anchor - The anchor of the current selection, where the selection should be pointing.
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
*/
export declare function $trimTextContentFromAnchor(editor: LexicalEditor, anchor: Point, delCount: number): void;
/**
* @deprecated node styles are parsed on demand and not cached eternally
*/
export declare const $addNodeStyle: (_node: TextNode) => void;
/**
* Applies the provided styles to the given TextNode, ElementNode, or
* collapsed RangeSelection.
*
* @param target - The TextNode, ElementNode, or collapsed RangeSelection to apply the styles to
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
export declare function $patchStyle(target: TextNode | RangeSelection | ElementNode, patch: Record<string, string | null | ((currentStyleValue: string | null, _target: typeof target) => string)>): void;
/**
* Applies the provided styles to the TextNodes in the provided Selection.
* Will update partially selected TextNodes by splitting the TextNode and applying
* the styles to the appropriate one.
* @param selection - The selected node(s) to update.
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
export declare function $patchStyleText(selection: BaseSelection, patch: Record<string, string | null | ((currentStyleValue: string | null, target: TextNode | RangeSelection | ElementNode) => string)>): void;
export declare function $forEachSelectedTextNode(fn: (textNode: TextNode) => void): void;
/**
* Ensure that the given RangeSelection is not backwards. If it
* is backwards, then the anchor and focus points will be swapped
* in-place. Ensuring that the selection is a writable RangeSelection
* is the responsibility of the caller (e.g. in a read-only context
* you will want to clone $getSelection() before using this).
*
* @param selection a writable RangeSelection
*/
export declare function $ensureForwardRangeSelection(selection: RangeSelection): void;
/**
* 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.
*
*/
'use strict';
var lexical = require('lexical');
/**
* 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.
*
*/
// Do not require this module directly! Use normal `invariant` calls.
function formatDevErrorMessage(message) {
throw new Error(message);
}
/**
* 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.
*
*/
/*@__INLINE__*/
function warnOnlyOnce(message) {
{
let run = false;
return () => {
if (!run) {
console.warn(message);
}
run = 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.
*
*/
function getDOMTextNode(element) {
let node = element;
while (node != null) {
if (node.nodeType === Node.TEXT_NODE) {
return node;
}
node = node.firstChild;
}
return null;
}
function getDOMIndexWithinParent(node) {
const parent = node.parentNode;
if (parent == null) {
throw new Error('Should never happen');
}
return [parent, Array.from(parent.childNodes).indexOf(node)];
}
/**
* Creates a selection range for the DOM.
* @param editor - The lexical editor.
* @param anchorNode - The anchor node of a selection.
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
* @param focusNode - The current focus.
* @param _focusOffset - The amount of space offset from the focus to the anchor.
* @returns The range of selection for the DOM that was created.
*/
function createDOMRange(editor, anchorNode, _anchorOffset, focusNode, _focusOffset) {
const anchorKey = anchorNode.getKey();
const focusKey = focusNode.getKey();
const range = document.createRange();
let anchorDOM = editor.getElementByKey(anchorKey);
let focusDOM = editor.getElementByKey(focusKey);
let anchorOffset = _anchorOffset;
let focusOffset = _focusOffset;
if (lexical.$isTextNode(anchorNode)) {
anchorDOM = getDOMTextNode(anchorDOM);
}
if (lexical.$isTextNode(focusNode)) {
focusDOM = getDOMTextNode(focusDOM);
}
if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
return null;
}
if (anchorDOM.nodeName === 'BR') {
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
}
if (focusDOM.nodeName === 'BR') {
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
}
const firstChild = anchorDOM.firstChild;
if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
focusOffset = 1;
}
try {
range.setStart(anchorDOM, anchorOffset);
range.setEnd(focusDOM, focusOffset);
} catch (_e) {
return null;
}
if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
// Range is backwards, we need to reverse it
range.setStart(focusDOM, focusOffset);
range.setEnd(anchorDOM, anchorOffset);
}
return range;
}
/**
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
* @param editor - The lexical editor
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
* @returns The selectionRects as an array.
*/
function createRectsFromDOMRange(editor, range) {
const rootElement = editor.getRootElement();
if (rootElement === null) {
return [];
}
const rootRect = rootElement.getBoundingClientRect();
const computedStyle = getComputedStyle(rootElement);
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
const selectionRects = Array.from(range.getClientRects());
let selectionRectsLength = selectionRects.length;
//sort rects from top left to bottom right.
selectionRects.sort((a, b) => {
const top = a.top - b.top;
// Some rects match position closely, but not perfectly,
// so we give a 3px tolerance.
if (Math.abs(top) <= 3) {
return a.left - b.left;
}
return top;
});
let prevRect;
for (let i = 0; i < selectionRectsLength; i++) {
const selectionRect = selectionRects[i];
// Exclude rects that overlap preceding Rects in the sorted list.
const isOverlappingRect = prevRect && prevRect.top <= selectionRect.top && prevRect.top + prevRect.height > selectionRect.top && prevRect.left + prevRect.width > selectionRect.left;
// Exclude selections that span the entire element
const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
if (isOverlappingRect || selectionSpansElement) {
selectionRects.splice(i--, 1);
selectionRectsLength--;
continue;
}
prevRect = selectionRect;
}
return selectionRects;
}
/**
* Serializes a style object into a CSS declaration string, the inverse of
* {@link getStyleObjectFromCSS}.
* @param styles - An object mapping CSS property names to their values.
* @returns A CSS string of the form `prop: value;` for each entry, concatenated together.
*/
function getCSSFromStyleObject(styles) {
let css = '';
for (const style in styles) {
if (style) {
css += `${style}: ${styles[style]};`;
}
}
return css;
}
/**
* Gets the computed DOM styles of the element.
* @param element - The node to check the styles for.
* @returns the computed styles of the element or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForElement(element) {
const editor = lexical.$getEditor();
const domElement = editor.getElementByKey(element.getKey());
if (domElement === null) {
return null;
}
const view = domElement.ownerDocument.defaultView;
if (view === null) {
return null;
}
return view.getComputedStyle(domElement);
}
/**
* Gets the computed DOM styles of the parent of the node.
* @param node - The node to check its parent's styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForParent(node) {
const parent = lexical.$isRootNode(node) ? node : node.getParentOrThrow();
return $getComputedStyleForElement(parent);
}
/**
* Determines whether a node's parent is RTL.
* @param node - The node to check whether it is RTL.
* @returns whether the node is RTL.
*/
function $isParentRTL(node) {
const styles = $getComputedStyleForParent(node);
return styles !== null && styles.direction === 'rtl';
}
/**
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
* it to be generated into the new TextNode.
* @param selection - The selection containing the node whose TextNode is to be edited.
* @param textNode - The TextNode to be edited.
* @param mutates - 'clone' to return a clone before mutating, 'self' to update in-place
* @returns The updated TextNode or clone.
*/
function $sliceSelectedTextNodeContent(selection, textNode, mutates = 'self') {
const anchorAndFocus = selection.getStartEndPoints();
if (textNode.isSelected(selection) && !lexical.$isTokenOrSegmented(textNode) && anchorAndFocus !== null) {
const [anchor, focus] = anchorAndFocus;
const isBackward = selection.isBackward();
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const isAnchor = textNode.is(anchorNode);
const isFocus = textNode.is(focusNode);
if (isAnchor || isFocus) {
const [anchorOffset, focusOffset] = lexical.$getCharacterOffsets(selection);
const isSame = anchorNode.is(focusNode);
const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
let startOffset = 0;
let endOffset = undefined;
if (isSame) {
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
} else if (isFirst) {
const offset = isBackward ? focusOffset : anchorOffset;
startOffset = offset;
endOffset = undefined;
} else if (isLast) {
const offset = isBackward ? anchorOffset : focusOffset;
startOffset = 0;
endOffset = offset;
}
// NOTE: This mutates __text directly because the primary use case is to
// modify a $cloneWithProperties node that should never be added
// to the EditorState so we must not call getWritable via setTextContent
const text = textNode.__text.slice(startOffset, endOffset);
if (text !== textNode.__text) {
if (mutates === 'clone') {
textNode = lexical.$cloneWithPropertiesEphemeral(textNode);
}
textNode.__text = text;
}
}
}
return textNode;
}
/**
* Determines if the current selection is at the end of the node.
* @param point - The point of the selection to test.
* @returns true if the provided point offset is in the last possible position, false otherwise.
*/
function $isAtNodeEnd(point) {
if (point.type === 'text') {
return point.offset === point.getNode().getTextContentSize();
}
const node = point.getNode();
if (!lexical.$isElementNode(node)) {
formatDevErrorMessage(`isAtNodeEnd: node must be a TextNode or ElementNode`);
}
return point.offset === node.getChildrenSize();
}
/**
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
* @param editor - The lexical editor.
* @param anchor - The anchor of the current selection, where the selection should be pointing.
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
*/
function $trimTextContentFromAnchor(editor, anchor, delCount) {
// Work from the current selection anchor point
let currentNode = anchor.getNode();
let remaining = delCount;
if (lexical.$isElementNode(currentNode)) {
const descendantNode = currentNode.getDescendantByIndex(anchor.offset);
if (descendantNode !== null) {
currentNode = descendantNode;
}
}
while (remaining > 0 && currentNode !== null) {
if (lexical.$isElementNode(currentNode)) {
const lastDescendant = currentNode.getLastDescendant();
if (lastDescendant !== null) {
currentNode = lastDescendant;
}
}
let nextNode = currentNode.getPreviousSibling();
let additionalElementWhitespace = 0;
if (nextNode === null) {
let parent = currentNode.getParentOrThrow();
let parentSibling = parent.getPreviousSibling();
while (parentSibling === null) {
parent = parent.getParent();
if (parent === null) {
nextNode = null;
break;
}
parentSibling = parent.getPreviousSibling();
}
if (parent !== null) {
additionalElementWhitespace = parent.isInline() ? 0 : 2;
nextNode = parentSibling;
}
}
let text = currentNode.getTextContent();
// If the text is empty, we need to consider adding in two line breaks to match
// the content if we were to get it from its parent.
if (text === '' && lexical.$isElementNode(currentNode) && !currentNode.isInline()) {
// TODO: should this be handled in core?
text = '\n\n';
}
const currentNodeSize = text.length;
if (!lexical.$isTextNode(currentNode) || remaining >= currentNodeSize) {
const parent = currentNode.getParent();
currentNode.remove();
if (parent != null && parent.getChildrenSize() === 0 && !lexical.$isRootNode(parent)) {
parent.remove();
}
remaining -= currentNodeSize + additionalElementWhitespace;
currentNode = nextNode;
} else {
const key = currentNode.getKey();
// See if we can just revert it to what was in the last editor state
const prevTextContent = editor.getEditorState().read(() => {
const prevNode = lexical.$getNodeByKey(key);
if (lexical.$isTextNode(prevNode) && prevNode.isSimpleText()) {
return prevNode.getTextContent();
}
return null;
});
const offset = currentNodeSize - remaining;
const slicedText = text.slice(0, offset);
if (prevTextContent !== null && prevTextContent !== text) {
const prevSelection = lexical.$getPreviousSelection();
let target = currentNode;
if (!currentNode.isSimpleText()) {
const textNode = lexical.$createTextNode(prevTextContent);
currentNode.replace(textNode);
target = textNode;
} else {
currentNode.setTextContent(prevTextContent);
}
if (lexical.$isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
const prevOffset = prevSelection.anchor.offset;
target.select(prevOffset, prevOffset);
}
} else if (currentNode.isSimpleText()) {
// Split text
const isSelected = anchor.key === key;
let anchorOffset = anchor.offset;
// Move offset to end if it's less than the remaining number, otherwise
// we'll have a negative splitStart.
if (anchorOffset < remaining) {
anchorOffset = currentNodeSize;
}
const splitStart = isSelected ? anchorOffset - remaining : 0;
const splitEnd = isSelected ? anchorOffset : offset;
if (isSelected && splitStart === 0) {
const [excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
} else {
const [, excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
}
} else {
const textNode = lexical.$createTextNode(slicedText);
currentNode.replace(textNode);
}
remaining = 0;
}
}
}
/**
* @deprecated node styles are parsed on demand and not cached eternally
*/
const $addNodeStyle = warnOnlyOnce('$addNodeStyle is a deprecated no-op and calls should be removed');
/**
* Applies the provided styles to the given TextNode, ElementNode, or
* collapsed RangeSelection.
*
* @param target - The TextNode, ElementNode, or collapsed RangeSelection to apply the styles to
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyle(target, patch) {
if (!(lexical.$isRangeSelection(target) ? target.isCollapsed() : lexical.$isTextNode(target) || lexical.$isElementNode(target))) {
formatDevErrorMessage(`$patchStyle must only be called with a TextNode, ElementNode, or collapsed RangeSelection`);
}
const prevStyles = lexical.getStyleObjectFromCSS(lexical.$isRangeSelection(target) ? target.style : lexical.$isTextNode(target) ? target.getStyle() : target.getTextStyle());
const newStyles = Object.entries(patch).reduce((styles, [key, value]) => {
if (typeof value === 'function') {
styles[key] = value(prevStyles[key], target);
} else if (value === null) {
delete styles[key];
} else {
styles[key] = value;
}
return styles;
}, {
...prevStyles
});
const newCSSText = getCSSFromStyleObject(newStyles);
if (lexical.$isRangeSelection(target) || lexical.$isTextNode(target)) {
target.setStyle(newCSSText);
} else {
target.setTextStyle(newCSSText);
}
}
/**
* Applies the provided styles to the TextNodes in the provided Selection.
* Will update partially selected TextNodes by splitting the TextNode and applying
* the styles to the appropriate one.
* @param selection - The selected node(s) to update.
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyleText(selection, patch) {
if (lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
$patchStyle(selection, patch);
const emptyNode = selection.anchor.getNode();
if (lexical.$isElementNode(emptyNode) && emptyNode.isEmpty()) {
$patchStyle(emptyNode, patch);
}
}
$forEachSelectedTextNode(textNode => {
$patchStyle(textNode, patch);
});
const nodes = selection.getNodes();
if (nodes.length > 0) {
const patchedElementKeys = new Set();
for (const node of nodes) {
if (!lexical.$isElementNode(node) || !node.canBeEmpty() || node.getChildrenSize() !== 0) {
continue;
}
const key = node.getKey();
if (patchedElementKeys.has(key)) {
continue;
}
patchedElementKeys.add(key);
$patchStyle(node, patch);
}
}
}
function $forEachSelectedTextNode(fn) {
const selection = lexical.$getSelection();
if (!selection) {
return;
}
const slicedTextNodes = new Map();
const getSliceIndices = node => slicedTextNodes.get(node.getKey()) || [0, node.getTextContentSize()];
if (lexical.$isRangeSelection(selection)) {
for (const slice of lexical.$caretRangeFromSelection(selection).getTextSlices()) {
if (slice) {
slicedTextNodes.set(slice.caret.origin.getKey(), slice.getSliceIndices());
}
}
}
const selectedNodes = selection.getNodes();
for (const selectedNode of selectedNodes) {
if (!(lexical.$isTextNode(selectedNode) && selectedNode.canHaveFormat())) {
continue;
}
const [startOffset, endOffset] = getSliceIndices(selectedNode);
// No actual text is selected, so do nothing.
if (endOffset === startOffset) {
continue;
}
// The entire node is selected or a token/segment, so just format it
if (lexical.$isTokenOrSegmented(selectedNode) || startOffset === 0 && endOffset === selectedNode.getTextContentSize()) {
fn(selectedNode);
} else {
// The node is partially selected, so split it into two or three nodes
// and style the selected one.
const splitNodes = selectedNode.splitText(startOffset, endOffset);
const replacement = splitNodes[startOffset === 0 ? 0 : 1];
fn(replacement);
}
}
// Prior to NodeCaret #7046 this would have been a side-effect
// so we do this for test compatibility.
// TODO: we may want to consider simplifying by removing this
if (lexical.$isRangeSelection(selection) && selection.anchor.type === 'text' && selection.focus.type === 'text' && selection.anchor.key === selection.focus.key) {
$ensureForwardRangeSelection(selection);
}
}
/**
* Ensure that the given RangeSelection is not backwards. If it
* is backwards, then the anchor and focus points will be swapped
* in-place. Ensuring that the selection is a writable RangeSelection
* is the responsibility of the caller (e.g. in a read-only context
* you will want to clone $getSelection() before using this).
*
* @param selection a writable RangeSelection
*/
function $ensureForwardRangeSelection(selection) {
if (selection.isBackward()) {
const {
anchor,
focus
} = selection;
// stash for the in-place swap
const {
key,
offset,
type
} = anchor;
anchor.set(focus.key, focus.offset, focus.type);
focus.set(key, offset, type);
}
}
function $copyBlockFormatIndent(srcNode, destNode) {
const format = srcNode.getFormatType();
const indent = srcNode.getIndent();
if (format !== destNode.getFormatType()) {
destNode.setFormat(format);
}
if (indent !== destNode.getIndent()) {
destNode.setIndent(indent);
}
}
/**
* Converts all nodes in the selection that are of one block type to another.
* @param selection - The selected blocks to be converted.
* @param $createElement - The function that creates the node. eg. $createParagraphNode.
* @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
*/
function $setBlocksType(selection, $createElement, $afterCreateElement = $copyBlockFormatIndent) {
if (selection === null) {
return;
}
// Selections tend to not include their containing blocks so we effectively
// expand it here
const anchorAndFocus = selection.getStartEndPoints();
const blockMap = new Map();
if (anchorAndFocus) {
const [anchor, focus] = anchorAndFocus;
const anchorBlock = lexical.$findMatchingParent(anchor.getNode(), lexical.INTERNAL_$isBlock);
const focusBlock = lexical.$findMatchingParent(focus.getNode(), lexical.INTERNAL_$isBlock);
if (lexical.$isElementNode(anchorBlock)) {
blockMap.set(anchorBlock.getKey(), anchorBlock);
}
if (lexical.$isElementNode(focusBlock)) {
blockMap.set(focusBlock.getKey(), focusBlock);
}
}
for (const node of selection.getNodes()) {
if (lexical.$isElementNode(node) && lexical.INTERNAL_$isBlock(node)) {
blockMap.set(node.getKey(), node);
} else if (anchorAndFocus === null) {
const ancestorBlock = lexical.$findMatchingParent(node, lexical.INTERNAL_$isBlock);
if (lexical.$isElementNode(ancestorBlock)) {
blockMap.set(ancestorBlock.getKey(), ancestorBlock);
}
}
}
// Selection remapping is delegated to LexicalNode.replace (and the
// ListItemNode.replace override): both remap an element-anchored point
// on the replaced block to {key: replacement, offset: prevSize + offset}.
for (const [, prevNode] of blockMap) {
const element = $createElement();
$afterCreateElement(prevNode, element);
prevNode.replace(element, true);
}
}
function isPointAttached(point) {
return point.getNode().isAttached();
}
function $removeParentEmptyElements(startingNode) {
let node = startingNode;
while (node !== null && !lexical.$isRootOrShadowRoot(node)) {
const latest = node.getLatest();
const parentNode = node.getParent();
if (latest.getChildrenSize() === 0) {
node.remove(true);
}
node = parentNode;
}
}
/**
* @deprecated In favor of $setBlockTypes
* Wraps all nodes in the selection into another node of the type returned by createElement.
* @param selection - The selection of nodes to be wrapped.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to append the wrapped selection and its children to.
*/
function $wrapNodes(selection, createElement, wrappingElement = null) {
const anchorAndFocus = selection.getStartEndPoints();
const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
const nodes = selection.getNodes();
const nodesLength = nodes.length;
if (anchor !== null && (nodesLength === 0 || nodesLength === 1 && anchor.type === 'element' && anchor.getNode().getChildrenSize() === 0)) {
const target = anchor.type === 'text' ? anchor.getNode().getParentOrThrow() : anchor.getNode();
const children = target.getChildren();
let element = createElement();
element.setFormat(target.getFormatType());
element.setIndent(target.getIndent());
children.forEach(child => element.append(child));
if (wrappingElement) {
element = wrappingElement.append(element);
}
target.replace(element);
return;
}
let topLevelNode = null;
let descendants = [];
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
// Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
// user selected multiple Root-like nodes that have to be treated separately as if they are
// their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
// of each of the cell nodes.
if (lexical.$isRootOrShadowRoot(node)) {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [];
topLevelNode = node;
} else if (topLevelNode === null || topLevelNode !== null && lexical.$hasAncestor(node, topLevelNode)) {
descendants.push(node);
} else {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [node];
}
}
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
}
/**
* Wraps each node into a new ElementNode.
* @param selection - The selection of nodes to wrap.
* @param nodes - An array of nodes, generally the descendants of the selection.
* @param nodesLength - The length of nodes.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to wrap all the nodes into.
* @returns
*/
function $wrapNodesImpl(selection, nodes, nodesLength, createElement, wrappingElement = null) {
if (nodes.length === 0) {
return;
}
const firstNode = nodes[0];
const elementMapping = new Map();
const elements = [];
// The below logic is to find the right target for us to
// either insertAfter/insertBefore/append the corresponding
// elements to. This is made more complicated due to nested
// structures.
let target = lexical.$isElementNode(firstNode) ? firstNode : firstNode.getParentOrThrow();
if (target.isInline()) {
target = target.getParentOrThrow();
}
let targetIsPrevSibling = false;
while (target !== null) {
const prevSibling = target.getPreviousSibling();
if (prevSibling !== null) {
target = prevSibling;
targetIsPrevSibling = true;
break;
}
target = target.getParentOrThrow();
if (lexical.$isRootOrShadowRoot(target)) {
break;
}
}
const emptyElements = new Set();
// Find any top level empty elements
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
if (lexical.$isElementNode(node) && node.getChildrenSize() === 0) {
emptyElements.add(node.getKey());
}
}
const movedNodes = new Set();
// Move out all leaf nodes into our elements array.
// If we find a top level empty element, also move make
// an element for that.
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
let parent = node.getParent();
if (parent !== null && parent.isInline()) {
parent = parent.getParent();
}
if (parent !== null && lexical.$isLeafNode(node) && !movedNodes.has(node.getKey())) {
const parentKey = parent.getKey();
if (elementMapping.get(parentKey) === undefined) {
const targetElement = createElement();
targetElement.setFormat(parent.getFormatType());
targetElement.setIndent(parent.getIndent());
elements.push(targetElement);
elementMapping.set(parentKey, targetElement);
// Move node and its siblings to the new
// element.
const children = parent.getChildren();
targetElement.splice(targetElement.getChildrenSize(), 0, children);
for (const child of children) {
movedNodes.add(child.getKey());
if (lexical.$isElementNode(child)) {
// Skip nested leaf nodes if the parent has already been moved
for (const key of child.getChildrenKeys()) {
movedNodes.add(key);
}
}
}
$removeParentEmptyElements(parent);
}
} else if (emptyElements.has(node.getKey())) {
if (!lexical.$isElementNode(node)) {
formatDevErrorMessage(`Expected node in emptyElements to be an ElementNode`);
}
const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
elements.push(targetElement);
node.remove(true);
}
}
if (wrappingElement !== null) {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
wrappingElement.append(element);
}
}
let lastElement = null;
// If our target is Root-like, let's see if we can re-adjust
// so that the target is the first child instead.
if (lexical.$isRootOrShadowRoot(target)) {
if (targetIsPrevSibling) {
if (wrappingElement !== null) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
}
}
} else {
const firstChild = target.getFirstChild();
if (lexical.$isElementNode(firstChild)) {
target = firstChild;
}
if (firstChild === null) {
if (wrappingElement) {
target.append(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
target.append(element);
lastElement = element;
}
}
} else {
if (wrappingElement !== null) {
firstChild.insertBefore(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
firstChild.insertBefore(element);
lastElement = element;
}
}
}
}
} else {
if (wrappingElement) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
lastElement = element;
}
}
}
const prevSelection = lexical.$getPreviousSelection();
if (lexical.$isRangeSelection(prevSelection) && isPointAttached(prevSelection.anchor) && isPointAttached(prevSelection.focus)) {
lexical.$setSelection(prevSelection.clone());
} else if (lastElement !== null) {
lastElement.selectEnd();
} else {
selection.dirty = true;
}
}
/**
* Tests if the selection's parent element has vertical writing mode.
* @param selection - The selection whose parent to test.
* @returns true if the selection's parent has vertical writing mode (writing-mode: vertical-rl), false otherwise.
*/
function $isEditorVerticalOrientation(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.writingMode === 'vertical-rl';
}
/**
* Gets the computed DOM styles of the parent of the selection's anchor node.
* @param selection - The selection to check the styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyle(selection) {
const anchorNode = selection.anchor.getNode();
if (lexical.$isElementNode(anchorNode)) {
return $getComputedStyleForElement(anchorNode);
}
return $getComputedStyleForParent(anchorNode);
}
/**
* Determines if the default character selection should be overridden. Used with DecoratorNodes
* @param selection - The selection whose default character selection may need to be overridden.
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
* @returns true if it should be overridden, false if not.
*/
function $shouldOverrideDefaultCharacterSelection(selection, isBackward) {
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical writing mode, we adjust the direction for correct caret movement
let adjustedIsBackward = isVertical ? !isBackward : isBackward;
// In right-to-left writing mode, we invert the direction for correct caret movement
if ($isParentElementRTL(selection)) {
adjustedIsBackward = !adjustedIsBackward;
}
const focusCaret = lexical.$caretFromPoint(selection.focus, adjustedIsBackward ? 'previous' : 'next');
if (lexical.$isExtendableTextPointCaret(focusCaret)) {
return false;
}
for (const nextCaret of lexical.$extendCaretToRange(focusCaret)) {
if (lexical.$isChildCaret(nextCaret)) {
return !nextCaret.origin.isInline();
} else if (lexical.$isElementNode(nextCaret.origin)) {
continue;
} else if (lexical.$isDecoratorNode(nextCaret.origin)) {
return true;
}
break;
}
return false;
}
/**
* Moves the selection according to the arguments.
* @param selection - The selected text or nodes.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
* @param granularity - The distance to adjust the current selection.
*/
function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
}
/**
* Tests a parent element for right to left direction.
* @param selection - The selection whose parent is to be tested.
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
*/
function $isParentElementRTL(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.direction === 'rtl';
}
/**
* Moves selection by character according to arguments.
* @param selection - The selection of the characters to move.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
*/
function $moveCharacter(selection, isHoldingShift, isBackward) {
const isRTL = $isParentElementRTL(selection);
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical-rl writing mode, arrow key directions need to be flipped
// to match the visual flow of text (top to bottom, right to left)
let adjustedIsBackward;
if (isVertical) {
// In vertical-rl mode, we need to completely invert the direction
// Left arrow (backward) should move down (forward)
// Right arrow (forward) should move up (backward)
adjustedIsBackward = !isBackward;
} else if (isRTL) {
// In horizontal RTL mode, use the standard RTL behavior
adjustedIsBackward = !isBackward;
} else {
// Standard LTR horizontal text
adjustedIsBackward = isBackward;
}
// Apply the direction adjustment to move the caret
$moveCaretSelection(selection, isHoldingShift, adjustedIsBackward, 'character');
}
/**
* Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.
* @param node - The node whose style value to get.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property.
* @returns The value of the property for node.
*/
function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
const css = node.getStyle();
const styleObject = lexical.getStyleObjectFromCSS(css);
if (styleObject !== null) {
return styleObject[styleProperty] || defaultValue;
}
return defaultValue;
}
/**
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
* If all TextNodes do not have the same value, it returns an empty string.
* @param selection - The selection of TextNodes whose value to find.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property, defaults to an empty string.
* @returns The value of the property for the selected TextNodes.
*/
function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
let styleValue = null;
const nodes = selection.getNodes();
const anchor = selection.anchor;
const focus = selection.focus;
const isBackward = selection.isBackward();
const startNode = isBackward ? focus.getNode() : anchor.getNode();
const endNode = isBackward ? anchor.getNode() : focus.getNode();
const startOffset = isBackward ? focus.offset : anchor.offset;
const endOffset = isBackward ? anchor.offset : focus.offset;
if (lexical.$isRangeSelection(selection) && selection.isCollapsed() && selection.style !== '') {
const css = selection.style;
const styleObject = lexical.getStyleObjectFromCSS(css);
if (styleObject !== null && styleProperty in styleObject) {
return styleObject[styleProperty];
}
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (i === 0 && node.is(startNode) && lexical.$isTextNode(node) && startOffset === node.getTextContentSize()) {
continue;
}
if (i !== 0 && node.is(endNode) && endOffset === 0) {
continue;
}
if (lexical.$isTextNode(node)) {
const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
if (styleValue === null) {
styleValue = nodeStyleValue;
} else if (styleValue !== nodeStyleValue) {
// multiple text nodes are in the selection and they don't all
// have the same style.
styleValue = '';
break;
}
}
}
return styleValue === null ? defaultValue : styleValue;
}
/**
* 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.
*
*/
/** @deprecated moved to the `lexical` package */
const getStyleObjectFromCSS = lexical.getStyleObjectFromCSS;
/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */
const trimTextContentFromAnchor = $trimTextContentFromAnchor;
exports.$cloneWithProperties = lexical.$cloneWithProperties;
exports.$selectAll = lexical.$selectAll;
exports.$addNodeStyle = $addNodeStyle;
exports.$copyBlockFormatIndent = $copyBlockFormatIndent;
exports.$ensureForwardRangeSelection = $ensureForwardRangeSelection;
exports.$forEachSelectedTextNode = $forEachSelectedTextNode;
exports.$getComputedStyleForElement = $getComputedStyleForElement;
exports.$getComputedStyleForParent = $getComputedStyleForParent;
exports.$getSelectionStyleValueForProperty = $getSelectionStyleValueForProperty;
exports.$isAtNodeEnd = $isAtNodeEnd;
exports.$isParentElementRTL = $isParentElementRTL;
exports.$isParentRTL = $isParentRTL;
exports.$moveCaretSelection = $moveCaretSelection;
exports.$moveCharacter = $moveCharacter;
exports.$patchStyleText = $patchStyleText;
exports.$setBlocksType = $setBlocksType;
exports.$shouldOverrideDefaultCharacterSelection = $shouldOverrideDefaultCharacterSelection;
exports.$sliceSelectedTextNodeContent = $sliceSelectedTextNodeContent;
exports.$trimTextContentFromAnchor = $trimTextContentFromAnchor;
exports.$wrapNodes = $wrapNodes;
exports.createDOMRange = createDOMRange;
exports.createRectsFromDOMRange = createRectsFromDOMRange;
exports.getCSSFromStyleObject = getCSSFromStyleObject;
exports.getStyleObjectFromCSS = getStyleObjectFromCSS;
exports.trimTextContentFromAnchor = trimTextContentFromAnchor;
/**
* 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.
*
*/
import { $isTextNode, $getEditor, $isRootNode, $getSelection, $isRangeSelection, $caretRangeFromSelection, $isTokenOrSegmented, $isElementNode, $getCharacterOffsets, $cloneWithPropertiesEphemeral, $getNodeByKey, $getPreviousSelection, $createTextNode, getStyleObjectFromCSS as getStyleObjectFromCSS$1, $findMatchingParent, INTERNAL_$isBlock, $caretFromPoint, $isExtendableTextPointCaret, $extendCaretToRange, $isChildCaret, $isDecoratorNode, $isRootOrShadowRoot, $hasAncestor, $isLeafNode, $setSelection } from 'lexical';
export { $cloneWithProperties, $selectAll } from 'lexical';
/**
* 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.
*
*/
// Do not require this module directly! Use normal `invariant` calls.
function formatDevErrorMessage(message) {
throw new Error(message);
}
/**
* 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.
*
*/
/*@__INLINE__*/
function warnOnlyOnce(message) {
{
let run = false;
return () => {
if (!run) {
console.warn(message);
}
run = 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.
*
*/
function getDOMTextNode(element) {
let node = element;
while (node != null) {
if (node.nodeType === Node.TEXT_NODE) {
return node;
}
node = node.firstChild;
}
return null;
}
function getDOMIndexWithinParent(node) {
const parent = node.parentNode;
if (parent == null) {
throw new Error('Should never happen');
}
return [parent, Array.from(parent.childNodes).indexOf(node)];
}
/**
* Creates a selection range for the DOM.
* @param editor - The lexical editor.
* @param anchorNode - The anchor node of a selection.
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
* @param focusNode - The current focus.
* @param _focusOffset - The amount of space offset from the focus to the anchor.
* @returns The range of selection for the DOM that was created.
*/
function createDOMRange(editor, anchorNode, _anchorOffset, focusNode, _focusOffset) {
const anchorKey = anchorNode.getKey();
const focusKey = focusNode.getKey();
const range = document.createRange();
let anchorDOM = editor.getElementByKey(anchorKey);
let focusDOM = editor.getElementByKey(focusKey);
let anchorOffset = _anchorOffset;
let focusOffset = _focusOffset;
if ($isTextNode(anchorNode)) {
anchorDOM = getDOMTextNode(anchorDOM);
}
if ($isTextNode(focusNode)) {
focusDOM = getDOMTextNode(focusDOM);
}
if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
return null;
}
if (anchorDOM.nodeName === 'BR') {
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
}
if (focusDOM.nodeName === 'BR') {
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
}
const firstChild = anchorDOM.firstChild;
if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
focusOffset = 1;
}
try {
range.setStart(anchorDOM, anchorOffset);
range.setEnd(focusDOM, focusOffset);
} catch (_e) {
return null;
}
if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
// Range is backwards, we need to reverse it
range.setStart(focusDOM, focusOffset);
range.setEnd(anchorDOM, anchorOffset);
}
return range;
}
/**
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
* @param editor - The lexical editor
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
* @returns The selectionRects as an array.
*/
function createRectsFromDOMRange(editor, range) {
const rootElement = editor.getRootElement();
if (rootElement === null) {
return [];
}
const rootRect = rootElement.getBoundingClientRect();
const computedStyle = getComputedStyle(rootElement);
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
const selectionRects = Array.from(range.getClientRects());
let selectionRectsLength = selectionRects.length;
//sort rects from top left to bottom right.
selectionRects.sort((a, b) => {
const top = a.top - b.top;
// Some rects match position closely, but not perfectly,
// so we give a 3px tolerance.
if (Math.abs(top) <= 3) {
return a.left - b.left;
}
return top;
});
let prevRect;
for (let i = 0; i < selectionRectsLength; i++) {
const selectionRect = selectionRects[i];
// Exclude rects that overlap preceding Rects in the sorted list.
const isOverlappingRect = prevRect && prevRect.top <= selectionRect.top && prevRect.top + prevRect.height > selectionRect.top && prevRect.left + prevRect.width > selectionRect.left;
// Exclude selections that span the entire element
const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
if (isOverlappingRect || selectionSpansElement) {
selectionRects.splice(i--, 1);
selectionRectsLength--;
continue;
}
prevRect = selectionRect;
}
return selectionRects;
}
/**
* Serializes a style object into a CSS declaration string, the inverse of
* {@link getStyleObjectFromCSS}.
* @param styles - An object mapping CSS property names to their values.
* @returns A CSS string of the form `prop: value;` for each entry, concatenated together.
*/
function getCSSFromStyleObject(styles) {
let css = '';
for (const style in styles) {
if (style) {
css += `${style}: ${styles[style]};`;
}
}
return css;
}
/**
* Gets the computed DOM styles of the element.
* @param element - The node to check the styles for.
* @returns the computed styles of the element or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForElement(element) {
const editor = $getEditor();
const domElement = editor.getElementByKey(element.getKey());
if (domElement === null) {
return null;
}
const view = domElement.ownerDocument.defaultView;
if (view === null) {
return null;
}
return view.getComputedStyle(domElement);
}
/**
* Gets the computed DOM styles of the parent of the node.
* @param node - The node to check its parent's styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyleForParent(node) {
const parent = $isRootNode(node) ? node : node.getParentOrThrow();
return $getComputedStyleForElement(parent);
}
/**
* Determines whether a node's parent is RTL.
* @param node - The node to check whether it is RTL.
* @returns whether the node is RTL.
*/
function $isParentRTL(node) {
const styles = $getComputedStyleForParent(node);
return styles !== null && styles.direction === 'rtl';
}
/**
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
* it to be generated into the new TextNode.
* @param selection - The selection containing the node whose TextNode is to be edited.
* @param textNode - The TextNode to be edited.
* @param mutates - 'clone' to return a clone before mutating, 'self' to update in-place
* @returns The updated TextNode or clone.
*/
function $sliceSelectedTextNodeContent(selection, textNode, mutates = 'self') {
const anchorAndFocus = selection.getStartEndPoints();
if (textNode.isSelected(selection) && !$isTokenOrSegmented(textNode) && anchorAndFocus !== null) {
const [anchor, focus] = anchorAndFocus;
const isBackward = selection.isBackward();
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const isAnchor = textNode.is(anchorNode);
const isFocus = textNode.is(focusNode);
if (isAnchor || isFocus) {
const [anchorOffset, focusOffset] = $getCharacterOffsets(selection);
const isSame = anchorNode.is(focusNode);
const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
let startOffset = 0;
let endOffset = undefined;
if (isSame) {
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
} else if (isFirst) {
const offset = isBackward ? focusOffset : anchorOffset;
startOffset = offset;
endOffset = undefined;
} else if (isLast) {
const offset = isBackward ? anchorOffset : focusOffset;
startOffset = 0;
endOffset = offset;
}
// NOTE: This mutates __text directly because the primary use case is to
// modify a $cloneWithProperties node that should never be added
// to the EditorState so we must not call getWritable via setTextContent
const text = textNode.__text.slice(startOffset, endOffset);
if (text !== textNode.__text) {
if (mutates === 'clone') {
textNode = $cloneWithPropertiesEphemeral(textNode);
}
textNode.__text = text;
}
}
}
return textNode;
}
/**
* Determines if the current selection is at the end of the node.
* @param point - The point of the selection to test.
* @returns true if the provided point offset is in the last possible position, false otherwise.
*/
function $isAtNodeEnd(point) {
if (point.type === 'text') {
return point.offset === point.getNode().getTextContentSize();
}
const node = point.getNode();
if (!$isElementNode(node)) {
formatDevErrorMessage(`isAtNodeEnd: node must be a TextNode or ElementNode`);
}
return point.offset === node.getChildrenSize();
}
/**
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
* @param editor - The lexical editor.
* @param anchor - The anchor of the current selection, where the selection should be pointing.
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
*/
function $trimTextContentFromAnchor(editor, anchor, delCount) {
// Work from the current selection anchor point
let currentNode = anchor.getNode();
let remaining = delCount;
if ($isElementNode(currentNode)) {
const descendantNode = currentNode.getDescendantByIndex(anchor.offset);
if (descendantNode !== null) {
currentNode = descendantNode;
}
}
while (remaining > 0 && currentNode !== null) {
if ($isElementNode(currentNode)) {
const lastDescendant = currentNode.getLastDescendant();
if (lastDescendant !== null) {
currentNode = lastDescendant;
}
}
let nextNode = currentNode.getPreviousSibling();
let additionalElementWhitespace = 0;
if (nextNode === null) {
let parent = currentNode.getParentOrThrow();
let parentSibling = parent.getPreviousSibling();
while (parentSibling === null) {
parent = parent.getParent();
if (parent === null) {
nextNode = null;
break;
}
parentSibling = parent.getPreviousSibling();
}
if (parent !== null) {
additionalElementWhitespace = parent.isInline() ? 0 : 2;
nextNode = parentSibling;
}
}
let text = currentNode.getTextContent();
// If the text is empty, we need to consider adding in two line breaks to match
// the content if we were to get it from its parent.
if (text === '' && $isElementNode(currentNode) && !currentNode.isInline()) {
// TODO: should this be handled in core?
text = '\n\n';
}
const currentNodeSize = text.length;
if (!$isTextNode(currentNode) || remaining >= currentNodeSize) {
const parent = currentNode.getParent();
currentNode.remove();
if (parent != null && parent.getChildrenSize() === 0 && !$isRootNode(parent)) {
parent.remove();
}
remaining -= currentNodeSize + additionalElementWhitespace;
currentNode = nextNode;
} else {
const key = currentNode.getKey();
// See if we can just revert it to what was in the last editor state
const prevTextContent = editor.getEditorState().read(() => {
const prevNode = $getNodeByKey(key);
if ($isTextNode(prevNode) && prevNode.isSimpleText()) {
return prevNode.getTextContent();
}
return null;
});
const offset = currentNodeSize - remaining;
const slicedText = text.slice(0, offset);
if (prevTextContent !== null && prevTextContent !== text) {
const prevSelection = $getPreviousSelection();
let target = currentNode;
if (!currentNode.isSimpleText()) {
const textNode = $createTextNode(prevTextContent);
currentNode.replace(textNode);
target = textNode;
} else {
currentNode.setTextContent(prevTextContent);
}
if ($isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
const prevOffset = prevSelection.anchor.offset;
target.select(prevOffset, prevOffset);
}
} else if (currentNode.isSimpleText()) {
// Split text
const isSelected = anchor.key === key;
let anchorOffset = anchor.offset;
// Move offset to end if it's less than the remaining number, otherwise
// we'll have a negative splitStart.
if (anchorOffset < remaining) {
anchorOffset = currentNodeSize;
}
const splitStart = isSelected ? anchorOffset - remaining : 0;
const splitEnd = isSelected ? anchorOffset : offset;
if (isSelected && splitStart === 0) {
const [excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
} else {
const [, excessNode] = currentNode.splitText(splitStart, splitEnd);
excessNode.remove();
}
} else {
const textNode = $createTextNode(slicedText);
currentNode.replace(textNode);
}
remaining = 0;
}
}
}
/**
* @deprecated node styles are parsed on demand and not cached eternally
*/
const $addNodeStyle = warnOnlyOnce('$addNodeStyle is a deprecated no-op and calls should be removed');
/**
* Applies the provided styles to the given TextNode, ElementNode, or
* collapsed RangeSelection.
*
* @param target - The TextNode, ElementNode, or collapsed RangeSelection to apply the styles to
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyle(target, patch) {
if (!($isRangeSelection(target) ? target.isCollapsed() : $isTextNode(target) || $isElementNode(target))) {
formatDevErrorMessage(`$patchStyle must only be called with a TextNode, ElementNode, or collapsed RangeSelection`);
}
const prevStyles = getStyleObjectFromCSS$1($isRangeSelection(target) ? target.style : $isTextNode(target) ? target.getStyle() : target.getTextStyle());
const newStyles = Object.entries(patch).reduce((styles, [key, value]) => {
if (typeof value === 'function') {
styles[key] = value(prevStyles[key], target);
} else if (value === null) {
delete styles[key];
} else {
styles[key] = value;
}
return styles;
}, {
...prevStyles
});
const newCSSText = getCSSFromStyleObject(newStyles);
if ($isRangeSelection(target) || $isTextNode(target)) {
target.setStyle(newCSSText);
} else {
target.setTextStyle(newCSSText);
}
}
/**
* Applies the provided styles to the TextNodes in the provided Selection.
* Will update partially selected TextNodes by splitting the TextNode and applying
* the styles to the appropriate one.
* @param selection - The selected node(s) to update.
* @param patch - The patch to apply, which can include multiple styles. \\{CSSProperty: value\\} . Can also accept a function that returns the new property value.
*/
function $patchStyleText(selection, patch) {
if ($isRangeSelection(selection) && selection.isCollapsed()) {
$patchStyle(selection, patch);
const emptyNode = selection.anchor.getNode();
if ($isElementNode(emptyNode) && emptyNode.isEmpty()) {
$patchStyle(emptyNode, patch);
}
}
$forEachSelectedTextNode(textNode => {
$patchStyle(textNode, patch);
});
const nodes = selection.getNodes();
if (nodes.length > 0) {
const patchedElementKeys = new Set();
for (const node of nodes) {
if (!$isElementNode(node) || !node.canBeEmpty() || node.getChildrenSize() !== 0) {
continue;
}
const key = node.getKey();
if (patchedElementKeys.has(key)) {
continue;
}
patchedElementKeys.add(key);
$patchStyle(node, patch);
}
}
}
function $forEachSelectedTextNode(fn) {
const selection = $getSelection();
if (!selection) {
return;
}
const slicedTextNodes = new Map();
const getSliceIndices = node => slicedTextNodes.get(node.getKey()) || [0, node.getTextContentSize()];
if ($isRangeSelection(selection)) {
for (const slice of $caretRangeFromSelection(selection).getTextSlices()) {
if (slice) {
slicedTextNodes.set(slice.caret.origin.getKey(), slice.getSliceIndices());
}
}
}
const selectedNodes = selection.getNodes();
for (const selectedNode of selectedNodes) {
if (!($isTextNode(selectedNode) && selectedNode.canHaveFormat())) {
continue;
}
const [startOffset, endOffset] = getSliceIndices(selectedNode);
// No actual text is selected, so do nothing.
if (endOffset === startOffset) {
continue;
}
// The entire node is selected or a token/segment, so just format it
if ($isTokenOrSegmented(selectedNode) || startOffset === 0 && endOffset === selectedNode.getTextContentSize()) {
fn(selectedNode);
} else {
// The node is partially selected, so split it into two or three nodes
// and style the selected one.
const splitNodes = selectedNode.splitText(startOffset, endOffset);
const replacement = splitNodes[startOffset === 0 ? 0 : 1];
fn(replacement);
}
}
// Prior to NodeCaret #7046 this would have been a side-effect
// so we do this for test compatibility.
// TODO: we may want to consider simplifying by removing this
if ($isRangeSelection(selection) && selection.anchor.type === 'text' && selection.focus.type === 'text' && selection.anchor.key === selection.focus.key) {
$ensureForwardRangeSelection(selection);
}
}
/**
* Ensure that the given RangeSelection is not backwards. If it
* is backwards, then the anchor and focus points will be swapped
* in-place. Ensuring that the selection is a writable RangeSelection
* is the responsibility of the caller (e.g. in a read-only context
* you will want to clone $getSelection() before using this).
*
* @param selection a writable RangeSelection
*/
function $ensureForwardRangeSelection(selection) {
if (selection.isBackward()) {
const {
anchor,
focus
} = selection;
// stash for the in-place swap
const {
key,
offset,
type
} = anchor;
anchor.set(focus.key, focus.offset, focus.type);
focus.set(key, offset, type);
}
}
function $copyBlockFormatIndent(srcNode, destNode) {
const format = srcNode.getFormatType();
const indent = srcNode.getIndent();
if (format !== destNode.getFormatType()) {
destNode.setFormat(format);
}
if (indent !== destNode.getIndent()) {
destNode.setIndent(indent);
}
}
/**
* Converts all nodes in the selection that are of one block type to another.
* @param selection - The selected blocks to be converted.
* @param $createElement - The function that creates the node. eg. $createParagraphNode.
* @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
*/
function $setBlocksType(selection, $createElement, $afterCreateElement = $copyBlockFormatIndent) {
if (selection === null) {
return;
}
// Selections tend to not include their containing blocks so we effectively
// expand it here
const anchorAndFocus = selection.getStartEndPoints();
const blockMap = new Map();
if (anchorAndFocus) {
const [anchor, focus] = anchorAndFocus;
const anchorBlock = $findMatchingParent(anchor.getNode(), INTERNAL_$isBlock);
const focusBlock = $findMatchingParent(focus.getNode(), INTERNAL_$isBlock);
if ($isElementNode(anchorBlock)) {
blockMap.set(anchorBlock.getKey(), anchorBlock);
}
if ($isElementNode(focusBlock)) {
blockMap.set(focusBlock.getKey(), focusBlock);
}
}
for (const node of selection.getNodes()) {
if ($isElementNode(node) && INTERNAL_$isBlock(node)) {
blockMap.set(node.getKey(), node);
} else if (anchorAndFocus === null) {
const ancestorBlock = $findMatchingParent(node, INTERNAL_$isBlock);
if ($isElementNode(ancestorBlock)) {
blockMap.set(ancestorBlock.getKey(), ancestorBlock);
}
}
}
// Selection remapping is delegated to LexicalNode.replace (and the
// ListItemNode.replace override): both remap an element-anchored point
// on the replaced block to {key: replacement, offset: prevSize + offset}.
for (const [, prevNode] of blockMap) {
const element = $createElement();
$afterCreateElement(prevNode, element);
prevNode.replace(element, true);
}
}
function isPointAttached(point) {
return point.getNode().isAttached();
}
function $removeParentEmptyElements(startingNode) {
let node = startingNode;
while (node !== null && !$isRootOrShadowRoot(node)) {
const latest = node.getLatest();
const parentNode = node.getParent();
if (latest.getChildrenSize() === 0) {
node.remove(true);
}
node = parentNode;
}
}
/**
* @deprecated In favor of $setBlockTypes
* Wraps all nodes in the selection into another node of the type returned by createElement.
* @param selection - The selection of nodes to be wrapped.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to append the wrapped selection and its children to.
*/
function $wrapNodes(selection, createElement, wrappingElement = null) {
const anchorAndFocus = selection.getStartEndPoints();
const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
const nodes = selection.getNodes();
const nodesLength = nodes.length;
if (anchor !== null && (nodesLength === 0 || nodesLength === 1 && anchor.type === 'element' && anchor.getNode().getChildrenSize() === 0)) {
const target = anchor.type === 'text' ? anchor.getNode().getParentOrThrow() : anchor.getNode();
const children = target.getChildren();
let element = createElement();
element.setFormat(target.getFormatType());
element.setIndent(target.getIndent());
children.forEach(child => element.append(child));
if (wrappingElement) {
element = wrappingElement.append(element);
}
target.replace(element);
return;
}
let topLevelNode = null;
let descendants = [];
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
// Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
// user selected multiple Root-like nodes that have to be treated separately as if they are
// their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
// of each of the cell nodes.
if ($isRootOrShadowRoot(node)) {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [];
topLevelNode = node;
} else if (topLevelNode === null || topLevelNode !== null && $hasAncestor(node, topLevelNode)) {
descendants.push(node);
} else {
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
descendants = [node];
}
}
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
}
/**
* Wraps each node into a new ElementNode.
* @param selection - The selection of nodes to wrap.
* @param nodes - An array of nodes, generally the descendants of the selection.
* @param nodesLength - The length of nodes.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to wrap all the nodes into.
* @returns
*/
function $wrapNodesImpl(selection, nodes, nodesLength, createElement, wrappingElement = null) {
if (nodes.length === 0) {
return;
}
const firstNode = nodes[0];
const elementMapping = new Map();
const elements = [];
// The below logic is to find the right target for us to
// either insertAfter/insertBefore/append the corresponding
// elements to. This is made more complicated due to nested
// structures.
let target = $isElementNode(firstNode) ? firstNode : firstNode.getParentOrThrow();
if (target.isInline()) {
target = target.getParentOrThrow();
}
let targetIsPrevSibling = false;
while (target !== null) {
const prevSibling = target.getPreviousSibling();
if (prevSibling !== null) {
target = prevSibling;
targetIsPrevSibling = true;
break;
}
target = target.getParentOrThrow();
if ($isRootOrShadowRoot(target)) {
break;
}
}
const emptyElements = new Set();
// Find any top level empty elements
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
if ($isElementNode(node) && node.getChildrenSize() === 0) {
emptyElements.add(node.getKey());
}
}
const movedNodes = new Set();
// Move out all leaf nodes into our elements array.
// If we find a top level empty element, also move make
// an element for that.
for (let i = 0; i < nodesLength; i++) {
const node = nodes[i];
let parent = node.getParent();
if (parent !== null && parent.isInline()) {
parent = parent.getParent();
}
if (parent !== null && $isLeafNode(node) && !movedNodes.has(node.getKey())) {
const parentKey = parent.getKey();
if (elementMapping.get(parentKey) === undefined) {
const targetElement = createElement();
targetElement.setFormat(parent.getFormatType());
targetElement.setIndent(parent.getIndent());
elements.push(targetElement);
elementMapping.set(parentKey, targetElement);
// Move node and its siblings to the new
// element.
const children = parent.getChildren();
targetElement.splice(targetElement.getChildrenSize(), 0, children);
for (const child of children) {
movedNodes.add(child.getKey());
if ($isElementNode(child)) {
// Skip nested leaf nodes if the parent has already been moved
for (const key of child.getChildrenKeys()) {
movedNodes.add(key);
}
}
}
$removeParentEmptyElements(parent);
}
} else if (emptyElements.has(node.getKey())) {
if (!$isElementNode(node)) {
formatDevErrorMessage(`Expected node in emptyElements to be an ElementNode`);
}
const targetElement = createElement();
targetElement.setFormat(node.getFormatType());
targetElement.setIndent(node.getIndent());
elements.push(targetElement);
node.remove(true);
}
}
if (wrappingElement !== null) {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
wrappingElement.append(element);
}
}
let lastElement = null;
// If our target is Root-like, let's see if we can re-adjust
// so that the target is the first child instead.
if ($isRootOrShadowRoot(target)) {
if (targetIsPrevSibling) {
if (wrappingElement !== null) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
}
}
} else {
const firstChild = target.getFirstChild();
if ($isElementNode(firstChild)) {
target = firstChild;
}
if (firstChild === null) {
if (wrappingElement) {
target.append(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
target.append(element);
lastElement = element;
}
}
} else {
if (wrappingElement !== null) {
firstChild.insertBefore(wrappingElement);
} else {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
firstChild.insertBefore(element);
lastElement = element;
}
}
}
}
} else {
if (wrappingElement) {
target.insertAfter(wrappingElement);
} else {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
target.insertAfter(element);
lastElement = element;
}
}
}
const prevSelection = $getPreviousSelection();
if ($isRangeSelection(prevSelection) && isPointAttached(prevSelection.anchor) && isPointAttached(prevSelection.focus)) {
$setSelection(prevSelection.clone());
} else if (lastElement !== null) {
lastElement.selectEnd();
} else {
selection.dirty = true;
}
}
/**
* Tests if the selection's parent element has vertical writing mode.
* @param selection - The selection whose parent to test.
* @returns true if the selection's parent has vertical writing mode (writing-mode: vertical-rl), false otherwise.
*/
function $isEditorVerticalOrientation(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.writingMode === 'vertical-rl';
}
/**
* Gets the computed DOM styles of the parent of the selection's anchor node.
* @param selection - The selection to check the styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
function $getComputedStyle(selection) {
const anchorNode = selection.anchor.getNode();
if ($isElementNode(anchorNode)) {
return $getComputedStyleForElement(anchorNode);
}
return $getComputedStyleForParent(anchorNode);
}
/**
* Determines if the default character selection should be overridden. Used with DecoratorNodes
* @param selection - The selection whose default character selection may need to be overridden.
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
* @returns true if it should be overridden, false if not.
*/
function $shouldOverrideDefaultCharacterSelection(selection, isBackward) {
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical writing mode, we adjust the direction for correct caret movement
let adjustedIsBackward = isVertical ? !isBackward : isBackward;
// In right-to-left writing mode, we invert the direction for correct caret movement
if ($isParentElementRTL(selection)) {
adjustedIsBackward = !adjustedIsBackward;
}
const focusCaret = $caretFromPoint(selection.focus, adjustedIsBackward ? 'previous' : 'next');
if ($isExtendableTextPointCaret(focusCaret)) {
return false;
}
for (const nextCaret of $extendCaretToRange(focusCaret)) {
if ($isChildCaret(nextCaret)) {
return !nextCaret.origin.isInline();
} else if ($isElementNode(nextCaret.origin)) {
continue;
} else if ($isDecoratorNode(nextCaret.origin)) {
return true;
}
break;
}
return false;
}
/**
* Moves the selection according to the arguments.
* @param selection - The selected text or nodes.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
* @param granularity - The distance to adjust the current selection.
*/
function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
}
/**
* Tests a parent element for right to left direction.
* @param selection - The selection whose parent is to be tested.
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
*/
function $isParentElementRTL(selection) {
const computedStyle = $getComputedStyle(selection);
return computedStyle !== null && computedStyle.direction === 'rtl';
}
/**
* Moves selection by character according to arguments.
* @param selection - The selection of the characters to move.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
*/
function $moveCharacter(selection, isHoldingShift, isBackward) {
const isRTL = $isParentElementRTL(selection);
const isVertical = $isEditorVerticalOrientation(selection);
// In vertical-rl writing mode, arrow key directions need to be flipped
// to match the visual flow of text (top to bottom, right to left)
let adjustedIsBackward;
if (isVertical) {
// In vertical-rl mode, we need to completely invert the direction
// Left arrow (backward) should move down (forward)
// Right arrow (forward) should move up (backward)
adjustedIsBackward = !isBackward;
} else if (isRTL) {
// In horizontal RTL mode, use the standard RTL behavior
adjustedIsBackward = !isBackward;
} else {
// Standard LTR horizontal text
adjustedIsBackward = isBackward;
}
// Apply the direction adjustment to move the caret
$moveCaretSelection(selection, isHoldingShift, adjustedIsBackward, 'character');
}
/**
* Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.
* @param node - The node whose style value to get.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property.
* @returns The value of the property for node.
*/
function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
const css = node.getStyle();
const styleObject = getStyleObjectFromCSS$1(css);
if (styleObject !== null) {
return styleObject[styleProperty] || defaultValue;
}
return defaultValue;
}
/**
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
* If all TextNodes do not have the same value, it returns an empty string.
* @param selection - The selection of TextNodes whose value to find.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property, defaults to an empty string.
* @returns The value of the property for the selected TextNodes.
*/
function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
let styleValue = null;
const nodes = selection.getNodes();
const anchor = selection.anchor;
const focus = selection.focus;
const isBackward = selection.isBackward();
const startNode = isBackward ? focus.getNode() : anchor.getNode();
const endNode = isBackward ? anchor.getNode() : focus.getNode();
const startOffset = isBackward ? focus.offset : anchor.offset;
const endOffset = isBackward ? anchor.offset : focus.offset;
if ($isRangeSelection(selection) && selection.isCollapsed() && selection.style !== '') {
const css = selection.style;
const styleObject = getStyleObjectFromCSS$1(css);
if (styleObject !== null && styleProperty in styleObject) {
return styleObject[styleProperty];
}
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (i === 0 && node.is(startNode) && $isTextNode(node) && startOffset === node.getTextContentSize()) {
continue;
}
if (i !== 0 && node.is(endNode) && endOffset === 0) {
continue;
}
if ($isTextNode(node)) {
const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
if (styleValue === null) {
styleValue = nodeStyleValue;
} else if (styleValue !== nodeStyleValue) {
// multiple text nodes are in the selection and they don't all
// have the same style.
styleValue = '';
break;
}
}
}
return styleValue === null ? defaultValue : styleValue;
}
/**
* 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.
*
*/
/** @deprecated moved to the `lexical` package */
const getStyleObjectFromCSS = getStyleObjectFromCSS$1;
/** @deprecated renamed to {@link $trimTextContentFromAnchor} by @lexical/eslint-plugin rules-of-lexical */
const trimTextContentFromAnchor = $trimTextContentFromAnchor;
export { $addNodeStyle, $copyBlockFormatIndent, $ensureForwardRangeSelection, $forEachSelectedTextNode, $getComputedStyleForElement, $getComputedStyleForParent, $getSelectionStyleValueForProperty, $isAtNodeEnd, $isParentElementRTL, $isParentRTL, $moveCaretSelection, $moveCharacter, $patchStyleText, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $sliceSelectedTextNodeContent, $trimTextContentFromAnchor, $wrapNodes, createDOMRange, createRectsFromDOMRange, getCSSFromStyleObject, getStyleObjectFromCSS, trimTextContentFromAnchor };
/**
* 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.
*
*/
'use strict'
const LexicalSelection = process.env.NODE_ENV !== 'production' ? require('./LexicalSelection.dev.js') : require('./LexicalSelection.prod.js');
module.exports = LexicalSelection;

Sorry, the diff of this file is not supported yet

/**
* 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.
*
*/
import * as modDev from './LexicalSelection.dev.mjs';
import * as modProd from './LexicalSelection.prod.mjs';
const mod = process.env.NODE_ENV !== 'production' ? modDev : modProd;
export const $addNodeStyle = mod.$addNodeStyle;
export const $cloneWithProperties = mod.$cloneWithProperties;
export const $copyBlockFormatIndent = mod.$copyBlockFormatIndent;
export const $ensureForwardRangeSelection = mod.$ensureForwardRangeSelection;
export const $forEachSelectedTextNode = mod.$forEachSelectedTextNode;
export const $getComputedStyleForElement = mod.$getComputedStyleForElement;
export const $getComputedStyleForParent = mod.$getComputedStyleForParent;
export const $getSelectionStyleValueForProperty = mod.$getSelectionStyleValueForProperty;
export const $isAtNodeEnd = mod.$isAtNodeEnd;
export const $isParentElementRTL = mod.$isParentElementRTL;
export const $isParentRTL = mod.$isParentRTL;
export const $moveCaretSelection = mod.$moveCaretSelection;
export const $moveCharacter = mod.$moveCharacter;
export const $patchStyleText = mod.$patchStyleText;
export const $selectAll = mod.$selectAll;
export const $setBlocksType = mod.$setBlocksType;
export const $shouldOverrideDefaultCharacterSelection = mod.$shouldOverrideDefaultCharacterSelection;
export const $sliceSelectedTextNodeContent = mod.$sliceSelectedTextNodeContent;
export const $trimTextContentFromAnchor = mod.$trimTextContentFromAnchor;
export const $wrapNodes = mod.$wrapNodes;
export const createDOMRange = mod.createDOMRange;
export const createRectsFromDOMRange = mod.createRectsFromDOMRange;
export const getCSSFromStyleObject = mod.getCSSFromStyleObject;
export const getStyleObjectFromCSS = mod.getStyleObjectFromCSS;
export const trimTextContentFromAnchor = mod.trimTextContentFromAnchor;
/**
* 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 mod = await (process.env.NODE_ENV !== 'production' ? import('./LexicalSelection.dev.mjs') : import('./LexicalSelection.prod.mjs'));
export const $addNodeStyle = mod.$addNodeStyle;
export const $cloneWithProperties = mod.$cloneWithProperties;
export const $copyBlockFormatIndent = mod.$copyBlockFormatIndent;
export const $ensureForwardRangeSelection = mod.$ensureForwardRangeSelection;
export const $forEachSelectedTextNode = mod.$forEachSelectedTextNode;
export const $getComputedStyleForElement = mod.$getComputedStyleForElement;
export const $getComputedStyleForParent = mod.$getComputedStyleForParent;
export const $getSelectionStyleValueForProperty = mod.$getSelectionStyleValueForProperty;
export const $isAtNodeEnd = mod.$isAtNodeEnd;
export const $isParentElementRTL = mod.$isParentElementRTL;
export const $isParentRTL = mod.$isParentRTL;
export const $moveCaretSelection = mod.$moveCaretSelection;
export const $moveCharacter = mod.$moveCharacter;
export const $patchStyleText = mod.$patchStyleText;
export const $selectAll = mod.$selectAll;
export const $setBlocksType = mod.$setBlocksType;
export const $shouldOverrideDefaultCharacterSelection = mod.$shouldOverrideDefaultCharacterSelection;
export const $sliceSelectedTextNodeContent = mod.$sliceSelectedTextNodeContent;
export const $trimTextContentFromAnchor = mod.$trimTextContentFromAnchor;
export const $wrapNodes = mod.$wrapNodes;
export const createDOMRange = mod.createDOMRange;
export const createRectsFromDOMRange = mod.createRectsFromDOMRange;
export const getCSSFromStyleObject = mod.getCSSFromStyleObject;
export const getStyleObjectFromCSS = mod.getStyleObjectFromCSS;
export const trimTextContentFromAnchor = mod.trimTextContentFromAnchor;
/**
* 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.
*
*/
"use strict";var e=require("lexical");function t(e,...t){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",e);for(const e of t)o.append("v",e);throw n.search=o.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}function n(e){let t=e;for(;null!=t;){if(t.nodeType===Node.TEXT_NODE)return t;t=t.firstChild}return null}function o(e){const t=e.parentNode;if(null==t)throw new Error("Should never happen");return[t,Array.from(t.childNodes).indexOf(e)]}function r(e){let t="";for(const n in e)n&&(t+=`${n}: ${e[n]};`);return t}function s(t){const n=e.$getEditor().getElementByKey(t.getKey());if(null===n)return null;const o=n.ownerDocument.defaultView;return null===o?null:o.getComputedStyle(n)}function l(t){return s(e.$isRootNode(t)?t:t.getParentOrThrow())}function i(t,n,o){let r=n.getNode(),s=o;if(e.$isElementNode(r)){const e=r.getDescendantByIndex(n.offset);null!==e&&(r=e)}for(;s>0&&null!==r;){if(e.$isElementNode(r)){const e=r.getLastDescendant();null!==e&&(r=e)}let o=r.getPreviousSibling(),l=0;if(null===o){let e=r.getParentOrThrow(),t=e.getPreviousSibling();for(;null===t;){if(e=e.getParent(),null===e){o=null;break}t=e.getPreviousSibling()}null!==e&&(l=e.isInline()?0:2,o=t)}let i=r.getTextContent();""===i&&e.$isElementNode(r)&&!r.isInline()&&(i="\n\n");const c=i.length;if(!e.$isTextNode(r)||s>=c){const t=r.getParent();r.remove(),null==t||0!==t.getChildrenSize()||e.$isRootNode(t)||t.remove(),s-=c+l,r=o}else{const o=r.getKey(),l=t.getEditorState().read(()=>{const t=e.$getNodeByKey(o);return e.$isTextNode(t)&&t.isSimpleText()?t.getTextContent():null}),d=c-s,f=i.slice(0,d);if(null!==l&&l!==i){const t=e.$getPreviousSelection();let n=r;if(r.isSimpleText())r.setTextContent(l);else{const t=e.$createTextNode(l);r.replace(t),n=t}if(e.$isRangeSelection(t)&&t.isCollapsed()){const e=t.anchor.offset;n.select(e,e)}}else if(r.isSimpleText()){const e=n.key===o;let t=n.offset;t<s&&(t=c);const l=e?t-s:0,i=e?t:d;if(e&&0===l){const[e]=r.splitText(l,i);e.remove()}else{const[,e]=r.splitText(l,i);e.remove()}}else{const t=e.$createTextNode(f);r.replace(t)}s=0}}}const c=()=>{};function d(n,o){(e.$isRangeSelection(n)?n.isCollapsed():e.$isTextNode(n)||e.$isElementNode(n))||t(280);const s=e.getStyleObjectFromCSS(e.$isRangeSelection(n)?n.style:e.$isTextNode(n)?n.getStyle():n.getTextStyle()),l=r(Object.entries(o).reduce((e,[t,o])=>("function"==typeof o?e[t]=o(s[t],n):null===o?delete e[t]:e[t]=o,e),{...s}));e.$isRangeSelection(n)||e.$isTextNode(n)?n.setStyle(l):n.setTextStyle(l)}function f(t){const n=e.$getSelection();if(!n)return;const o=new Map,r=e=>o.get(e.getKey())||[0,e.getTextContentSize()];if(e.$isRangeSelection(n))for(const t of e.$caretRangeFromSelection(n).getTextSlices())t&&o.set(t.caret.origin.getKey(),t.getSliceIndices());const s=n.getNodes();for(const n of s){if(!e.$isTextNode(n)||!n.canHaveFormat())continue;const[o,s]=r(n);if(s!==o)if(e.$isTokenOrSegmented(n)||0===o&&s===n.getTextContentSize())t(n);else{t(n.splitText(o,s)[0===o?0:1])}}e.$isRangeSelection(n)&&"text"===n.anchor.type&&"text"===n.focus.type&&n.anchor.key===n.focus.key&&a(n)}function a(e){if(e.isBackward()){const{anchor:t,focus:n}=e,{key:o,offset:r,type:s}=t;t.set(n.key,n.offset,n.type),n.set(o,r,s)}}function g(e,t){const n=e.getFormatType(),o=e.getIndent();n!==t.getFormatType()&&t.setFormat(n),o!==t.getIndent()&&t.setIndent(o)}function u(e){return e.getNode().isAttached()}function p(t){let n=t;for(;null!==n&&!e.$isRootOrShadowRoot(n);){const e=n.getLatest(),t=n.getParent();0===e.getChildrenSize()&&n.remove(!0),n=t}}function $(n,o,r,s,l=null){if(0===o.length)return;const i=o[0],c=new Map,d=[];let f=e.$isElementNode(i)?i:i.getParentOrThrow();f.isInline()&&(f=f.getParentOrThrow());let a=!1;for(;null!==f;){const t=f.getPreviousSibling();if(null!==t){f=t,a=!0;break}if(f=f.getParentOrThrow(),e.$isRootOrShadowRoot(f))break}const g=new Set;for(let t=0;t<r;t++){const n=o[t];e.$isElementNode(n)&&0===n.getChildrenSize()&&g.add(n.getKey())}const $=new Set;for(let n=0;n<r;n++){const r=o[n];let l=r.getParent();if(null!==l&&l.isInline()&&(l=l.getParent()),null!==l&&e.$isLeafNode(r)&&!$.has(r.getKey())){const t=l.getKey();if(void 0===c.get(t)){const n=s();n.setFormat(l.getFormatType()),n.setIndent(l.getIndent()),d.push(n),c.set(t,n);const o=l.getChildren();n.splice(n.getChildrenSize(),0,o);for(const t of o)if($.add(t.getKey()),e.$isElementNode(t))for(const e of t.getChildrenKeys())$.add(e);p(l)}}else if(g.has(r.getKey())){e.$isElementNode(r)||t(179);const n=s();n.setFormat(r.getFormatType()),n.setIndent(r.getIndent()),d.push(n),r.remove(!0)}}if(null!==l)for(let e=0;e<d.length;e++){const t=d[e];l.append(t)}let h=null;if(e.$isRootOrShadowRoot(f))if(a)if(null!==l)f.insertAfter(l);else for(let e=d.length-1;e>=0;e--){const t=d[e];f.insertAfter(t)}else{const t=f.getFirstChild();if(e.$isElementNode(t)&&(f=t),null===t)if(l)f.append(l);else for(let e=0;e<d.length;e++){const t=d[e];f.append(t),h=t}else if(null!==l)t.insertBefore(l);else for(let e=0;e<d.length;e++){const n=d[e];t.insertBefore(n),h=n}}else if(l)f.insertAfter(l);else for(let e=d.length-1;e>=0;e--){const t=d[e];f.insertAfter(t),h=t}const S=e.$getPreviousSelection();e.$isRangeSelection(S)&&u(S.anchor)&&u(S.focus)?e.$setSelection(S.clone()):null!==h?h.selectEnd():n.dirty=!0}function h(e){const t=S(e);return null!==t&&"vertical-rl"===t.writingMode}function S(t){const n=t.anchor.getNode();return e.$isElementNode(n)?s(n):l(n)}function m(e,t,n,o){e.modify(t?"extend":"move",n,o)}function N(e){const t=S(e);return null!==t&&"rtl"===t.direction}function y(t,n,o){const r=t.getStyle(),s=e.getStyleObjectFromCSS(r);return null!==s&&s[n]||o}const x=e.getStyleObjectFromCSS,T=i;exports.$cloneWithProperties=e.$cloneWithProperties,exports.$selectAll=e.$selectAll,exports.$addNodeStyle=c,exports.$copyBlockFormatIndent=g,exports.$ensureForwardRangeSelection=a,exports.$forEachSelectedTextNode=f,exports.$getComputedStyleForElement=s,exports.$getComputedStyleForParent=l,exports.$getSelectionStyleValueForProperty=function(t,n,o=""){let r=null;const s=t.getNodes(),l=t.anchor,i=t.focus,c=t.isBackward(),d=c?i.getNode():l.getNode(),f=c?l.getNode():i.getNode(),a=c?i.offset:l.offset,g=c?l.offset:i.offset;if(e.$isRangeSelection(t)&&t.isCollapsed()&&""!==t.style){const o=t.style,r=e.getStyleObjectFromCSS(o);if(null!==r&&n in r)return r[n]}for(let t=0;t<s.length;t++){const l=s[t];if((0!==t||!l.is(d)||!e.$isTextNode(l)||a!==l.getTextContentSize())&&((0===t||!l.is(f)||0!==g)&&e.$isTextNode(l))){const e=y(l,n,o);if(null===r)r=e;else if(r!==e){r="";break}}}return null===r?o:r},exports.$isAtNodeEnd=function(n){if("text"===n.type)return n.offset===n.getNode().getTextContentSize();const o=n.getNode();return e.$isElementNode(o)||t(177),n.offset===o.getChildrenSize()},exports.$isParentElementRTL=N,exports.$isParentRTL=function(e){const t=l(e);return null!==t&&"rtl"===t.direction},exports.$moveCaretSelection=m,exports.$moveCharacter=function(e,t,n){const o=N(e);let r;r=h(e)||o?!n:n,m(e,t,r,"character")},exports.$patchStyleText=function(t,n){if(e.$isRangeSelection(t)&&t.isCollapsed()){d(t,n);const o=t.anchor.getNode();e.$isElementNode(o)&&o.isEmpty()&&d(o,n)}f(e=>{d(e,n)});const o=t.getNodes();if(o.length>0){const t=new Set;for(const r of o){if(!e.$isElementNode(r)||!r.canBeEmpty()||0!==r.getChildrenSize())continue;const o=r.getKey();t.has(o)||(t.add(o),d(r,n))}}},exports.$setBlocksType=function(t,n,o=g){if(null===t)return;const r=t.getStartEndPoints(),s=new Map;if(r){const[t,n]=r,o=e.$findMatchingParent(t.getNode(),e.INTERNAL_$isBlock),l=e.$findMatchingParent(n.getNode(),e.INTERNAL_$isBlock);e.$isElementNode(o)&&s.set(o.getKey(),o),e.$isElementNode(l)&&s.set(l.getKey(),l)}for(const n of t.getNodes())if(e.$isElementNode(n)&&e.INTERNAL_$isBlock(n))s.set(n.getKey(),n);else if(null===r){const t=e.$findMatchingParent(n,e.INTERNAL_$isBlock);e.$isElementNode(t)&&s.set(t.getKey(),t)}for(const[,e]of s){const t=n();o(e,t),e.replace(t,!0)}},exports.$shouldOverrideDefaultCharacterSelection=function(t,n){let o=h(t)?!n:n;N(t)&&(o=!o);const r=e.$caretFromPoint(t.focus,o?"previous":"next");if(e.$isExtendableTextPointCaret(r))return!1;for(const t of e.$extendCaretToRange(r)){if(e.$isChildCaret(t))return!t.origin.isInline();if(!e.$isElementNode(t.origin)){if(e.$isDecoratorNode(t.origin))return!0;break}}return!1},exports.$sliceSelectedTextNodeContent=function(t,n,o="self"){const r=t.getStartEndPoints();if(n.isSelected(t)&&!e.$isTokenOrSegmented(n)&&null!==r){const[s,l]=r,i=t.isBackward(),c=s.getNode(),d=l.getNode(),f=n.is(c),a=n.is(d);if(f||a){const[r,s]=e.$getCharacterOffsets(t),l=c.is(d),f=n.is(i?d:c),a=n.is(i?c:d);let g,u=0;if(l)u=r>s?s:r,g=r>s?r:s;else if(f){u=i?s:r,g=void 0}else if(a){u=0,g=i?r:s}const p=n.__text.slice(u,g);p!==n.__text&&("clone"===o&&(n=e.$cloneWithPropertiesEphemeral(n)),n.__text=p)}}return n},exports.$trimTextContentFromAnchor=i,exports.$wrapNodes=function(t,n,o=null){const r=t.getStartEndPoints(),s=r?r[0]:null,l=t.getNodes(),i=l.length;if(null!==s&&(0===i||1===i&&"element"===s.type&&0===s.getNode().getChildrenSize())){const e="text"===s.type?s.getNode().getParentOrThrow():s.getNode(),t=e.getChildren();let r=n();return r.setFormat(e.getFormatType()),r.setIndent(e.getIndent()),t.forEach(e=>r.append(e)),o&&(r=o.append(r)),void e.replace(r)}let c=null,d=[];for(let r=0;r<i;r++){const s=l[r];e.$isRootOrShadowRoot(s)?($(t,d,d.length,n,o),d=[],c=s):null===c||null!==c&&e.$hasAncestor(s,c)?d.push(s):($(t,d,d.length,n,o),d=[s])}$(t,d,d.length,n,o)},exports.createDOMRange=function(t,r,s,l,i){const c=r.getKey(),d=l.getKey(),f=document.createRange();let a=t.getElementByKey(c),g=t.getElementByKey(d),u=s,p=i;if(e.$isTextNode(r)&&(a=n(a)),e.$isTextNode(l)&&(g=n(g)),void 0===r||void 0===l||null===a||null===g)return null;"BR"===a.nodeName&&([a,u]=o(a)),"BR"===g.nodeName&&([g,p]=o(g));const $=a.firstChild;a===g&&null!=$&&"BR"===$.nodeName&&0===u&&0===p&&(p=1);try{f.setStart(a,u),f.setEnd(g,p)}catch(e){return null}return!f.collapsed||u===p&&c===d||(f.setStart(g,p),f.setEnd(a,u)),f},exports.createRectsFromDOMRange=function(e,t){const n=e.getRootElement();if(null===n)return[];const o=n.getBoundingClientRect(),r=getComputedStyle(n),s=parseFloat(r.paddingLeft)+parseFloat(r.paddingRight),l=Array.from(t.getClientRects());let i,c=l.length;l.sort((e,t)=>{const n=e.top-t.top;return Math.abs(n)<=3?e.left-t.left:n});for(let e=0;e<c;e++){const t=l[e],n=i&&i.top<=t.top&&i.top+i.height>t.top&&i.left+i.width>t.left,r=t.width+s===o.width;n||r?(l.splice(e--,1),c--):i=t}return l},exports.getCSSFromStyleObject=r,exports.getStyleObjectFromCSS=x,exports.trimTextContentFromAnchor=T;
/**
* 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.
*
*/
import{$isTextNode as e,$getEditor as t,$isRootNode as n,$getSelection as o,$isRangeSelection as l,$caretRangeFromSelection as r,$isTokenOrSegmented as i,$isElementNode as s,$getCharacterOffsets as c,$cloneWithPropertiesEphemeral as f,$getNodeByKey as u,$getPreviousSelection as g,$createTextNode as d,getStyleObjectFromCSS as a,$findMatchingParent as p,INTERNAL_$isBlock as h,$caretFromPoint as y,$isExtendableTextPointCaret as m,$extendCaretToRange as S,$isChildCaret as x,$isDecoratorNode as T,$isRootOrShadowRoot as C,$hasAncestor as N,$isLeafNode as w,$setSelection as v}from"lexical";export{$cloneWithProperties,$selectAll}from"lexical";function K(e,...t){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",e);for(const e of t)o.append("v",e);throw n.search=o.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}function P(e){let t=e;for(;null!=t;){if(t.nodeType===Node.TEXT_NODE)return t;t=t.firstChild}return null}function E(e){const t=e.parentNode;if(null==t)throw new Error("Should never happen");return[t,Array.from(t.childNodes).indexOf(e)]}function I(t,n,o,l,r){const i=n.getKey(),s=l.getKey(),c=document.createRange();let f=t.getElementByKey(i),u=t.getElementByKey(s),g=o,d=r;if(e(n)&&(f=P(f)),e(l)&&(u=P(u)),void 0===n||void 0===l||null===f||null===u)return null;"BR"===f.nodeName&&([f,g]=E(f)),"BR"===u.nodeName&&([u,d]=E(u));const a=f.firstChild;f===u&&null!=a&&"BR"===a.nodeName&&0===g&&0===d&&(d=1);try{c.setStart(f,g),c.setEnd(u,d)}catch(e){return null}return!c.collapsed||g===d&&i===s||(c.setStart(u,d),c.setEnd(f,g)),c}function B(e,t){const n=e.getRootElement();if(null===n)return[];const o=n.getBoundingClientRect(),l=getComputedStyle(n),r=parseFloat(l.paddingLeft)+parseFloat(l.paddingRight),i=Array.from(t.getClientRects());let s,c=i.length;i.sort((e,t)=>{const n=e.top-t.top;return Math.abs(n)<=3?e.left-t.left:n});for(let e=0;e<c;e++){const t=i[e],n=s&&s.top<=t.top&&s.top+s.height>t.top&&s.left+s.width>t.left,l=t.width+r===o.width;n||l?(i.splice(e--,1),c--):s=t}return i}function F(e){let t="";for(const n in e)n&&(t+=`${n}: ${e[n]};`);return t}function k(e){const n=t().getElementByKey(e.getKey());if(null===n)return null;const o=n.ownerDocument.defaultView;return null===o?null:o.getComputedStyle(n)}function b(e){return k(n(e)?e:e.getParentOrThrow())}function z(e){const t=b(e);return null!==t&&"rtl"===t.direction}function O(e,t,n="self"){const o=e.getStartEndPoints();if(t.isSelected(e)&&!i(t)&&null!==o){const[l,r]=o,i=e.isBackward(),s=l.getNode(),u=r.getNode(),g=t.is(s),d=t.is(u);if(g||d){const[o,l]=c(e),r=s.is(u),g=t.is(i?u:s),d=t.is(i?s:u);let a,p=0;if(r)p=o>l?l:o,a=o>l?o:l;else if(g){p=i?l:o,a=void 0}else if(d){p=0,a=i?o:l}const h=t.__text.slice(p,a);h!==t.__text&&("clone"===n&&(t=f(t)),t.__text=h)}}return t}function R(e){if("text"===e.type)return e.offset===e.getNode().getTextContentSize();const t=e.getNode();return s(t)||K(177),e.offset===t.getChildrenSize()}function A(t,o,r){let i=o.getNode(),c=r;if(s(i)){const e=i.getDescendantByIndex(o.offset);null!==e&&(i=e)}for(;c>0&&null!==i;){if(s(i)){const e=i.getLastDescendant();null!==e&&(i=e)}let r=i.getPreviousSibling(),f=0;if(null===r){let e=i.getParentOrThrow(),t=e.getPreviousSibling();for(;null===t;){if(e=e.getParent(),null===e){r=null;break}t=e.getPreviousSibling()}null!==e&&(f=e.isInline()?0:2,r=t)}let a=i.getTextContent();""===a&&s(i)&&!i.isInline()&&(a="\n\n");const p=a.length;if(!e(i)||c>=p){const e=i.getParent();i.remove(),null==e||0!==e.getChildrenSize()||n(e)||e.remove(),c-=p+f,i=r}else{const n=i.getKey(),r=t.getEditorState().read(()=>{const t=u(n);return e(t)&&t.isSimpleText()?t.getTextContent():null}),s=p-c,f=a.slice(0,s);if(null!==r&&r!==a){const e=g();let t=i;if(i.isSimpleText())i.setTextContent(r);else{const e=d(r);i.replace(e),t=e}if(l(e)&&e.isCollapsed()){const n=e.anchor.offset;t.select(n,n)}}else if(i.isSimpleText()){const e=o.key===n;let t=o.offset;t<c&&(t=p);const l=e?t-c:0,r=e?t:s;if(e&&0===l){const[e]=i.splitText(l,r);e.remove()}else{const[,e]=i.splitText(l,r);e.remove()}}else{const e=d(f);i.replace(e)}c=0}}}const _=()=>{};function L(t,n){(l(t)?t.isCollapsed():e(t)||s(t))||K(280);const o=a(l(t)?t.style:e(t)?t.getStyle():t.getTextStyle()),r=F(Object.entries(n).reduce((e,[n,l])=>("function"==typeof l?e[n]=l(o[n],t):null===l?delete e[n]:e[n]=l,e),{...o}));l(t)||e(t)?t.setStyle(r):t.setTextStyle(r)}function M(e,t){if(l(e)&&e.isCollapsed()){L(e,t);const n=e.anchor.getNode();s(n)&&n.isEmpty()&&L(n,t)}$(e=>{L(e,t)});const n=e.getNodes();if(n.length>0){const e=new Set;for(const o of n){if(!s(o)||!o.canBeEmpty()||0!==o.getChildrenSize())continue;const n=o.getKey();e.has(n)||(e.add(n),L(o,t))}}}function $(t){const n=o();if(!n)return;const s=new Map,c=e=>s.get(e.getKey())||[0,e.getTextContentSize()];if(l(n))for(const e of r(n).getTextSlices())e&&s.set(e.caret.origin.getKey(),e.getSliceIndices());const f=n.getNodes();for(const n of f){if(!e(n)||!n.canHaveFormat())continue;const[o,l]=c(n);if(l!==o)if(i(n)||0===o&&l===n.getTextContentSize())t(n);else{t(n.splitText(o,l)[0===o?0:1])}}l(n)&&"text"===n.anchor.type&&"text"===n.focus.type&&n.anchor.key===n.focus.key&&D(n)}function D(e){if(e.isBackward()){const{anchor:t,focus:n}=e,{key:o,offset:l,type:r}=t;t.set(n.key,n.offset,n.type),n.set(o,l,r)}}function j(e,t){const n=e.getFormatType(),o=e.getIndent();n!==t.getFormatType()&&t.setFormat(n),o!==t.getIndent()&&t.setIndent(o)}function U(e,t,n=j){if(null===e)return;const o=e.getStartEndPoints(),l=new Map;if(o){const[e,t]=o,n=p(e.getNode(),h),r=p(t.getNode(),h);s(n)&&l.set(n.getKey(),n),s(r)&&l.set(r.getKey(),r)}for(const t of e.getNodes())if(s(t)&&h(t))l.set(t.getKey(),t);else if(null===o){const e=p(t,h);s(e)&&l.set(e.getKey(),e)}for(const[,e]of l){const o=t();n(e,o),e.replace(o,!0)}}function H(e){return e.getNode().isAttached()}function V(e){let t=e;for(;null!==t&&!C(t);){const e=t.getLatest(),n=t.getParent();0===e.getChildrenSize()&&t.remove(!0),t=n}}function W(e,t,n=null){const o=e.getStartEndPoints(),l=o?o[0]:null,r=e.getNodes(),i=r.length;if(null!==l&&(0===i||1===i&&"element"===l.type&&0===l.getNode().getChildrenSize())){const e="text"===l.type?l.getNode().getParentOrThrow():l.getNode(),o=e.getChildren();let r=t();return r.setFormat(e.getFormatType()),r.setIndent(e.getIndent()),o.forEach(e=>r.append(e)),n&&(r=n.append(r)),void e.replace(r)}let s=null,c=[];for(let o=0;o<i;o++){const l=r[o];C(l)?(X(e,c,c.length,t,n),c=[],s=l):null===s||null!==s&&N(l,s)?c.push(l):(X(e,c,c.length,t,n),c=[l])}X(e,c,c.length,t,n)}function X(e,t,n,o,r=null){if(0===t.length)return;const i=t[0],c=new Map,f=[];let u=s(i)?i:i.getParentOrThrow();u.isInline()&&(u=u.getParentOrThrow());let d=!1;for(;null!==u;){const e=u.getPreviousSibling();if(null!==e){u=e,d=!0;break}if(u=u.getParentOrThrow(),C(u))break}const a=new Set;for(let e=0;e<n;e++){const n=t[e];s(n)&&0===n.getChildrenSize()&&a.add(n.getKey())}const p=new Set;for(let e=0;e<n;e++){const n=t[e];let l=n.getParent();if(null!==l&&l.isInline()&&(l=l.getParent()),null!==l&&w(n)&&!p.has(n.getKey())){const e=l.getKey();if(void 0===c.get(e)){const t=o();t.setFormat(l.getFormatType()),t.setIndent(l.getIndent()),f.push(t),c.set(e,t);const n=l.getChildren();t.splice(t.getChildrenSize(),0,n);for(const e of n)if(p.add(e.getKey()),s(e))for(const t of e.getChildrenKeys())p.add(t);V(l)}}else if(a.has(n.getKey())){s(n)||K(179);const e=o();e.setFormat(n.getFormatType()),e.setIndent(n.getIndent()),f.push(e),n.remove(!0)}}if(null!==r)for(let e=0;e<f.length;e++){const t=f[e];r.append(t)}let h=null;if(C(u))if(d)if(null!==r)u.insertAfter(r);else for(let e=f.length-1;e>=0;e--){const t=f[e];u.insertAfter(t)}else{const e=u.getFirstChild();if(s(e)&&(u=e),null===e)if(r)u.append(r);else for(let e=0;e<f.length;e++){const t=f[e];u.append(t),h=t}else if(null!==r)e.insertBefore(r);else for(let t=0;t<f.length;t++){const n=f[t];e.insertBefore(n),h=n}}else if(r)u.insertAfter(r);else for(let e=f.length-1;e>=0;e--){const t=f[e];u.insertAfter(t),h=t}const y=g();l(y)&&H(y.anchor)&&H(y.focus)?v(y.clone()):null!==h?h.selectEnd():e.dirty=!0}function q(e){const t=G(e);return null!==t&&"vertical-rl"===t.writingMode}function G(e){const t=e.anchor.getNode();return s(t)?k(t):b(t)}function J(e,t){let n=q(e)?!t:t;Y(e)&&(n=!n);const o=y(e.focus,n?"previous":"next");if(m(o))return!1;for(const e of S(o)){if(x(e))return!e.origin.isInline();if(!s(e.origin)){if(T(e.origin))return!0;break}}return!1}function Q(e,t,n,o){e.modify(t?"extend":"move",n,o)}function Y(e){const t=G(e);return null!==t&&"rtl"===t.direction}function Z(e,t,n){const o=Y(e);let l;l=q(e)||o?!n:n,Q(e,t,l,"character")}function ee(e,t,n){const o=e.getStyle(),l=a(o);return null!==l&&l[t]||n}function te(t,n,o=""){let r=null;const i=t.getNodes(),s=t.anchor,c=t.focus,f=t.isBackward(),u=f?c.getNode():s.getNode(),g=f?s.getNode():c.getNode(),d=f?c.offset:s.offset,p=f?s.offset:c.offset;if(l(t)&&t.isCollapsed()&&""!==t.style){const e=t.style,o=a(e);if(null!==o&&n in o)return o[n]}for(let t=0;t<i.length;t++){const l=i[t];if((0!==t||!l.is(u)||!e(l)||d!==l.getTextContentSize())&&((0===t||!l.is(g)||0!==p)&&e(l))){const e=ee(l,n,o);if(null===r)r=e;else if(r!==e){r="";break}}}return null===r?o:r}const ne=a,oe=A;export{_ as $addNodeStyle,j as $copyBlockFormatIndent,D as $ensureForwardRangeSelection,$ as $forEachSelectedTextNode,k as $getComputedStyleForElement,b as $getComputedStyleForParent,te as $getSelectionStyleValueForProperty,R as $isAtNodeEnd,Y as $isParentElementRTL,z as $isParentRTL,Q as $moveCaretSelection,Z as $moveCharacter,M as $patchStyleText,U as $setBlocksType,J as $shouldOverrideDefaultCharacterSelection,O as $sliceSelectedTextNodeContent,A as $trimTextContentFromAnchor,W as $wrapNodes,I as createDOMRange,B as createRectsFromDOMRange,F as getCSSFromStyleObject,ne as getStyleObjectFromCSS,oe as trimTextContentFromAnchor};
/**
* 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.
*
*/
import type { BaseSelection, ElementNode, LexicalNode, RangeSelection } from 'lexical';
import { TableSelection } from '@lexical/table';
export declare function $copyBlockFormatIndent(srcNode: ElementNode, destNode: ElementNode): void;
/**
* Converts all nodes in the selection that are of one block type to another.
* @param selection - The selected blocks to be converted.
* @param $createElement - The function that creates the node. eg. $createParagraphNode.
* @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
*/
export declare function $setBlocksType<T extends ElementNode>(selection: BaseSelection | null, $createElement: () => T, $afterCreateElement?: (prevNodeSrc: ElementNode, newNodeDest: T) => void): void;
/**
* @deprecated In favor of $setBlockTypes
* Wraps all nodes in the selection into another node of the type returned by createElement.
* @param selection - The selection of nodes to be wrapped.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to append the wrapped selection and its children to.
*/
export declare function $wrapNodes(selection: BaseSelection, createElement: () => ElementNode, wrappingElement?: null | ElementNode): void;
/**
* Wraps each node into a new ElementNode.
* @param selection - The selection of nodes to wrap.
* @param nodes - An array of nodes, generally the descendants of the selection.
* @param nodesLength - The length of nodes.
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
* @param wrappingElement - An element to wrap all the nodes into.
* @returns
*/
export declare function $wrapNodesImpl(selection: BaseSelection, nodes: LexicalNode[], nodesLength: number, createElement: () => ElementNode, wrappingElement?: null | ElementNode): void;
/**
* Determines if the default character selection should be overridden. Used with DecoratorNodes
* @param selection - The selection whose default character selection may need to be overridden.
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
* @returns true if it should be overridden, false if not.
*/
export declare function $shouldOverrideDefaultCharacterSelection(selection: RangeSelection, isBackward: boolean): boolean;
/**
* Moves the selection according to the arguments.
* @param selection - The selected text or nodes.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
* @param granularity - The distance to adjust the current selection.
*/
export declare function $moveCaretSelection(selection: RangeSelection, isHoldingShift: boolean, isBackward: boolean, granularity: 'character' | 'word' | 'lineboundary'): void;
/**
* Tests a parent element for right to left direction.
* @param selection - The selection whose parent is to be tested.
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
*/
export declare function $isParentElementRTL(selection: RangeSelection): boolean;
/**
* Moves selection by character according to arguments.
* @param selection - The selection of the characters to move.
* @param isHoldingShift - Is the shift key being held down during the operation.
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
*/
export declare function $moveCharacter(selection: RangeSelection, isHoldingShift: boolean, isBackward: boolean): void;
/**
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
* If all TextNodes do not have the same value, it returns an empty string.
* @param selection - The selection of TextNodes whose value to find.
* @param styleProperty - The CSS style property.
* @param defaultValue - The default value for the property, defaults to an empty string.
* @returns The value of the property for the selected TextNodes.
*/
export declare function $getSelectionStyleValueForProperty(selection: RangeSelection | TableSelection, styleProperty: string, defaultValue?: string): string;
/**
* 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.
*
*/
import type { ElementNode, LexicalEditor, LexicalNode } from 'lexical';
import { getStyleObjectFromCSS } from 'lexical';
/**
* Creates a selection range for the DOM.
* @param editor - The lexical editor.
* @param anchorNode - The anchor node of a selection.
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
* @param focusNode - The current focus.
* @param _focusOffset - The amount of space offset from the focus to the anchor.
* @returns The range of selection for the DOM that was created.
*/
export declare function createDOMRange(editor: LexicalEditor, anchorNode: LexicalNode, _anchorOffset: number, focusNode: LexicalNode, _focusOffset: number): Range | null;
/**
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
* @param editor - The lexical editor
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
* @returns The selectionRects as an array.
*/
export declare function createRectsFromDOMRange(editor: LexicalEditor, range: Range): Array<ClientRect>;
/**
* @deprecated Use {@link getStyleObjectFromCSS}, this is just an alias for backwards compatibility.
*/
export declare const getStyleObjectFromRawCSS: typeof getStyleObjectFromCSS;
/**
* Serializes a style object into a CSS declaration string, the inverse of
* {@link getStyleObjectFromCSS}.
* @param styles - An object mapping CSS property names to their values.
* @returns A CSS string of the form `prop: value;` for each entry, concatenated together.
*/
export declare function getCSSFromStyleObject(styles: Record<string, string>): string;
/**
* Gets the computed DOM styles of the element.
* @param element - The node to check the styles for.
* @returns the computed styles of the element or null if there is no DOM element or no default view for the document.
*/
export declare function $getComputedStyleForElement(element: ElementNode): CSSStyleDeclaration | null;
/**
* Gets the computed DOM styles of the parent of the node.
* @param node - The node to check its parent's styles for.
* @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
*/
export declare function $getComputedStyleForParent(node: LexicalNode): CSSStyleDeclaration | null;
/**
* Determines whether a node's parent is RTL.
* @param node - The node to check whether it is RTL.
* @returns whether the node is RTL.
*/
export declare function $isParentRTL(node: LexicalNode): boolean;