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 | * @format
|
8 | *
|
9 | * @emails oncall+draft_js
|
10 | */
|
11 | ;
|
12 |
|
13 | var DraftModifier = require("./DraftModifier");
|
14 |
|
15 | var EditorState = require("./EditorState");
|
16 |
|
17 | var UserAgent = require("fbjs/lib/UserAgent");
|
18 |
|
19 | var getEntityKeyForSelection = require("./getEntityKeyForSelection");
|
20 |
|
21 | var isEventHandled = require("./isEventHandled");
|
22 |
|
23 | var isSelectionAtLeafStart = require("./isSelectionAtLeafStart");
|
24 |
|
25 | var nullthrows = require("fbjs/lib/nullthrows");
|
26 |
|
27 | var setImmediate = require("fbjs/lib/setImmediate"); // When nothing is focused, Firefox regards two characters, `'` and `/`, as
|
28 | // commands that should open and focus the "quickfind" search bar. This should
|
29 | // *never* happen while a contenteditable is focused, but as of v28, it
|
30 | // sometimes does, even when the keypress event target is the contenteditable.
|
31 | // This breaks the input. Special case these characters to ensure that when
|
32 | // they are typed, we prevent default on the event to make sure not to
|
33 | // trigger quickfind.
|
34 |
|
35 |
|
36 | var FF_QUICKFIND_CHAR = "'";
|
37 | var FF_QUICKFIND_LINK_CHAR = '/';
|
38 | var isFirefox = UserAgent.isBrowser('Firefox');
|
39 |
|
40 | function mustPreventDefaultForCharacter(character) {
|
41 | return isFirefox && (character == FF_QUICKFIND_CHAR || character == FF_QUICKFIND_LINK_CHAR);
|
42 | }
|
43 | /**
|
44 | * Replace the current selection with the specified text string, with the
|
45 | * inline style and entity key applied to the newly inserted text.
|
46 | */
|
47 |
|
48 |
|
49 | function replaceText(editorState, text, inlineStyle, entityKey, forceSelection) {
|
50 | var contentState = DraftModifier.replaceText(editorState.getCurrentContent(), editorState.getSelection(), text, inlineStyle, entityKey);
|
51 | return EditorState.push(editorState, contentState, 'insert-characters', forceSelection);
|
52 | }
|
53 | /**
|
54 | * When `onBeforeInput` executes, the browser is attempting to insert a
|
55 | * character into the editor. Apply this character data to the document,
|
56 | * allowing native insertion if possible.
|
57 | *
|
58 | * Native insertion is encouraged in order to limit re-rendering and to
|
59 | * preserve spellcheck highlighting, which disappears or flashes if re-render
|
60 | * occurs on the relevant text nodes.
|
61 | */
|
62 |
|
63 |
|
64 | function editOnBeforeInput(editor, e) {
|
65 | if (editor._pendingStateFromBeforeInput !== undefined) {
|
66 | editor.update(editor._pendingStateFromBeforeInput);
|
67 | editor._pendingStateFromBeforeInput = undefined;
|
68 | }
|
69 |
|
70 | var editorState = editor._latestEditorState;
|
71 | var chars = e.data; // In some cases (ex: IE ideographic space insertion) no character data
|
72 | // is provided. There's nothing to do when this happens.
|
73 |
|
74 | if (!chars) {
|
75 | return;
|
76 | } // Allow the top-level component to handle the insertion manually. This is
|
77 | // useful when triggering interesting behaviors for a character insertion,
|
78 | // Simple examples: replacing a raw text ':)' with a smile emoji or image
|
79 | // decorator, or setting a block to be a list item after typing '- ' at the
|
80 | // start of the block.
|
81 |
|
82 |
|
83 | if (editor.props.handleBeforeInput && isEventHandled(editor.props.handleBeforeInput(chars, editorState, e.timeStamp))) {
|
84 | e.preventDefault();
|
85 | return;
|
86 | } // If selection is collapsed, conditionally allow native behavior. This
|
87 | // reduces re-renders and preserves spellcheck highlighting. If the selection
|
88 | // is not collapsed, we will re-render.
|
89 |
|
90 |
|
91 | var selection = editorState.getSelection();
|
92 | var selectionStart = selection.getStartOffset();
|
93 | var anchorKey = selection.getAnchorKey();
|
94 |
|
95 | if (!selection.isCollapsed()) {
|
96 | e.preventDefault();
|
97 | editor.update(replaceText(editorState, chars, editorState.getCurrentInlineStyle(), getEntityKeyForSelection(editorState.getCurrentContent(), editorState.getSelection()), true));
|
98 | return;
|
99 | }
|
100 |
|
101 | var newEditorState = replaceText(editorState, chars, editorState.getCurrentInlineStyle(), getEntityKeyForSelection(editorState.getCurrentContent(), editorState.getSelection()), false); // Bunch of different cases follow where we need to prevent native insertion.
|
102 |
|
103 | var mustPreventNative = false;
|
104 |
|
105 | if (!mustPreventNative) {
|
106 | // Browsers tend to insert text in weird places in the DOM when typing at
|
107 | // the start of a leaf, so we'll handle it ourselves.
|
108 | mustPreventNative = isSelectionAtLeafStart(editor._latestCommittedEditorState);
|
109 | }
|
110 |
|
111 | if (!mustPreventNative) {
|
112 | // Let's say we have a decorator that highlights hashtags. In many cases
|
113 | // we need to prevent native behavior and rerender ourselves --
|
114 | // particularly, any case *except* where the inserted characters end up
|
115 | // anywhere except exactly where you put them.
|
116 | //
|
117 | // Using [] to denote a decorated leaf, some examples:
|
118 | //
|
119 | // 1. 'hi #' and append 'f'
|
120 | // desired rendering: 'hi [#f]'
|
121 | // native rendering would be: 'hi #f' (incorrect)
|
122 | //
|
123 | // 2. 'x [#foo]' and insert '#' before 'f'
|
124 | // desired rendering: 'x #[#foo]'
|
125 | // native rendering would be: 'x [##foo]' (incorrect)
|
126 | //
|
127 | // 3. '[#foobar]' and insert ' ' between 'foo' and 'bar'
|
128 | // desired rendering: '[#foo] bar'
|
129 | // native rendering would be: '[#foo bar]' (incorrect)
|
130 | //
|
131 | // 4. '[#foo]' and delete '#' [won't use this beforeinput codepath though]
|
132 | // desired rendering: 'foo'
|
133 | // native rendering would be: '[foo]' (incorrect)
|
134 | //
|
135 | // 5. '[#foo]' and append 'b'
|
136 | // desired rendering: '[#foob]'
|
137 | // native rendering would be: '[#foob]'
|
138 | // (native insertion here would be ok for decorators like simple spans,
|
139 | // but not more complex decorators. To be safe, we need to prevent it.)
|
140 | //
|
141 | // It is safe to allow native insertion if and only if the full list of
|
142 | // decorator ranges matches what we expect native insertion to give, and
|
143 | // the range lengths have not changed. We don't need to compare the content
|
144 | // because the only possible mutation to consider here is inserting plain
|
145 | // text and decorators can't affect text content.
|
146 | var oldBlockTree = editorState.getBlockTree(anchorKey);
|
147 | var newBlockTree = newEditorState.getBlockTree(anchorKey);
|
148 | mustPreventNative = oldBlockTree.size !== newBlockTree.size || oldBlockTree.zip(newBlockTree).some(function (_ref) {
|
149 | var oldLeafSet = _ref[0],
|
150 | newLeafSet = _ref[1];
|
151 | // selectionStart is guaranteed to be selectionEnd here
|
152 | var oldStart = oldLeafSet.get('start');
|
153 | var adjustedStart = oldStart + (oldStart >= selectionStart ? chars.length : 0);
|
154 | var oldEnd = oldLeafSet.get('end');
|
155 | var adjustedEnd = oldEnd + (oldEnd >= selectionStart ? chars.length : 0);
|
156 | var newStart = newLeafSet.get('start');
|
157 | var newEnd = newLeafSet.get('end');
|
158 | var newDecoratorKey = newLeafSet.get('decoratorKey');
|
159 | return (// Different decorators
|
160 | oldLeafSet.get('decoratorKey') !== newDecoratorKey || // Different number of inline styles
|
161 | oldLeafSet.get('leaves').size !== newLeafSet.get('leaves').size || // Different effective decorator position
|
162 | adjustedStart !== newStart || adjustedEnd !== newEnd || // Decorator already existed and its length changed
|
163 | newDecoratorKey != null && newEnd - newStart !== oldEnd - oldStart
|
164 | );
|
165 | });
|
166 | }
|
167 |
|
168 | if (!mustPreventNative) {
|
169 | mustPreventNative = mustPreventDefaultForCharacter(chars);
|
170 | }
|
171 |
|
172 | if (!mustPreventNative) {
|
173 | mustPreventNative = nullthrows(newEditorState.getDirectionMap()).get(anchorKey) !== nullthrows(editorState.getDirectionMap()).get(anchorKey);
|
174 | }
|
175 |
|
176 | if (mustPreventNative) {
|
177 | e.preventDefault();
|
178 | newEditorState = EditorState.set(newEditorState, {
|
179 | forceSelection: true
|
180 | });
|
181 | editor.update(newEditorState);
|
182 | return;
|
183 | } // We made it all the way! Let the browser do its thing and insert the char.
|
184 |
|
185 |
|
186 | newEditorState = EditorState.set(newEditorState, {
|
187 | nativelyRenderedContent: newEditorState.getCurrentContent()
|
188 | }); // The native event is allowed to occur. To allow user onChange handlers to
|
189 | // change the inserted text, we wait until the text is actually inserted
|
190 | // before we actually update our state. That way when we rerender, the text
|
191 | // we see in the DOM will already have been inserted properly.
|
192 |
|
193 | editor._pendingStateFromBeforeInput = newEditorState;
|
194 | setImmediate(function () {
|
195 | if (editor._pendingStateFromBeforeInput !== undefined) {
|
196 | editor.update(editor._pendingStateFromBeforeInput);
|
197 | editor._pendingStateFromBeforeInput = undefined;
|
198 | }
|
199 | });
|
200 | }
|
201 |
|
202 | module.exports = editOnBeforeInput; |
\ | No newline at end of file |