Skip to content
Open
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
17 changes: 11 additions & 6 deletions packages/lexical/src/LexicalSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@ export class RangeSelection implements BaseSelection {

const firstPoint = this.isBackward() ? this.focus : this.anchor;
const firstNode = firstPoint.getNode();
const firstBlock = $getAncestor(firstNode, INTERNAL_$isBlock);
let firstBlock = $getAncestor(firstNode, INTERNAL_$isBlock);

const last = nodes[nodes.length - 1]!;

Expand Down Expand Up @@ -1383,17 +1383,22 @@ export class RangeSelection implements BaseSelection {
const blocksParent = $wrapInlineNodes(nodes);
const nodeToSelect = blocksParent.getLastDescendant()!;
const blocks = blocksParent.getChildren();
const shouldInsert = !$isElementNode(firstBlock) || !firstBlock.isEmpty();
const insertedParagraph = shouldInsert ? this.insertParagraph() : null;
const lastToInsert: LexicalNode | undefined = blocks[blocks.length - 1];
let firstToInsert: LexicalNode | undefined = blocks[0];
if (!firstBlock || !firstBlock.isAttached()) {
firstBlock = $getAncestor(
firstNode,
INTERNAL_$isBlock,
).getPreviousSibling();
}
const isMergeable = (node: LexicalNode): node is ElementNode =>
$isElementNode(node) &&
INTERNAL_$isBlock(node) &&
!node.isEmpty() &&
$isElementNode(firstBlock) &&
(!firstBlock.isEmpty() || firstBlock.canMergeWhenEmpty());

const shouldInsert = !$isElementNode(firstBlock) || !firstBlock.isEmpty();
const insertedParagraph = shouldInsert ? this.insertParagraph() : null;
const lastToInsert: LexicalNode | undefined = blocks[blocks.length - 1];
let firstToInsert: LexicalNode | undefined = blocks[0];
if (isMergeable(firstToInsert)) {
invariant(
$isElementNode(firstBlock),
Expand Down
113 changes: 67 additions & 46 deletions packages/lexical/src/__tests__/unit/LexicalSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ListItemNode,
ListNode,
} from '@lexical/list';
import {$createHeadingNode} from '@lexical/rich-text';
import {
$caretRangeFromSelection,
$comparePointCaretNext,
Expand Down Expand Up @@ -839,49 +840,6 @@ describe('LexicalSelection tests', () => {
});
});

describe('Regression tests for #6701', () => {
test('insertNodes fails an invariant when there is no Block ancestor', async () => {
class InlineElementNode extends ElementNode {
static clone(prevNode: InlineElementNode): InlineElementNode {
return new InlineElementNode(prevNode.__key);
}
static getType() {
return 'inline-element-node';
}
static importJSON(serializedNode: SerializedElementNode) {
return new InlineElementNode().updateFromJSON(serializedNode);
}
isInline() {
return true;
}
createDOM() {
return document.createElement('span');
}
updateDOM() {
return false;
}
}
const editor = createEditor({
nodes: [InlineElementNode],
onError: (err) => {
throw err;
},
});
expect(() =>
editor.update(
() => {
const textNode = $createTextNode('test');
$getRoot().clear().append(new InlineElementNode().append(textNode));
textNode.select().insertNodes([$createTextNode('more text')]);
},
{discrete: true},
),
).toThrow(
/Expected node TextNode of type text to have a block ElementNode ancestor/,
);
});
});

describe('getNodes()', () => {
initializeUnitTest((testEnv) => {
let paragraphNode: ParagraphNode;
Expand Down Expand Up @@ -1553,7 +1511,50 @@ describe('extract()', () => {
});
});

describe('Regression #7081', () => {
describe('Regression tests for #6701', () => {
test('insertNodes fails an invariant when there is no Block ancestor', async () => {
class InlineElementNode extends ElementNode {
static clone(prevNode: InlineElementNode): InlineElementNode {
return new InlineElementNode(prevNode.__key);
}
static getType() {
return 'inline-element-node';
}
static importJSON(serializedNode: SerializedElementNode) {
return new InlineElementNode().updateFromJSON(serializedNode);
}
isInline() {
return true;
}
createDOM() {
return document.createElement('span');
}
updateDOM() {
return false;
}
}
const editor = createEditor({
nodes: [InlineElementNode],
onError: (err) => {
throw err;
},
});
expect(() =>
editor.update(
() => {
const textNode = $createTextNode('test');
$getRoot().clear().append(new InlineElementNode().append(textNode));
textNode.select().insertNodes([$createTextNode('more text')]);
},
{discrete: true},
),
).toThrow(
/Expected node TextNode of type text to have a block ElementNode ancestor/,
);
});
});

describe('Regression tests for #7081', () => {
initializeUnitTest((testEnv) => {
test('Firefox selection & paste before linebreak', () => {
testEnv.editor.update(
Expand Down Expand Up @@ -1587,7 +1588,7 @@ describe('Regression #7081', () => {
});
});

describe('Regression #7173', () => {
describe('Regression tests for #7173', () => {
initializeUnitTest((testEnv) => {
test('Can insertNodes of multiple blocks with a target of an initial empty block and the entire next block', () => {
testEnv.editor.update(
Expand All @@ -1613,7 +1614,27 @@ describe('Regression #7173', () => {
});
});

describe('Regression #3181', () => {
describe('Regression tests for #7846', () => {
initializeUnitTest((testEnv) => {
test('insertNodes can insert in HeadingNode', async () => {
testEnv.editor.update(
() => {
const heading = $createHeadingNode('h1');
const headingText = $createTextNode(' chyeah');
$getRoot().clear().append(heading.append(headingText));
const paragraph = $createParagraphNode();
const paragraphText = $createTextNode('finna');
const selection = headingText.selectStart();
selection.insertNodes([paragraph.append(paragraphText)]);
expect($getRoot().getTextContent()).toEqual('finna chyeah');
},
{discrete: true},
);
});
});
});

describe('Regression tests for #3181', () => {
initializeUnitTest((testEnv) => {
test('Point.isBefore edge case with mixed TextNode & ElementNode and matching descendants', () => {
testEnv.editor.update(
Expand Down
Loading