/** * 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. */ /** * Implements the `EventTarget` interface from the DOM. */ import { setPlatformObject } from '../webidl/PlatformObjects' 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 = { handleEvent(event: Event): void } export type EventListener = EventCallback | EventHandler export type EventListenerOptions = Readonly<{ capture?: boolean }> export type AddEventListenerOptions = Readonly<{ capture?: boolean passive?: boolean once?: boolean signal?: AbortSignal }> type EventListenerRegistration = { readonly callback: EventListener readonly passive: boolean readonly once: boolean removed: boolean } type ListenersMap = Map> 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: boolean let passive: boolean let once: boolean let signal: AbortSignal | null | undefined if ( optionsOrUseCapture != null && (typeof optionsOrUseCapture === 'object' || typeof optionsOrUseCapture === 'function') ) { const opts = optionsOrUseCapture as AddEventListenerOptions capture = Boolean(opts.capture) passive = opts.passive == null ? getDefaultPassiveValue(processedType, this) : Boolean(opts.passive) once = Boolean(opts.once) signal = opts.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 | null, 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 as EventListenerOptions).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 } // "protected" for subclasses: get the parent (see spec) [EVENT_TARGET_GET_THE_PARENT_KEY](): EventTarget | null { return null } // "protected" to dispatch trusted events [INTERNAL_DISPATCH_METHOD_KEY](event: Event): void { dispatch(this, event) } } setPlatformObject(EventTarget) 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 } 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 (!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) } function getEventPath(eventTarget: EventTarget, _event: Event): ReadonlyArray { const path: EventTarget[] = [] let target: EventTarget | null = eventTarget while (target != null) { path.push(target) target = target[EVENT_TARGET_GET_THE_PARENT_KEY]() } return path } function invoke(eventTarget: EventTarget, event: Event, eventPhase: import('./Event').EventPhase) { const listenersByType = getListenersForPhase(eventTarget, eventPhase === Event.CAPTURING_PHASE) setCurrentTarget(event, eventTarget) const maybeListeners = listenersByType?.get(event.type) if (maybeListeners == null) { return } 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 as any).event ;(global as any).event = event const callback = listener.callback as any try { if (typeof callback === 'function') { callback.call(eventTarget, event) } 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 as any).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 | null | undefined { // @ts-expect-error index on symbol return isCapture ? eventTarget[CAPTURING_LISTENERS_KEY] : eventTarget[BUBBLING_LISTENERS_KEY] } function setListenersMap( eventTarget: EventTarget, isCapture: boolean, listenersMap: ListenersMap ): void { if (isCapture) { ;(eventTarget as any)[CAPTURING_LISTENERS_KEY] = listenersMap } else { ;(eventTarget as any)[BUBBLING_LISTENERS_KEY] = listenersMap } } const EVENT_DISPATCH_FLAG = Symbol('Event.dispatch') function getEventDispatchFlag(event: Event): boolean { return (event as any)[EVENT_DISPATCH_FLAG] } function setEventDispatchFlag(event: Event, value: boolean): void { ;(event as any)[EVENT_DISPATCH_FLAG] = value }