From 4fbe7fec1a6f7c44bd828ec11460751f9cbcfe16 Mon Sep 17 00:00:00 2001
From: James Fitzsimmons
Date: Tue, 29 Oct 2024 15:57:49 +1100
Subject: [PATCH] Fix: clean up dangling text in YJS
---
.../__tests__/e2e/Collaboration.spec.mjs | 91 +++++++++++++++++--
packages/lexical-yjs/src/CollabElementNode.ts | 32 ++++---
2 files changed, 99 insertions(+), 24 deletions(-)
diff --git a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs
index df5414b355f..f83590e5cc4 100644
--- a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs
+++ b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs
@@ -231,7 +231,7 @@ test.describe('Collaboration', () => {
});
});
- test('Undo with two collaborators editing same paragraph', async ({
+ test('Remove dangling text from YJS when there is no preceding text node', async ({
isRichText,
page,
isCollab,
@@ -273,11 +273,12 @@ test.describe('Collaboration', () => {
`,
);
- // Left collaborator undoes their second paragraph
- await sleep(1050);
+ // Left collaborator undoes their text in the second paragraph.
+ await sleep(50);
await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click();
- // Only left collaborator's text should have been undone
+ // The undo also removed the text node from YJS.
+ // Check that the dangling text from right user was also removed.
await assertHTML(
page,
html`
@@ -286,11 +287,7 @@ test.describe('Collaboration', () => {
dir="ltr">
Line 1
-
- Word
-
+
`,
);
@@ -310,10 +307,84 @@ test.describe('Collaboration', () => {
dir="ltr">
Line 1
+
+ `,
+ );
+ });
+
+ test('Merge dangling text into preceding text node', async ({
+ isRichText,
+ page,
+ isCollab,
+ browserName,
+ }) => {
+ // test.skip(!isCollab || IS_MAC);
+ test.skip(!isCollab);
+
+ // Left collaborator types two pieces of text in the same paragraph, but with different styling.
+ await focusEditor(page);
+ await page.keyboard.type('normal');
+ await sleep(1050);
+ await toggleBold(page);
+ await page.keyboard.type('bold');
+
+ // Right collaborator types at the end of the paragraph.
+ await sleep(50);
+ await page
+ .frameLocator('iframe[name="right"]')
+ .locator('[data-lexical-editor="true"]')
+ .focus();
+ await page.keyboard.press('ArrowDown'); // Move caret to end of paragraph
+ await page.keyboard.type('BOLD');
+
+ await assertHTML(
+ page,
+ html`
+
+ normal
+
+ boldBOLD
+
+
+ `,
+ );
+
+ // Left collaborator undoes their bold text.
+ await sleep(50);
+ await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click();
+
+ // The undo also removed bold the text node from YJS.
+ // Check that the dangling text from right user was merged into the preceding text node.
+ await assertHTML(
+ page,
+ html`
+
+ normalBOLD
+
+ `,
+ );
+
+ // Left collaborator refreshes their page
+ await page.evaluate(() => {
+ document
+ .querySelector('iframe[name="left"]')
+ .contentDocument.location.reload();
+ });
+
+ // Page content should be the same as before the refresh
+ await assertHTML(
+ page,
+ html`
- Word
+ normalBOLD
`,
);
diff --git a/packages/lexical-yjs/src/CollabElementNode.ts b/packages/lexical-yjs/src/CollabElementNode.ts
index f4c3f124c55..c38171af0e8 100644
--- a/packages/lexical-yjs/src/CollabElementNode.ts
+++ b/packages/lexical-yjs/src/CollabElementNode.ts
@@ -157,21 +157,25 @@ export class CollabElementNode {
nodeIndex !== 0 ? children[nodeIndex - 1] : null;
const nodeSize = node.getSize();
- if (
- offset === 0 &&
- delCount === 1 &&
- nodeIndex > 0 &&
- prevCollabNode instanceof CollabTextNode &&
- length === nodeSize &&
- // If the node has no keys, it's been deleted
- Array.from(node._map.keys()).length === 0
- ) {
- // Merge the text node with previous.
- prevCollabNode._text += node._text;
- children.splice(nodeIndex, 1);
- } else if (offset === 0 && delCount === nodeSize) {
- // The entire thing needs removing
+ if (offset === 0 && length === nodeSize) {
+ // Text node has been deleted.
children.splice(nodeIndex, 1);
+ // If this was caused by an undo from YJS, there could be dangling text.
+ const danglingText = spliceString(
+ node._text,
+ offset,
+ delCount - 1,
+ '',
+ );
+ if (danglingText.length > 0) {
+ if (prevCollabNode instanceof CollabTextNode) {
+ // Merge the text node with previous.
+ prevCollabNode._text += danglingText;
+ } else {
+ // No previous text node to merge into, just delete the text.
+ this._xmlText.delete(offset, danglingText.length);
+ }
+ }
} else {
node._text = spliceString(node._text, offset, delCount, '');
}