Skip to content
This repository was archived by the owner on Feb 6, 2023. It is now read-only.

Commit fa88ee1

Browse files
sophiebitsfacebook-github-bot
authored andcommitted
Fix block tree before/after comparison
Summary: In some cases, we need to prevent native insertion because native insertion would leave the decorators in the wrong place (and critically – editOnInput's attempt to patch things up would just make things worse and process the insertion twice). This new logic should be correct. Previously, typing '#' before 'f' in 'x #foo' would result in 'x ###foo' (extra '#' inserted). Reviewed By: mitermayer Differential Revision: D7941738 fbshipit-source-id: eeff55ff082fc80c19f76fe1ea915bcbf0e7d86e
1 parent a1f4593 commit fa88ee1

File tree

2 files changed

+143
-13
lines changed

2 files changed

+143
-13
lines changed

src/component/handlers/edit/__tests__/editOnBeforeInput.test.js

+75-3
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
'use strict';
1515

1616
jest.disableAutomock();
17+
jest.mock('gkx', () => name => name === 'draft_improved_decorator_fingerprint');
1718

19+
const CompositeDraftDecorator = require('CompositeDraftDecorator');
1820
const ContentBlock = require('ContentBlock');
1921
const ContentState = require('ContentState');
2022
const EditorState = require('EditorState');
2123
const SelectionState = require('SelectionState');
24+
2225
const onBeforeInput = require('editOnBeforeInput');
2326

2427
const DEFAULT_SELECTION = {
@@ -40,20 +43,20 @@ const rangedSelectionBackwards = new SelectionState({
4043
isBackward: true,
4144
});
4245

43-
const getEditorState = () => {
46+
const getEditorState = (text: string = 'Arsenal') => {
4447
return EditorState.createWithContent(
4548
ContentState.createFromBlockArray([
4649
new ContentBlock({
4750
key: 'a',
48-
text: 'Arsenal',
51+
text,
4952
}),
5053
]),
5154
);
5255
};
5356

5457
const getInputEvent = data => ({
5558
data,
56-
preventDefault: () => {},
59+
preventDefault: jest.fn(),
5760
});
5861

5962
test('editor is not updated if no character data is provided', () => {
@@ -151,3 +154,72 @@ test('editor selectionstate is updated if new text matches current selection and
151154
const newEditorState = editor.update.mock.calls[0][0];
152155
expect(newEditorState.getSelection()).toMatchSnapshot();
153156
});
157+
158+
const HASHTAG_REGEX = /#[a-z]+/g;
159+
function hashtagStrategy(contentBlock, callback, contentState) {
160+
findWithRegex(HASHTAG_REGEX, contentBlock, callback);
161+
}
162+
163+
function findWithRegex(regex, contentBlock, callback) {
164+
const text = contentBlock.getText();
165+
let matchArr, start;
166+
while ((matchArr = regex.exec(text)) !== null) {
167+
start = matchArr.index;
168+
callback(start, start + matchArr[0].length);
169+
}
170+
}
171+
172+
function testDecoratorFingerprint(
173+
text,
174+
selection,
175+
charToInsert,
176+
shouldPrevent,
177+
) {
178+
const editorState = EditorState.acceptSelection(
179+
EditorState.set(getEditorState(text), {
180+
decorator: new CompositeDraftDecorator([
181+
{
182+
strategy: hashtagStrategy,
183+
component: null,
184+
},
185+
]),
186+
}),
187+
new SelectionState({
188+
...DEFAULT_SELECTION,
189+
anchorOffset: selection,
190+
focusOffset: selection,
191+
}),
192+
);
193+
194+
const editor = {
195+
_latestEditorState: editorState,
196+
_latestCommittedEditorState: editorState,
197+
props: {},
198+
update: jest.fn(),
199+
};
200+
201+
const ev = getInputEvent(charToInsert);
202+
onBeforeInput(editor, ev);
203+
204+
expect(ev.preventDefault.mock.calls.length).toBe(shouldPrevent ? 1 : 0);
205+
}
206+
207+
test('decorator fingerprint logic bails out of native insertion', () => {
208+
const oldGetSelection = global.getSelection;
209+
try {
210+
global.getSelection = () => ({});
211+
212+
// Make sure we prevent native insertion in the right cases
213+
testDecoratorFingerprint('hi #', 4, 'f', true);
214+
testDecoratorFingerprint('x #foo', 3, '#', true);
215+
testDecoratorFingerprint('#foobar', 4, ' ', true);
216+
217+
// but these are OK to let through
218+
testDecoratorFingerprint('#foo', 4, 'b', false);
219+
testDecoratorFingerprint('#foo bar #baz', 2, 'o', false);
220+
testDecoratorFingerprint('#foo bar #baz', 7, 'o', false);
221+
testDecoratorFingerprint('#foo bar #baz', 12, 'a', false);
222+
} finally {
223+
global.getSelection = oldGetSelection;
224+
}
225+
});

src/component/handlers/edit/editOnBeforeInput.js

+68-10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const EditorState = require('EditorState');
2121
const UserAgent = require('UserAgent');
2222

2323
const getEntityKeyForSelection = require('getEntityKeyForSelection');
24+
const gkx = require('gkx');
2425
const isEventHandled = require('isEventHandled');
2526
const isSelectionAtLeafStart = require('isSelectionAtLeafStart');
2627
const nullthrows = require('nullthrows');
@@ -187,16 +188,73 @@ function editOnBeforeInput(
187188
}
188189
}
189190
if (!mustPreventNative) {
190-
// Check the old and new "fingerprints" of the current block to determine
191-
// whether this insertion requires any addition or removal of text nodes,
192-
// in which case we would prevent the native character insertion.
193-
const originalFingerprint = BlockTree.getFingerprint(
194-
editorState.getBlockTree(anchorKey),
195-
);
196-
const newFingerprint = BlockTree.getFingerprint(
197-
newEditorState.getBlockTree(anchorKey),
198-
);
199-
mustPreventNative = originalFingerprint !== newFingerprint;
191+
if (gkx('draft_improved_decorator_fingerprint')) {
192+
// Let's say we have a decorator that highlights hashtags. In many cases
193+
// we need to prevent native behavior and rerender ourselves --
194+
// particularly, any case *except* where the inserted characters end up
195+
// anywhere except exactly where you put them.
196+
//
197+
// Using [] to denote a decorated leaf, some examples:
198+
//
199+
// 1. 'hi #' and append 'f'
200+
// desired rendering: 'hi [#f]'
201+
// native rendering would be: 'hi #f' (incorrect)
202+
//
203+
// 2. 'x [#foo]' and insert '#' before 'f'
204+
// desired rendering: 'x #[#foo]'
205+
// native rendering would be: 'x [##foo]' (incorrect)
206+
//
207+
// 3. '[#foobar]' and insert ' ' between 'foo' and 'bar'
208+
// desired rendering: '[#foo] bar'
209+
// native rendering would be: '[#foo bar]' (incorrect)
210+
//
211+
// 4. '[#foo]' and delete '#' [won't use this beforeinput codepath though]
212+
// desired rendering: 'foo'
213+
// native rendering would be: '[foo]' (incorrect)
214+
//
215+
// 5. '[#foo]' and append 'b'
216+
// desired rendering: '[#foob]'
217+
// native rendering would be: '[#foob]' (native insertion is OK here)
218+
//
219+
// It is safe to allow native insertion if and only if the full list of
220+
// decorator ranges matches what we expect native insertion to give. We
221+
// don't need to compare the content because the only possible mutation
222+
// to consider here is inserting plain text and decorators can't affect
223+
// text content.
224+
const oldBlockTree = editorState.getBlockTree(anchorKey);
225+
const newBlockTree = newEditorState.getBlockTree(anchorKey);
226+
mustPreventNative =
227+
oldBlockTree.size !== newBlockTree.size ||
228+
oldBlockTree.zip(newBlockTree).some(([oldLeafSet, newLeafSet]) => {
229+
// selectionStart is guaranteed to be selectionEnd here
230+
const oldStart = oldLeafSet.get('start');
231+
const adjustedStart =
232+
oldStart + (oldStart >= selectionStart ? chars.length : 0);
233+
const oldEnd = oldLeafSet.get('end');
234+
const adjustedEnd =
235+
oldEnd + (oldEnd >= selectionStart ? chars.length : 0);
236+
return (
237+
// Different decorators
238+
oldLeafSet.get('decoratorKey') !== newLeafSet.get('decoratorKey') ||
239+
// Different number of inline styles
240+
oldLeafSet.get('leaves').size !== newLeafSet.get('leaves').size ||
241+
// Different effective decorator position
242+
adjustedStart !== newLeafSet.get('start') ||
243+
adjustedEnd !== newLeafSet.get('end')
244+
);
245+
});
246+
} else {
247+
// Check the old and new "fingerprints" of the current block to determine
248+
// whether this insertion requires any addition or removal of text nodes,
249+
// in which case we would prevent the native character insertion.
250+
const originalFingerprint = BlockTree.getFingerprint(
251+
editorState.getBlockTree(anchorKey),
252+
);
253+
const newFingerprint = BlockTree.getFingerprint(
254+
newEditorState.getBlockTree(anchorKey),
255+
);
256+
mustPreventNative = originalFingerprint !== newFingerprint;
257+
}
200258
}
201259
if (!mustPreventNative) {
202260
mustPreventNative = mustPreventDefaultForCharacter(chars);

0 commit comments

Comments
 (0)