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).
This commit is contained in:
Ben Alpert
2013-06-09 03:52:01 -07:00
parent 7e7579e1ba
commit c19bf9cffe
5 changed files with 212 additions and 3 deletions
+3 -1
View File
@@ -23,6 +23,7 @@ var ReactDOMForm = require('ReactDOMForm');
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
var TextChangeEventPlugin = require('TextChangeEventPlugin');
var EventPluginHub = require('EventPluginHub');
var ReactInstanceHandles = require('ReactInstanceHandles');
var SimpleEventPlugin = require('SimpleEventPlugin');
@@ -40,7 +41,8 @@ function inject() {
*/
EventPluginHub.injection.injectEventPluginsByName({
'SimpleEventPlugin': SimpleEventPlugin,
'EnterLeaveEventPlugin': EnterLeaveEventPlugin
'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
'TextChangeEventPlugin': TextChangeEventPlugin
});
/*
+5
View File
@@ -189,6 +189,11 @@ function listenAtTopLevel(touchNotMouse) {
trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt);
trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt);
trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt);
trapBubbledEvent(
topLevelTypes.topSelectionChange,
'selectionchange',
mountAt
);
trapBubbledEvent(
topLevelTypes.topDOMCharacterDataModified,
'DOMCharacterDataModified',
+3 -2
View File
@@ -41,13 +41,14 @@ var topLevelTypes = keyMirror({
topMouseOut: null,
topMouseOver: null,
topMouseUp: null,
topWheel: null,
topScroll: null,
topSelectionChange: null,
topSubmit: null,
topTouchCancel: null,
topTouchEnd: null,
topTouchMove: null,
topTouchStart: null
topTouchStart: null,
topWheel: null
});
var EventConstants = {
@@ -34,6 +34,7 @@ var DefaultEventPluginOrder = [
keyOf({SimpleEventPlugin: null}),
keyOf({TapEventPlugin: null}),
keyOf({EnterLeaveEventPlugin: null}),
keyOf({TextChangeEventPlugin: null}),
keyOf({AnalyticsEventPlugin: null})
];
+200
View File
@@ -0,0 +1,200 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule TextChangeEventPlugin
*/
"use strict";
var AbstractEvent = require('AbstractEvent');
var EventConstants = require('EventConstants');
var EventPluginHub = require('EventPluginHub');
var EventPropagators = require('EventPropagators');
var isEventSupported = require('isEventSupported');
var keyOf = require('keyOf');
var topLevelTypes = EventConstants.topLevelTypes;
var abstractEventTypes = {
textChange: {
phasedRegistrationNames: {
bubbled: keyOf({onTextChange: null}),
captured: keyOf({onTextChangeCapture: null})
}
}
};
// IE9 claims to support the input event but fails to trigger it when deleting
// text, so we ignore its input events
var isInputSupported = isEventSupported('input') && (
!("documentMode" in document) || document.documentMode > 9
);
var hasInputCapabilities = function(elem) {
// The HTML5 spec lists many more types than `text` and `password` on which
// the input event is triggered but none of them exist in old IE, so we don't
// check them here.
// TODO: <textarea> should be supported too but IE seems to reset the
// selection when changing textarea contents during a selectionchange event
// so it's not listed here for now.
return (
elem.nodeName === 'INPUT' &&
(elem.type === 'text' || elem.type === 'password')
);
};
var activeElement = null;
var activeElementID = null;
var activeElementValue = null;
var activeElementValueProp = null;
// Replacement getter/setter for the `value` property for old IE that gets set
// on the active element
var newValueProp = {
get: function() {
return activeElementValueProp.get.call(this);
},
set: function(val) {
activeElementValue = val;
activeElementValueProp.set.call(this, val);
}
};
var handlePropertyChange = function(nativeEvent) {
var value;
var abstractEvent;
if (nativeEvent.propertyName === "value") {
value = nativeEvent.srcElement.value;
if (value !== activeElementValue) {
activeElementValue = value;
abstractEvent = AbstractEvent.getPooled(
abstractEventTypes.textChange,
activeElementID,
nativeEvent
);
EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
EventPluginHub.enqueueAbstractEvents(abstractEvent);
EventPluginHub.processAbstractEventQueue();
}
}
};
/**
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of `AbstractEvent`s.
* @see {EventPluginHub.extractAbstractEvents}
*/
var extractAbstractEvents = function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
var targetID;
if (isInputSupported && topLevelType === topLevelTypes.topInput) {
// In modern browsers (i.e., not IE8 or IE9), the input event is exactly
// what we want so fall through here and trigger an abstract event...
if (topLevelTarget.nodeName === 'TEXTAREA') {
// ...unless it's a textarea, in which case we don't fire an event (so
// that we have consistency with our old-IE shim).
return;
}
targetID = topLevelTargetID;
} else if (!isInputSupported && topLevelType === topLevelTypes.topFocus) {
// In IE8, we can capture almost all .value changes by adding a
// propertychange handler and looking for events with propertyName 'value'
// In IE9, propertychange fires for most input events but is buggy and
// doesn't fire when text is deleted, but conveniently, selectionchange
// appears to fire in all of the remaining cases so we catch those and
// forward the event if the value has changed
// In either case, we don't want to call the event handler if the value is
// changed from JS so we redefine a setter for `.value` that updates our
// activeElementValue variable, allowing us to ignore those changes
if (hasInputCapabilities(topLevelTarget)) {
activeElement = topLevelTarget;
activeElementID = topLevelTargetID;
activeElementValue = topLevelTarget.value;
activeElementValueProp = Object.getOwnPropertyDescriptor(
topLevelTarget.constructor.prototype,
'value'
);
Object.defineProperty(topLevelTarget, 'value', newValueProp);
topLevelTarget.attachEvent('onpropertychange', handlePropertyChange);
}
return;
} else if (!isInputSupported && topLevelType === topLevelTypes.topBlur) {
// If blur is triggered due to element removal, the target is set to the
// <html> element so ignore that and unbind from activeElement instead
if (activeElement) {
// delete restores the original property definition
delete activeElement.value;
activeElement.detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementID = null;
activeElementValue = null;
activeElementValueProp = null;
}
return;
} else if (!isInputSupported && (
topLevelType === topLevelTypes.topSelectionChange ||
topLevelType === topLevelTypes.topKeyUp ||
topLevelType === topLevelTypes.topKeyDown)) {
// On the selectionchange event, the target is just document which isn't
// helpful for us so just check activeElement instead.
//
// 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
// propertychange on the first input event after setting `value` from a
// script and fires only keydown, keypress, keyup. Catching keyup usually
// gets it and catching keydown lets us fire an event for the first
// keystroke if user does a key repeat (it'll be a little delayed: right
// before the second keystroke). Other input methods (e.g., paste) seem to
// fire selectionchange normally.
if (!activeElement || activeElement.value === activeElementValue) {
return;
}
activeElementValue = activeElement.value;
targetID = activeElementID;
} else {
return;
}
// If we've made it to this point, some value change occurred
var abstractEvent = AbstractEvent.getPooled(
abstractEventTypes.textChange,
targetID,
nativeEvent
);
EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
return abstractEvent;
};
var TextChangeEventPlugin = {
abstractEventTypes: abstractEventTypes,
extractAbstractEvents: extractAbstractEvents
};
module.exports = TextChangeEventPlugin;