diff --git a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs index 296d9ac21a0..90d4edeefca 100644 --- a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs @@ -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` +
+ normal + + bold + +
+ `, + ); + + // 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` ++ normal + + boBOLDld + +
+ `, + ); + + // 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` ++ normal boldBOLD +
+ `, + ); + + // 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` ++ normal + + boBOLDld + +
+ `, + ); + + // 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` ++ normal + + boBOLDld text + +
+ `, + ); + }); + + 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` ++ Check out the + + website + + ! +
+ `, + ); + + // 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` ++ Check out the + + website + + now! +
+ `, + ); + + // 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` ++ Check out the website! now +
+ `, + ); + + // 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` ++ Check out the + + website + + now! +
+ `, + ); + + // 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` ++ Check out the + + website + + now! Check it out. +
+ `, + ); + }); }); diff --git a/packages/lexical-yjs/src/CollabElementNode.ts b/packages/lexical-yjs/src/CollabElementNode.ts index c38171af0e8..14fd1fbf0c8 100644 --- a/packages/lexical-yjs/src/CollabElementNode.ts +++ b/packages/lexical-yjs/src/CollabElementNode.ts @@ -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]; @@ -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, @@ -221,7 +222,30 @@ export class CollabElementNode { sharedType as XmlText | YMap