345 lines
12 KiB
JavaScript
345 lines
12 KiB
JavaScript
|
/**
|
||
|
* Copyright 2013-present, 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.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var EventPluginHub = require('./EventPluginHub');
|
||
|
var EventPropagators = require('./EventPropagators');
|
||
|
var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
|
||
|
var ReactDOMComponentTree = require('./ReactDOMComponentTree');
|
||
|
var ReactUpdates = require('./ReactUpdates');
|
||
|
var SyntheticEvent = require('./SyntheticEvent');
|
||
|
|
||
|
var getEventTarget = require('./getEventTarget');
|
||
|
var isEventSupported = require('./isEventSupported');
|
||
|
var isTextInputElement = require('./isTextInputElement');
|
||
|
|
||
|
var eventTypes = {
|
||
|
change: {
|
||
|
phasedRegistrationNames: {
|
||
|
bubbled: 'onChange',
|
||
|
captured: 'onChangeCapture'
|
||
|
},
|
||
|
dependencies: ['topBlur', 'topChange', 'topClick', 'topFocus', 'topInput', 'topKeyDown', 'topKeyUp', 'topSelectionChange']
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* For IE shims
|
||
|
*/
|
||
|
var activeElement = null;
|
||
|
var activeElementInst = null;
|
||
|
var activeElementValue = null;
|
||
|
var activeElementValueProp = null;
|
||
|
|
||
|
/**
|
||
|
* SECTION: handle `change` event
|
||
|
*/
|
||
|
function shouldUseChangeEvent(elem) {
|
||
|
var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
|
||
|
return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';
|
||
|
}
|
||
|
|
||
|
var doesChangeEventBubble = false;
|
||
|
if (ExecutionEnvironment.canUseDOM) {
|
||
|
// See `handleChange` comment below
|
||
|
doesChangeEventBubble = isEventSupported('change') && (!document.documentMode || document.documentMode > 8);
|
||
|
}
|
||
|
|
||
|
function manualDispatchChangeEvent(nativeEvent) {
|
||
|
var event = SyntheticEvent.getPooled(eventTypes.change, activeElementInst, nativeEvent, getEventTarget(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(false);
|
||
|
}
|
||
|
|
||
|
function startWatchingForChangeEventIE8(target, targetInst) {
|
||
|
activeElement = target;
|
||
|
activeElementInst = targetInst;
|
||
|
activeElement.attachEvent('onchange', manualDispatchChangeEvent);
|
||
|
}
|
||
|
|
||
|
function stopWatchingForChangeEventIE8() {
|
||
|
if (!activeElement) {
|
||
|
return;
|
||
|
}
|
||
|
activeElement.detachEvent('onchange', manualDispatchChangeEvent);
|
||
|
activeElement = null;
|
||
|
activeElementInst = null;
|
||
|
}
|
||
|
|
||
|
function getTargetInstForChangeEvent(topLevelType, targetInst) {
|
||
|
if (topLevelType === 'topChange') {
|
||
|
return targetInst;
|
||
|
}
|
||
|
}
|
||
|
function handleEventsForChangeEventIE8(topLevelType, target, targetInst) {
|
||
|
if (topLevelType === 'topFocus') {
|
||
|
// stopWatching() should be a noop here but we call it just in case we
|
||
|
// missed a blur event somehow.
|
||
|
stopWatchingForChangeEventIE8();
|
||
|
startWatchingForChangeEventIE8(target, targetInst);
|
||
|
} else if (topLevelType === '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.
|
||
|
// IE10+ fire input events to often, such when a placeholder
|
||
|
// changes or when an input with a placeholder is focused.
|
||
|
isInputEventSupported = isEventSupported('input') && (!document.documentMode || document.documentMode > 11);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* (For IE <=11) 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 IE <=11) 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, targetInst) {
|
||
|
activeElement = target;
|
||
|
activeElementInst = targetInst;
|
||
|
activeElementValue = target.value;
|
||
|
activeElementValueProp = Object.getOwnPropertyDescriptor(target.constructor.prototype, 'value');
|
||
|
|
||
|
// Not guarded in a canDefineProperty check: IE8 supports defineProperty only
|
||
|
// on DOM elements
|
||
|
Object.defineProperty(activeElement, 'value', newValueProp);
|
||
|
if (activeElement.attachEvent) {
|
||
|
activeElement.attachEvent('onpropertychange', handlePropertyChange);
|
||
|
} else {
|
||
|
activeElement.addEventListener('propertychange', handlePropertyChange, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* (For IE <=11) 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;
|
||
|
|
||
|
if (activeElement.detachEvent) {
|
||
|
activeElement.detachEvent('onpropertychange', handlePropertyChange);
|
||
|
} else {
|
||
|
activeElement.removeEventListener('propertychange', handlePropertyChange, false);
|
||
|
}
|
||
|
|
||
|
activeElement = null;
|
||
|
activeElementInst = null;
|
||
|
activeElementValue = null;
|
||
|
activeElementValueProp = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* (For IE <=11) 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 getTargetInstForInputEvent(topLevelType, targetInst) {
|
||
|
if (topLevelType === '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 targetInst;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function handleEventsForInputEventIE(topLevelType, target, targetInst) {
|
||
|
if (topLevelType === '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-11, 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(target, targetInst);
|
||
|
} else if (topLevelType === 'topBlur') {
|
||
|
stopWatchingForValueChange();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// For IE8 and IE9.
|
||
|
function getTargetInstForInputEventIE(topLevelType, targetInst) {
|
||
|
if (topLevelType === 'topSelectionChange' || topLevelType === 'topKeyUp' || topLevelType === '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 activeElementInst;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 && elem.nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio');
|
||
|
}
|
||
|
|
||
|
function getTargetInstForClickEvent(topLevelType, targetInst) {
|
||
|
if (topLevelType === 'topClick') {
|
||
|
return targetInst;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function handleControlledInputBlur(inst, node) {
|
||
|
// TODO: In IE, inst is occasionally null. Why?
|
||
|
if (inst == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Fiber and ReactDOM keep wrapper state in separate places
|
||
|
var state = inst._wrapperState || node._wrapperState;
|
||
|
|
||
|
if (!state || !state.controlled || node.type !== 'number') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If controlled, assign the value attribute to the current value on blur
|
||
|
var value = '' + node.value;
|
||
|
if (node.getAttribute('value') !== value) {
|
||
|
node.setAttribute('value', value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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,
|
||
|
|
||
|
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
|
||
|
var targetNode = targetInst ? ReactDOMComponentTree.getNodeFromInstance(targetInst) : window;
|
||
|
|
||
|
var getTargetInstFunc, handleEventFunc;
|
||
|
if (shouldUseChangeEvent(targetNode)) {
|
||
|
if (doesChangeEventBubble) {
|
||
|
getTargetInstFunc = getTargetInstForChangeEvent;
|
||
|
} else {
|
||
|
handleEventFunc = handleEventsForChangeEventIE8;
|
||
|
}
|
||
|
} else if (isTextInputElement(targetNode)) {
|
||
|
if (isInputEventSupported) {
|
||
|
getTargetInstFunc = getTargetInstForInputEvent;
|
||
|
} else {
|
||
|
getTargetInstFunc = getTargetInstForInputEventIE;
|
||
|
handleEventFunc = handleEventsForInputEventIE;
|
||
|
}
|
||
|
} else if (shouldUseClickEvent(targetNode)) {
|
||
|
getTargetInstFunc = getTargetInstForClickEvent;
|
||
|
}
|
||
|
|
||
|
if (getTargetInstFunc) {
|
||
|
var inst = getTargetInstFunc(topLevelType, targetInst);
|
||
|
if (inst) {
|
||
|
var event = SyntheticEvent.getPooled(eventTypes.change, inst, nativeEvent, nativeEventTarget);
|
||
|
event.type = 'change';
|
||
|
EventPropagators.accumulateTwoPhaseDispatches(event);
|
||
|
return event;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (handleEventFunc) {
|
||
|
handleEventFunc(topLevelType, targetNode, targetInst);
|
||
|
}
|
||
|
|
||
|
// When blurring, set the value attribute for number inputs
|
||
|
if (topLevelType === 'topBlur') {
|
||
|
handleControlledInputBlur(targetInst, targetNode);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
module.exports = ChangeEventPlugin;
|