main
js 460 lines 13.9 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 * @flow
8 */
9
10 import type {DOMEventName} from '../../events/DOMEventNames';
11 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 import type {AnyNativeEvent} from '../../events/PluginModuleType';
13 import type {DispatchQueue} from '../DOMPluginEventSystem';
14 import type {EventSystemFlags} from '../EventSystemFlags';
15 import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
16
17 import {canUseDOM} from 'shared/ExecutionEnvironment';
18
19 import {registerTwoPhaseEvent} from '../EventRegistry';
20 import {
21 getData as FallbackCompositionStateGetData,
22 initialize as FallbackCompositionStateInitialize,
23 reset as FallbackCompositionStateReset,
24 } from '../FallbackCompositionState';
25 import {
26 SyntheticCompositionEvent,
27 SyntheticInputEvent,
28 } from '../SyntheticEvent';
29 import {accumulateTwoPhaseListeners} from '../DOMPluginEventSystem';
30
31 const END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
32 const START_KEYCODE = 229;
33
34 const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window;
35
36 let documentMode = null;
37 if (canUseDOM && 'documentMode' in document) {
38 documentMode = document.documentMode;
39 }
40
41 // Webkit offers a very useful `textInput` event that can be used to
42 // directly represent `beforeInput`. The IE `textinput` event is not as
43 // useful, so we don't use it.
44 const canUseTextInputEvent =
45 canUseDOM && 'TextEvent' in window && !documentMode;
46
47 // In IE9+, we have access to composition events, but the data supplied
48 // by the native compositionend event may be incorrect. Japanese ideographic
49 // spaces, for instance (\u3000) are not recorded correctly.
50 const useFallbackCompositionData =
51 canUseDOM &&
52 (!canUseCompositionEvent ||
53 (documentMode && documentMode > 8 && documentMode <= 11));
54
55 const SPACEBAR_CODE = 32;
56 const SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
57
58 function registerEvents() {
59 registerTwoPhaseEvent('onBeforeInput', [
60 'compositionend',
61 'keypress',
62 'textInput',
63 'paste',
64 ]);
65 registerTwoPhaseEvent('onCompositionEnd', [
66 'compositionend',
67 'focusout',
68 'keydown',
69 'keypress',
70 'keyup',
71 'mousedown',
72 ]);
73 registerTwoPhaseEvent('onCompositionStart', [
74 'compositionstart',
75 'focusout',
76 'keydown',
77 'keypress',
78 'keyup',
79 'mousedown',
80 ]);
81 registerTwoPhaseEvent('onCompositionUpdate', [
82 'compositionupdate',
83 'focusout',
84 'keydown',
85 'keypress',
86 'keyup',
87 'mousedown',
88 ]);
89 }
90
91 // Track whether we've ever handled a keypress on the space key.
92 let hasSpaceKeypress = false;
93
94 /**
95 * Return whether a native keypress event is assumed to be a command.
96 * This is required because Firefox fires `keypress` events for key commands
97 * (cut, copy, select-all, etc.) even though no character is inserted.
98 */
99 function isKeypressCommand(nativeEvent: any) {
100 return (
101 (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
102 // ctrlKey && altKey is equivalent to AltGr, and is not a command.
103 !(nativeEvent.ctrlKey && nativeEvent.altKey)
104 );
105 }
106
107 /**
108 * Translate native top level events into event types.
109 */
110 function getCompositionEventType(domEventName: DOMEventName) {
111 switch (domEventName) {
112 case 'compositionstart':
113 return 'onCompositionStart';
114 case 'compositionend':
115 return 'onCompositionEnd';
116 case 'compositionupdate':
117 return 'onCompositionUpdate';
118 }
119 }
120
121 /**
122 * Does our fallback best-guess model think this event signifies that
123 * composition has begun?
124 */
125 function isFallbackCompositionStart(
126 domEventName: DOMEventName,
127 nativeEvent: any,
128 ): boolean {
129 return domEventName === 'keydown' && nativeEvent.keyCode === START_KEYCODE;
130 }
131
132 /**
133 * Does our fallback mode think that this event is the end of composition?
134 */
135 function isFallbackCompositionEnd(
136 domEventName: DOMEventName,
137 nativeEvent: any,
138 ): boolean {
139 switch (domEventName) {
140 case 'keyup':
141 // Command keys insert or clear IME input.
142 return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
143 case 'keydown':
144 // Expect IME keyCode on each keydown. If we get any other
145 // code we must have exited earlier.
146 return nativeEvent.keyCode !== START_KEYCODE;
147 case 'keypress':
148 case 'mousedown':
149 case 'focusout':
150 // Events are not possible without cancelling IME.
151 return true;
152 default:
153 return false;
154 }
155 }
156
157 /**
158 * Google Input Tools provides composition data via a CustomEvent,
159 * with the `data` property populated in the `detail` object. If this
160 * is available on the event object, use it. If not, this is a plain
161 * composition event and we have nothing special to extract.
162 *
163 * @param {object} nativeEvent
164 * @return {?string}
165 */
166 function getDataFromCustomEvent(nativeEvent: any) {
167 const detail = nativeEvent.detail;
168 if (typeof detail === 'object' && 'data' in detail) {
169 return detail.data;
170 }
171 return null;
172 }
173
174 /**
175 * Check if a composition event was triggered by Korean IME.
176 * Our fallback mode does not work well with IE's Korean IME,
177 * so just use native composition events when Korean IME is used.
178 * Although CompositionEvent.locale property is deprecated,
179 * it is available in IE, where our fallback mode is enabled.
180 *
181 * @param {object} nativeEvent
182 * @return {boolean}
183 */
184 function isUsingKoreanIME(nativeEvent: any) {
185 return nativeEvent.locale === 'ko';
186 }
187
188 // Track the current IME composition status, if any.
189 let isComposing = false;
190
191 /**
192 * @return {?object} A SyntheticCompositionEvent.
193 */
194 function extractCompositionEvent(
195 dispatchQueue: DispatchQueue,
196 domEventName: DOMEventName,
197 targetInst: null | Fiber,
198 nativeEvent: AnyNativeEvent,
199 nativeEventTarget: null | EventTarget,
200 ) {
201 let eventType;
202 let fallbackData;
203
204 if (canUseCompositionEvent) {
205 eventType = getCompositionEventType(domEventName);
206 } else if (!isComposing) {
207 if (isFallbackCompositionStart(domEventName, nativeEvent)) {
208 eventType = 'onCompositionStart';
209 }
210 } else if (isFallbackCompositionEnd(domEventName, nativeEvent)) {
211 eventType = 'onCompositionEnd';
212 }
213
214 if (!eventType) {
215 return null;
216 }
217
218 if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {
219 // The current composition is stored statically and must not be
220 // overwritten while composition continues.
221 if (!isComposing && eventType === 'onCompositionStart') {
222 isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
223 } else if (eventType === 'onCompositionEnd') {
224 if (isComposing) {
225 fallbackData = FallbackCompositionStateGetData();
226 }
227 }
228 }
229
230 const listeners = accumulateTwoPhaseListeners(targetInst, eventType);
231 if (listeners.length > 0) {
232 const event: ReactSyntheticEvent = new SyntheticCompositionEvent(
233 eventType,
234 domEventName,
235 null,
236 nativeEvent,
237 nativeEventTarget,
238 );
239 dispatchQueue.push({event, listeners});
240 if (fallbackData) {
241 // Inject data generated from fallback path into the synthetic event.
242 // This matches the property of native CompositionEventInterface.
243 // $FlowFixMe[incompatible-use]
244 event.data = fallbackData;
245 } else {
246 const customData = getDataFromCustomEvent(nativeEvent);
247 if (customData !== null) {
248 // $FlowFixMe[incompatible-use]
249 event.data = customData;
250 }
251 }
252 }
253 }
254
255 function getNativeBeforeInputChars(
256 domEventName: DOMEventName,
257 nativeEvent: any,
258 ): ?string {
259 switch (domEventName) {
260 case 'compositionend':
261 return getDataFromCustomEvent(nativeEvent);
262 case 'keypress':
263 /**
264 * If native `textInput` events are available, our goal is to make
265 * use of them. However, there is a special case: the spacebar key.
266 * In Webkit, preventing default on a spacebar `textInput` event
267 * cancels character insertion, but it *also* causes the browser
268 * to fall back to its default spacebar behavior of scrolling the
269 * page.
270 *
271 * Tracking at:
272 * https://code.google.com/p/chromium/issues/detail?id=355103
273 *
274 * To avoid this issue, use the keypress event as if no `textInput`
275 * event is available.
276 */
277 const which = nativeEvent.which;
278 if (which !== SPACEBAR_CODE) {
279 return null;
280 }
281
282 hasSpaceKeypress = true;
283 return SPACEBAR_CHAR;
284
285 case 'textInput':
286 // Record the characters to be added to the DOM.
287 const chars = nativeEvent.data;
288
289 // If it's a spacebar character, assume that we have already handled
290 // it at the keypress level and bail immediately. Android Chrome
291 // doesn't give us keycodes, so we need to ignore it.
292 if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
293 return null;
294 }
295
296 return chars;
297
298 default:
299 // For other native event types, do nothing.
300 return null;
301 }
302 }
303
304 /**
305 * For browsers that do not provide the `textInput` event, extract the
306 * appropriate string to use for SyntheticInputEvent.
307 */
308 function getFallbackBeforeInputChars(
309 domEventName: DOMEventName,
310 nativeEvent: any,
311 ): ?string {
312 // If we are currently composing (IME) and using a fallback to do so,
313 // try to extract the composed characters from the fallback object.
314 // If composition event is available, we extract a string only at
315 // compositionevent, otherwise extract it at fallback events.
316 if (isComposing) {
317 if (
318 domEventName === 'compositionend' ||
319 (!canUseCompositionEvent &&
320 isFallbackCompositionEnd(domEventName, nativeEvent))
321 ) {
322 const chars = FallbackCompositionStateGetData();
323 FallbackCompositionStateReset();
324 isComposing = false;
325 return chars;
326 }
327 return null;
328 }
329
330 switch (domEventName) {
331 case 'paste':
332 // If a paste event occurs after a keypress, throw out the input
333 // chars. Paste events should not lead to BeforeInput events.
334 return null;
335 case 'keypress':
336 /**
337 * As of v27, Firefox may fire keypress events even when no character
338 * will be inserted. A few possibilities:
339 *
340 * - `which` is `0`. Arrow keys, Esc key, etc.
341 *
342 * - `which` is the pressed key code, but no char is available.
343 * Ex: 'AltGr + d` in Polish. There is no modified character for
344 * this key combination and no character is inserted into the
345 * document, but FF fires the keypress for char code `100` anyway.
346 * No `input` event will occur.
347 *
348 * - `which` is the pressed key code, but a command combination is
349 * being used. Ex: `Cmd+C`. No character is inserted, and no
350 * `input` event will occur.
351 */
352 if (!isKeypressCommand(nativeEvent)) {
353 // IE fires the `keypress` event when a user types an emoji via
354 // Touch keyboard of Windows. In such a case, the `char` property
355 // holds an emoji character like `\uD83D\uDE0A`. Because its length
356 // is 2, the property `which` does not represent an emoji correctly.
357 // In such a case, we directly return the `char` property instead of
358 // using `which`.
359 if (nativeEvent.char && nativeEvent.char.length > 1) {
360 return nativeEvent.char;
361 } else if (nativeEvent.which) {
362 return String.fromCharCode(nativeEvent.which);
363 }
364 }
365 return null;
366 case 'compositionend':
367 return useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)
368 ? null
369 : nativeEvent.data;
370 default:
371 return null;
372 }
373 }
374
375 /**
376 * Extract a SyntheticInputEvent for `beforeInput`, based on either native
377 * `textInput` or fallback behavior.
378 *
379 * @return {?object} A SyntheticInputEvent.
380 */
381 function extractBeforeInputEvent(
382 dispatchQueue: DispatchQueue,
383 domEventName: DOMEventName,
384 targetInst: null | Fiber,
385 nativeEvent: AnyNativeEvent,
386 nativeEventTarget: null | EventTarget,
387 ) {
388 let chars;
389
390 if (canUseTextInputEvent) {
391 chars = getNativeBeforeInputChars(domEventName, nativeEvent);
392 } else {
393 chars = getFallbackBeforeInputChars(domEventName, nativeEvent);
394 }
395
396 // If no characters are being inserted, no BeforeInput event should
397 // be fired.
398 if (!chars) {
399 return null;
400 }
401
402 const listeners = accumulateTwoPhaseListeners(targetInst, 'onBeforeInput');
403 if (listeners.length > 0) {
404 const event: ReactSyntheticEvent = new SyntheticInputEvent(
405 'onBeforeInput',
406 'beforeinput',
407 null,
408 nativeEvent,
409 nativeEventTarget,
410 );
411 dispatchQueue.push({event, listeners});
412 // $FlowFixMe[incompatible-use]
413 event.data = chars;
414 }
415 }
416
417 /**
418 * Create an `onBeforeInput` event to match
419 * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
420 *
421 * This event plugin is based on the native `textInput` event
422 * available in Chrome, Safari, Opera, and IE. This event fires after
423 * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
424 *
425 * `beforeInput` is spec'd but not implemented in any browsers, and
426 * the `input` event does not provide any useful information about what has
427 * actually been added, contrary to the spec. Thus, `textInput` is the best
428 * available event to identify the characters that have actually been inserted
429 * into the target node.
430 *
431 * This plugin is also responsible for emitting `composition` events, thus
432 * allowing us to share composition fallback code for both `beforeInput` and
433 * `composition` event types.
434 */
435 function extractEvents(
436 dispatchQueue: DispatchQueue,
437 domEventName: DOMEventName,
438 targetInst: null | Fiber,
439 nativeEvent: AnyNativeEvent,
440 nativeEventTarget: null | EventTarget,
441 eventSystemFlags: EventSystemFlags,
442 targetContainer: EventTarget,
443 ): void {
444 extractCompositionEvent(
445 dispatchQueue,
446 domEventName,
447 targetInst,
448 nativeEvent,
449 nativeEventTarget,
450 );
451 extractBeforeInputEvent(
452 dispatchQueue,
453 domEventName,
454 targetInst,
455 nativeEvent,
456 nativeEventTarget,
457 );
458 }
459
460 export {registerEvents, extractEvents};