@samitouri / QOS-React-2 / commits / 5d7633d74c

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,