@lexical/selection
Advanced tools
| /** | ||
| * 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; |
+47
| /** | ||
| * 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; | ||
| } |
+238
| /** | ||
| * 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" | ||
| ] | ||
| } |
-19
| /** | ||
| * 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; |
-55
| /** | ||
| * 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; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
168027
39.31%19
26.67%3666
58.22%2
100%1
Infinity%+ Added
+ Added
+ Added
- Removed
Updated