diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index ebe8d56b8ea..f1b1559b59c 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -15,6 +15,7 @@ import invariant from 'shared/invariant'; import { $createParagraphNode, + $isDecoratorNode, $isElementNode, $isParagraphNode, $isRootNode, @@ -281,14 +282,41 @@ export class LexicalNode { } // For inline images inside of element nodes. // Without this change the image will be selected if the cursor is before or after it. - if ( + const isElementRangeSelection = $isRangeSelection(targetSelection) && targetSelection.anchor.type === 'element' && - targetSelection.focus.type === 'element' && - targetSelection.anchor.key === targetSelection.focus.key && - targetSelection.anchor.offset === targetSelection.focus.offset - ) { - return false; + targetSelection.focus.type === 'element'; + + if (isElementRangeSelection) { + if (targetSelection.isCollapsed()) { + return false; + } + + const parentNode = this.getParent(); + if ($isDecoratorNode(this) && this.isInline() && parentNode) { + const {anchor, focus} = targetSelection; + + if (anchor.isBefore(focus)) { + const anchorNode = anchor.getNode() as ElementNode; + const isAnchorPointToLast = + anchor.offset === anchorNode.getChildrenSize(); + const isAnchorNodeIsParent = anchorNode.is(parentNode); + const isLastChild = anchorNode.getLastChildOrThrow().is(this); + + if (isAnchorPointToLast && isAnchorNodeIsParent && isLastChild) { + return false; + } + } else { + const focusNode = focus.getNode() as ElementNode; + const isFocusPointToLast = + focus.offset === focusNode.getChildrenSize(); + const isFocusNodeIsParent = focusNode.is(parentNode); + const isLastChild = focusNode.getLastChildOrThrow().is(this); + if (isFocusPointToLast && isFocusNodeIsParent && isLastChild) { + return false; + } + } + } } return isSelected; } diff --git a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts index 1f11b93b846..c34ad1a2643 100644 --- a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts @@ -10,6 +10,7 @@ import { $getRoot, $getSelection, $isRangeSelection, + DecoratorNode, ParagraphNode, TextNode, } from 'lexical'; @@ -47,6 +48,40 @@ class TestNode extends LexicalNode { } } +class InlineDecoratorNode extends DecoratorNode { + static getType(): string { + return 'inline-decorator'; + } + + static clone(): InlineDecoratorNode { + return new InlineDecoratorNode(); + } + + static importJSON() { + return new InlineDecoratorNode(); + } + + exportJSON() { + return {type: 'inline-decorator', version: 1}; + } + + createDOM(): HTMLElement { + return document.createElement('span'); + } + + isInline(): true { + return true; + } + + isParentRequired(): true { + return true; + } + + decorate() { + return 'inline-decorator'; + } +} + // This is a hack to bypass the node type validation on LexicalNode. We never want to create // an LexicalNode directly but we're testing the base functionality in this module. LexicalNode.getType = function () { @@ -266,6 +301,56 @@ describe('LexicalNode tests', () => { await Promise.resolve().then(); }); + test('LexicalNode.isSelected(): with inline decorator node', async () => { + const {editor} = testEnv; + let paragraphNode1: ParagraphNode; + let paragraphNode2: ParagraphNode; + let inlineDecoratorNode: InlineDecoratorNode; + + editor.update(() => { + paragraphNode1 = $createParagraphNode(); + paragraphNode2 = $createParagraphNode(); + inlineDecoratorNode = new InlineDecoratorNode(); + paragraphNode1.append(inlineDecoratorNode); + $getRoot().append(paragraphNode1, paragraphNode2); + paragraphNode1.selectEnd(); + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + expect(selection.anchor.getNode().is(paragraphNode1)).toBe(true); + } + }); + + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + expect(selection.anchor.key).toBe(paragraphNode1.getKey()); + + selection.focus.set(paragraphNode2.getKey(), 1, 'element'); + } + }); + + await Promise.resolve().then(); + + editor.getEditorState().read(() => { + expect(inlineDecoratorNode.isSelected()).toBe(false); + }); + + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.anchor.set(paragraphNode2.getKey(), 0, 'element'); + selection.focus.set(paragraphNode1.getKey(), 1, 'element'); + } + }); + + await Promise.resolve().then(); + + editor.getEditorState().read(() => { + expect(inlineDecoratorNode.isSelected()).toBe(false); + }); + }); + test('LexicalNode.getKey()', async () => { expect(textNode.getKey()).toEqual(textNode.__key); }); @@ -1206,7 +1291,7 @@ describe('LexicalNode tests', () => { }, { namespace: '', - nodes: [LexicalNode, TestNode], + nodes: [LexicalNode, TestNode, InlineDecoratorNode], theme: {}, }, );