UNPKG

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