This repository has been archived by the owner on Feb 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
DraftEditorCompositionHandler.js
243 lines (215 loc) · 7.86 KB
/
DraftEditorCompositionHandler.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
* @emails oncall+draft_js
*/
'use strict';
import type DraftEditor from 'DraftEditor.react';
const DOMObserver = require('DOMObserver');
const DraftModifier = require('DraftModifier');
const DraftOffsetKey = require('DraftOffsetKey');
const EditorState = require('EditorState');
const Keys = require('Keys');
const editOnSelect = require('editOnSelect');
const getContentEditableContainer = require('getContentEditableContainer');
const getDraftEditorSelection = require('getDraftEditorSelection');
const getEntityKeyForSelection = require('getEntityKeyForSelection');
const nullthrows = require('nullthrows');
/**
* Millisecond delay to allow `compositionstart` to fire again upon
* `compositionend`.
*
* This is used for Korean input to ensure that typing can continue without
* the editor trying to render too quickly. More specifically, Safari 7.1+
* triggers `compositionstart` a little slower than Chrome/FF, which
* leads to composed characters being resolved and re-render occurring
* sooner than we want.
*/
const RESOLVE_DELAY = 20;
/**
* A handful of variables used to track the current composition and its
* resolution status. These exist at the module level because it is not
* possible to have compositions occurring in multiple editors simultaneously,
* and it simplifies state management with respect to the DraftEditor component.
*/
let resolved = false;
let stillComposing = false;
let domObserver = null;
function startDOMObserver(editor: DraftEditor) {
if (!domObserver) {
domObserver = new DOMObserver(getContentEditableContainer(editor));
domObserver.start();
}
}
const DraftEditorCompositionHandler = {
/**
* A `compositionstart` event has fired while we're still in composition
* mode. Continue the current composition session to prevent a re-render.
*/
onCompositionStart: function(editor: DraftEditor): void {
stillComposing = true;
startDOMObserver(editor);
},
/**
* Attempt to end the current composition session.
*
* Defer handling because browser will still insert the chars into active
* element after `compositionend`. If a `compositionstart` event fires
* before `resolveComposition` executes, our composition session will
* continue.
*
* The `resolved` flag is useful because certain IME interfaces fire the
* `compositionend` event multiple times, thus queueing up multiple attempts
* at handling the composition. Since handling the same composition event
* twice could break the DOM, we only use the first event. Example: Arabic
* Google Input Tools on Windows 8.1 fires `compositionend` three times.
*/
onCompositionEnd: function(editor: DraftEditor): void {
resolved = false;
stillComposing = false;
setTimeout(() => {
if (!resolved) {
DraftEditorCompositionHandler.resolveComposition(editor);
}
}, RESOLVE_DELAY);
},
onSelect: editOnSelect,
/**
* In Safari, keydown events may fire when committing compositions. If
* the arrow keys are used to commit, prevent default so that the cursor
* doesn't move, otherwise it will jump back noticeably on re-render.
*/
onKeyDown: function(editor: DraftEditor, e: SyntheticKeyboardEvent<>): void {
if (!stillComposing) {
// If a keydown event is received after compositionend but before the
// 20ms timer expires (ex: type option-E then backspace, or type A then
// backspace in 2-Set Korean), we should immediately resolve the
// composition and reinterpret the key press in edit mode.
DraftEditorCompositionHandler.resolveComposition(editor);
editor._onKeyDown(e);
return;
}
if (e.which === Keys.RIGHT || e.which === Keys.LEFT) {
e.preventDefault();
}
},
/**
* Keypress events may fire when committing compositions. In Firefox,
* pressing RETURN commits the composition and inserts extra newline
* characters that we do not want. `preventDefault` allows the composition
* to be committed while preventing the extra characters.
*/
onKeyPress: function(editor: DraftEditor, e: SyntheticKeyboardEvent<>): void {
if (e.which === Keys.RETURN) {
e.preventDefault();
}
},
/**
* Attempt to insert composed characters into the document.
*
* If we are still in a composition session, do nothing. Otherwise, insert
* the characters into the document and terminate the composition session.
*
* If no characters were composed -- for instance, the user
* deleted all composed characters and committed nothing new --
* force a re-render. We also re-render when the composition occurs
* at the beginning of a leaf, to ensure that if the browser has
* created a new text node for the composition, we will discard it.
*
* Resetting innerHTML will move focus to the beginning of the editor,
* so we update to force it back to the correct place.
*/
resolveComposition: function(editor: DraftEditor): void {
if (stillComposing) {
return;
}
const mutations = nullthrows(domObserver).stopAndFlushMutations();
domObserver = null;
resolved = true;
let editorState = EditorState.set(editor._latestEditorState, {
inCompositionMode: false,
});
editor.exitCurrentMode();
if (!mutations.size) {
editor.update(editorState);
return;
}
// TODO, check if Facebook still needs this flag or if it could be removed.
// Since there can be multiple mutations providing a `composedChars` doesn't
// apply well on this new model.
// if (
// gkx('draft_handlebeforeinput_composed_text') &&
// editor.props.handleBeforeInput &&
// isEventHandled(
// editor.props.handleBeforeInput(
// composedChars,
// editorState,
// event.timeStamp,
// ),
// )
// ) {
// return;
// }
let contentState = editorState.getCurrentContent();
mutations.forEach((composedChars, offsetKey) => {
const {blockKey, decoratorKey, leafKey} = DraftOffsetKey.decode(
offsetKey,
);
const {start, end} = editorState
.getBlockTree(blockKey)
.getIn([decoratorKey, 'leaves', leafKey]);
const replacementRange = editorState.getSelection().merge({
anchorKey: blockKey,
focusKey: blockKey,
anchorOffset: start,
focusOffset: end,
isBackward: false,
});
const entityKey = getEntityKeyForSelection(
contentState,
replacementRange,
);
const currentStyle = contentState
.getBlockForKey(blockKey)
.getInlineStyleAt(start);
contentState = DraftModifier.replaceText(
contentState,
replacementRange,
composedChars,
currentStyle,
entityKey,
);
// We need to update the editorState so the leaf node ranges are properly
// updated and multiple mutations are correctly applied.
editorState = EditorState.set(editorState, {
currentContent: contentState,
});
});
// When we apply the text changes to the ContentState, the selection always
// goes to the end of the field, but it should just stay where it is
// after compositionEnd.
const documentSelection = getDraftEditorSelection(
editorState,
getContentEditableContainer(editor),
);
const compositionEndSelectionState = documentSelection.selectionState;
editor.restoreEditorDOM();
const editorStateWithUpdatedSelection = EditorState.acceptSelection(
editorState,
compositionEndSelectionState,
);
editor.update(
EditorState.push(
editorStateWithUpdatedSelection,
contentState,
'insert-characters',
),
);
},
};
module.exports = DraftEditorCompositionHandler;