Skip to content

Commit

Permalink
[lexical][lexical-mark] Bug Fix: $wrapSelectionInMarkNode with elemen…
Browse files Browse the repository at this point in the history
…t points (#7132)
  • Loading branch information
etrepum authored Feb 5, 2025
1 parent aa9158f commit dde1d3f
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 39 deletions.
18 changes: 9 additions & 9 deletions packages/lexical-mark/__tests__/unit/LexicalMarkNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ describe('LexicalMarkNode tests', () => {
$getRoot().getFirstChildOrThrow<ParagraphNode>();
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');

Expand All @@ -132,11 +132,11 @@ describe('LexicalMarkNode tests', () => {
$getRoot().getFirstChildOrThrow<ParagraphNode>();
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');

Expand All @@ -160,11 +160,11 @@ describe('LexicalMarkNode tests', () => {
$getRoot().getFirstChildOrThrow<ParagraphNode>();
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');

Expand Down
58 changes: 28 additions & 30 deletions packages/lexical-mark/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -34,55 +39,48 @@ export function $wrapSelectionInMarkNode(
id: string,
createNode?: (ids: Array<string>) => 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)
) {
// 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<MarkNode>` somewhere else in the
// codebase.

continue;
} else if (
($isElementNode(node) || $isDecoratorNode(node)) &&
Expand Down
10 changes: 10 additions & 0 deletions packages/lexical/src/LexicalSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit dde1d3f

Please sign in to comment.