UNPKG

8.51 kBJavaScriptView Raw
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'use strict';
12
13var DraftModifier = require("./DraftModifier");
14
15var EditorState = require("./EditorState");
16
17var UserAgent = require("fbjs/lib/UserAgent");
18
19var getEntityKeyForSelection = require("./getEntityKeyForSelection");
20
21var isEventHandled = require("./isEventHandled");
22
23var isSelectionAtLeafStart = require("./isSelectionAtLeafStart");
24
25var nullthrows = require("fbjs/lib/nullthrows");
26
27var 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
36var FF_QUICKFIND_CHAR = "'";
37var FF_QUICKFIND_LINK_CHAR = '/';
38var isFirefox = UserAgent.isBrowser('Firefox');
39
40function 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
49function 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
64function 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
202module.exports = editOnBeforeInput;
\No newline at end of file