Skip to content

Commit

Permalink
[lexical-mark] Feature: include inline decorator nodes in marks (face…
Browse files Browse the repository at this point in the history
  • Loading branch information
james-atticus authored Jan 30, 2025
1 parent 881c7fe commit a62a1a6
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 4 deletions.
185 changes: 185 additions & 0 deletions packages/lexical-mark/__tests__/unit/LexicalMarkNode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {$wrapSelectionInMarkNode, MarkNode} from '@lexical/mark';
import {
$createParagraphNode,
$createRangeSelection,
$createTextNode,
$getRoot,
ParagraphNode,
} from 'lexical';
import {
$createTestDecoratorNode,
$createTestElementNode,
$createTestInlineElementNode,
initializeUnitTest,
} from 'lexical/src/__tests__/utils';

describe('LexicalMarkNode tests', () => {
initializeUnitTest((testEnv) => {
describe('$wrapSelectionInMarkNode', () => {
beforeEach(() => {
testEnv.editor.update(
() => {
$getRoot().clear().append($createParagraphNode());
},
{discrete: true},
);
});

test('wraps a whole text node', () => {
const {editor} = testEnv;

editor.update(() => {
const textNode = $createTextNode('marked');
const paragraphNode =
$getRoot().getFirstChildOrThrow<ParagraphNode>();
paragraphNode.append(textNode);
const selection = $createRangeSelection();
selection.anchor.set(textNode.getKey(), 0, 'text');
selection.focus.set(
textNode.getKey(),
textNode.getTextContent().length,
'text',
);
$wrapSelectionInMarkNode(selection, false, 'my-id');

expect(paragraphNode.getChildren()).toHaveLength(1);
const markNode = paragraphNode.getFirstChildOrThrow<MarkNode>();
expect(markNode.getType()).toEqual('mark');
expect(markNode.getIDs()).toEqual(['my-id']);
expect(markNode.getChildren()).toHaveLength(1);
expect(markNode.getFirstChildOrThrow().getKey()).toEqual(
textNode.getKey(),
);
expect(markNode.getFirstChildOrThrow().getTextContent()).toEqual(
'marked',
);
});
});

test('splits a text node if the selection is not at the start/end', () => {
const {editor} = testEnv;

editor.update(() => {
const textNode = $createTextNode('unmarked marked unmarked');
const paragraphNode =
$getRoot().getFirstChildOrThrow<ParagraphNode>();
paragraphNode.append(textNode);
const selection = $createRangeSelection();
selection.anchor.set(textNode.getKey(), 'unmarked '.length, 'text');
selection.focus.set(
textNode.getKey(),
'unmarked marked'.length,
'text',
);
$wrapSelectionInMarkNode(selection, false, 'my-id');

expect(paragraphNode.getTextContent()).toEqual(
'unmarked marked unmarked',
);
expect(paragraphNode.getChildren().map((c) => c.getType())).toEqual([
'text',
'mark',
'text',
]);
expect(
paragraphNode.getChildren().map((c) => c.getTextContent()),
).toEqual(['unmarked ', 'marked', ' unmarked']);
});
});

test('includes inline decorator nodes', () => {
const {editor} = testEnv;

editor.update(() => {
const decoratorNode = $createTestDecoratorNode();
const textNode = $createTextNode('more text');
const paragraphNode =
$getRoot().getFirstChildOrThrow<ParagraphNode>();
paragraphNode.append(decoratorNode, textNode);
const selection = $createRangeSelection();
selection.anchor.set(paragraphNode.getKey(), 0, 'text');
selection.focus.set(
paragraphNode.getKey(),
paragraphNode.getTextContent().length,
'text',
);
$wrapSelectionInMarkNode(selection, false, 'my-id');

expect(paragraphNode.getChildren()).toHaveLength(1);
const markNode = paragraphNode.getFirstChildOrThrow<MarkNode>();
expect(markNode.getType()).toEqual('mark');
expect(markNode.getChildren().map((c) => c.getKey())).toEqual([
decoratorNode.getKey(),
textNode.getKey(),
]);
});
});

test('includes inline element nodes', () => {
const {editor} = testEnv;

editor.update(() => {
const elementNode = $createTestInlineElementNode();
const textNode = $createTextNode('more text');
const paragraphNode =
$getRoot().getFirstChildOrThrow<ParagraphNode>();
paragraphNode.append(elementNode, textNode);
const selection = $createRangeSelection();
selection.anchor.set(paragraphNode.getKey(), 0, 'text');
selection.focus.set(
paragraphNode.getKey(),
paragraphNode.getTextContent().length,
'text',
);
$wrapSelectionInMarkNode(selection, false, 'my-id');

expect(paragraphNode.getChildren()).toHaveLength(1);
const markNode = paragraphNode.getFirstChildOrThrow<MarkNode>();
expect(markNode.getType()).toEqual('mark');
expect(markNode.getChildren().map((c) => c.getKey())).toEqual([
elementNode.getKey(),
textNode.getKey(),
]);
});
});

test('does not include block element nodes', () => {
const {editor} = testEnv;

editor.update(() => {
const elementNode = $createTestElementNode();
const textNode = $createTextNode('more text');
const paragraphNode =
$getRoot().getFirstChildOrThrow<ParagraphNode>();
paragraphNode.append(elementNode, textNode);
const selection = $createRangeSelection();
selection.anchor.set(paragraphNode.getKey(), 0, 'text');
selection.focus.set(
paragraphNode.getKey(),
paragraphNode.getTextContent().length,
'text',
);
$wrapSelectionInMarkNode(selection, false, 'my-id');

expect(paragraphNode.getChildren()).toHaveLength(2);
expect(paragraphNode.getChildAtIndex(0)!.getKey()).toEqual(
elementNode.getKey(),
);

// the text part of the selection should still be marked
const markNode = paragraphNode.getChildAtIndex(1) as MarkNode;
expect(markNode.getType()).toEqual('mark');
expect(markNode.getChildren()).toHaveLength(1);
expect(markNode.getTextContent()).toEqual('more text');
});
});
});
});
});
11 changes: 7 additions & 4 deletions packages/lexical-mark/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import type {SerializedMarkNode} from './MarkNode';
import type {LexicalNode, RangeSelection, TextNode} from 'lexical';

import {$isElementNode, $isTextNode} from 'lexical';
import {$isDecoratorNode, $isElementNode, $isTextNode} from 'lexical';

import {$createMarkNode, $isMarkNode, MarkNode} from './MarkNode';

Expand Down Expand Up @@ -84,9 +84,12 @@ export function $wrapSelectionInMarkNode(
// codebase.

continue;
} else if ($isElementNode(node) && node.isInline()) {
// Case 3: inline element nodes can be added in their entirety to the new
// mark
} else if (
($isElementNode(node) || $isDecoratorNode(node)) &&
node.isInline()
) {
// Case 3: inline element/decorator nodes can be added in their entirety
// to the new mark
targetNode = node;
}

Expand Down

0 comments on commit a62a1a6

Please sign in to comment.