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:
parent
7e7579e1ba
commit
c19bf9cffe
|
|
@ -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
|
||||
});
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue