From 55ae9536ebc1cd6ca59442f06deff153dafd1615 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 1 Oct 2024 11:31:43 -0700 Subject: [PATCH] More test coverage and change the behavior --- packages/lexical/src/LexicalSelection.ts | 11 ++ .../__tests__/unit/LexicalSelection.test.ts | 154 +++++++++++++++++- 2 files changed, 157 insertions(+), 8 deletions(-) diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 423a11cdf98..8f851d4cfa4 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -1117,6 +1117,17 @@ export class RangeSelection implements BaseSelection { let lastNode = lastPoint.getNode(); const firstBlock = $getAncestor(firstNode, INTERNAL_$isBlock); const lastBlock = $getAncestor(lastNode, INTERNAL_$isBlock); + // If a token is partially selected then move the selection to cover the whole selection + if ( + $isTextNode(firstNode) && + firstNode.isToken() && + firstPoint.offset < firstNode.getTextContentSize() + ) { + firstPoint.offset = 0; + } + if (lastPoint.offset > 0 && $isTextNode(lastNode) && lastNode.isToken()) { + lastPoint.offset = lastNode.getTextContentSize(); + } selectedNodes.forEach((node) => { if ( diff --git a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts index 09ec4752cd5..87b20b797b3 100644 --- a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts @@ -485,14 +485,152 @@ describe('LexicalSelection tests', () => { }, {discrete: true}, ); - testEnv.editor.getEditorState().read(() => { - const allTextNodes = $getRoot().getAllTextNodes(); - // These should get merged in reconciliation - expect(allTextNodes.map((node) => node.getTextContent())).toEqual([ - 'leadtext', - ]); - expect(leadingText.isAttached()).toBe(true); - }); + }); + }); + describe('with a leading token TextNode and a trailing TextNode', () => { + let leadingTokenText: TextNode; + let trailingText: TextNode; + let paragraph: ParagraphNode; + beforeEach(() => { + testEnv.editor.update( + () => { + leadingTokenText = $createTextNode('token text').setMode('token'); + trailingText = $createTextNode('trailing text'); + paragraph = $createParagraphNode().append( + leadingTokenText, + trailingText, + ); + $getRoot().clear().append(paragraph); + }, + {discrete: true}, + ); + }); + test('remove all text', () => { + testEnv.editor.update( + () => { + const sel = $createRangeSelection(); + sel.anchor.set(leadingTokenText.getKey(), 0, 'text'); + sel.focus.set( + trailingText.getKey(), + trailingText.getTextContentSize(), + 'text', + ); + $setSelection(sel); + sel.removeText(); + expect(leadingTokenText.isAttached()).toBe(false); + expect(trailingText.isAttached()).toBe(false); + expect($getRoot().getAllTextNodes()).toHaveLength(0); + const selection = $assertRangeSelection($getSelection()); + expect(selection.isCollapsed()).toBe(true); + expect(selection.anchor.key).toBe(paragraph.getKey()); + expect(selection.anchor.offset).toBe(0); + }, + {discrete: true}, + ); + }); + test('remove trailing TextNode', () => { + testEnv.editor.update( + () => { + const sel = $createRangeSelection(); + sel.anchor.set(trailingText.getKey(), 0, 'text'); + sel.focus.set( + trailingText.getKey(), + trailingText.getTextContentSize(), + 'text', + ); + $setSelection(sel); + sel.removeText(); + expect(leadingTokenText.isAttached()).toBe(true); + expect(trailingText.isAttached()).toBe(false); + expect($getRoot().getAllTextNodes()).toHaveLength(1); + const selection = $assertRangeSelection($getSelection()); + expect(selection.isCollapsed()).toBe(true); + expect(selection.anchor.key).toBe(leadingTokenText.getKey()); + expect(selection.anchor.offset).toBe( + leadingTokenText.getTextContentSize(), + ); + }, + {discrete: true}, + ); + }); + test('remove leading token TextNode', () => { + testEnv.editor.update( + () => { + const sel = $createRangeSelection(); + sel.anchor.set(leadingTokenText.getKey(), 0, 'text'); + sel.focus.set( + leadingTokenText.getKey(), + leadingTokenText.getTextContentSize(), + 'text', + ); + $setSelection(sel); + sel.removeText(); + expect(leadingTokenText.isAttached()).toBe(false); + expect(trailingText.isAttached()).toBe(true); + expect($getRoot().getAllTextNodes()).toHaveLength(1); + const selection = $assertRangeSelection($getSelection()); + expect(selection.isCollapsed()).toBe(true); + expect(selection.anchor.key).toBe(trailingText.getKey()); + expect(selection.anchor.offset).toBe(0); + }, + {discrete: true}, + ); + }); + test('remove partial leading token TextNode and trailing TextNode', () => { + testEnv.editor.update( + () => { + const sel = $createRangeSelection(); + sel.anchor.set( + leadingTokenText.getKey(), + 'token '.length, + 'text', + ); + sel.focus.set( + trailingText.getKey(), + trailingText.getTextContentSize(), + 'text', + ); + $setSelection(sel); + sel.removeText(); + expect(trailingText.isAttached()).toBe(false); + // expecting no node since it was token + expect(leadingTokenText.isAttached()).toBe(false); + const allTextNodes = $getRoot().getAllTextNodes(); + expect(allTextNodes).toHaveLength(0); + const selection = $assertRangeSelection($getSelection()); + expect(selection.isCollapsed()).toBe(true); + expect(selection.anchor.key).toBe(paragraph.getKey()); + expect(selection.anchor.offset).toBe(0); + }, + {discrete: true}, + ); + }); + test('remove partial token TextNode and partial trailing TextNode', () => { + testEnv.editor.update( + () => { + const sel = $createRangeSelection(); + sel.anchor.set( + leadingTokenText.getKey(), + 'token '.length, + 'text', + ); + sel.focus.set(trailingText.getKey(), 'trail'.length, 'text'); + $setSelection(sel); + sel.removeText(); + expect(leadingTokenText.isAttached()).toBe(false); + expect(trailingText.isAttached()).toBe(true); + const allTextNodes = $getRoot().getAllTextNodes(); + // The token node will be completely removed + expect(allTextNodes.map((node) => node.getTextContent())).toEqual( + ['ing text'], + ); + const selection = $assertRangeSelection($getSelection()); + expect(selection.isCollapsed()).toBe(true); + expect(selection.anchor.key).toBe(trailingText.getKey()); + expect(selection.anchor.offset).toBe(0); + }, + {discrete: true}, + ); }); }); describe('with a leading TextNode and a trailing segmented TextNode', () => {