@samitouri / QOS-React-2 / commits / 116ee058eb

TextInputEventPlugin, SyntheticTextInputEvent

This diff introduces `TextInputEventPlugin` and `SyntheticTextInputEvent`, which are based on Webkit's native `textInput` event. In Chrome, Safari, and Opera, the `textInput` event fires prior to the insertion of character data into the document. For normal typing, for example, thevent sequence is: `keydown`, `keypress`, `textInput`, `input`, `keyup`. The `textInput` event contains a `data` field corresponding to the character data that will actually be inserted. There is also a `beforeinput` event described by http://www.w3.org/TR/DOM-Level-3-Events/#event-type-beforeinput, and this is essentially that event, so it may make sense to rename the plugin. This event is especially useful because it solves a number of issues we can't currently handle with only keypress and composition events: - **Windows Chrome: Trailing characters discarded in Korean IME.** For instance, `안녕하세요` becomes `안녕하세` with the final character discarded by the final `compositionend` event. The `textInput` event fires correctly with the final character. - **Windows Chrome: Special characters discarded in IME.** Certain ideographs are discarded in IME mode. In Japanese, typing the ideographic space character is not represented by keypress but //is// represented by the subsequent `textInput`. This issue also applies to punctuation characters in Chinese. - **OSX Chrome: Characters from palette discarded.** Inserting characters from the OSX character palette fails, since no keypress is fired. The plugin is useful for Firefox and IE. For these, we record inserted characters via keypress and compositionend events and dispatch the synthetic event with these characters as the `data` field to match the native `textInput` event. - Firefox has no corresponding `textInput` event and has not yet implemented `beforeinput`. - IE has a native `textinput` event, but it fires after the DOM mutation has already occurred, so it isn't very useful as an analog to the Webkit version. I'm just not going to bother with it.

Isaac Salier-Hellendag committed Mar 25, 2014 at 12:30 UTC 116ee058ebd76071a8aefcd95254f772ec18605f
7 files changed +234 -2
src/browser/ReactEventEmitter.js
+1
@@ -115,6 +115,7 @@ var topEventMapping = {
115 topPaste: 'paste',
116 topScroll: 'scroll',
117 topSelectionChange: 'selectionchange',
118 + topTextInput: 'textInput',
119 topTouchCancel: 'touchcancel',
120 topTouchEnd: 'touchend',
121 topTouchMove: 'touchmove',
src/browser/eventPlugins/BeforeInputEventPlugin.js new
+171
@@ -0,0 +1,171 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule BeforeInputEventPlugin
17 + * @typechecks static-only
18 + */
19 +
20 +"use strict";
21 +
22 +var EventConstants = require('EventConstants');
23 +var EventPropagators = require('EventPropagators');
24 +var ExecutionEnvironment = require('ExecutionEnvironment');
25 +var SyntheticInputEvent = require('SyntheticInputEvent');
26 +
27 +var keyOf = require('keyOf');
28 +
29 +var useBeforeInputEvent = (
30 + ExecutionEnvironment.canUseDOM &&
31 + 'TextEvent' in window &&
32 + !('documentMode' in document)
33 +);
34 +
35 +var topLevelTypes = EventConstants.topLevelTypes;
36 +
37 +// Events and their corresponding property names.
38 +var eventTypes = {
39 + beforeInput: {
40 + phasedRegistrationNames: {
41 + bubbled: keyOf({onBeforeInput: null}),
42 + captured: keyOf({onBeforeInputCapture: null})
43 + },
44 + dependencies: [
45 + topLevelTypes.topCompositionEnd,
46 + topLevelTypes.topKeyPress,
47 + topLevelTypes.topTextInput,
48 + topLevelTypes.topPaste
49 + ]
50 + }
51 +};
52 +
53 +// Track characters inserted via keypress and composition events.
54 +var fallbackChars = null;
55 +
56 +/**
57 + * Return whether a native keypress event is assumed to be a command.
58 + * This is required because Firefox fires `keypress` events for key commands
59 + * (cut, copy, select-all, etc.) even though no character is inserted.
60 + */
61 +function isKeypressCommand(nativeEvent) {
62 + return (
63 + (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
64 + // ctrlKey && altKey is equivalent to AltGr, and is not a command.
65 + !(nativeEvent.ctrlKey && nativeEvent.altKey)
66 + );
67 +}
68 +
69 +/**
70 + * Create an `onBeforeInput` event to match
71 + * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
72 + *
73 + * This event plugin is based on the native `textInput` event
74 + * available in Chrome, Safari, Opera, and IE. This event fires after
75 + * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
76 + *
77 + * `beforeInput` is spec'd but not implemented in any browsers, and
78 + * the `input` event does not provide any useful information about what has
79 + * actually been added, contrary to the spec. Thus, `textInput` is the best
80 + * available event to identify the characters that have actually been inserted
81 + * into the target node.
82 + */
83 +var BeforeInputEventPlugin = {
84 +
85 + eventTypes: eventTypes,
86 +
87 + /**
88 + * @param {string} topLevelType Record from `EventConstants`.
89 + * @param {DOMEventTarget} topLevelTarget The listening component root node.
90 + * @param {string} topLevelTargetID ID of `topLevelTarget`.
91 + * @param {object} nativeEvent Native browser event.
92 + * @return {*} An accumulation of synthetic events.
93 + * @see {EventPluginHub.extractEvents}
94 + */
95 + extractEvents: function(
96 + topLevelType,
97 + topLevelTarget,
98 + topLevelTargetID,
99 + nativeEvent) {
100 +
101 + var chars;
102 +
103 + if (useBeforeInputEvent) {
104 + // For browsers that support `textInput` events natively, don't do
105 + // anything with keypress, composition, etc.
106 + if (topLevelType !== topLevelTypes.topTextInput) {
107 + return;
108 + }
109 + chars = nativeEvent.data;
110 + } else {
111 + switch (topLevelType) {
112 + case topLevelTypes.topPaste:
113 + // If a paste event occurs after a keypress, throw out the input
114 + // chars. Paste events should not lead to BeforeInput events.
115 + fallbackChars = null;
116 + break;
117 + case topLevelTypes.topKeyPress:
118 + /**
119 + * As of v27, Firefox may fire keypress events even when no character
120 + * will be inserted. A few possibilities:
121 + *
122 + * - `which` is `0`. Arrow keys, Esc key, etc.
123 + *
124 + * - `which` is the pressed key code, but no char is available.
125 + * Ex: 'AltGr + d` in Polish. There is no modified character for
126 + * this key combination and no character is inserted into the
127 + * document, but FF fires the keypress for char code `100` anyway.
128 + * No `input` event will occur.
129 + *
130 + * - `which` is the pressed key code, but a command combination is
131 + * being used. Ex: `Cmd+C`. No character is inserted, and no
132 + * `input` event will occur.
133 + */
134 + if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {
135 + fallbackChars = String.fromCharCode(nativeEvent.which);
136 + }
137 + break;
138 + case topLevelTypes.topCompositionEnd:
139 + fallbackChars = nativeEvent.data;
140 + break;
141 + }
142 +
143 + // If no changes have occurred to the fallback string, no relevant
144 + // event has fired and we're done.
145 + if (fallbackChars === null) {
146 + return;
147 + }
148 +
149 + chars = fallbackChars;
150 + }
151 +
152 + // If no characters are being inserted, no BeforeInput event should
153 + // be fired.
154 + if (!chars) {
155 + return;
156 + }
157 +
158 + var event = SyntheticInputEvent.getPooled(
159 + eventTypes.beforeInput,
160 + topLevelTargetID,
161 + nativeEvent
162 + );
163 +
164 + event.data = chars;
165 + fallbackChars = null;
166 + EventPropagators.accumulateTwoPhaseDispatches(event);
167 + return event;
168 + }
169 +};
170 +
171 +module.exports = BeforeInputEventPlugin;
src/browser/eventPlugins/CompositionEventPlugin.js
+5 -1
@@ -44,7 +44,11 @@ var useCompositionEvent = (
44 // events as triggers.
45 var useFallbackData = (
46 !useCompositionEvent ||
47 - 'documentMode' in document && document.documentMode > 8
47 + (
48 + 'documentMode' in document &&
49 + document.documentMode > 8 &&
50 + document.documentMode <= 11
51 + )
52 );
53
54 var topLevelTypes = EventConstants.topLevelTypes;
src/browser/eventPlugins/DefaultEventPluginOrder.js
+1
@@ -37,6 +37,7 @@ var DefaultEventPluginOrder = [
37 keyOf({ChangeEventPlugin: null}),
38 keyOf({SelectEventPlugin: null}),
39 keyOf({CompositionEventPlugin: null}),
40 + keyOf({BeforeInputEventPlugin: null}),
41 keyOf({AnalyticsEventPlugin: null}),
42 keyOf({MobileSafariClickEventPlugin: null})
43 ];
src/browser/syntheticEvents/SyntheticInputEvent.js new
+52
@@ -0,0 +1,52 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule SyntheticInputEvent
17 + * @typechecks static-only
18 + */
19 +
20 +"use strict";
21 +
22 +var SyntheticEvent = require('SyntheticEvent');
23 +
24 +/**
25 + * @interface Event
26 + * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105
27 + * /#events-inputevents
28 + */
29 +var InputEventInterface = {
30 + data: null
31 +};
32 +
33 +/**
34 + * @param {object} dispatchConfig Configuration used to dispatch this event.
35 + * @param {string} dispatchMarker Marker identifying the event target.
36 + * @param {object} nativeEvent Native browser event.
37 + * @extends {SyntheticUIEvent}
38 + */
39 +function SyntheticInputEvent(
40 + dispatchConfig,
41 + dispatchMarker,
42 + nativeEvent) {
43 + SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);
44 +}
45 +
46 +SyntheticEvent.augmentClass(
47 + SyntheticInputEvent,
48 + InputEventInterface
49 +);
50 +
51 +module.exports = SyntheticInputEvent;
52 +
src/browser/ui/ReactDefaultInjection.js
+3 -1
@@ -47,6 +47,7 @@ var ReactMount = require('ReactMount');
47 var SelectEventPlugin = require('SelectEventPlugin');
48 var ServerReactRootIndex = require('ServerReactRootIndex');
49 var SimpleEventPlugin = require('SimpleEventPlugin');
50 +var BeforeInputEventPlugin = require('BeforeInputEventPlugin');
51
52 var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
53
@@ -74,7 +75,8 @@ function inject() {
75 ChangeEventPlugin: ChangeEventPlugin,
76 CompositionEventPlugin: CompositionEventPlugin,
77 MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
77 - SelectEventPlugin: SelectEventPlugin
78 + SelectEventPlugin: SelectEventPlugin,
79 + BeforeInputEventPlugin: BeforeInputEventPlugin
80 });
81
82 ReactInjection.DOM.injectComponentClasses({
src/event/EventConstants.js
+1
@@ -61,6 +61,7 @@ var topLevelTypes = keyMirror({
61 topScroll: null,
62 topSelectionChange: null,
63 topSubmit: null,
64 + topTextInput: null,
65 topTouchCancel: null,
66 topTouchEnd: null,
67 topTouchMove: null,