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

fix(LexicalNode): fix inline decorator isSelected #5948

Merged
merged 5 commits into from
Jul 20, 2024
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
40 changes: 34 additions & 6 deletions packages/lexical/src/LexicalNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import invariant from 'shared/invariant';

import {
$createParagraphNode,
$isDecoratorNode,
$isElementNode,
$isParagraphNode,
$isRootNode,
Expand Down Expand Up @@ -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;
}
Expand Down
87 changes: 86 additions & 1 deletion packages/lexical/src/__tests__/unit/LexicalNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
$getRoot,
$getSelection,
$isRangeSelection,
DecoratorNode,
ParagraphNode,
TextNode,
} from 'lexical';
Expand Down Expand Up @@ -47,6 +48,40 @@ class TestNode extends LexicalNode {
}
}

class InlineDecoratorNode extends DecoratorNode<string> {
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 () {
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -1206,7 +1291,7 @@ describe('LexicalNode tests', () => {
},
{
namespace: '',
nodes: [LexicalNode, TestNode],
nodes: [LexicalNode, TestNode, InlineDecoratorNode],
theme: {},
},
);
Expand Down
Loading