diff --git a/packages/lexical-mark/__tests__/unit/LexicalMarkNode.test.ts b/packages/lexical-mark/__tests__/unit/LexicalMarkNode.test.ts index 5f9061e32df..3caa0c79525 100644 --- a/packages/lexical-mark/__tests__/unit/LexicalMarkNode.test.ts +++ b/packages/lexical-mark/__tests__/unit/LexicalMarkNode.test.ts @@ -104,11 +104,11 @@ describe('LexicalMarkNode tests', () => { $getRoot().getFirstChildOrThrow(); paragraphNode.append(decoratorNode, textNode); const selection = $createRangeSelection(); - selection.anchor.set(paragraphNode.getKey(), 0, 'text'); + selection.anchor.set(paragraphNode.getKey(), 0, 'element'); selection.focus.set( paragraphNode.getKey(), - paragraphNode.getTextContent().length, - 'text', + paragraphNode.getChildrenSize(), + 'element', ); $wrapSelectionInMarkNode(selection, false, 'my-id'); @@ -132,11 +132,11 @@ describe('LexicalMarkNode tests', () => { $getRoot().getFirstChildOrThrow(); paragraphNode.append(elementNode, textNode); const selection = $createRangeSelection(); - selection.anchor.set(paragraphNode.getKey(), 0, 'text'); + selection.anchor.set(paragraphNode.getKey(), 0, 'element'); selection.focus.set( paragraphNode.getKey(), - paragraphNode.getTextContent().length, - 'text', + paragraphNode.getChildrenSize(), + 'element', ); $wrapSelectionInMarkNode(selection, false, 'my-id'); @@ -160,11 +160,11 @@ describe('LexicalMarkNode tests', () => { $getRoot().getFirstChildOrThrow(); paragraphNode.append(elementNode, textNode); const selection = $createRangeSelection(); - selection.anchor.set(paragraphNode.getKey(), 0, 'text'); + selection.anchor.set(paragraphNode.getKey(), 0, 'element'); selection.focus.set( paragraphNode.getKey(), - paragraphNode.getTextContent().length, - 'text', + paragraphNode.getChildrenSize(), + 'element', ); $wrapSelectionInMarkNode(selection, false, 'my-id'); diff --git a/packages/lexical-mark/src/index.ts b/packages/lexical-mark/src/index.ts index d5fa9e27e43..e2b83c96c6d 100644 --- a/packages/lexical-mark/src/index.ts +++ b/packages/lexical-mark/src/index.ts @@ -7,9 +7,14 @@ */ import type {SerializedMarkNode} from './MarkNode'; -import type {LexicalNode, RangeSelection, TextNode} from 'lexical'; +import type {ElementNode, LexicalNode, RangeSelection, TextNode} from 'lexical'; -import {$isDecoratorNode, $isElementNode, $isTextNode} from 'lexical'; +import { + $createRangeSelection, + $isDecoratorNode, + $isElementNode, + $isTextNode, +} from 'lexical'; import {$createMarkNode, $isMarkNode, MarkNode} from './MarkNode'; @@ -34,21 +39,30 @@ export function $wrapSelectionInMarkNode( id: string, createNode?: (ids: Array) => MarkNode, ): void { - const nodes = selection.getNodes(); - const anchorOffset = selection.anchor.offset; - const focusOffset = selection.focus.offset; - const nodesLength = nodes.length; - const startOffset = isBackward ? focusOffset : anchorOffset; - const endOffset = isBackward ? anchorOffset : focusOffset; - let currentNodeParent; - let lastCreatedMarkNode; + // Force a forwards selection since append is used, ignore the argument. + // A new selection is used to avoid side-effects of flipping the given + // selection + const forwardSelection = $createRangeSelection(); + const [startPoint, endPoint] = selection.isBackward() + ? [selection.focus, selection.anchor] + : [selection.anchor, selection.focus]; + forwardSelection.anchor.set( + startPoint.key, + startPoint.offset, + startPoint.type, + ); + forwardSelection.focus.set(endPoint.key, endPoint.offset, endPoint.type); + let currentNodeParent: ElementNode | null | undefined; + let lastCreatedMarkNode: MarkNode | undefined; + + // Note that extract will split text nodes at the boundaries + const nodes = forwardSelection.extract(); // We only want wrap adjacent text nodes, line break nodes // and inline element nodes. For decorator nodes and block // element nodes, we step out of their boundary and start // again after, if there are more nodes. - for (let i = 0; i < nodesLength; i++) { - const node = nodes[i]; + for (const node of nodes) { if ( $isElementNode(lastCreatedMarkNode) && lastCreatedMarkNode.isParentOf(node) @@ -56,33 +70,17 @@ export function $wrapSelectionInMarkNode( // If the current node is a child of the last created mark node, there is nothing to do here continue; } - const isFirstNode = i === 0; - const isLastNode = i === nodesLength - 1; let targetNode: LexicalNode | null = null; if ($isTextNode(node)) { - // Case 1: The node is a text node and we can split it - const textContentSize = node.getTextContentSize(); - const startTextOffset = isFirstNode ? startOffset : 0; - const endTextOffset = isLastNode ? endOffset : textContentSize; - if (startTextOffset === 0 && endTextOffset === 0) { - continue; - } - const splitNodes = node.splitText(startTextOffset, endTextOffset); - targetNode = - splitNodes.length > 1 && - (splitNodes.length === 3 || - (isFirstNode && !isLastNode) || - endTextOffset === textContentSize) - ? splitNodes[1] - : splitNodes[0]; + // Case 1: The node is a text node and we can include it + targetNode = node; } else if ($isMarkNode(node)) { // Case 2: the node is a mark node and we can ignore it as a target, // moving on to its children. Note that when we make a mark inside // another mark, it may utlimately be unnested by a call to // `registerNestedElementResolver` somewhere else in the // codebase. - continue; } else if ( ($isElementNode(node) || $isDecoratorNode(node)) && diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index a071dc3111c..769fc6ef46b 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -178,6 +178,16 @@ export class Point { this.key = key; this.offset = offset; this.type = type; + if (__DEV__) { + const node = $getNodeByKey(key); + invariant( + type === 'text' ? $isTextNode(node) : $isElementNode(node), + 'PointType.set: node with key %s is %s and can not be used for a %s point', + key, + node ? node.__type : '[not found]', + type, + ); + } if (!isCurrentlyReadOnlyMode()) { if ($getCompositionKey() === oldKey) { $setCompositionKey(key);