Skip to content

Commit aaa9009

Browse files
authored
[lexical] Bug Fix: Fix getNodes over-selection (facebook#7006)
1 parent 803391d commit aaa9009

File tree

2 files changed

+109
-2
lines changed

2 files changed

+109
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import {
10+
deleteBackward,
11+
moveToLineBeginning,
12+
} from '../keyboardShortcuts/index.mjs';
13+
import {
14+
assertHTML,
15+
focusEditor,
16+
html,
17+
initialize,
18+
test,
19+
} from '../utils/index.mjs';
20+
21+
test.describe('Regression tests for #6974', () => {
22+
test.beforeEach(({isPlainText, isCollab, page}) =>
23+
initialize({isCollab, isPlainText, page}),
24+
);
25+
26+
test(`deleteCharacter merges children from adjacent blocks even if the previous leaf is an inline decorator`, async ({
27+
page,
28+
isCollab,
29+
isPlainText,
30+
}) => {
31+
test.skip(isCollab || isPlainText);
32+
await focusEditor(page);
33+
const testEquation = '$x$';
34+
const testString = 'test';
35+
await page.keyboard.type(testEquation);
36+
await page.keyboard.press('Enter');
37+
await page.keyboard.type(testString);
38+
const beforeHtml = html`
39+
<p>
40+
<span contenteditable="false" data-lexical-decorator="true">
41+
<img alt="" src="#" />
42+
<span role="button" tabindex="-1">
43+
<span>
44+
<span aria-hidden="true">
45+
<span>
46+
<span></span>
47+
<span>x</span>
48+
</span>
49+
</span>
50+
</span>
51+
</span>
52+
<img alt="" src="#" />
53+
</span>
54+
<br />
55+
</p>
56+
<p dir="ltr"><span data-lexical-text="true">test</span></p>
57+
`;
58+
await assertHTML(page, beforeHtml, beforeHtml, {
59+
ignoreClasses: true,
60+
ignoreInlineStyles: true,
61+
});
62+
await moveToLineBeginning(page);
63+
await deleteBackward(page);
64+
const afterHtml = html`
65+
<p dir="ltr">
66+
<span contenteditable="false" data-lexical-decorator="true">
67+
<img alt="" src="#" />
68+
<span role="button" tabindex="-1">
69+
<span>
70+
<span aria-hidden="true">
71+
<span>
72+
<span></span>
73+
<span>x</span>
74+
</span>
75+
</span>
76+
</span>
77+
</span>
78+
<img alt="" src="#" />
79+
</span>
80+
<span data-lexical-text="true">test</span>
81+
</p>
82+
`;
83+
await assertHTML(page, afterHtml, afterHtml, {
84+
ignoreClasses: true,
85+
ignoreInlineStyles: true,
86+
});
87+
});
88+
});

packages/lexical/src/LexicalSelection.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ export class Point {
9696
_selection: BaseSelection | null;
9797

9898
constructor(key: NodeKey, offset: number, type: 'text' | 'element') {
99+
if (__DEV__) {
100+
// This prevents a circular reference error when serialized as JSON,
101+
// which happens on unit test failures
102+
Object.defineProperty(this, '_selection', {
103+
enumerable: false,
104+
writable: true,
105+
});
106+
}
99107
this._selection = null;
100108
this.key = key;
101109
this.offset = offset;
@@ -473,6 +481,10 @@ export class RangeSelection implements BaseSelection {
473481
const lastPoint = isBefore ? focus : anchor;
474482
let firstNode = firstPoint.getNode();
475483
let lastNode = lastPoint.getNode();
484+
const overselectedFirstNode =
485+
$isElementNode(firstNode) &&
486+
firstPoint.offset > 0 &&
487+
firstPoint.offset >= firstNode.getChildrenSize();
476488
const startOffset = firstPoint.offset;
477489
const endOffset = lastPoint.offset;
478490

@@ -506,6 +518,13 @@ export class RangeSelection implements BaseSelection {
506518
}
507519
} else {
508520
nodes = firstNode.getNodesBetween(lastNode);
521+
// Prevent over-selection due to the edge case of getDescendantByIndex always returning something #6974
522+
if (overselectedFirstNode) {
523+
const deleteCount = nodes.findIndex(
524+
(node) => !node.is(firstNode) && !node.isBefore(firstNode),
525+
);
526+
nodes.splice(0, deleteCount);
527+
}
509528
}
510529
if (!isCurrentlyReadOnlyMode()) {
511530
this._cachedNodes = nodes;
@@ -1129,7 +1148,7 @@ export class RangeSelection implements BaseSelection {
11291148
lastPoint.offset = lastNode.getTextContentSize();
11301149
}
11311150

1132-
selectedNodes.forEach((node) => {
1151+
for (const node of selectedNodes) {
11331152
if (
11341153
!$hasAncestor(firstNode, node) &&
11351154
!$hasAncestor(lastNode, node) &&
@@ -1138,7 +1157,7 @@ export class RangeSelection implements BaseSelection {
11381157
) {
11391158
node.remove();
11401159
}
1141-
});
1160+
}
11421161

11431162
const fixText = (node: TextNode, del: number) => {
11441163
if (node.getTextContent() === '') {

0 commit comments

Comments
 (0)