You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
11 KiB
380 lines
11 KiB
/** |
|
* Copyright 2013-2015, Facebook, Inc. |
|
* All rights reserved. |
|
* |
|
* This source code is licensed under the BSD-style license found in the |
|
* LICENSE file in the root directory of this source tree. An additional grant |
|
* of patent rights can be found in the PATENTS file in the same directory. |
|
* |
|
* @providesModule ChangeEventPlugin |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var EventConstants = require("./EventConstants"); |
|
var EventPluginHub = require("./EventPluginHub"); |
|
var EventPropagators = require("./EventPropagators"); |
|
var ExecutionEnvironment = require("./ExecutionEnvironment"); |
|
var ReactUpdates = require("./ReactUpdates"); |
|
var SyntheticEvent = require("./SyntheticEvent"); |
|
|
|
var isEventSupported = require("./isEventSupported"); |
|
var isTextInputElement = require("./isTextInputElement"); |
|
var keyOf = require("./keyOf"); |
|
|
|
var topLevelTypes = EventConstants.topLevelTypes; |
|
|
|
var eventTypes = { |
|
change: { |
|
phasedRegistrationNames: { |
|
bubbled: keyOf({onChange: null}), |
|
captured: keyOf({onChangeCapture: null}) |
|
}, |
|
dependencies: [ |
|
topLevelTypes.topBlur, |
|
topLevelTypes.topChange, |
|
topLevelTypes.topClick, |
|
topLevelTypes.topFocus, |
|
topLevelTypes.topInput, |
|
topLevelTypes.topKeyDown, |
|
topLevelTypes.topKeyUp, |
|
topLevelTypes.topSelectionChange |
|
] |
|
} |
|
}; |
|
|
|
/** |
|
* For IE shims |
|
*/ |
|
var activeElement = null; |
|
var activeElementID = null; |
|
var activeElementValue = null; |
|
var activeElementValueProp = null; |
|
|
|
/** |
|
* SECTION: handle `change` event |
|
*/ |
|
function shouldUseChangeEvent(elem) { |
|
return ( |
|
elem.nodeName === 'SELECT' || |
|
(elem.nodeName === 'INPUT' && elem.type === 'file') |
|
); |
|
} |
|
|
|
var doesChangeEventBubble = false; |
|
if (ExecutionEnvironment.canUseDOM) { |
|
// See `handleChange` comment below |
|
doesChangeEventBubble = isEventSupported('change') && ( |
|
(!('documentMode' in document) || document.documentMode > 8) |
|
); |
|
} |
|
|
|
function manualDispatchChangeEvent(nativeEvent) { |
|
var event = SyntheticEvent.getPooled( |
|
eventTypes.change, |
|
activeElementID, |
|
nativeEvent |
|
); |
|
EventPropagators.accumulateTwoPhaseDispatches(event); |
|
|
|
// If change and propertychange bubbled, we'd just bind to it like all the |
|
// other events and have it go through ReactBrowserEventEmitter. Since it |
|
// doesn't, we manually listen for the events and so we have to enqueue and |
|
// process the abstract event manually. |
|
// |
|
// Batching is necessary here in order to ensure that all event handlers run |
|
// before the next rerender (including event handlers attached to ancestor |
|
// elements instead of directly on the input). Without this, controlled |
|
// components don't work properly in conjunction with event bubbling because |
|
// the component is rerendered and the value reverted before all the event |
|
// handlers can run. See https://github.com/facebook/react/issues/708. |
|
ReactUpdates.batchedUpdates(runEventInBatch, event); |
|
} |
|
|
|
function runEventInBatch(event) { |
|
EventPluginHub.enqueueEvents(event); |
|
EventPluginHub.processEventQueue(); |
|
} |
|
|
|
function startWatchingForChangeEventIE8(target, targetID) { |
|
activeElement = target; |
|
activeElementID = targetID; |
|
activeElement.attachEvent('onchange', manualDispatchChangeEvent); |
|
} |
|
|
|
function stopWatchingForChangeEventIE8() { |
|
if (!activeElement) { |
|
return; |
|
} |
|
activeElement.detachEvent('onchange', manualDispatchChangeEvent); |
|
activeElement = null; |
|
activeElementID = null; |
|
} |
|
|
|
function getTargetIDForChangeEvent( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID) { |
|
if (topLevelType === topLevelTypes.topChange) { |
|
return topLevelTargetID; |
|
} |
|
} |
|
function handleEventsForChangeEventIE8( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID) { |
|
if (topLevelType === topLevelTypes.topFocus) { |
|
// stopWatching() should be a noop here but we call it just in case we |
|
// missed a blur event somehow. |
|
stopWatchingForChangeEventIE8(); |
|
startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID); |
|
} else if (topLevelType === topLevelTypes.topBlur) { |
|
stopWatchingForChangeEventIE8(); |
|
} |
|
} |
|
|
|
|
|
/** |
|
* SECTION: handle `input` event |
|
*/ |
|
var isInputEventSupported = false; |
|
if (ExecutionEnvironment.canUseDOM) { |
|
// IE9 claims to support the input event but fails to trigger it when |
|
// deleting text, so we ignore its input events |
|
isInputEventSupported = isEventSupported('input') && ( |
|
(!('documentMode' in document) || document.documentMode > 9) |
|
); |
|
} |
|
|
|
/** |
|
* (For old IE.) Replacement getter/setter for the `value` property that gets |
|
* set on the active element. |
|
*/ |
|
var newValueProp = { |
|
get: function() { |
|
return activeElementValueProp.get.call(this); |
|
}, |
|
set: function(val) { |
|
// Cast to a string so we can do equality checks. |
|
activeElementValue = '' + val; |
|
activeElementValueProp.set.call(this, val); |
|
} |
|
}; |
|
|
|
/** |
|
* (For old IE.) Starts tracking propertychange events on the passed-in element |
|
* and override the value property so that we can distinguish user events from |
|
* value changes in JS. |
|
*/ |
|
function startWatchingForValueChange(target, targetID) { |
|
activeElement = target; |
|
activeElementID = targetID; |
|
activeElementValue = target.value; |
|
activeElementValueProp = Object.getOwnPropertyDescriptor( |
|
target.constructor.prototype, |
|
'value' |
|
); |
|
|
|
Object.defineProperty(activeElement, 'value', newValueProp); |
|
activeElement.attachEvent('onpropertychange', handlePropertyChange); |
|
} |
|
|
|
/** |
|
* (For old IE.) Removes the event listeners from the currently-tracked element, |
|
* if any exists. |
|
*/ |
|
function stopWatchingForValueChange() { |
|
if (!activeElement) { |
|
return; |
|
} |
|
|
|
// delete restores the original property definition |
|
delete activeElement.value; |
|
activeElement.detachEvent('onpropertychange', handlePropertyChange); |
|
|
|
activeElement = null; |
|
activeElementID = null; |
|
activeElementValue = null; |
|
activeElementValueProp = null; |
|
} |
|
|
|
/** |
|
* (For old IE.) Handles a propertychange event, sending a `change` event if |
|
* the value of the active element has changed. |
|
*/ |
|
function handlePropertyChange(nativeEvent) { |
|
if (nativeEvent.propertyName !== 'value') { |
|
return; |
|
} |
|
var value = nativeEvent.srcElement.value; |
|
if (value === activeElementValue) { |
|
return; |
|
} |
|
activeElementValue = value; |
|
|
|
manualDispatchChangeEvent(nativeEvent); |
|
} |
|
|
|
/** |
|
* If a `change` event should be fired, returns the target's ID. |
|
*/ |
|
function getTargetIDForInputEvent( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID) { |
|
if (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 |
|
return topLevelTargetID; |
|
} |
|
} |
|
|
|
// For IE8 and IE9. |
|
function handleEventsForInputEventIE( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID) { |
|
if (topLevelType === topLevelTypes.topFocus) { |
|
// In IE8, we can capture almost all .value changes by adding a |
|
// propertychange handler and looking for events with propertyName |
|
// equal to '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 |
|
// |
|
// stopWatching() should be a noop here but we call it just in case we |
|
// missed a blur event somehow. |
|
stopWatchingForValueChange(); |
|
startWatchingForValueChange(topLevelTarget, topLevelTargetID); |
|
} else if (topLevelType === topLevelTypes.topBlur) { |
|
stopWatchingForValueChange(); |
|
} |
|
} |
|
|
|
// For IE8 and IE9. |
|
function getTargetIDForInputEventIE( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID) { |
|
if (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) { |
|
activeElementValue = activeElement.value; |
|
return activeElementID; |
|
} |
|
} |
|
} |
|
|
|
|
|
/** |
|
* SECTION: handle `click` event |
|
*/ |
|
function shouldUseClickEvent(elem) { |
|
// Use the `click` event to detect changes to checkbox and radio inputs. |
|
// This approach works across all browsers, whereas `change` does not fire |
|
// until `blur` in IE8. |
|
return ( |
|
elem.nodeName === 'INPUT' && |
|
(elem.type === 'checkbox' || elem.type === 'radio') |
|
); |
|
} |
|
|
|
function getTargetIDForClickEvent( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID) { |
|
if (topLevelType === topLevelTypes.topClick) { |
|
return topLevelTargetID; |
|
} |
|
} |
|
|
|
/** |
|
* This plugin creates an `onChange` event that normalizes change events |
|
* across form elements. This event fires at a time when it's possible to |
|
* change the element's value without seeing a flicker. |
|
* |
|
* Supported elements are: |
|
* - input (see `isTextInputElement`) |
|
* - textarea |
|
* - select |
|
*/ |
|
var ChangeEventPlugin = { |
|
|
|
eventTypes: eventTypes, |
|
|
|
/** |
|
* @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 synthetic events. |
|
* @see {EventPluginHub.extractEvents} |
|
*/ |
|
extractEvents: function( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID, |
|
nativeEvent) { |
|
|
|
var getTargetIDFunc, handleEventFunc; |
|
if (shouldUseChangeEvent(topLevelTarget)) { |
|
if (doesChangeEventBubble) { |
|
getTargetIDFunc = getTargetIDForChangeEvent; |
|
} else { |
|
handleEventFunc = handleEventsForChangeEventIE8; |
|
} |
|
} else if (isTextInputElement(topLevelTarget)) { |
|
if (isInputEventSupported) { |
|
getTargetIDFunc = getTargetIDForInputEvent; |
|
} else { |
|
getTargetIDFunc = getTargetIDForInputEventIE; |
|
handleEventFunc = handleEventsForInputEventIE; |
|
} |
|
} else if (shouldUseClickEvent(topLevelTarget)) { |
|
getTargetIDFunc = getTargetIDForClickEvent; |
|
} |
|
|
|
if (getTargetIDFunc) { |
|
var targetID = getTargetIDFunc( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID |
|
); |
|
if (targetID) { |
|
var event = SyntheticEvent.getPooled( |
|
eventTypes.change, |
|
targetID, |
|
nativeEvent |
|
); |
|
EventPropagators.accumulateTwoPhaseDispatches(event); |
|
return event; |
|
} |
|
} |
|
|
|
if (handleEventFunc) { |
|
handleEventFunc( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID |
|
); |
|
} |
|
} |
|
|
|
}; |
|
|
|
module.exports = ChangeEventPlugin;
|
|
|