@samitouri / QOS-React-2 / commits / c19bf9cffe
Fork 0

The library for web and native user interfaces.

Add new textChange event: input + IE shim

IE8 doesn't support oninput and IE9 supports it badly but we can do almost a perfect shim by listening to a handful of different events (focus, blur, propertychange, selectionchange, keyup, keydown). This always triggers event handlers during the browser's event loop (not later in a setTimeout) and after the value property has been updated. The only case I know of where this doesn't fire the event immediately is if (in IE8) you modify the input value using JS and then the user does a key repeat, in which case we fire the event on the second keydown. Test Plan: Modify ballmer-peak example to add es5-shim and to use onTextChange instead of onInput. In IE8, IE9, and latest Chrome, make sure that the event is fired upon: * typing normally, * backspacing, * forward-deleting, * cutting, * pasting, * context-menu deleting, * dragging text to reorder characters. After modifying the example to change .value, make sure that the event is not fired as a result of the changes from JS (even when the input box is focused).

Ben Alpert committed Jun 9, 2013 at 03:52 UTC c19bf9cffe163dce8bc687e6276c533ea5199808
5 files changed +212 -3
src/core/ReactDefaultInjection.js
+3 -1
@@ -23,6 +23,7 @@ var ReactDOMForm = require('ReactDOMForm');
23
24 var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
25 var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
26 +var TextChangeEventPlugin = require('TextChangeEventPlugin');
27 var EventPluginHub = require('EventPluginHub');
28 var ReactInstanceHandles = require('ReactInstanceHandles');
29 var SimpleEventPlugin = require('SimpleEventPlugin');
@@ -40,7 +41,8 @@ function inject() {
41 */
42 EventPluginHub.injection.injectEventPluginsByName({
43 'SimpleEventPlugin': SimpleEventPlugin,
43 - 'EnterLeaveEventPlugin': EnterLeaveEventPlugin
44 + 'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
45 + 'TextChangeEventPlugin': TextChangeEventPlugin
46 });
47
48 /*
src/core/ReactEventEmitter.js
+5
@@ -189,6 +189,11 @@ function listenAtTopLevel(touchNotMouse) {
189 trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt);
190 trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt);
191 trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt);
192 + trapBubbledEvent(
193 + topLevelTypes.topSelectionChange,
194 + 'selectionchange',
195 + mountAt
196 + );
197 trapBubbledEvent(
198 topLevelTypes.topDOMCharacterDataModified,
199 'DOMCharacterDataModified',
src/event/EventConstants.js
+3 -2
@@ -41,13 +41,14 @@ var topLevelTypes = keyMirror({
41 topMouseOut: null,
42 topMouseOver: null,
43 topMouseUp: null,
44 - topWheel: null,
44 topScroll: null,
45 + topSelectionChange: null,
46 topSubmit: null,
47 topTouchCancel: null,
48 topTouchEnd: null,
49 topTouchMove: null,
50 - topTouchStart: null
50 + topTouchStart: null,
51 + topWheel: null
52 });
53
54 var EventConstants = {
src/eventPlugins/DefaultEventPluginOrder.js
+1
@@ -34,6 +34,7 @@ var DefaultEventPluginOrder = [
34 keyOf({SimpleEventPlugin: null}),
35 keyOf({TapEventPlugin: null}),
36 keyOf({EnterLeaveEventPlugin: null}),
37 + keyOf({TextChangeEventPlugin: null}),
38 keyOf({AnalyticsEventPlugin: null})
39 ];
40
src/eventPlugins/TextChangeEventPlugin.js new
+200
@@ -0,0 +1,200 @@
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 TextChangeEventPlugin
17 + */
18 +
19 +"use strict";
20 +
21 +var AbstractEvent = require('AbstractEvent');
22 +var EventConstants = require('EventConstants');
23 +var EventPluginHub = require('EventPluginHub');
24 +var EventPropagators = require('EventPropagators');
25 +
26 +var isEventSupported = require('isEventSupported');
27 +var keyOf = require('keyOf');
28 +
29 +var topLevelTypes = EventConstants.topLevelTypes;
30 +
31 +var abstractEventTypes = {
32 + textChange: {
33 + phasedRegistrationNames: {
34 + bubbled: keyOf({onTextChange: null}),
35 + captured: keyOf({onTextChangeCapture: null})
36 + }
37 + }
38 +};
39 +
40 +// IE9 claims to support the input event but fails to trigger it when deleting
41 +// text, so we ignore its input events
42 +var isInputSupported = isEventSupported('input') && (
43 + !("documentMode" in document) || document.documentMode > 9
44 +);
45 +
46 +var hasInputCapabilities = function(elem) {
47 + // The HTML5 spec lists many more types than `text` and `password` on which
48 + // the input event is triggered but none of them exist in old IE, so we don't
49 + // check them here.
50 + // TODO: <textarea> should be supported too but IE seems to reset the
51 + // selection when changing textarea contents during a selectionchange event
52 + // so it's not listed here for now.
53 + return (
54 + elem.nodeName === 'INPUT' &&
55 + (elem.type === 'text' || elem.type === 'password')
56 + );
57 +};
58 +
59 +var activeElement = null;
60 +var activeElementID = null;
61 +var activeElementValue = null;
62 +var activeElementValueProp = null;
63 +
64 +// Replacement getter/setter for the `value` property for old IE that gets set
65 +// on the active element
66 +var newValueProp = {
67 + get: function() {
68 + return activeElementValueProp.get.call(this);
69 + },
70 + set: function(val) {
71 + activeElementValue = val;
72 + activeElementValueProp.set.call(this, val);
73 + }
74 +};
75 +
76 +var handlePropertyChange = function(nativeEvent) {
77 + var value;
78 + var abstractEvent;
79 +
80 + if (nativeEvent.propertyName === "value") {
81 + value = nativeEvent.srcElement.value;
82 + if (value !== activeElementValue) {
83 + activeElementValue = value;
84 +
85 + abstractEvent = AbstractEvent.getPooled(
86 + abstractEventTypes.textChange,
87 + activeElementID,
88 + nativeEvent
89 + );
90 + EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
91 + EventPluginHub.enqueueAbstractEvents(abstractEvent);
92 + EventPluginHub.processAbstractEventQueue();
93 + }
94 + }
95 +};
96 +
97 +/**
98 + * @param {string} topLevelType Record from `EventConstants`.
99 + * @param {DOMEventTarget} topLevelTarget The listening component root node.
100 + * @param {string} topLevelTargetID ID of `topLevelTarget`.
101 + * @param {object} nativeEvent Native browser event.
102 + * @return {*} An accumulation of `AbstractEvent`s.
103 + * @see {EventPluginHub.extractAbstractEvents}
104 + */
105 +var extractAbstractEvents = function(
106 + topLevelType,
107 + topLevelTarget,
108 + topLevelTargetID,
109 + nativeEvent) {
110 + var targetID;
111 +
112 + if (isInputSupported && topLevelType === topLevelTypes.topInput) {
113 + // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
114 + // what we want so fall through here and trigger an abstract event...
115 + if (topLevelTarget.nodeName === 'TEXTAREA') {
116 + // ...unless it's a textarea, in which case we don't fire an event (so
117 + // that we have consistency with our old-IE shim).
118 + return;
119 + }
120 + targetID = topLevelTargetID;
121 +
122 + } else if (!isInputSupported && topLevelType === topLevelTypes.topFocus) {
123 + // In IE8, we can capture almost all .value changes by adding a
124 + // propertychange handler and looking for events with propertyName 'value'
125 + // In IE9, propertychange fires for most input events but is buggy and
126 + // doesn't fire when text is deleted, but conveniently, selectionchange
127 + // appears to fire in all of the remaining cases so we catch those and
128 + // forward the event if the value has changed
129 + // In either case, we don't want to call the event handler if the value is
130 + // changed from JS so we redefine a setter for `.value` that updates our
131 + // activeElementValue variable, allowing us to ignore those changes
132 + if (hasInputCapabilities(topLevelTarget)) {
133 + activeElement = topLevelTarget;
134 + activeElementID = topLevelTargetID;
135 + activeElementValue = topLevelTarget.value;
136 + activeElementValueProp = Object.getOwnPropertyDescriptor(
137 + topLevelTarget.constructor.prototype,
138 + 'value'
139 + );
140 +
141 + Object.defineProperty(topLevelTarget, 'value', newValueProp);
142 + topLevelTarget.attachEvent('onpropertychange', handlePropertyChange);
143 + }
144 + return;
145 +
146 + } else if (!isInputSupported && topLevelType === topLevelTypes.topBlur) {
147 + // If blur is triggered due to element removal, the target is set to the
148 + // <html> element so ignore that and unbind from activeElement instead
149 + if (activeElement) {
150 + // delete restores the original property definition
151 + delete activeElement.value;
152 + activeElement.detachEvent('onpropertychange', handlePropertyChange);
153 +
154 + activeElement = null;
155 + activeElementID = null;
156 + activeElementValue = null;
157 + activeElementValueProp = null;
158 + }
159 + return;
160 +
161 + } else if (!isInputSupported && (
162 + topLevelType === topLevelTypes.topSelectionChange ||
163 + topLevelType === topLevelTypes.topKeyUp ||
164 + topLevelType === topLevelTypes.topKeyDown)) {
165 + // On the selectionchange event, the target is just document which isn't
166 + // helpful for us so just check activeElement instead.
167 + //
168 + // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
169 + // propertychange on the first input event after setting `value` from a
170 + // script and fires only keydown, keypress, keyup. Catching keyup usually
171 + // gets it and catching keydown lets us fire an event for the first
172 + // keystroke if user does a key repeat (it'll be a little delayed: right
173 + // before the second keystroke). Other input methods (e.g., paste) seem to
174 + // fire selectionchange normally.
175 + if (!activeElement || activeElement.value === activeElementValue) {
176 + return;
177 + }
178 + activeElementValue = activeElement.value;
179 + targetID = activeElementID;
180 +
181 + } else {
182 + return;
183 + }
184 +
185 + // If we've made it to this point, some value change occurred
186 + var abstractEvent = AbstractEvent.getPooled(
187 + abstractEventTypes.textChange,
188 + targetID,
189 + nativeEvent
190 + );
191 + EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
192 + return abstractEvent;
193 +};
194 +
195 +var TextChangeEventPlugin = {
196 + abstractEventTypes: abstractEventTypes,
197 + extractAbstractEvents: extractAbstractEvents
198 +};
199 +
200 +module.exports = TextChangeEventPlugin;