/** * Copyright (c) Facebook, Inc. and its 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 {AnyNativeEvent} from 'legacy-events/PluginModuleType'; import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; import type { ElementListenerMap, ElementListenerMapEntry, } from '../events/DOMEventListenerMap'; import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; import type {EventPriority, ReactScopeMethods} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {PluginModule} from 'legacy-events/PluginModuleType'; import type { ReactSyntheticEvent, CustomDispatchConfig, } from 'legacy-events/ReactSyntheticEventType'; import type {ReactDOMListener} from '../shared/ReactDOMTypes'; import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching'; import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils'; import {plugins} from 'legacy-events/EventPluginRegistry'; import { LEGACY_FB_SUPPORT, IS_REPLAYED, IS_TARGET_EVENT_ONLY, } from 'legacy-events/EventSystemFlags'; import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags'; import { addTrappedEventListener, removeTrappedEventListener, } from './ReactDOMEventListener'; import getEventTarget from './getEventTarget'; import {getListenerMapForElement} from './DOMEventListenerMap'; import { TOP_FOCUS, TOP_LOAD, TOP_ABORT, TOP_CANCEL, TOP_INVALID, TOP_BLUR, TOP_SCROLL, TOP_CLOSE, TOP_RESET, TOP_SUBMIT, TOP_CAN_PLAY, TOP_CAN_PLAY_THROUGH, TOP_DURATION_CHANGE, TOP_EMPTIED, TOP_ENCRYPTED, TOP_ENDED, TOP_ERROR, TOP_WAITING, TOP_VOLUME_CHANGE, TOP_TIME_UPDATE, TOP_SUSPEND, TOP_STALLED, TOP_SEEKING, TOP_SEEKED, TOP_PLAY, TOP_PAUSE, TOP_LOAD_START, TOP_LOADED_DATA, TOP_LOADED_METADATA, TOP_RATE_CHANGE, TOP_PROGRESS, TOP_PLAYING, TOP_CLICK, TOP_BEFORE_BLUR, TOP_AFTER_BLUR, } from './DOMTopLevelEventTypes'; import { getClosestInstanceFromNode, getListenersFromTarget, initListenersSet, } from '../client/ReactDOMComponentTree'; import {COMMENT_NODE} from '../shared/HTMLNodeType'; import {topLevelEventsToDispatchConfig} from './DOMEventProperties'; import { enableLegacyFBSupport, enableUseEventAPI, } from 'shared/ReactFeatureFlags'; const capturePhaseEvents = new Set([ TOP_FOCUS, TOP_BLUR, TOP_SCROLL, TOP_LOAD, TOP_ABORT, TOP_CANCEL, TOP_CLOSE, TOP_INVALID, TOP_RESET, TOP_SUBMIT, TOP_ABORT, TOP_CAN_PLAY, TOP_CAN_PLAY_THROUGH, TOP_DURATION_CHANGE, TOP_EMPTIED, TOP_ENCRYPTED, TOP_ENDED, TOP_ERROR, TOP_LOADED_DATA, TOP_LOADED_METADATA, TOP_LOAD_START, TOP_PAUSE, TOP_PLAY, TOP_PLAYING, TOP_PROGRESS, TOP_RATE_CHANGE, TOP_SEEKED, TOP_SEEKING, TOP_STALLED, TOP_SUSPEND, TOP_TIME_UPDATE, TOP_VOLUME_CHANGE, TOP_WAITING, ]); if (enableUseEventAPI) { capturePhaseEvents.add(TOP_BEFORE_BLUR); capturePhaseEvents.add(TOP_AFTER_BLUR); } const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = { customEvent: true, phasedRegistrationNames: { bubbled: null, captured: null, }, }; const isArray = Array.isArray; // TODO: we should remove the FlowFixMes and the casting to figure out how to make // these patterns work properly. // $FlowFixMe: Flow struggles with this pattern, so we also have to cast it. const PossiblyWeakMap = ((typeof WeakMap === 'function' ? WeakMap : Map): any); // $FlowFixMe: Flow cannot handle polymorphic WeakMaps export const eventTargetEventListenerStore: WeakMap< EventTarget, Map< DOMTopLevelEventType, {bubbled: Set, captured: Set}, >, > = new PossiblyWeakMap(); // $FlowFixMe: Flow cannot handle polymorphic WeakMaps export const reactScopeListenerStore: WeakMap< ReactScopeMethods, Map< DOMTopLevelEventType, {bubbled: Set, captured: Set}, >, > = new PossiblyWeakMap(); function dispatchEventsForPlugins( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, targetContainer: null | EventTarget, ): void { const nativeEventTarget = getEventTarget(nativeEvent); const syntheticEvents: Array = []; for (let i = 0; i < plugins.length; i++) { const possiblePlugin: PluginModule = plugins[i]; if (possiblePlugin !== undefined) { const extractedEvents = possiblePlugin.extractEvents( topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); if (isArray(extractedEvents)) { // Flow complains about @@iterator being missing in ReactSyntheticEvent, // so we cast to avoid the Flow error. const arrOfExtractedEvents = ((extractedEvents: any): Array); syntheticEvents.push(...arrOfExtractedEvents); } else if (extractedEvents != null) { syntheticEvents.push(extractedEvents); } } } for (let i = 0; i < syntheticEvents.length; i++) { const syntheticEvent = syntheticEvents[i]; executeDispatchesInOrder(syntheticEvent); // Release the event from the pool if needed if (!syntheticEvent.isPersistent()) { syntheticEvent.constructor.release(syntheticEvent); } } } function shouldUpgradeListener( listenerEntry: void | ElementListenerMapEntry, passive: void | boolean, ): boolean { if (listenerEntry === undefined) { return false; } // Upgrade from passive to active. if (passive !== true && listenerEntry.passive) { return true; } // Upgrade from default-active (browser default) to active. if (passive === false && listenerEntry.passive === undefined) { return true; } // Otherwise, do not upgrade return false; } export function listenToTopLevelEvent( topLevelType: DOMTopLevelEventType, targetContainer: EventTarget, listenerMap: ElementListenerMap, passive?: boolean, priority?: EventPriority, capture?: boolean, ): void { // If we explicitly define capture, then these are for EventTarget objects, // rather than React managed DOM elements. So we need to ensure we separate // capture and non-capture events. For React managed DOM nodes we only use // one or the other, never both. Which one we use is determined by the the // capturePhaseEvents Set (in this module) that defines if the event listener // should use the capture phase – otherwise we always use the bubble phase. // Finally, when we get to dispatching and accumulating event listeners, we // check if the user wanted capture/bubble and emulate the behavior at that // point (we call this accumulating two phase listeners). const typeStr = ((topLevelType: any): string); const listenerMapKey = capture === undefined ? topLevelType : `${typeStr}_${capture ? 'capture' : 'bubble'}`; const listenerEntry = listenerMap.get(listenerMapKey); const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive); if (listenerEntry === undefined || shouldUpgrade) { const isCapturePhase = capture === undefined ? capturePhaseEvents.has(topLevelType) : capture; // If we should upgrade, then we need to remove the existing trapped // event listener for the target container. if (shouldUpgrade) { removeTrappedEventListener( targetContainer, topLevelType, isCapturePhase, ((listenerEntry: any): ElementListenerMapEntry).listener, ); } const listener = addTrappedEventListener( targetContainer, topLevelType, isCapturePhase, false, passive, priority, ); listenerMap.set(listenerMapKey, {passive, listener}); } } export function listenToEvent( registrationName: string, rootContainerElement: Element, ): void { const listenerMap = getListenerMapForElement(rootContainerElement); const dependencies = registrationNameDependencies[registrationName]; for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; listenToTopLevelEvent(dependency, rootContainerElement, listenerMap); } } function willDeferLaterForLegacyFBSupport( topLevelType: DOMTopLevelEventType, targetContainer: EventTarget, ): boolean { if (topLevelType !== TOP_CLICK) { return false; } // We defer all click events with legacy FB support mode on. // This means we add a one time event listener to trigger // after the FB delegated listeners fire. const isDeferredListenerForLegacyFBSupport = true; addTrappedEventListener( targetContainer, topLevelType, false, isDeferredListenerForLegacyFBSupport, ); return true; } function isMatchingRootContainer( grandContainer: Element, targetContainer: EventTarget, ): boolean { return ( grandContainer === targetContainer || (grandContainer.nodeType === COMMENT_NODE && grandContainer.parentNode === targetContainer) ); } export function isManagedDOMElement( target: EventTarget | ReactScopeMethods, ): boolean { return getClosestInstanceFromNode(((target: any): Node)) !== null; } export function isValidEventTarget( target: EventTarget | ReactScopeMethods, ): boolean { return typeof (target: Object).addEventListener === 'function'; } export function isReactScope(target: EventTarget | ReactScopeMethods): boolean { return typeof (target: Object).getChildContextValues === 'function'; } export function dispatchEventForPluginEventSystem( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, targetContainer: null | EventTarget, ): void { let ancestorInst = targetInst; if (targetContainer !== null) { if (eventTargetEventListenerStore.has(targetContainer)) { // For TargetEvent nodes (i.e. document, window) ancestorInst = null; eventSystemFlags |= IS_TARGET_EVENT_ONLY; } else { const targetContainerNode = ((targetContainer: any): Node); // If we are using the legacy FB support flag, we // defer the event to the null with a one // time event listener so we can defer the event. if ( enableLegacyFBSupport && // We do not want to defer if the event system has already been // set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when // we call willDeferLaterForLegacyFBSupport, thus not bailing out // will result in endless cycles like an infinite loop. (eventSystemFlags & LEGACY_FB_SUPPORT) === 0 && // We also don't want to defer during event replaying. (eventSystemFlags & IS_REPLAYED) === 0 && willDeferLaterForLegacyFBSupport(topLevelType, targetContainer) ) { return; } if (targetInst !== null) { // The below logic attempts to work out if we need to change // the target fiber to a different ancestor. We had similar logic // in the legacy event system, except the big difference between // systems is that the modern event system now has an event listener // attached to each React Root and React Portal Root. Together, // the DOM nodes representing these roots are the "rootContainer". // To figure out which ancestor instance we should use, we traverse // up the fiber tree from the target instance and attempt to find // root boundaries that match that of our current "rootContainer". // If we find that "rootContainer", we find the parent fiber // sub-tree for that root and make that our ancestor instance. let node = targetInst; while (true) { if (node === null) { return; } if (node.tag === HostRoot || node.tag === HostPortal) { const container = node.stateNode.containerInfo; if (isMatchingRootContainer(container, targetContainerNode)) { break; } if (node.tag === HostPortal) { // The target is a portal, but it's not the rootContainer we're looking for. // Normally portals handle their own events all the way down to the root. // So we should be able to stop now. However, we don't know if this portal // was part of *our* root. let grandNode = node.return; while (grandNode !== null) { if ( grandNode.tag === HostRoot || grandNode.tag === HostPortal ) { const grandContainer = grandNode.stateNode.containerInfo; if ( isMatchingRootContainer(grandContainer, targetContainerNode) ) { // This is the rootContainer we're looking for and we found it as // a parent of the Portal. That means we can ignore it because the // Portal will bubble through to us. return; } } grandNode = grandNode.return; } } const parentSubtreeInst = getClosestInstanceFromNode(container); if (parentSubtreeInst === null) { return; } node = ancestorInst = parentSubtreeInst; continue; } node = node.return; } } } } batchedEventUpdates(() => dispatchEventsForPlugins( topLevelType, eventSystemFlags, nativeEvent, ancestorInst, targetContainer, ), ); } function getNearestRootOrPortalContainer(instance: Element): Element { let node = getClosestInstanceFromNode(instance); while (node !== null) { const tag = node.tag; // Once we encounter a host container or root container // we can return their DOM instance. if (tag === HostRoot || tag === HostPortal) { return node.stateNode.containerInfo; } node = node.return; } return instance; } function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void { const dispatchConfig = topLevelEventsToDispatchConfig.get(type); // If we don't have a dispatchConfig, then we're dealing with // an event type that React does not know about (i.e. a custom event). // We need to register an event config for this or the SimpleEventPlugin // will not appropriately provide a SyntheticEvent, so we use out empty // dispatch config for custom events. if (dispatchConfig === undefined) { topLevelEventsToDispatchConfig.set( type, emptyDispatchConfigForCustomEvents, ); } } export function attachListenerToManagedDOMElement( listener: ReactDOMListener, ): void { const {event, target} = listener; const {passive, priority, type} = event; const managedTargetElement = ((target: any): Element); const containerEventTarget = getNearestRootOrPortalContainer( managedTargetElement, ); const listenerMap = getListenerMapForElement(containerEventTarget); // Add the event listener to the target container (falling back to // the target if we didn't find one). listenToTopLevelEvent( type, containerEventTarget, listenerMap, passive, priority, ); // Get the internal listeners Set from the target instance. let listeners = getListenersFromTarget(managedTargetElement); // If we don't have any listeners, then we need to init them. if (listeners === null) { listeners = new Set(); initListenersSet(managedTargetElement, listeners); } // Add our listener to the listeners Set. listeners.add(listener); // Finally, add the event to our known event types list. addEventTypeToDispatchConfig(type); } export function detachListenerFromManagedDOMElement( listener: ReactDOMListener, ): void { const {target} = listener; const managedTargetElement = ((target: any): Element); // Get the internal listeners Set from the target instance. const listeners = getListenersFromTarget(managedTargetElement); if (listeners !== null) { // Remove out listener from the listeners Set. listeners.delete(listener); } } export function attachTargetEventListener(listener: ReactDOMListener): void { const {event, target} = listener; const {capture, passive, priority, type} = event; const eventTarget = ((target: any): EventTarget); const listenerMap = getListenerMapForElement(eventTarget); // Add the event listener to the TargetEvent object. listenToTopLevelEvent( type, eventTarget, listenerMap, passive, priority, capture, ); let eventTypeMap = eventTargetEventListenerStore.get(eventTarget); if (eventTypeMap === undefined) { eventTypeMap = new Map(); eventTargetEventListenerStore.set(eventTarget, eventTypeMap); } // Get the listeners by the event type let listeners = eventTypeMap.get(type); if (listeners === undefined) { listeners = {captured: new Set(), bubbled: new Set()}; eventTypeMap.set(type, listeners); } // Add our listener to the listeners Set. if (capture) { listeners.captured.add(listener); } else { listeners.bubbled.add(listener); } // Finally, add the event to our known event types list. addEventTypeToDispatchConfig(type); } export function detachTargetEventListener(listener: ReactDOMListener): void { const {event, target} = listener; const {capture, type} = event; const validEventTarget = ((target: any): EventTarget); const eventTypeMap = eventTargetEventListenerStore.get(validEventTarget); if (eventTypeMap !== undefined) { const listeners = eventTypeMap.get(type); if (listeners !== undefined) { // Remove out listener from the listeners Set. if (capture) { listeners.captured.delete(listener); } else { listeners.bubbled.delete(listener); } } } } export function attachListenerToReactScope(listener: ReactDOMListener): void { const {event, target} = listener; const {capture, type} = event; const reactScope = ((target: any): ReactScopeMethods); let eventTypeMap = reactScopeListenerStore.get(reactScope); if (eventTypeMap === undefined) { eventTypeMap = new Map(); reactScopeListenerStore.set(reactScope, eventTypeMap); } // Get the listeners by the event type let listeners = eventTypeMap.get(type); if (listeners === undefined) { listeners = {captured: new Set(), bubbled: new Set()}; eventTypeMap.set(type, listeners); } // Add our listener to the listeners Set. if (capture) { listeners.captured.add(listener); } else { listeners.bubbled.add(listener); } // Finally, add the event to our known event types list. addEventTypeToDispatchConfig(type); } export function detachListenerFromReactScope(listener: ReactDOMListener): void { const {event, target} = listener; const {capture, type} = event; const reactScope = ((target: any): ReactScopeMethods); const eventTypeMap = reactScopeListenerStore.get(reactScope); if (eventTypeMap !== undefined) { const listeners = eventTypeMap.get(type); if (listeners !== undefined) { // Remove out listener from the listeners Set. if (capture) { listeners.captured.delete(listener); } else { listeners.bubbled.delete(listener); } } } }