import type {DOMEventName} from './DOMEventNames';
import type {EventSystemFlags} from './EventSystemFlags';
import type {AnyNativeEvent} from './PluginModuleType';
import type {
KnownReactSyntheticEvent,
ReactSyntheticEvent,
} from './ReactSyntheticEventType';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import {allNativeEvents} from './EventRegistry';
import {
SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE,
IS_LEGACY_FB_SUPPORT_MODE,
SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS,
IS_CAPTURE_PHASE,
IS_EVENT_HANDLE_NON_MANAGED_NODE,
IS_NON_DELEGATED,
} from './EventSystemFlags';
import {isReplayingEvent} from './CurrentReplayingEvent';
import {
HostRoot,
HostPortal,
HostComponent,
HostHoistable,
HostSingleton,
HostText,
ScopeComponent,
} from 'react-reconciler/src/ReactWorkTags';
import {getLowestCommonAncestor} from 'react-reconciler/src/ReactFiberTreeReflection';
import getEventTarget from './getEventTarget';
import {
getClosestInstanceFromNode,
getEventListenerSet,
getEventHandlerListeners,
} from '../client/ReactDOMComponentTree';
import {COMMENT_NODE, DOCUMENT_NODE} from '../client/HTMLNodeType';
import {batchedUpdates} from './ReactDOMUpdateBatching';
import getListener from './getListener';
import {passiveBrowserEventsSupported} from './checkPassiveEvents';
import {
enableLegacyFBSupport,
enableCreateEventHandleAPI,
enableScopeAPI,
disableCommentsAsDOMContainers,
enableScrollEndPolyfill,
} from 'shared/ReactFeatureFlags';
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
import {
removeEventListener,
addEventCaptureListener,
addEventBubbleListener,
addEventBubbleListenerWithPassiveFlag,
addEventCaptureListenerWithPassiveFlag,
} from './EventListener';
import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
import * as FormActionEventPlugin from './plugins/FormActionEventPlugin';
import * as ScrollEndEventPlugin from './plugins/ScrollEndEventPlugin';
import reportGlobalError from 'shared/reportGlobalError';
import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
type DispatchListener = {
instance: null | Fiber,
listener: Function,
currentTarget: EventTarget,
};
type DispatchEntry = {
event: ReactSyntheticEvent,
listeners: Array<DispatchListener>,
};
export type DispatchQueue = Array<DispatchEntry>;
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
if (enableScrollEndPolyfill) {
ScrollEndEventPlugin.registerEvents();
}
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
if (shouldProcessPolyfillPlugins) {
EnterLeaveEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
ChangeEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
SelectEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
BeforeInputEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
FormActionEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
}
if (enableScrollEndPolyfill) {
ScrollEndEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
}
}
export const mediaEventTypes: Array<DOMEventName> = [
'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'encrypted',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'loadstart',
'pause',
'play',
'playing',
'progress',
'ratechange',
'resize',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting',
];
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
'beforetoggle',
'cancel',
'close',
'invalid',
'load',
'scroll',
'scrollend',
'toggle',
...mediaEventTypes,
]);
function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget,
): void {
event.currentTarget = currentTarget;
try {
listener(event);
} catch (error) {
reportGlobalError(error);
}
event.currentTarget = null;
}
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
if (__DEV__ && instance !== null) {
runWithFiberInDEV(
instance,
executeDispatch,
event,
listener,
currentTarget,
);
} else {
executeDispatch(event, listener, currentTarget);
}
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
if (__DEV__ && instance !== null) {
runWithFiberInDEV(
instance,
executeDispatch,
event,
listener,
currentTarget,
);
} else {
executeDispatch(event, listener, currentTarget);
}
previousInstance = instance;
}
}
}
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags,
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0; i < dispatchQueue.length; i++) {
const {event, listeners} = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
}
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
export function listenToNonDelegatedEvent(
domEventName: DOMEventName,
targetElement: Element,
): void {
if (__DEV__) {
if (!nonDelegatedEvents.has(domEventName)) {
console.error(
'Did not expect a listenToNonDelegatedEvent() call for "%s". ' +
'This is a bug in React. Please file an issue.',
domEventName,
);
}
}
const isCapturePhaseListener = false;
const listenerSet = getEventListenerSet(targetElement);
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
if (!listenerSet.has(listenerSetKey)) {
addTrappedEventListener(
targetElement,
domEventName,
IS_NON_DELEGATED,
isCapturePhaseListener,
);
listenerSet.add(listenerSetKey);
}
}
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget,
): void {
if (__DEV__) {
if (nonDelegatedEvents.has(domEventName) && !isCapturePhaseListener) {
console.error(
'Did not expect a listenToNativeEvent() call for "%s" in the bubble phase. ' +
'This is a bug in React. Please file an issue.',
domEventName,
);
}
}
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}
export function listenToNativeEventForNonManagedEventTarget(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget,
): void {
let eventSystemFlags = IS_EVENT_HANDLE_NON_MANAGED_NODE;
const listenerSet = getEventListenerSet(target);
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
if (!listenerSet.has(listenerSetKey)) {
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
listenerSet.add(listenerSetKey);
}
}
const listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
let isPassiveListener: void | boolean = undefined;
if (passiveBrowserEventsSupported) {
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}
targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
? (targetContainer: any).ownerDocument
: targetContainer;
let unsubscribeListener;
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener;
listener = function (...p) {
removeEventListener(
targetContainer,
domEventName,
unsubscribeListener,
isCapturePhaseListener,
);
return originalListener.apply(this, p);
};
}
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
}
function deferClickToDocumentForLegacyFBSupport(
domEventName: DOMEventName,
targetContainer: EventTarget,
): void {
const isDeferredListenerForLegacyFBSupport = true;
addTrappedEventListener(
targetContainer,
domEventName,
IS_LEGACY_FB_SUPPORT_MODE,
false,
isDeferredListenerForLegacyFBSupport,
);
}
function isMatchingRootContainer(
grandContainer: Element,
targetContainer: EventTarget,
): boolean {
return (
grandContainer === targetContainer ||
(!disableCommentsAsDOMContainers &&
grandContainer.nodeType === COMMENT_NODE &&
grandContainer.parentNode === targetContainer)
);
}
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = ((targetContainer: any): Node);
if (
enableLegacyFBSupport &&
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
}
if (targetInst !== null) {
let node: null | Fiber = targetInst;
mainLoop: while (true) {
if (node === null) {
return;
}
const nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
let container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
let grandNode = node.return;
while (grandNode !== null) {
const grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
const grandContainer = grandNode.stateNode.containerInfo;
if (
isMatchingRootContainer(grandContainer, targetContainerNode)
) {
return;
}
}
grandNode = grandNode.return;
}
}
while (container !== null) {
const parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
const parentTag = parentNode.tag;
if (
parentTag === HostComponent ||
parentTag === HostText ||
parentTag === HostHoistable ||
parentTag === HostSingleton
) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
function createDispatchListener(
instance: null | Fiber,
listener: Function,
currentTarget: EventTarget,
): DispatchListener {
return {
instance,
listener,
currentTarget,
};
}
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
nativeEvent: AnyNativeEvent,
): Array<DispatchListener> {
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
let listeners: Array<DispatchListener> = [];
let instance = targetFiber;
let lastHostComponent = null;
while (instance !== null) {
const {stateNode, tag} = instance;
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
stateNode !== null
) {
lastHostComponent = stateNode;
if (enableCreateEventHandleAPI) {
const eventHandlerListeners =
getEventHandlerListeners(lastHostComponent);
if (eventHandlerListeners !== null) {
eventHandlerListeners.forEach(entry => {
if (
entry.type === nativeEventType &&
entry.capture === inCapturePhase
) {
listeners.push(
createDispatchListener(
instance,
entry.callback,
(lastHostComponent: any),
),
);
}
});
}
}
if (reactEventName !== null) {
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
} else if (
enableCreateEventHandleAPI &&
enableScopeAPI &&
tag === ScopeComponent &&
lastHostComponent !== null &&
stateNode !== null
) {
const reactScopeInstance = stateNode;
const eventHandlerListeners =
getEventHandlerListeners(reactScopeInstance);
if (eventHandlerListeners !== null) {
eventHandlerListeners.forEach(entry => {
if (
entry.type === nativeEventType &&
entry.capture === inCapturePhase
) {
listeners.push(
createDispatchListener(
instance,
entry.callback,
(lastHostComponent: any),
),
);
}
});
}
}
if (accumulateTargetOnly) {
break;
}
if (enableCreateEventHandleAPI && nativeEvent.type === 'beforeblur') {
const detachedInterceptFiber = nativeEvent._detachedInterceptFiber;
if (
detachedInterceptFiber !== null &&
(detachedInterceptFiber === instance ||
detachedInterceptFiber === instance.alternate)
) {
listeners = [];
}
}
instance = instance.return;
}
return listeners;
}
export function accumulateTwoPhaseListeners(
targetFiber: Fiber | null,
reactName: string,
): Array<DispatchListener> {
const captureName = reactName + 'Capture';
const listeners: Array<DispatchListener> = [];
let instance = targetFiber;
while (instance !== null) {
const {stateNode, tag} = instance;
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
stateNode !== null
) {
const currentTarget = stateNode;
const captureListener = getListener(instance, captureName);
if (captureListener != null) {
listeners.unshift(
createDispatchListener(instance, captureListener, currentTarget),
);
}
const bubbleListener = getListener(instance, reactName);
if (bubbleListener != null) {
listeners.push(
createDispatchListener(instance, bubbleListener, currentTarget),
);
}
}
if (instance.tag === HostRoot) {
return listeners;
}
instance = instance.return;
}
return [];
}
function getParent(inst: Fiber | null): Fiber | null {
if (inst === null) {
return null;
}
do {
inst = inst.return;
} while (inst && inst.tag !== HostComponent && inst.tag !== HostSingleton);
if (inst) {
return inst;
}
return null;
}
function accumulateEnterLeaveListenersForEvent(
dispatchQueue: DispatchQueue,
event: KnownReactSyntheticEvent,
target: Fiber,
common: Fiber | null,
inCapturePhase: boolean,
): void {
const registrationName = event._reactName;
const listeners: Array<DispatchListener> = [];
let instance: null | Fiber = target;
while (instance !== null) {
if (instance === common) {
break;
}
const {alternate, stateNode, tag} = instance;
if (alternate !== null && alternate === common) {
break;
}
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
stateNode !== null
) {
const currentTarget = stateNode;
if (inCapturePhase) {
const captureListener = getListener(instance, registrationName);
if (captureListener != null) {
listeners.unshift(
createDispatchListener(instance, captureListener, currentTarget),
);
}
} else if (!inCapturePhase) {
const bubbleListener = getListener(instance, registrationName);
if (bubbleListener != null) {
listeners.push(
createDispatchListener(instance, bubbleListener, currentTarget),
);
}
}
}
instance = instance.return;
}
if (listeners.length !== 0) {
dispatchQueue.push({event, listeners});
}
}
export function accumulateEnterLeaveTwoPhaseListeners(
dispatchQueue: DispatchQueue,
leaveEvent: KnownReactSyntheticEvent,
enterEvent: null | KnownReactSyntheticEvent,
from: Fiber | null,
to: Fiber | null,
): void {
const common =
from && to ? getLowestCommonAncestor(from, to, getParent) : null;
if (from !== null) {
accumulateEnterLeaveListenersForEvent(
dispatchQueue,
leaveEvent,
from,
common,
false,
);
}
if (to !== null && enterEvent !== null) {
accumulateEnterLeaveListenersForEvent(
dispatchQueue,
enterEvent,
to,
common,
true,
);
}
}
export function accumulateEventHandleNonManagedNodeListeners(
reactEventType: DOMEventName,
currentTarget: EventTarget,
inCapturePhase: boolean,
): Array<DispatchListener> {
const listeners: Array<DispatchListener> = [];
const eventListeners = getEventHandlerListeners(currentTarget);
if (eventListeners !== null) {
eventListeners.forEach(entry => {
if (entry.type === reactEventType && entry.capture === inCapturePhase) {
listeners.push(
createDispatchListener(null, entry.callback, currentTarget),
);
}
});
}
return listeners;
}
export function getListenerSetKey(
domEventName: DOMEventName,
capture: boolean,
): string {
return `${domEventName}__${capture ? 'capture' : 'bubble'}`;
}