Skip to content

Commit 7eae19a

Browse files
Claudio Procidammissey
authored andcommitted
Fixes runtime error when cutting empty selection at the end of the content
Summary: This diff introduces an undesired behavior introduced by D13085302 where the removeRange operation triggers an error if the selection is collapsed at the end of the last content block. Reviewed By: elboman Differential Revision: D15758815 fbshipit-source-id: 0f0c2cc94aed23643efb36a66587189e624addc6
1 parent c3a0726 commit 7eae19a

File tree

3 files changed

+909
-3
lines changed

3 files changed

+909
-3
lines changed

src/component/handlers/edit/commands/SecondaryClipboard.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ const SecondaryClipboard = {
3737
const blockEnd = content.getBlockForKey(anchorKey).getLength();
3838

3939
if (blockEnd === selection.getAnchorOffset()) {
40-
targetRange = selection
41-
.set('focusKey', content.getKeyAfter(anchorKey))
42-
.set('focusOffset', 0);
40+
const keyAfter = content.getKeyAfter(anchorKey);
41+
if (keyAfter == null) {
42+
return editorState;
43+
}
44+
targetRange = selection.set('focusKey', keyAfter).set('focusOffset', 0);
4345
} else {
4446
targetRange = selection.set('focusOffset', blockEnd);
4547
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
* @emails oncall+draft_js
8+
* @format
9+
* @flow strict-local
10+
*/
11+
12+
'use strict';
13+
14+
jest.disableAutomock();
15+
16+
jest.mock('generateRandomKey');
17+
18+
const toggleExperimentalTreeDataSupport = enabled => {
19+
jest.doMock('gkx', () => name => {
20+
return name === 'draft_tree_data_support' ? enabled : false;
21+
});
22+
};
23+
24+
// Seems to be important to put this at the top
25+
toggleExperimentalTreeDataSupport(true);
26+
27+
const BlockMapBuilder = require('BlockMapBuilder');
28+
const ContentBlockNode = require('ContentBlockNode');
29+
const EditorState = require('EditorState');
30+
const SecondaryClipboard = require('SecondaryClipboard');
31+
const SelectionState = require('SelectionState');
32+
33+
const getSampleStateForTesting = require('getSampleStateForTesting');
34+
const Immutable = require('immutable');
35+
36+
const {List} = Immutable;
37+
38+
const {contentState} = getSampleStateForTesting();
39+
40+
const contentBlockNodes = [
41+
new ContentBlockNode({
42+
key: 'A',
43+
nextSibling: 'B',
44+
text: 'Alpha',
45+
type: 'blockquote',
46+
}),
47+
new ContentBlockNode({
48+
key: 'B',
49+
prevSibling: 'A',
50+
nextSibling: 'G',
51+
type: 'ordered-list-item',
52+
children: List(['C', 'F']),
53+
}),
54+
new ContentBlockNode({
55+
parent: 'B',
56+
key: 'C',
57+
nextSibling: 'F',
58+
type: 'blockquote',
59+
children: List(['D', 'E']),
60+
}),
61+
new ContentBlockNode({
62+
parent: 'C',
63+
key: 'D',
64+
nextSibling: 'E',
65+
type: 'header-two',
66+
text: 'Delta',
67+
}),
68+
new ContentBlockNode({
69+
parent: 'C',
70+
key: 'E',
71+
prevSibling: 'D',
72+
type: 'unstyled',
73+
text: 'Elephant',
74+
}),
75+
new ContentBlockNode({
76+
parent: 'B',
77+
key: 'F',
78+
prevSibling: 'C',
79+
type: 'code-block',
80+
text: 'Fire',
81+
}),
82+
new ContentBlockNode({
83+
key: 'G',
84+
prevSibling: 'B',
85+
nextSibling: 'H',
86+
type: 'ordered-list-item',
87+
text: 'Gorila',
88+
}),
89+
new ContentBlockNode({
90+
key: 'H',
91+
prevSibling: 'G',
92+
nextSibling: 'I',
93+
text: ' ',
94+
type: 'atomic',
95+
}),
96+
new ContentBlockNode({
97+
key: 'I',
98+
prevSibling: 'H',
99+
text: 'last',
100+
type: 'unstyled',
101+
}),
102+
];
103+
104+
const assertCutOperation = (
105+
operation,
106+
selection = {},
107+
content = contentBlockNodes,
108+
) => {
109+
const result = operation(
110+
EditorState.forceSelection(
111+
EditorState.createWithContent(
112+
contentState.set('blockMap', BlockMapBuilder.createFromArray(content)),
113+
),
114+
SelectionState.createEmpty(content[0].key).merge(selection),
115+
),
116+
);
117+
const expected = result
118+
.getCurrentContent()
119+
.getBlockMap()
120+
.toJS();
121+
122+
expect(expected).toMatchSnapshot();
123+
};
124+
125+
test(`in the middle of a block, cut removes the remainder of the block`, () => {
126+
assertCutOperation(editorState => SecondaryClipboard.cut(editorState), {
127+
anchorKey: 'E',
128+
anchorOffset: contentBlockNodes[4].getLength() - 2,
129+
focusKey: 'E',
130+
focusOffset: contentBlockNodes[4].getLength() - 2,
131+
});
132+
});
133+
134+
test(`at the end of an intermediate block, cut merges with the adjacent content block`, () => {
135+
assertCutOperation(editorState => SecondaryClipboard.cut(editorState), {
136+
anchorKey: 'H',
137+
anchorOffset: contentBlockNodes[7].getLength(),
138+
focusKey: 'H',
139+
focusOffset: contentBlockNodes[7].getLength(),
140+
});
141+
});
142+
143+
test(`at the end of the last block, cut is a no-op`, () => {
144+
assertCutOperation(editorState => SecondaryClipboard.cut(editorState), {
145+
anchorKey: 'I',
146+
anchorOffset: contentBlockNodes[8].getLength(),
147+
focusKey: 'I',
148+
focusOffset: contentBlockNodes[8].getLength(),
149+
});
150+
});

0 commit comments

Comments
 (0)