Move composition event to plugin with polyfill
Move compositionstart/compositionend to a new event plugin. Add a polyfill that listens to key and mouse events and uses selection to determine which text has changed.
Josh Duck committed
Sep 23, 2013 at 16:16 UTC
5d7633d74cc79d6bb6498c9dd57d62b6718648e2
4 files changed
+213
-27
src/core/ReactDefaultInjection.js
+2
@@ -35,6 +35,7 @@ var DOMProperty = require('DOMProperty');
35
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
36
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
37
var ChangeEventPlugin = require('ChangeEventPlugin');
38
+var CompositionEventPlugin = require('CompositionEventPlugin');
39
var EventPluginHub = require('EventPluginHub');
40
var ReactInstanceHandles = require('ReactInstanceHandles');
41
var SimpleEventPlugin = require('SimpleEventPlugin');
@@ -59,6 +60,7 @@ function inject() {
60
'SimpleEventPlugin': SimpleEventPlugin,
61
'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
62
'ChangeEventPlugin': ChangeEventPlugin,
63
+ 'CompositionEventPlugin': CompositionEventPlugin,
64
'MobileSafariClickEventPlugin': MobileSafariClickEventPlugin
65
});
66
src/eventPlugins/CompositionEventPlugin.js
new
+210
@@ -0,0 +1,210 @@
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 CompositionEventPlugin
17
+ * @typechecks static-only
18
+ */
19
+
20
+"use strict";
21
+
22
+var EventConstants = require('EventConstants');
23
+var EventPropagators = require('EventPropagators');
24
+var ReactInputSelection = require('ReactInputSelection');
25
+var SyntheticCompositionEvent = require('SyntheticCompositionEvent');
26
+
27
+var getTextContentAccessor = require('getTextContentAccessor');
28
+var keyOf = require('keyOf');
29
+
30
+var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
31
+var START_KEYCODE = 229;
32
+
33
+var useCompositionEvent = 'CompositionEvent' in window;
34
+var topLevelTypes = EventConstants.topLevelTypes;
35
+var currentComposition = null;
36
+
37
+// Events and their corresponding property names.
38
+var eventTypes = {
39
+ compositionEnd: {
40
+ phasedRegistrationNames: {
41
+ bubbled: keyOf({onCompositionEnd: null}),
42
+ captured: keyOf({onCompositionEndCapture: null})
43
+ }
44
+ },
45
+ compositionStart: {
46
+ phasedRegistrationNames: {
47
+ bubbled: keyOf({onCompositionStart: null}),
48
+ captured: keyOf({onCompositionStartCapture: null})
49
+ }
50
+ },
51
+ compositionUpdate: {
52
+ phasedRegistrationNames: {
53
+ bubbled: keyOf({onCompositionUpdate: null}),
54
+ captured: keyOf({onCompositionUpdateCapture: null})
55
+ }
56
+ }
57
+};
58
+
59
+/**
60
+ * Translate native top level events into event types.
61
+ *
62
+ * @param {string} topLevelType
63
+ * @return {object}
64
+ */
65
+function getCompositionEventType(topLevelType) {
66
+ switch (topLevelType) {
67
+ case topLevelTypes.topCompositionStart:
68
+ return eventTypes.compositionStart;
69
+ case topLevelTypes.topCompositionEnd:
70
+ return eventTypes.compositionEnd;
71
+ case topLevelTypes.topCompositionUpdate:
72
+ return eventTypes.compositionUpdate;
73
+ }
74
+}
75
+
76
+/**
77
+ * Does our fallback best-guess model think this event signifies that
78
+ * composition has begun?
79
+ *
80
+ * @param {string} topLevelType
81
+ * @param {object} nativeEvent
82
+ * @return {boolean}
83
+ */
84
+function isFallbackStart(topLevelType, nativeEvent) {
85
+ return (
86
+ topLevelType === topLevelTypes.topKeyDown &&
87
+ nativeEvent.keyCode === START_KEYCODE
88
+ );
89
+}
90
+
91
+/**
92
+ * Does our fallback mode think that this event is the end of composition?
93
+ *
94
+ * @param {string} topLevelType
95
+ * @param {object} nativeEvent
96
+ * @return {boolean}
97
+ */
98
+function isFallbackEnd(topLevelType, nativeEvent) {
99
+ switch (topLevelType) {
100
+ case topLevelTypes.topKeyUp:
101
+ // Command keys insert or clear IME input.
102
+ return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1);
103
+ case topLevelTypes.topKeyDown:
104
+ // Expect IME keyCode on each keydown. If we get any other
105
+ // code we must have exited earlier.
106
+ return (nativeEvent.keyCode !== START_KEYCODE);
107
+ case topLevelTypes.topKeyPress:
108
+ case topLevelTypes.topMouseDown:
109
+ case topLevelTypes.topBlur:
110
+ // Events are not possible without cancelling IME.
111
+ return true;
112
+ default:
113
+ return false;
114
+ }
115
+}
116
+
117
+/**
118
+ * Helper class stores information about selection and document state
119
+ * so we can figure out what changed at a later date.
120
+ *
121
+ * @param {DOMEventTarget} root
122
+ */
123
+function FallbackCompositionState(root) {
124
+ this.root = root;
125
+ this.startSelection = ReactInputSelection.getSelection(root);
126
+ this.startValue = this.getText();
127
+}
128
+
129
+/**
130
+ * Get current text of input.
131
+ *
132
+ * @return {string}
133
+ */
134
+FallbackCompositionState.prototype.getText = function() {
135
+ return this.root.value || this.root[getTextContentAccessor()];
136
+};
137
+
138
+/**
139
+ * Text that has changed since the start of composition.
140
+ *
141
+ * @return {string}
142
+ */
143
+FallbackCompositionState.prototype.getData = function() {
144
+ var endValue = this.getText();
145
+ var prefixLength = this.startSelection.start;
146
+ var suffixLength = this.startValue.length - this.startSelection.end;
147
+
148
+ return endValue.substr(
149
+ prefixLength,
150
+ endValue.length - suffixLength - prefixLength
151
+ );
152
+};
153
+
154
+/**
155
+ * This plugin creates `onCompositionStart`, `onCompositionUpdate` and
156
+ * `onCompositionEnd` events on inputs, textareas and contentEditable
157
+ * nodes.
158
+ */
159
+var CompositionEventPlugin = {
160
+
161
+ eventTypes: eventTypes,
162
+
163
+ /**
164
+ * @param {string} topLevelType Record from `EventConstants`.
165
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
166
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
167
+ * @param {object} nativeEvent Native browser event.
168
+ * @return {*} An accumulation of synthetic events.
169
+ * @see {EventPluginHub.extractEvents}
170
+ */
171
+ extractEvents: function(
172
+ topLevelType,
173
+ topLevelTarget,
174
+ topLevelTargetID,
175
+ nativeEvent) {
176
+
177
+ var eventType;
178
+ var data;
179
+
180
+ if (useCompositionEvent) {
181
+ eventType = getCompositionEventType(topLevelType);
182
+ } else if (!currentComposition) {
183
+ if (isFallbackStart(topLevelType, nativeEvent)) {
184
+ eventType = eventTypes.start;
185
+ currentComposition = new FallbackCompositionState(topLevelTarget);
186
+ }
187
+ } else if (isFallbackEnd(topLevelType, nativeEvent)) {
188
+ eventType = eventTypes.compositionEnd;
189
+ data = currentComposition.getData();
190
+ currentComposition = null;
191
+ }
192
+
193
+ if (eventType) {
194
+ var event = SyntheticCompositionEvent.getPooled(
195
+ eventType,
196
+ topLevelTargetID,
197
+ nativeEvent
198
+ );
199
+ if (data) {
200
+ // Inject data generated from fallback path into the synthetic event.
201
+ // This matches the property of native CompositionEventInterface.
202
+ event.data = data;
203
+ }
204
+ EventPropagators.accumulateTwoPhaseDispatches(event);
205
+ return event;
206
+ }
207
+ }
208
+};
209
+
210
+module.exports = CompositionEventPlugin;
src/eventPlugins/DefaultEventPluginOrder.js
+1
@@ -35,6 +35,7 @@ var DefaultEventPluginOrder = [
35
keyOf({TapEventPlugin: null}),
36
keyOf({EnterLeaveEventPlugin: null}),
37
keyOf({ChangeEventPlugin: null}),
38
+ keyOf({CompositionEventPlugin: null}),
39
keyOf({AnalyticsEventPlugin: null}),
40
keyOf({MobileSafariClickEventPlugin: null})
41
];
src/eventPlugins/SimpleEventPlugin.js
-27
@@ -21,7 +21,6 @@
21
var EventConstants = require('EventConstants');
22
var EventPropagators = require('EventPropagators');
23
var SyntheticClipboardEvent = require('SyntheticClipboardEvent');
24
-var SyntheticCompositionEvent = require('SyntheticCompositionEvent');
24
var SyntheticEvent = require('SyntheticEvent');
25
var SyntheticFocusEvent = require('SyntheticFocusEvent');
26
var SyntheticKeyboardEvent = require('SyntheticKeyboardEvent');
@@ -49,24 +48,6 @@ var eventTypes = {
48
captured: keyOf({onClickCapture: true})
49
}
50
},
52
- compositionEnd: {
53
- phasedRegistrationNames: {
54
- bubbled: keyOf({onCompositionEnd: true}),
55
- captured: keyOf({onCompositionEndCapture: true})
56
- }
57
- },
58
- compositionStart: {
59
- phasedRegistrationNames: {
60
- bubbled: keyOf({onCompositionStart: true}),
61
- captured: keyOf({onCompositionStartCapture: true})
62
- }
63
- },
64
- compositionUpdate: {
65
- phasedRegistrationNames: {
66
- bubbled: keyOf({onCompositionUpdate: true}),
67
- captured: keyOf({onCompositionUpdateCapture: true})
68
- }
69
- },
51
copy: {
52
phasedRegistrationNames: {
53
bubbled: keyOf({onCopy: true}),
@@ -244,9 +225,6 @@ var topLevelEventsToDispatchConfig = {
225
topClick: eventTypes.click,
226
topCopy: eventTypes.copy,
227
topCut: eventTypes.cut,
247
- topCompositionEnd: eventTypes.compositionEnd,
248
- topCompositionStart: eventTypes.compositionStart,
249
- topCompositionUpdate: eventTypes.compositionUpdate,
228
topDoubleClick: eventTypes.doubleClick,
229
topDOMCharacterDataModified: eventTypes.DOMCharacterDataModified,
230
topDrag: eventTypes.drag,
@@ -364,11 +342,6 @@ var SimpleEventPlugin = {
342
case topLevelTypes.topPaste:
343
EventConstructor = SyntheticClipboardEvent;
344
break;
367
- case topLevelTypes.topCompositionStart:
368
- case topLevelTypes.topCompositionEnd:
369
- case topLevelTypes.topCompositionUpdate:
370
- EventConstructor = SyntheticCompositionEvent;
371
- break;
345
}
346
invariant(
347
EventConstructor,