Skip to content

Commit 5d7633d

Browse files
joshduckzpao
authored andcommitted
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.
1 parent 8f15eea commit 5d7633d

File tree

4 files changed

+213
-27
lines changed

4 files changed

+213
-27
lines changed

src/core/ReactDefaultInjection.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var DOMProperty = require('DOMProperty');
3535
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
3636
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
3737
var ChangeEventPlugin = require('ChangeEventPlugin');
38+
var CompositionEventPlugin = require('CompositionEventPlugin');
3839
var EventPluginHub = require('EventPluginHub');
3940
var ReactInstanceHandles = require('ReactInstanceHandles');
4041
var SimpleEventPlugin = require('SimpleEventPlugin');
@@ -59,6 +60,7 @@ function inject() {
5960
'SimpleEventPlugin': SimpleEventPlugin,
6061
'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
6162
'ChangeEventPlugin': ChangeEventPlugin,
63+
'CompositionEventPlugin': CompositionEventPlugin,
6264
'MobileSafariClickEventPlugin': MobileSafariClickEventPlugin
6365
});
6466

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var DefaultEventPluginOrder = [
3535
keyOf({TapEventPlugin: null}),
3636
keyOf({EnterLeaveEventPlugin: null}),
3737
keyOf({ChangeEventPlugin: null}),
38+
keyOf({CompositionEventPlugin: null}),
3839
keyOf({AnalyticsEventPlugin: null}),
3940
keyOf({MobileSafariClickEventPlugin: null})
4041
];

src/eventPlugins/SimpleEventPlugin.js

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
var EventConstants = require('EventConstants');
2222
var EventPropagators = require('EventPropagators');
2323
var SyntheticClipboardEvent = require('SyntheticClipboardEvent');
24-
var SyntheticCompositionEvent = require('SyntheticCompositionEvent');
2524
var SyntheticEvent = require('SyntheticEvent');
2625
var SyntheticFocusEvent = require('SyntheticFocusEvent');
2726
var SyntheticKeyboardEvent = require('SyntheticKeyboardEvent');
@@ -49,24 +48,6 @@ var eventTypes = {
4948
captured: keyOf({onClickCapture: true})
5049
}
5150
},
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-
},
7051
copy: {
7152
phasedRegistrationNames: {
7253
bubbled: keyOf({onCopy: true}),
@@ -244,9 +225,6 @@ var topLevelEventsToDispatchConfig = {
244225
topClick: eventTypes.click,
245226
topCopy: eventTypes.copy,
246227
topCut: eventTypes.cut,
247-
topCompositionEnd: eventTypes.compositionEnd,
248-
topCompositionStart: eventTypes.compositionStart,
249-
topCompositionUpdate: eventTypes.compositionUpdate,
250228
topDoubleClick: eventTypes.doubleClick,
251229
topDOMCharacterDataModified: eventTypes.DOMCharacterDataModified,
252230
topDrag: eventTypes.drag,
@@ -364,11 +342,6 @@ var SimpleEventPlugin = {
364342
case topLevelTypes.topPaste:
365343
EventConstructor = SyntheticClipboardEvent;
366344
break;
367-
case topLevelTypes.topCompositionStart:
368-
case topLevelTypes.topCompositionEnd:
369-
case topLevelTypes.topCompositionUpdate:
370-
EventConstructor = SyntheticCompositionEvent;
371-
break;
372345
}
373346
invariant(
374347
EventConstructor,

0 commit comments

Comments
 (0)