/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @flow*/import type {TextInstance, Instance} from '../../client/ReactFiberConfigDOM';
import type {AnyNativeEvent} from '../PluginModuleType';
import type {DOMEventName} from '../DOMEventNames';
import type {DispatchQueue} from '../DOMPluginEventSystem';
import type {EventSystemFlags} from '../EventSystemFlags';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
import {registerTwoPhaseEvent} from '../EventRegistry';
import {SyntheticEvent} from '../SyntheticEvent';
import isTextInputElement from '../isTextInputElement';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import getEventTarget from '../getEventTarget';
import isEventSupported from '../isEventSupported';
import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
import {updateValueIfChanged} from '../../client/inputValueTracking';
import {setDefaultValue} from '../../client/ReactDOMInput';
import {enqueueStateRestore} from '../ReactDOMControlledComponent';
import {
disableInputAttributeSyncing,
enableCustomElementPropertySupport,
} from 'shared/ReactFeatureFlags';
import {batchedUpdates} from '../ReactDOMUpdateBatching';
import {
processDispatchQueue,
accumulateTwoPhaseListeners,
} from '../DOMPluginEventSystem';
import isCustomElement from '../../shared/isCustomElement';
function registerEvents() {
registerTwoPhaseEvent('onChange', [
'change',
'click',
'focusin',
'focusout',
'input',
'keydown',
'keyup',
'selectionchange',
]);}function createAndAccumulateChangeEvent(
dispatchQueue: DispatchQueue,inst: null | Fiber,nativeEvent: AnyNativeEvent,target: null | EventTarget,) {// Flag this event loop as needing state restore.
enqueueStateRestore(((target: any): Node));
const listeners = accumulateTwoPhaseListeners(inst, 'onChange');
if (listeners.length > 0) {
const event: ReactSyntheticEvent = new SyntheticEvent(
'onChange',
'change',
null,
nativeEvent,
target,
);dispatchQueue.push({event, listeners});
}}/*** For IE shims*/let activeElement = null;
let activeElementInst = null;
/*** SECTION: handle `change` event*/function shouldUseChangeEvent(elem: Instance | TextInstance) {
const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
return (
nodeName === 'select' ||
(nodeName === 'input' && (elem: any).type === 'file')
);}function manualDispatchChangeEvent(nativeEvent: AnyNativeEvent) {
const dispatchQueue: DispatchQueue = [];
createAndAccumulateChangeEvent(
dispatchQueue,
activeElementInst,
nativeEvent,
getEventTarget(nativeEvent),
);// 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.
batchedUpdates(runEventInBatch, dispatchQueue);
}function runEventInBatch(dispatchQueue: DispatchQueue) {
processDispatchQueue(dispatchQueue, 0);
}function getInstIfValueChanged(targetInst: Object) {
const targetNode = getNodeFromInstance(targetInst);
if (updateValueIfChanged(((targetNode: any): HTMLInputElement))) {
return targetInst;
}}function getTargetInstForChangeEvent(
domEventName: DOMEventName,targetInst: null | Fiber,) {if (domEventName === 'change') {
return targetInst;
}}/*** SECTION: handle `input` event*/let isInputEventSupported = false;
if (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') &&
(!document.documentMode || document.documentMode > 9);
}/*** (For IE <=9) 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: Instance | TextInstance,targetInst: null | Fiber,) {activeElement = target;
activeElementInst = targetInst;
(activeElement: any).attachEvent('onpropertychange', handlePropertyChange);
}/*** (For IE <=9) Removes the event listeners from the currently-tracked element,* if any exists.*/function stopWatchingForValueChange() {
if (!activeElement) {
return;
}(activeElement: any).detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementInst = null;
}/*** (For IE <=9) Handles a propertychange event, sending a `change` event if* the value of the active element has changed.*/// $FlowFixMe[missing-local-annot]function handlePropertyChange(nativeEvent) {
if (nativeEvent.propertyName !== 'value') {
return;
}if (getInstIfValueChanged(activeElementInst)) {
manualDispatchChangeEvent(nativeEvent);
}}function handleEventsForInputEventPolyfill(
domEventName: DOMEventName,target: Instance | TextInstance,targetInst: null | Fiber,) {if (domEventName === 'focusin') {
// 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(target, targetInst);
} else if (domEventName === 'focusout') {
stopWatchingForValueChange();
}}// For IE8 and IE9.function getTargetInstForInputEventPolyfill(
domEventName: DOMEventName,targetInst: null | Fiber,) {if (
domEventName === 'selectionchange' ||
domEventName === 'keyup' ||
domEventName === 'keydown'
) {// 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.
return getInstIfValueChanged(activeElementInst);
}}/*** SECTION: handle `click` event*/function shouldUseClickEvent(elem: any) {
// 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.
const nodeName = elem.nodeName;
return (
nodeName &&
nodeName.toLowerCase() === 'input' &&
(elem.type === 'checkbox' || elem.type === 'radio')
);}function getTargetInstForClickEvent(
domEventName: DOMEventName,targetInst: null | Fiber,) {if (domEventName === 'click') {
return getInstIfValueChanged(targetInst);
}}function getTargetInstForInputOrChangeEvent(
domEventName: DOMEventName,targetInst: null | Fiber,) {if (domEventName === 'input' || domEventName === 'change') {
return getInstIfValueChanged(targetInst);
}}function handleControlledInputBlur(node: HTMLInputElement, props: any) {
if (node.type !== 'number') {
return;
}if (!disableInputAttributeSyncing) {
const isControlled = props.value != null;
if (isControlled) {
// If controlled, assign the value attribute to the current value on blur
setDefaultValue((node: any), 'number', (node: any).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*/function extractEvents(
dispatchQueue: DispatchQueue,domEventName: DOMEventName,targetInst: null | Fiber,nativeEvent: AnyNativeEvent,nativeEventTarget: null | EventTarget,eventSystemFlags: EventSystemFlags,targetContainer: null | EventTarget,) {const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
let getTargetInstFunc, handleEventFunc;
if (shouldUseChangeEvent(targetNode)) {
getTargetInstFunc = getTargetInstForChangeEvent;
} else if (isTextInputElement(((targetNode: any): HTMLElement))) {
if (isInputEventSupported) {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
getTargetInstFunc = getTargetInstForInputEventPolyfill;
handleEventFunc = handleEventsForInputEventPolyfill;
}} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
} else if (
enableCustomElementPropertySupport &&
targetInst &&
isCustomElement(targetInst.elementType, targetInst.memoizedProps)
) {getTargetInstFunc = getTargetInstForChangeEvent;
}if (getTargetInstFunc) {
const inst = getTargetInstFunc(domEventName, targetInst);
if (inst) {
createAndAccumulateChangeEvent(
dispatchQueue,
inst,
nativeEvent,
nativeEventTarget,
);return;
}}if (handleEventFunc) {
handleEventFunc(domEventName, targetNode, targetInst);
}// When blurring, set the value attribute for number inputs
if (domEventName === 'focusout' && targetInst) {
// These props aren't necessarily the most current but we warn for changing
// between controlled and uncontrolled, so it doesn't matter and the previous
// code was also broken for changes.
const props = targetInst.memoizedProps;
handleControlledInputBlur(((targetNode: any): HTMLInputElement), props);
}}export {registerEvents, extractEvents};