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

$splitNode & $insertNodeToNearestRoot for root selection #3442

Merged
merged 1 commit into from
Nov 28, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
$createHorizontalRuleNode,
INSERT_HORIZONTAL_RULE_COMMAND,
} from '@lexical/react/LexicalHorizontalRuleNode';
import {$insertNodeToNearestRoot} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
Expand All @@ -35,11 +36,7 @@ export default function HorizontalRulePlugin(): null {

if (focusNode !== null) {
const horizontalRuleNode = $createHorizontalRuleNode();
selection.insertParagraph();
selection.focus
.getNode()
.getTopLevelElementOrThrow()
.insertBefore(horizontalRuleNode);
$insertNodeToNearestRoot(horizontalRuleNode);
}

return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,10 @@ describe('LexicalUtils#splitNode', () => {
});
});
}

it('throws when splitting root', async () => {
await update(() => {
expect(() => $splitNode($getRoot(), 0)).toThrow();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import type {LexicalEditor, LexicalNode} from 'lexical';

import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import {$getRoot, $isElementNode} from 'lexical';
import {$getRoot, $isElementNode, $setSelection} from 'lexical';
import {$createRangeSelection} from 'lexical/src';
import {
$createTestDecoratorNode,
createTestEditor,
Expand Down Expand Up @@ -94,6 +95,38 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
selectionOffset: 0, // Selection on text node after "Hello" world
selectionPath: [0, 0],
},
{
_: 'insert with selection on root start',
expectedHtml:
'<test-decorator></test-decorator>' +
'<test-decorator></test-decorator>' +
'<p><span>Before</span></p>' +
'<p><span>After</span></p>',
initialHtml:
'<test-decorator></test-decorator>' +
'<p><span>Before</span></p>' +
'<p><span>After</span></p>',
selectionOffset: 0,
selectionPath: [],
},
{
_: 'insert with selection on root child',
expectedHtml:
'<p><span>Before</span></p>' +
'<test-decorator></test-decorator>' +
'<p><span>After</span></p>',
initialHtml: '<p>Before</p><p>After</p>',
selectionOffset: 1,
selectionPath: [],
},
{
_: 'insert with selection on root end',
expectedHtml:
'<p><span>Before</span></p>' + '<test-decorator></test-decorator>',
initialHtml: '<p>Before</p>',
selectionOffset: 1,
selectionPath: [],
},
];

for (const testCase of testCases) {
Expand All @@ -110,17 +143,27 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
.clear()
.append(...nodesToInsert);

let nodeToSplit: LexicalNode = $getRoot();
let selectionNode: LexicalNode = $getRoot();
for (const index of testCase.selectionPath) {
if (!$isElementNode(nodeToSplit)) {
if (!$isElementNode(selectionNode)) {
throw new Error(
'Expected node to be element (to traverse the tree)',
);
}
nodeToSplit = nodeToSplit.getChildAtIndex(index);
selectionNode = selectionNode.getChildAtIndex(index);
}

nodeToSplit.select(testCase.selectionOffset, testCase.selectionOffset);
// Calling selectionNode.select() would "normalize" selection and move it
// to text node (if available), while for the purpose of the test we'd want
// to use whatever was passed (e.g. keep selection on root node)
const selection = $createRangeSelection();
const type = $isElementNode(selectionNode) ? 'element' : 'text';
selection.anchor.key = selection.focus.key = selectionNode.getKey();
selection.anchor.offset = selection.focus.offset =
testCase.selectionOffset;
selection.anchor.type = selection.focus.type = type;
$setSelection(selection);

$insertNodeToNearestRoot($createTestDecoratorNode());

// Cleaning up list value attributes as it's not really needed in this test
Expand Down
43 changes: 29 additions & 14 deletions packages/lexical-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,23 +473,33 @@ export function $insertNodeToNearestRoot<T extends LexicalNode>(node: T): T {
const {focus} = selection;
const focusNode = focus.getNode();
const focusOffset = focus.offset;
let splitNode: ElementNode;
let splitOffset: number;

if ($isTextNode(focusNode)) {
splitNode = focusNode.getParentOrThrow();
splitOffset = focusNode.getIndexWithinParent();
if (focusOffset > 0) {
splitOffset += 1;
focusNode.splitText(focusOffset);

if ($isRootOrShadowRoot(focusNode)) {
const focusChild = focusNode.getChildAtIndex(focusOffset);
if (focusChild == null) {
focusNode.append(node);
} else {
focusChild.insertBefore(node);
}
node.selectNext();
} else {
splitNode = focusNode;
splitOffset = focusOffset;
let splitNode: ElementNode;
let splitOffset: number;
if ($isTextNode(focusNode)) {
splitNode = focusNode.getParentOrThrow();
splitOffset = focusNode.getIndexWithinParent();
if (focusOffset > 0) {
splitOffset += 1;
focusNode.splitText(focusOffset);
}
} else {
splitNode = focusNode;
splitOffset = focusOffset;
}
const [, rightTree] = $splitNode(splitNode, splitOffset);
rightTree.insertBefore(node);
rightTree.selectStart();
}
const [, rightTree] = $splitNode(splitNode, splitOffset);
rightTree.insertBefore(node);
rightTree.selectStart();
} else {
if ($isNodeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
const nodes = selection.getNodes();
Expand Down Expand Up @@ -524,6 +534,11 @@ export function $splitNode(
startNode = node;
}

invariant(
!$isRootOrShadowRoot(node),
'Can not call $splitNode() on root element',
);

const recurse = (
currentNode: LexicalNode,
): [ElementNode, ElementNode, LexicalNode] => {
Expand Down
19 changes: 18 additions & 1 deletion packages/lexical/src/__tests__/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ export function $createTestElementNode(): TestElementNode {
return new TestElementNode();
}

type SerializedTestTextNode = Spread<
{type: 'test_text'; version: 1},
SerializedTextNode
>;
export class TestTextNode extends TextNode {
static getType() {
return 'test_text';
Expand All @@ -164,6 +168,19 @@ export class TestTextNode extends TextNode {
// @ts-ignore
return new TestTextNode(node.__text, node.__key);
}

static importJSON(serializedNode: SerializedTestTextNode): TestTextNode {
// @ts-ignore
return new TestTextNode(serializedNode.__text);
}

exportJSON(): SerializedTestTextNode {
return {
...super.exportJSON(),
type: 'test_text',
version: 1,
};
}
}

export type SerializedTestInlineElementNode = Spread<
Expand Down Expand Up @@ -342,7 +359,7 @@ export class TestDecoratorNode extends DecoratorNode<JSX.Element> {
};
}

importDOM() {
static importDOM() {
return {
'test-decorator': (domNode: HTMLElement) => {
return {
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/invariant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function invariant(

throw new Error(
'Internal Lexical error: invariant() is meant to be replaced at compile ' +
'time. There is no runtime version.',
'time. There is no runtime version. Error: ' +
message,
);
}