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

[lexical-yjs] Bug Fix: handle text node being split by Yjs redo #7098

Merged
merged 1 commit into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -413,4 +413,256 @@ test.describe('Collaboration', () => {
`,
);
});

test('Undo/redo where text node is split by formatting change', async ({
isRichText,
page,
isCollab,
browserName,
}) => {
test.skip(!isCollab);

// Left collaborator types two words, sets the second one to bold.
await focusEditor(page);
await page.keyboard.type('normal bold');

await sleep(1050);
await selectCharacters(page, 'left', 'bold'.length);
await toggleBold(page);

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">normal</span>
<strong
class="PlaygroundEditorTheme__textBold"
data-lexical-text="true">
bold
</strong>
</p>
`,
);

// Right collaborator types in the middle of the bold word.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50});
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.type('BOLD');

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">normal</span>
<strong
class="PlaygroundEditorTheme__textBold"
data-lexical-text="true">
boBOLDld
</strong>
</p>
`,
);

// Left collaborator undoes their bold text.
await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click();

// The undo causes the text to be appended to the original string, like in the above test.
await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">normal boldBOLD</span>
</p>
`,
);

// Left collaborator redoes the bold text.
await page.frameLocator('iframe[name="left"]').getByLabel('Redo').click();

// The text should be back as it was prior to the undo.
await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">normal</span>
<strong
class="PlaygroundEditorTheme__textBold"
data-lexical-text="true">
boBOLDld
</strong>
</p>
`,
);

// Collaboration should still work.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50});
await page.keyboard.type(' text');

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">normal</span>
<strong
class="PlaygroundEditorTheme__textBold"
data-lexical-text="true">
boBOLDld text
</strong>
</p>
`,
);
});

test('Undo/redo where text node is split by inline element node', async ({
isRichText,
page,
isCollab,
browserName,
}) => {
test.skip(!isCollab);

// Left collaborator types some text, then splits the text nodes with an element node.
await focusEditor(page);
await page.keyboard.type('Check out the website!');

await sleep(1050);
await page.keyboard.press('ArrowLeft');
await selectCharacters(page, 'left', 'website'.length);
await page
.frameLocator('iframe[name="left"]')
.getByLabel('Insert link')
.first()
.click();

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">Check out the</span>
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://"
rel="noreferrer">
<span data-lexical-text="true">website</span>
</a>
<span data-lexical-text="true">!</span>
</p>
`,
);

// Right collaborator adds some text.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50});
await page.keyboard.press('ArrowLeft');
await page.keyboard.type(' now');

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">Check out the</span>
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://"
rel="noreferrer">
<span data-lexical-text="true">website</span>
</a>
<span data-lexical-text="true">now!</span>
</p>
`,
);

// Left collaborator undoes the link.
await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click();

// The undo causes the text to be appended to the original string, like in the above test.
await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">Check out the website! now</span>
</p>
`,
);

// Left collaborator redoes the link.
await page.frameLocator('iframe[name="left"]').getByLabel('Redo').click();

// The text should be back as it was prior to the undo.
await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">Check out the</span>
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://"
rel="noreferrer">
<span data-lexical-text="true">website</span>
</a>
<span data-lexical-text="true">now!</span>
</p>
`,
);

// Collaboration should still work.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50});
await page.keyboard.type(' Check it out.');

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">Check out the</span>
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://"
rel="noreferrer">
<span data-lexical-text="true">website</span>
</a>
<span data-lexical-text="true">now! Check it out.</span>
</p>
`,
);
});
});
28 changes: 26 additions & 2 deletions packages/lexical-yjs/src/CollabElementNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export class CollabElementNode {
): void {
const children = this._children;
let currIndex = 0;
let pendingSplitText = null;

for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i];
Expand Down Expand Up @@ -211,7 +212,7 @@ export class CollabElementNode {
currIndex += insertDelta.length;
} else {
const sharedType = insertDelta;
const {nodeIndex} = getPositionFromElementAndOffset(
const {node, nodeIndex, length} = getPositionFromElementAndOffset(
this,
currIndex,
false,
Expand All @@ -221,7 +222,30 @@ export class CollabElementNode {
sharedType as XmlText | YMap<unknown> | XmlElement,
this,
);
children.splice(nodeIndex, 0, collabNode);
if (
node instanceof CollabTextNode &&
length > 0 &&
length < node._text.length
) {
// Trying to insert in the middle of a text node; split the text.
const text = node._text;
const splitIdx = text.length - length;
node._text = spliceString(text, splitIdx, length, '');
children.splice(nodeIndex + 1, 0, collabNode);
// The insert that triggers the text split might not be a text node. Need to keep a
// reference to the remaining text so that it can be added when we do create one.
pendingSplitText = spliceString(text, 0, splitIdx, '');
} else {
children.splice(nodeIndex, 0, collabNode);
}
if (
pendingSplitText !== null &&
collabNode instanceof CollabTextNode
) {
// Found a text node to insert the pending text into.
collabNode._text = pendingSplitText + collabNode._text;
pendingSplitText = null;
}
currIndex += 1;
}
} else {
Expand Down
Loading