Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix LexicalNode.isBefore(), clean up and optimize getCommonAncestor, isBefore, getNodesBetween #6310

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
188 changes: 101 additions & 87 deletions packages/lexical/src/LexicalNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

/* eslint-disable no-constant-condition */
import type {EditorConfig, LexicalEditor} from './LexicalEditor';
import type {BaseSelection, RangeSelection} from './LexicalSelection';
import type {Klass, KlassConstructor} from 'lexical';

Expand All @@ -22,6 +21,7 @@ import {
type DecoratorNode,
ElementNode,
} from '.';
import {EditorConfig, LexicalEditor} from './LexicalEditor';
import {
$getSelection,
$isNodeSelection,
Expand Down Expand Up @@ -564,9 +564,7 @@ export class LexicalNode {
*
* @param node - the other node to find the common ancestor of.
*/
getCommonAncestor<T extends ElementNode = ElementNode>(
node: LexicalNode,
): T | null {
getCommonAncestor(node: LexicalNode): ElementNode | null {
const a = this.getParents();
const b = node.getParents();
if ($isElementNode(this)) {
Expand All @@ -575,19 +573,18 @@ export class LexicalNode {
if ($isElementNode(node)) {
b.unshift(node);
}
const aLength = a.length;
const bLength = b.length;
if (aLength === 0 || bLength === 0 || a[aLength - 1] !== b[bLength - 1]) {
return null;
}
const bSet = new Set(b);
for (let i = 0; i < aLength; i++) {
const ancestor = a[i] as T;
if (bSet.has(ancestor)) {

let ancestor: ElementNode | null = null;
const itrCount = Math.min(a.length, b.length);
for (let i = 1; i <= itrCount; ++i) {
if (a[a.length - i] !== b[b.length - i]) {
return ancestor;
}

ancestor = a[a.length - i];
}
return null;

return ancestor;
}

/**
Expand All @@ -613,33 +610,61 @@ export class LexicalNode {
return false;
}
if (targetNode.isParentOf(this)) {
return true;
return false;
}
if (this.isParentOf(targetNode)) {
return false;
return true;
}
const commonAncestor = this.getCommonAncestor(targetNode);
let indexA = 0;
let indexB = 0;
let node: this | ElementNode | LexicalNode = this;
while (true) {
const parent: ElementNode = node.getParentOrThrow();
if (parent === commonAncestor) {
indexA = node.getIndexWithinParent();

const thisParents: LexicalNode[] = this.getParents();
const targetParents: LexicalNode[] = targetNode.getParents();
thisParents.unshift(this);
targetParents.unshift(targetNode);

let commonAncestor: LexicalNode | null = null;
const itrCount = Math.min(thisParents.length, targetParents.length);
let i = 1;
for (; i <= itrCount; ++i) {
if (
thisParents[thisParents.length - i] !==
targetParents[targetParents.length - i]
) {
break;
}
node = parent;

commonAncestor = thisParents[thisParents.length - i];
}
node = targetNode;

if (targetNode.is(commonAncestor)) {
return false;
}
if (this.is(commonAncestor)) {
return true;
}

const thisAncestor: LexicalNode = thisParents[thisParents.length - i];
const targetAncestor: LexicalNode = targetParents[targetParents.length - i];

let thisPrevSibling: LexicalNode | null = thisAncestor.getPreviousSibling();
let targetPrevSibling: LexicalNode | null =
targetAncestor.getPreviousSibling();
while (true) {
const parent: ElementNode = node.getParentOrThrow();
if (parent === commonAncestor) {
indexB = node.getIndexWithinParent();
break;
if (thisPrevSibling === null) {
return true;
}
node = parent;
if (targetPrevSibling === null) {
return false;
}
if (thisAncestor.is(targetPrevSibling)) {
return true;
}
if (targetAncestor.is(thisPrevSibling)) {
return false;
}

thisPrevSibling = thisPrevSibling.getPreviousSibling();
targetPrevSibling = targetPrevSibling.getPreviousSibling();
}
return indexA < indexB;
}

/**
Expand Down Expand Up @@ -670,68 +695,57 @@ export class LexicalNode {
* @param targetNode - the node that marks the other end of the range of nodes to be returned.
*/
getNodesBetween(targetNode: LexicalNode): Array<LexicalNode> {
if (this === targetNode) {
return [this];
}

const isBefore = this.isBefore(targetNode);
const nodes = [];
const visited = new Set();
let node: LexicalNode | this | null = this;
while (true) {
if (node === null) {
break;
}
const key = node.__key;
if (!visited.has(key)) {
visited.add(key);
nodes.push(node);
}
if (node === targetNode) {
break;
}
const child: LexicalNode | null = $isElementNode(node)
? isBefore
? node.getFirstChild()
: node.getLastChild()
: null;
if (child !== null) {
node = child;
continue;
}
const nextSibling: LexicalNode | null = isBefore
? node.getNextSibling()
: node.getPreviousSibling();
if (nextSibling !== null) {
node = nextSibling;
continue;
}
const parent: LexicalNode | null = node.getParentOrThrow();
if (!visited.has(parent.__key)) {
nodes.push(parent);
const firstNode = isBefore ? this : targetNode;
const lastNode = isBefore ? targetNode : this;

const addedNodes = new Set<LexicalNode>();
const nodes = new Array<LexicalNode>();

let currentNode: LexicalNode = firstNode;
while (!currentNode.is(lastNode)) {
if (!addedNodes.has(currentNode)) {
addedNodes.add(currentNode);
nodes.push(currentNode);
}
if (parent === targetNode) {
break;

let nextNode: LexicalNode | null = currentNode;
if ($isElementNode(nextNode)) {
const child = nextNode.getFirstChild();
nextNode = child === null ? nextNode.getNextSibling() : child;
} else {
nextNode = nextNode.getNextSibling();
}
let parentSibling = null;
let ancestor: LexicalNode | null = parent;
do {
if (ancestor === null) {
invariant(false, 'getNodesBetween: ancestor is null');

if (nextNode === null) {
nextNode = currentNode.getParentOrThrow();
if (!addedNodes.has(nextNode)) {
addedNodes.add(nextNode);
nodes.push(nextNode);
}
parentSibling = isBefore
? ancestor.getNextSibling()
: ancestor.getPreviousSibling();
ancestor = ancestor.getParent();
if (ancestor !== null) {
if (parentSibling === null && !visited.has(ancestor.__key)) {
nodes.push(ancestor);

let parentSiblingNode = nextNode.getNextSibling();
while (parentSiblingNode === null) {
nextNode = nextNode.getParentOrThrow();
if (!addedNodes.has(nextNode)) {
addedNodes.add(nextNode);
nodes.push(nextNode);
}
} else {
break;

parentSiblingNode = nextNode.getNextSibling();
}
} while (parentSibling === null);
node = parentSibling;
}
if (!isBefore) {
nodes.reverse();

nextNode = parentSiblingNode;
}

currentNode = nextNode as LexicalNode;
}

nodes.push(lastNode);
return nodes;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/lexical/src/__tests__/unit/LexicalNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,19 +816,22 @@ describe('LexicalNode tests', () => {

test('LexicalNode.isBefore()', async () => {
const {editor} = testEnv;
let newParagraphNode: ParagraphNode;
let barTextNode: TextNode;
let bazTextNode: TextNode;

await editor.update(() => {
newParagraphNode = new ParagraphNode();
barTextNode = new TextNode('bar');
barTextNode.toggleUnmergeable();
bazTextNode = new TextNode('baz');
bazTextNode.toggleUnmergeable();
paragraphNode.append(barTextNode, bazTextNode);
newParagraphNode.append(barTextNode, bazTextNode);
paragraphNode.append(newParagraphNode);
});

expect(testEnv.outerHTML).toBe(
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir="ltr"><span data-lexical-text="true">foo</span><span data-lexical-text="true">bar</span><span data-lexical-text="true">baz</span></p></div>',
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir="ltr"><span data-lexical-text="true">foo</span><p dir="ltr"><span data-lexical-text="true">bar</span><span data-lexical-text="true">baz</span></p></p></div>',
);

await editor.getEditorState().read(() => {
Expand All @@ -838,6 +841,8 @@ describe('LexicalNode tests', () => {
expect(barTextNode.isBefore(bazTextNode)).toBe(true);
expect(bazTextNode.isBefore(barTextNode)).toBe(false);
expect(bazTextNode.isBefore(textNode)).toBe(false);
expect(paragraphNode.isBefore(textNode)).toBe(true);
expect(paragraphNode.isBefore(barTextNode)).toBe(true);
});
expect(() => textNode.isBefore(barTextNode)).toThrow();
});
Expand All @@ -861,7 +866,8 @@ describe('LexicalNode tests', () => {
const {editor} = testEnv;
let barTextNode: TextNode;
let bazTextNode: TextNode;
let newParagraphNode: ParagraphNode;
let newParagraphNode0: ParagraphNode;
let newParagraphNode1: ParagraphNode;
let quxTextNode: TextNode;

await editor.update(() => {
Expand All @@ -870,16 +876,18 @@ describe('LexicalNode tests', () => {
barTextNode.toggleUnmergeable();
bazTextNode = new TextNode('baz');
bazTextNode.toggleUnmergeable();
newParagraphNode = new ParagraphNode();
newParagraphNode0 = new ParagraphNode();
newParagraphNode1 = new ParagraphNode();
quxTextNode = new TextNode('qux');
quxTextNode.toggleUnmergeable();
rootNode.append(newParagraphNode);
rootNode.append(newParagraphNode0);
rootNode.append(newParagraphNode1);
paragraphNode.append(barTextNode, bazTextNode);
newParagraphNode.append(quxTextNode);
newParagraphNode1.append(quxTextNode);
});

expect(testEnv.outerHTML).toBe(
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir="ltr"><span data-lexical-text="true">foo</span><span data-lexical-text="true">bar</span><span data-lexical-text="true">baz</span></p><p dir="ltr"><span data-lexical-text="true">qux</span></p></div>',
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir="ltr"><span data-lexical-text="true">foo</span><span data-lexical-text="true">bar</span><span data-lexical-text="true">baz</span></p><p><br></p><p dir="ltr"><span data-lexical-text="true">qux</span></p></div>',
);

await editor.getEditorState().read(() => {
Expand All @@ -898,9 +906,26 @@ describe('LexicalNode tests', () => {
barTextNode,
bazTextNode,
paragraphNode.getLatest(),
newParagraphNode,
newParagraphNode0,
newParagraphNode1,
quxTextNode,
]);
expect(
paragraphNode.getLatest().getNodesBetween(newParagraphNode1),
).toEqual([
paragraphNode.getLatest(),
textNode.getLatest(),
barTextNode,
bazTextNode,
newParagraphNode0,
newParagraphNode1,
]);
expect(paragraphNode.getNodesBetween(quxTextNode)).toEqual(
quxTextNode.getNodesBetween(paragraphNode),
);
expect(paragraphNode.getNodesBetween(newParagraphNode1)).toEqual(
newParagraphNode1.getNodesBetween(paragraphNode),
);
});
expect(() => textNode.getNodesBetween(bazTextNode)).toThrow();
});
Expand Down
Loading