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} 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,
) {
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});
}
}
let activeElement = null;
let activeElementInst = null;
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),
);
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;
}
}
let isInputEventSupported = false;
if (canUseDOM) {
isInputEventSupported =
isEventSupported('input') &&
(!document.documentMode || document.documentMode > 9);
}
function startWatchingForValueChange(
target: Instance | TextInstance,
targetInst: null | Fiber,
) {
activeElement = target;
activeElementInst = targetInst;
(activeElement: any).attachEvent('onpropertychange', handlePropertyChange);
}
function stopWatchingForValueChange() {
if (!activeElement) {
return;
}
(activeElement: any).detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementInst = null;
}
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') {
stopWatchingForValueChange();
startWatchingForValueChange(target, targetInst);
} else if (domEventName === 'focusout') {
stopWatchingForValueChange();
}
}
function getTargetInstForInputEventPolyfill(
domEventName: DOMEventName,
targetInst: null | Fiber,
) {
if (
domEventName === 'selectionchange' ||
domEventName === 'keyup' ||
domEventName === 'keydown'
) {
return getInstIfValueChanged(activeElementInst);
}
}
function shouldUseClickEvent(elem: any) {
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) {
setDefaultValue((node: any), 'number', (node: any).value);
}
}
}
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 (
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);
}
if (domEventName === 'focusout' && targetInst) {
const props = targetInst.memoizedProps;
handleControlledInputBlur(((targetNode: any): HTMLInputElement), props);
}
}
export {registerEvents, extractEvents};