Files
react/packages/react-dom/src/events/DOMModernPluginEventSystem.js
T
Sebastian Markbåge 3e94bce765 Enable prefer-const lint rules (#18451)
* Enable prefer-const rule

Stylistically I don't like this but Closure Compiler takes advantage of
this information.

* Auto-fix lints

* Manually fix the remaining callsites
2020-04-01 12:35:52 -07:00

610 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<ReactDOMListener>, captured: Set<ReactDOMListener>},
>,
> = new PossiblyWeakMap();
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
export const reactScopeListenerStore: WeakMap<
ReactScopeMethods,
Map<
DOMTopLevelEventType,
{bubbled: Set<ReactDOMListener>, captured: Set<ReactDOMListener>},
>,
> = new PossiblyWeakMap();
function dispatchEventsForPlugins(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: null | EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const syntheticEvents: Array<ReactSyntheticEvent> = [];
for (let i = 0; i < plugins.length; i++) {
const possiblePlugin: PluginModule<AnyNativeEvent> = 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<ReactSyntheticEvent>);
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);
}
}
}
}