/** * 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;