UNPKG

13.3 kBJavaScriptView Raw
1/**
2 * Copyright 2013-present Facebook, Inc.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
8 *
9 */
10
11'use strict';
12
13var EventPropagators = require('./EventPropagators');
14var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
15var FallbackCompositionState = require('./FallbackCompositionState');
16var SyntheticCompositionEvent = require('./SyntheticCompositionEvent');
17var SyntheticInputEvent = require('./SyntheticInputEvent');
18
19var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
20var START_KEYCODE = 229;
21
22var canUseCompositionEvent = ExecutionEnvironment.canUseDOM && 'CompositionEvent' in window;
23
24var documentMode = null;
25if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) {
26 documentMode = document.documentMode;
27}
28
29// Webkit offers a very useful `textInput` event that can be used to
30// directly represent `beforeInput`. The IE `textinput` event is not as
31// useful, so we don't use it.
32var canUseTextInputEvent = ExecutionEnvironment.canUseDOM && 'TextEvent' in window && !documentMode && !isPresto();
33
34// In IE9+, we have access to composition events, but the data supplied
35// by the native compositionend event may be incorrect. Japanese ideographic
36// spaces, for instance (\u3000) are not recorded correctly.
37var useFallbackCompositionData = ExecutionEnvironment.canUseDOM && (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11);
38
39/**
40 * Opera <= 12 includes TextEvent in window, but does not fire
41 * text input events. Rely on keypress instead.
42 */
43function isPresto() {
44 var opera = window.opera;
45 return typeof opera === 'object' && typeof opera.version === 'function' && parseInt(opera.version(), 10) <= 12;
46}
47
48var SPACEBAR_CODE = 32;
49var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
50
51// Events and their corresponding property names.
52var eventTypes = {
53 beforeInput: {
54 phasedRegistrationNames: {
55 bubbled: 'onBeforeInput',
56 captured: 'onBeforeInputCapture'
57 },
58 dependencies: ['topCompositionEnd', 'topKeyPress', 'topTextInput', 'topPaste']
59 },
60 compositionEnd: {
61 phasedRegistrationNames: {
62 bubbled: 'onCompositionEnd',
63 captured: 'onCompositionEndCapture'
64 },
65 dependencies: ['topBlur', 'topCompositionEnd', 'topKeyDown', 'topKeyPress', 'topKeyUp', 'topMouseDown']
66 },
67 compositionStart: {
68 phasedRegistrationNames: {
69 bubbled: 'onCompositionStart',
70 captured: 'onCompositionStartCapture'
71 },
72 dependencies: ['topBlur', 'topCompositionStart', 'topKeyDown', 'topKeyPress', 'topKeyUp', 'topMouseDown']
73 },
74 compositionUpdate: {
75 phasedRegistrationNames: {
76 bubbled: 'onCompositionUpdate',
77 captured: 'onCompositionUpdateCapture'
78 },
79 dependencies: ['topBlur', 'topCompositionUpdate', 'topKeyDown', 'topKeyPress', 'topKeyUp', 'topMouseDown']
80 }
81};
82
83// Track whether we've ever handled a keypress on the space key.
84var hasSpaceKeypress = false;
85
86/**
87 * Return whether a native keypress event is assumed to be a command.
88 * This is required because Firefox fires `keypress` events for key commands
89 * (cut, copy, select-all, etc.) even though no character is inserted.
90 */
91function isKeypressCommand(nativeEvent) {
92 return (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
93 // ctrlKey && altKey is equivalent to AltGr, and is not a command.
94 !(nativeEvent.ctrlKey && nativeEvent.altKey);
95}
96
97/**
98 * Translate native top level events into event types.
99 *
100 * @param {string} topLevelType
101 * @return {object}
102 */
103function getCompositionEventType(topLevelType) {
104 switch (topLevelType) {
105 case 'topCompositionStart':
106 return eventTypes.compositionStart;
107 case 'topCompositionEnd':
108 return eventTypes.compositionEnd;
109 case 'topCompositionUpdate':
110 return eventTypes.compositionUpdate;
111 }
112}
113
114/**
115 * Does our fallback best-guess model think this event signifies that
116 * composition has begun?
117 *
118 * @param {string} topLevelType
119 * @param {object} nativeEvent
120 * @return {boolean}
121 */
122function isFallbackCompositionStart(topLevelType, nativeEvent) {
123 return topLevelType === 'topKeyDown' && nativeEvent.keyCode === START_KEYCODE;
124}
125
126/**
127 * Does our fallback mode think that this event is the end of composition?
128 *
129 * @param {string} topLevelType
130 * @param {object} nativeEvent
131 * @return {boolean}
132 */
133function isFallbackCompositionEnd(topLevelType, nativeEvent) {
134 switch (topLevelType) {
135 case 'topKeyUp':
136 // Command keys insert or clear IME input.
137 return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
138 case 'topKeyDown':
139 // Expect IME keyCode on each keydown. If we get any other
140 // code we must have exited earlier.
141 return nativeEvent.keyCode !== START_KEYCODE;
142 case 'topKeyPress':
143 case 'topMouseDown':
144 case 'topBlur':
145 // Events are not possible without cancelling IME.
146 return true;
147 default:
148 return false;
149 }
150}
151
152/**
153 * Google Input Tools provides composition data via a CustomEvent,
154 * with the `data` property populated in the `detail` object. If this
155 * is available on the event object, use it. If not, this is a plain
156 * composition event and we have nothing special to extract.
157 *
158 * @param {object} nativeEvent
159 * @return {?string}
160 */
161function getDataFromCustomEvent(nativeEvent) {
162 var detail = nativeEvent.detail;
163 if (typeof detail === 'object' && 'data' in detail) {
164 return detail.data;
165 }
166 return null;
167}
168
169// Track the current IME composition fallback object, if any.
170var currentComposition = null;
171
172/**
173 * @return {?object} A SyntheticCompositionEvent.
174 */
175function extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
176 var eventType;
177 var fallbackData;
178
179 if (canUseCompositionEvent) {
180 eventType = getCompositionEventType(topLevelType);
181 } else if (!currentComposition) {
182 if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
183 eventType = eventTypes.compositionStart;
184 }
185 } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
186 eventType = eventTypes.compositionEnd;
187 }
188
189 if (!eventType) {
190 return null;
191 }
192
193 if (useFallbackCompositionData) {
194 // The current composition is stored statically and must not be
195 // overwritten while composition continues.
196 if (!currentComposition && eventType === eventTypes.compositionStart) {
197 currentComposition = FallbackCompositionState.getPooled(nativeEventTarget);
198 } else if (eventType === eventTypes.compositionEnd) {
199 if (currentComposition) {
200 fallbackData = currentComposition.getData();
201 }
202 }
203 }
204
205 var event = SyntheticCompositionEvent.getPooled(eventType, targetInst, nativeEvent, nativeEventTarget);
206
207 if (fallbackData) {
208 // Inject data generated from fallback path into the synthetic event.
209 // This matches the property of native CompositionEventInterface.
210 event.data = fallbackData;
211 } else {
212 var customData = getDataFromCustomEvent(nativeEvent);
213 if (customData !== null) {
214 event.data = customData;
215 }
216 }
217
218 EventPropagators.accumulateTwoPhaseDispatches(event);
219 return event;
220}
221
222/**
223 * @param {string} topLevelType Record from `EventConstants`.
224 * @param {object} nativeEvent Native browser event.
225 * @return {?string} The string corresponding to this `beforeInput` event.
226 */
227function getNativeBeforeInputChars(topLevelType, nativeEvent) {
228 switch (topLevelType) {
229 case 'topCompositionEnd':
230 return getDataFromCustomEvent(nativeEvent);
231 case 'topKeyPress':
232 /**
233 * If native `textInput` events are available, our goal is to make
234 * use of them. However, there is a special case: the spacebar key.
235 * In Webkit, preventing default on a spacebar `textInput` event
236 * cancels character insertion, but it *also* causes the browser
237 * to fall back to its default spacebar behavior of scrolling the
238 * page.
239 *
240 * Tracking at:
241 * https://code.google.com/p/chromium/issues/detail?id=355103
242 *
243 * To avoid this issue, use the keypress event as if no `textInput`
244 * event is available.
245 */
246 var which = nativeEvent.which;
247 if (which !== SPACEBAR_CODE) {
248 return null;
249 }
250
251 hasSpaceKeypress = true;
252 return SPACEBAR_CHAR;
253
254 case 'topTextInput':
255 // Record the characters to be added to the DOM.
256 var chars = nativeEvent.data;
257
258 // If it's a spacebar character, assume that we have already handled
259 // it at the keypress level and bail immediately. Android Chrome
260 // doesn't give us keycodes, so we need to blacklist it.
261 if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
262 return null;
263 }
264
265 return chars;
266
267 default:
268 // For other native event types, do nothing.
269 return null;
270 }
271}
272
273/**
274 * For browsers that do not provide the `textInput` event, extract the
275 * appropriate string to use for SyntheticInputEvent.
276 *
277 * @param {string} topLevelType Record from `EventConstants`.
278 * @param {object} nativeEvent Native browser event.
279 * @return {?string} The fallback string for this `beforeInput` event.
280 */
281function getFallbackBeforeInputChars(topLevelType, nativeEvent) {
282 // If we are currently composing (IME) and using a fallback to do so,
283 // try to extract the composed characters from the fallback object.
284 // If composition event is available, we extract a string only at
285 // compositionevent, otherwise extract it at fallback events.
286 if (currentComposition) {
287 if (topLevelType === 'topCompositionEnd' || !canUseCompositionEvent && isFallbackCompositionEnd(topLevelType, nativeEvent)) {
288 var chars = currentComposition.getData();
289 FallbackCompositionState.release(currentComposition);
290 currentComposition = null;
291 return chars;
292 }
293 return null;
294 }
295
296 switch (topLevelType) {
297 case 'topPaste':
298 // If a paste event occurs after a keypress, throw out the input
299 // chars. Paste events should not lead to BeforeInput events.
300 return null;
301 case 'topKeyPress':
302 /**
303 * As of v27, Firefox may fire keypress events even when no character
304 * will be inserted. A few possibilities:
305 *
306 * - `which` is `0`. Arrow keys, Esc key, etc.
307 *
308 * - `which` is the pressed key code, but no char is available.
309 * Ex: 'AltGr + d` in Polish. There is no modified character for
310 * this key combination and no character is inserted into the
311 * document, but FF fires the keypress for char code `100` anyway.
312 * No `input` event will occur.
313 *
314 * - `which` is the pressed key code, but a command combination is
315 * being used. Ex: `Cmd+C`. No character is inserted, and no
316 * `input` event will occur.
317 */
318 if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {
319 return String.fromCharCode(nativeEvent.which);
320 }
321 return null;
322 case 'topCompositionEnd':
323 return useFallbackCompositionData ? null : nativeEvent.data;
324 default:
325 return null;
326 }
327}
328
329/**
330 * Extract a SyntheticInputEvent for `beforeInput`, based on either native
331 * `textInput` or fallback behavior.
332 *
333 * @return {?object} A SyntheticInputEvent.
334 */
335function extractBeforeInputEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
336 var chars;
337
338 if (canUseTextInputEvent) {
339 chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
340 } else {
341 chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
342 }
343
344 // If no characters are being inserted, no BeforeInput event should
345 // be fired.
346 if (!chars) {
347 return null;
348 }
349
350 var event = SyntheticInputEvent.getPooled(eventTypes.beforeInput, targetInst, nativeEvent, nativeEventTarget);
351
352 event.data = chars;
353 EventPropagators.accumulateTwoPhaseDispatches(event);
354 return event;
355}
356
357/**
358 * Create an `onBeforeInput` event to match
359 * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
360 *
361 * This event plugin is based on the native `textInput` event
362 * available in Chrome, Safari, Opera, and IE. This event fires after
363 * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
364 *
365 * `beforeInput` is spec'd but not implemented in any browsers, and
366 * the `input` event does not provide any useful information about what has
367 * actually been added, contrary to the spec. Thus, `textInput` is the best
368 * available event to identify the characters that have actually been inserted
369 * into the target node.
370 *
371 * This plugin is also responsible for emitting `composition` events, thus
372 * allowing us to share composition fallback code for both `beforeInput` and
373 * `composition` event types.
374 */
375var BeforeInputEventPlugin = {
376 eventTypes: eventTypes,
377
378 extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
379 return [extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget), extractBeforeInputEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget)];
380 }
381};
382
383module.exports = BeforeInputEventPlugin;
\No newline at end of file