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