UNPKG

react-native

Version:

A framework for building native apps using React

437 lines (369 loc) • 11.3 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format */ /** * This module implements the `EventTarget` and related interfaces from the DOM. * See https://dom.spec.whatwg.org/#interface-eventtarget. */ import type {EventPhase} from './Event'; import Event from './Event'; import { getStopImmediatePropagationFlag, getStopPropagationFlag, setComposedPath, setCurrentTarget, setEventPhase, setInPassiveListenerFlag, setIsTrusted, setStopImmediatePropagationFlag, setStopPropagationFlag, setTarget, } from './internals/EventInternals'; import { EVENT_TARGET_GET_THE_PARENT_KEY, INTERNAL_DISPATCH_METHOD_KEY, } from './internals/EventTargetInternals'; export type EventCallback = (event: Event) => void; export type EventHandler = interface { handleEvent(event: Event): void, }; export type EventListener = EventCallback | EventHandler; export type EventListenerOptions = $ReadOnly<{ capture?: boolean, }>; export type AddEventListenerOptions = $ReadOnly<{ ...EventListenerOptions, passive?: boolean, once?: boolean, signal?: AbortSignal, }>; type EventListenerRegistration = { +callback: EventListener, +passive: boolean, +once: boolean, removed: boolean, }; type ListenersMap = Map<string, Map<EventListener, EventListenerRegistration>>; export default class EventTarget { addEventListener( type: string, callback: EventListener | null, optionsOrUseCapture?: AddEventListenerOptions | boolean = {}, ): void { if (arguments.length < 2) { throw new TypeError( `Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`, ); } if (callback == null) { return; } validateCallback(callback, 'addEventListener'); const processedType = String(type); let capture; let passive; let once; let signal; if ( optionsOrUseCapture != null && (typeof optionsOrUseCapture === 'object' || typeof optionsOrUseCapture === 'function') ) { capture = Boolean(optionsOrUseCapture.capture); passive = optionsOrUseCapture.passive == null ? getDefaultPassiveValue(processedType, this) : Boolean(optionsOrUseCapture.passive); once = Boolean(optionsOrUseCapture.once); signal = optionsOrUseCapture.signal; if (signal !== undefined && !(signal instanceof AbortSignal)) { throw new TypeError( "Failed to execute 'addEventListener' on 'EventTarget': Failed to read the 'signal' property from 'AddEventListenerOptions': Failed to convert value to 'AbortSignal'.", ); } } else { capture = Boolean(optionsOrUseCapture); passive = false; once = false; signal = null; } if (signal?.aborted) { return; } let listenersByType = getListenersForPhase(this, capture); let listeners = listenersByType?.get(processedType); if (listeners == null) { if (listenersByType == null) { listenersByType = new Map(); setListenersMap(this, capture, listenersByType); } listeners = new Map(); listenersByType.set(processedType, listeners); } else if (listeners.has(callback)) { return; } const listener: EventListenerRegistration = { callback, passive, once, removed: false, }; listeners.set(callback, listener); const nonNullListeners = listeners; if (signal != null) { signal.addEventListener( 'abort', () => { listener.removed = true; if (nonNullListeners.get(callback) === listener) { nonNullListeners.delete(callback); } }, { once: true, }, ); } } removeEventListener( type: string, callback: EventListener, optionsOrUseCapture?: EventListenerOptions | boolean = {}, ): void { if (arguments.length < 2) { throw new TypeError( `Failed to execute 'removeEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`, ); } if (callback == null) { return; } validateCallback(callback, 'removeEventListener'); const processedType = String(type); const capture = typeof optionsOrUseCapture === 'boolean' ? optionsOrUseCapture : Boolean(optionsOrUseCapture.capture); const listenersByType = getListenersForPhase(this, capture); const listeners = listenersByType?.get(processedType); if (listeners == null) { return; } const listener = listeners.get(callback); if (listener != null) { listener.removed = true; listeners.delete(callback); } } dispatchEvent(event: Event): boolean { if (!(event instanceof Event)) { throw new TypeError( "Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.", ); } if (getEventDispatchFlag(event)) { throw new Error( "Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.", ); } setIsTrusted(event, false); dispatch(this, event); return !event.defaultPrevented; } /** * This a "protected" method to be overridden by a subclass to allow event * propagation. * * Should implement the "get the parent" algorithm * (see https://dom.spec.whatwg.org/#get-the-parent). */ // $FlowExpectedError[unsupported-syntax] [EVENT_TARGET_GET_THE_PARENT_KEY](): EventTarget | null { return null; } /** * This is "protected" method to dispatch trusted events. */ // $FlowExpectedError[unsupported-syntax] [INTERNAL_DISPATCH_METHOD_KEY](event: Event): void { dispatch(this, event); } } function validateCallback(callback: EventListener, methodName: string): void { if (typeof callback !== 'function' && typeof callback !== 'object') { throw new TypeError( `Failed to execute '${methodName}' on 'EventTarget': parameter 2 is not of type 'Object'.`, ); } } function getDefaultPassiveValue( type: string, eventTarget: EventTarget, ): boolean { return false; } /** * This internal version of `dispatchEvent` does not validate the input and * does not reset the `isTrusted` flag, so it can be used for both trusted * and not trusted events. * * Implements the "event dispatch" concept * (see https://dom.spec.whatwg.org/#concept-event-dispatch). */ function dispatch(eventTarget: EventTarget, event: Event): void { setEventDispatchFlag(event, true); const eventPath = getEventPath(eventTarget, event); setComposedPath(event, eventPath); setTarget(event, eventTarget); for (let i = eventPath.length - 1; i >= 0; i--) { if (getStopPropagationFlag(event)) { break; } const target = eventPath[i]; setEventPhase( event, target === eventTarget ? Event.AT_TARGET : Event.CAPTURING_PHASE, ); invoke(target, event, Event.CAPTURING_PHASE); } for (const target of eventPath) { if (getStopPropagationFlag(event)) { break; } // If the event does NOT bubble, we only dispatch the event to the // target in the bubbling phase. if (!event.bubbles && target !== eventTarget) { break; } setEventPhase( event, target === eventTarget ? Event.AT_TARGET : Event.BUBBLING_PHASE, ); invoke(target, event, Event.BUBBLING_PHASE); } setEventPhase(event, Event.NONE); setCurrentTarget(event, null); setComposedPath(event, []); setEventDispatchFlag(event, false); setStopImmediatePropagationFlag(event, false); setStopPropagationFlag(event, false); } /** * Builds the event path for an event about to be dispatched in this target * (see https://dom.spec.whatwg.org/#event-path). * * The return value is also set as `composedPath` for the event. */ function getEventPath( eventTarget: EventTarget, event: Event, ): $ReadOnlyArray<EventTarget> { const path = []; let target: EventTarget | null = eventTarget; while (target != null) { path.push(target); // $FlowExpectedError[prop-missing] target = target[EVENT_TARGET_GET_THE_PARENT_KEY](); } return path; } /** * Implements the event listener invoke concept * (see https://dom.spec.whatwg.org/#concept-event-listener-invoke). */ function invoke( eventTarget: EventTarget, event: Event, eventPhase: EventPhase, ) { const listenersByType = getListenersForPhase( eventTarget, eventPhase === Event.CAPTURING_PHASE, ); setCurrentTarget(event, eventTarget); const maybeListeners = listenersByType?.get(event.type); if (maybeListeners == null) { return; } // This is a copy so listeners added during dispatch are NOT executed. // Note that `maybeListeners.values()` is a live view of the map instead of an // immutable copy. const listeners = Array.from(maybeListeners.values()); setCurrentTarget(event, eventTarget); for (const listener of listeners) { if (listener.removed) { continue; } if (listener.once) { eventTarget.removeEventListener( event.type, listener.callback, eventPhase === Event.CAPTURING_PHASE, ); } if (listener.passive) { setInPassiveListenerFlag(event, true); } const currentEvent = global.event; global.event = event; const callback = listener.callback; try { if (typeof callback === 'function') { callback.call(eventTarget, event); // $FlowExpectedError[method-unbinding] } else if (typeof callback.handleEvent === 'function') { callback.handleEvent(event); } } catch (error) { // TODO: replace with `reportError` when it's available. console.error(error); } if (listener.passive) { setInPassiveListenerFlag(event, false); } global.event = currentEvent; if (getStopImmediatePropagationFlag(event)) { break; } } } const CAPTURING_LISTENERS_KEY = Symbol('capturingListeners'); const BUBBLING_LISTENERS_KEY = Symbol('bubblingListeners'); function getListenersForPhase( eventTarget: EventTarget, isCapture: boolean, ): ?ListenersMap { return isCapture ? // $FlowExpectedError[prop-missing] eventTarget[CAPTURING_LISTENERS_KEY] : // $FlowExpectedError[prop-missing] eventTarget[BUBBLING_LISTENERS_KEY]; } function setListenersMap( eventTarget: EventTarget, isCapture: boolean, listenersMap: ListenersMap, ): void { if (isCapture) { // $FlowExpectedError[prop-missing] eventTarget[CAPTURING_LISTENERS_KEY] = listenersMap; } else { // $FlowExpectedError[prop-missing] eventTarget[BUBBLING_LISTENERS_KEY] = listenersMap; } } const EVENT_DISPATCH_FLAG = Symbol('Event.dispatch'); function getEventDispatchFlag(event: Event): boolean { // $FlowExpectedError[prop-missing] return event[EVENT_DISPATCH_FLAG]; } function setEventDispatchFlag(event: Event, value: boolean): void { // $FlowExpectedError[prop-missing] event[EVENT_DISPATCH_FLAG] = value; }