/** * 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 {DOMTopLevelEventType} from '../events/TopLevelEventTypes'; import type {EventPriority, ReactScopeInstance} from 'shared/ReactTypes'; import type { ReactDOMEventHandle, ReactDOMEventHandleListener, } from '../shared/ReactDOMTypes'; import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties'; import { getClosestInstanceFromNode, getEventHandlerListeners, setEventHandlerListeners, getEventListenerMap, getFiberFromScopeInstance, } from './ReactDOMComponentTree'; import {ELEMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType'; import { listenToNativeEvent, addEventTypeToDispatchConfig, } from '../events/DOMModernPluginEventSystem'; import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags'; import { PLUGIN_EVENT_SYSTEM, IS_EVENT_HANDLE_NON_MANAGED_NODE, } from '../events/EventSystemFlags'; import { enableScopeAPI, enableCreateEventHandleAPI, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; type EventHandleOptions = {| capture?: boolean, passive?: boolean, priority?: EventPriority, |}; const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; function getNearestRootOrPortalContainer(node: Fiber): null | Element { 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 null; } function isValidEventTarget(target: EventTarget | ReactScopeInstance): boolean { return typeof (target: Object).addEventListener === 'function'; } function isReactScope(target: EventTarget | ReactScopeInstance): boolean { return typeof (target: Object).getChildContextValues === 'function'; } function createEventHandleListener( type: DOMTopLevelEventType, isCapturePhaseListener: boolean, callback: (SyntheticEvent) => void, ): ReactDOMEventHandleListener { return { callback, capture: isCapturePhaseListener, type, }; } function registerEventOnNearestTargetContainer( targetFiber: Fiber, topLevelType: DOMTopLevelEventType, isPassiveListener: boolean | void, listenerPriority: EventPriority | void, isCapturePhaseListener: boolean, ): void { // If it is, find the nearest root or portal and make it // our event handle target container. let targetContainer = getNearestRootOrPortalContainer(targetFiber); if (targetContainer === null) { invariant( false, 'ReactDOM.createEventHandle: setListener called on an target ' + 'that did not have a corresponding root. This is likely a bug in React.', ); } if (targetContainer.nodeType === COMMENT_NODE) { targetContainer = ((targetContainer.parentNode: any): Element); } const listenerMap = getEventListenerMap(targetContainer); listenToNativeEvent( topLevelType, targetContainer, listenerMap, PLUGIN_EVENT_SYSTEM, isCapturePhaseListener, isPassiveListener, listenerPriority, ); } function registerReactDOMEvent( target: EventTarget | ReactScopeInstance, topLevelType: DOMTopLevelEventType, isPassiveListener: boolean | void, isCapturePhaseListener: boolean, listenerPriority: EventPriority | void, ): void { // Check if the target is a DOM element. if ((target: any).nodeType === ELEMENT_NODE) { const targetElement = ((target: any): Element); // Check if the DOM element is managed by React. const targetFiber = getClosestInstanceFromNode(targetElement); if (targetFiber === null) { invariant( false, 'ReactDOM.createEventHandle: setListener called on an element ' + 'target that is not managed by React. Ensure React rendered the DOM element.', ); } registerEventOnNearestTargetContainer( targetFiber, topLevelType, isPassiveListener, listenerPriority, isCapturePhaseListener, ); } else if (enableScopeAPI && isReactScope(target)) { const scopeTarget = ((target: any): ReactScopeInstance); const targetFiber = getFiberFromScopeInstance(scopeTarget); if (targetFiber === null) { // Scope is unmounted, do not proceed. return; } registerEventOnNearestTargetContainer( targetFiber, topLevelType, isPassiveListener, listenerPriority, isCapturePhaseListener, ); } else if (isValidEventTarget(target)) { const eventTarget = ((target: any): EventTarget); const listenerMap = getEventListenerMap(eventTarget); listenToNativeEvent( topLevelType, eventTarget, listenerMap, PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE, isCapturePhaseListener, isPassiveListener, listenerPriority, ); } else { invariant( false, 'ReactDOM.createEventHandle: setter called on an invalid ' + 'target. Provide a valid EventTarget or an element managed by React.', ); } } export function createEventHandle( type: string, options?: EventHandleOptions, ): ReactDOMEventHandle { if (enableCreateEventHandleAPI) { const topLevelType = ((type: any): DOMTopLevelEventType); let isCapturePhaseListener = false; let isPassiveListener = undefined; // Undefined means to use the browser default let listenerPriority; if (options != null) { const optionsCapture = options.capture; const optionsPassive = options.passive; const optionsPriority = options.priority; if (typeof optionsCapture === 'boolean') { isCapturePhaseListener = optionsCapture; } if (typeof optionsPassive === 'boolean') { isPassiveListener = optionsPassive; } if (typeof optionsPriority === 'number') { listenerPriority = optionsPriority; } } if (listenerPriority === undefined) { listenerPriority = getEventPriorityForListenerSystem(topLevelType); } const registeredReactDOMEvents = new PossiblyWeakSet(); return ( target: EventTarget | ReactScopeInstance, callback: (SyntheticEvent) => void, ) => { invariant( typeof callback === 'function', 'ReactDOM.createEventHandle: setter called with an invalid ' + 'callback. The callback must be a function.', ); if (!registeredReactDOMEvents.has(target)) { registeredReactDOMEvents.add(target); registerReactDOMEvent( target, topLevelType, isPassiveListener, isCapturePhaseListener, listenerPriority, ); // Add the event to our known event types list. addEventTypeToDispatchConfig(topLevelType); } const listener = createEventHandleListener( topLevelType, isCapturePhaseListener, callback, ); let targetListeners = getEventHandlerListeners(target); if (targetListeners === null) { targetListeners = new Set(); setEventHandlerListeners(target, targetListeners); } targetListeners.add(listener); return () => { ((targetListeners: any): Set).delete( listener, ); }; }; } return (null: any); }