diff options
Diffstat (limited to 'cli/js/web/event_target.ts')
-rw-r--r-- | cli/js/web/event_target.ts | 901 |
1 files changed, 496 insertions, 405 deletions
diff --git a/cli/js/web/event_target.ts b/cli/js/web/event_target.ts index 605504a3a..1a560dfbe 100644 --- a/cli/js/web/event_target.ts +++ b/cli/js/web/event_target.ts @@ -1,497 +1,588 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import * as domTypes from "./dom_types.ts"; -import { hasOwnProperty, requiredArguments } from "./util.ts"; + +// This module follows most of the WHATWG Living Standard for the DOM logic. +// Many parts of the DOM are not implemented in Deno, but the logic for those +// parts still exists. This means you will observe a lot of strange structures +// and impossible logic branches based on what Deno currently supports. + +import { DOMExceptionImpl as DOMException } from "./dom_exception.ts"; +import * as domTypes from "./dom_types.d.ts"; import { - getRoot, - isNode, - isShadowRoot, - isShadowInclusiveAncestor, - isSlotable, - retarget, -} from "./dom_util.ts"; - -// https://dom.spec.whatwg.org/#get-the-parent -// Note: Nodes, shadow roots, and documents override this algorithm so we set it to null. -function getEventTargetParent( - _eventTarget: domTypes.EventTarget, - _event: domTypes.Event -): null { - return null; + EventImpl as Event, + EventPath, + getDispatched, + getPath, + getStopImmediatePropagation, + hasRelatedTarget, + setCurrentTarget, + setDispatched, + setEventPhase, + setInPassiveListener, + setPath, + setRelatedTarget, + setStopImmediatePropagation, + setTarget, +} from "./event.ts"; +import { defineEnumerableProps, requiredArguments } from "./util.ts"; + +// This is currently the only node type we are using, so instead of implementing +// the whole of the Node interface at the moment, this just gives us the one +// value to power the standards based logic +const DOCUMENT_FRAGMENT_NODE = 11; + +// DOM Logic Helper functions and type guards + +/** Get the parent node, for event targets that have a parent. + * + * Ref: https://dom.spec.whatwg.org/#get-the-parent */ +function getParent(eventTarget: EventTarget): EventTarget | null { + return isNode(eventTarget) ? eventTarget.parentNode : null; } -export const eventTargetAssignedSlot: unique symbol = Symbol(); -export const eventTargetHasActivationBehavior: unique symbol = Symbol(); +function getRoot(eventTarget: EventTarget): EventTarget | null { + return isNode(eventTarget) + ? eventTarget.getRootNode({ composed: true }) + : null; +} -export class EventTarget implements domTypes.EventTarget { - public [domTypes.eventTargetHost]: domTypes.EventTarget | null = null; - public [domTypes.eventTargetListeners]: { - [type in string]: domTypes.EventTargetListener[]; - } = {}; - public [domTypes.eventTargetMode] = ""; - public [domTypes.eventTargetNodeType]: domTypes.NodeType = - domTypes.NodeType.DOCUMENT_FRAGMENT_NODE; - private [eventTargetAssignedSlot] = false; - private [eventTargetHasActivationBehavior] = false; +function isNode<T extends EventTarget>( + eventTarget: T | null +): eventTarget is T & domTypes.Node { + return Boolean(eventTarget && "nodeType" in eventTarget); +} - public addEventListener( - type: string, - callback: domTypes.EventListenerOrEventListenerObject | null, - options?: domTypes.AddEventListenerOptions | boolean - ): void { - const this_ = this || globalThis; +// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor +function isShadowInclusiveAncestor( + ancestor: EventTarget | null, + node: EventTarget | null +): boolean { + while (isNode(node)) { + if (node === ancestor) { + return true; + } - requiredArguments("EventTarget.addEventListener", arguments.length, 2); - const normalizedOptions: domTypes.AddEventListenerOptions = eventTargetHelpers.normalizeAddEventHandlerOptions( - options + if (isShadowRoot(node)) { + node = node && getHost(node); + } else { + node = getParent(node); + } + } + + return false; +} + +function isShadowRoot(nodeImpl: EventTarget | null): boolean { + return Boolean( + nodeImpl && + isNode(nodeImpl) && + nodeImpl.nodeType === DOCUMENT_FRAGMENT_NODE && + getHost(nodeImpl) != null + ); +} + +function isSlotable<T extends EventTarget>( + nodeImpl: T | null +): nodeImpl is T & domTypes.Node & domTypes.Slotable { + return Boolean(isNode(nodeImpl) && "assignedSlot" in nodeImpl); +} + +// DOM Logic functions + +/** Append a path item to an event's path. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-path-append + */ +function appendToEventPath( + eventImpl: Event, + target: EventTarget, + targetOverride: EventTarget | null, + relatedTarget: EventTarget | null, + touchTargets: EventTarget[], + slotInClosedTree: boolean +): void { + const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target)); + const rootOfClosedTree = isShadowRoot(target) && getMode(target) === "closed"; + + getPath(eventImpl).push({ + item: target, + itemInShadowTree, + target: targetOverride, + relatedTarget, + touchTargetList: touchTargets, + rootOfClosedTree, + slotInClosedTree, + }); +} + +function dispatch( + targetImpl: EventTarget, + eventImpl: Event, + targetOverride?: EventTarget +): boolean { + let clearTargets = false; + let activationTarget: EventTarget | null = null; + + setDispatched(eventImpl, true); + + targetOverride = targetOverride ?? targetImpl; + const eventRelatedTarget = hasRelatedTarget(eventImpl) + ? eventImpl.relatedTarget + : null; + let relatedTarget = retarget(eventRelatedTarget, targetImpl); + + if (targetImpl !== relatedTarget || targetImpl === eventRelatedTarget) { + const touchTargets: EventTarget[] = []; + + appendToEventPath( + eventImpl, + targetImpl, + targetOverride, + relatedTarget, + touchTargets, + false ); - if (callback === null) { - return; + const isActivationEvent = eventImpl.type === "click"; + + if (isActivationEvent && getHasActivationBehavior(targetImpl)) { + activationTarget = targetImpl; } - const listeners = this_[domTypes.eventTargetListeners]; + let slotInClosedTree = false; + let slotable = + isSlotable(targetImpl) && getAssignedSlot(targetImpl) ? targetImpl : null; + let parent = getParent(targetImpl); - if (!hasOwnProperty(listeners, type)) { - listeners[type] = []; - } + // Populate event path + // https://dom.spec.whatwg.org/#event-path + while (parent !== null) { + if (slotable !== null) { + slotable = null; + + const parentRoot = getRoot(parent); + if ( + isShadowRoot(parentRoot) && + parentRoot && + getMode(parentRoot) === "closed" + ) { + slotInClosedTree = true; + } + } + + relatedTarget = retarget(eventRelatedTarget, parent); - for (let i = 0; i < listeners[type].length; ++i) { - const listener = listeners[type][i]; if ( - ((typeof listener.options === "boolean" && - listener.options === normalizedOptions.capture) || - (typeof listener.options === "object" && - listener.options.capture === normalizedOptions.capture)) && - listener.callback === callback + isNode(parent) && + isShadowInclusiveAncestor(getRoot(targetImpl), parent) ) { - return; + appendToEventPath( + eventImpl, + parent, + null, + relatedTarget, + touchTargets, + slotInClosedTree + ); + } else if (parent === relatedTarget) { + parent = null; + } else { + targetImpl = parent; + + if ( + isActivationEvent && + activationTarget === null && + getHasActivationBehavior(targetImpl) + ) { + activationTarget = targetImpl; + } + + appendToEventPath( + eventImpl, + parent, + targetImpl, + relatedTarget, + touchTargets, + slotInClosedTree + ); } - } - listeners[type].push({ - callback, - options: normalizedOptions, - }); - } + if (parent !== null) { + parent = getParent(parent); + } - public removeEventListener( - type: string, - callback: domTypes.EventListenerOrEventListenerObject | null, - options?: domTypes.EventListenerOptions | boolean - ): void { - const this_ = this || globalThis; + slotInClosedTree = false; + } - requiredArguments("EventTarget.removeEventListener", arguments.length, 2); - const listeners = this_[domTypes.eventTargetListeners]; - if (hasOwnProperty(listeners, type) && callback !== null) { - listeners[type] = listeners[type].filter( - (listener): boolean => listener.callback !== callback - ); + let clearTargetsTupleIndex = -1; + const path = getPath(eventImpl); + for ( + let i = path.length - 1; + i >= 0 && clearTargetsTupleIndex === -1; + i-- + ) { + if (path[i].target !== null) { + clearTargetsTupleIndex = i; + } } + const clearTargetsTuple = path[clearTargetsTupleIndex]; - const normalizedOptions: domTypes.EventListenerOptions = eventTargetHelpers.normalizeEventHandlerOptions( - options - ); + clearTargets = + (isNode(clearTargetsTuple.target) && + isShadowRoot(getRoot(clearTargetsTuple.target))) || + (isNode(clearTargetsTuple.relatedTarget) && + isShadowRoot(getRoot(clearTargetsTuple.relatedTarget))); - if (callback === null) { - // Optimization, not in the spec. - return; - } + setEventPhase(eventImpl, Event.CAPTURING_PHASE); - if (!listeners[type]) { - return; + for (let i = path.length - 1; i >= 0; --i) { + const tuple = path[i]; + + if (tuple.target === null) { + invokeEventListeners(tuple, eventImpl); + } } - for (let i = 0; i < listeners[type].length; ++i) { - const listener = listeners[type][i]; + for (let i = 0; i < path.length; i++) { + const tuple = path[i]; + + if (tuple.target !== null) { + setEventPhase(eventImpl, Event.AT_TARGET); + } else { + setEventPhase(eventImpl, Event.BUBBLING_PHASE); + } if ( - ((typeof listener.options === "boolean" && - listener.options === normalizedOptions.capture) || - (typeof listener.options === "object" && - listener.options.capture === normalizedOptions.capture)) && - listener.callback === callback + (eventImpl.eventPhase === Event.BUBBLING_PHASE && eventImpl.bubbles) || + eventImpl.eventPhase === Event.AT_TARGET ) { - listeners[type].splice(i, 1); - break; + invokeEventListeners(tuple, eventImpl); } } } - public dispatchEvent(event: domTypes.Event): boolean { - const this_ = this || globalThis; + setEventPhase(eventImpl, Event.NONE); + setCurrentTarget(eventImpl, null); + setPath(eventImpl, []); + setDispatched(eventImpl, false); + eventImpl.cancelBubble = false; + setStopImmediatePropagation(eventImpl, false); - requiredArguments("EventTarget.dispatchEvent", arguments.length, 1); - const listeners = this_[domTypes.eventTargetListeners]; - if (!hasOwnProperty(listeners, event.type)) { - return true; - } + if (clearTargets) { + setTarget(eventImpl, null); + setRelatedTarget(eventImpl, null); + } - if (event.dispatched || !event.initialized) { - // TODO(bartlomieju): very likely that different error - // should be thrown here (DOMException?) - throw new TypeError("Tried to dispatch an uninitialized event"); - } + // TODO: invoke activation targets if HTML nodes will be implemented + // if (activationTarget !== null) { + // if (!eventImpl.defaultPrevented) { + // activationTarget._activationBehavior(); + // } + // } - if (event.eventPhase !== domTypes.EventPhase.NONE) { - // TODO(bartlomieju): very likely that different error - // should be thrown here (DOMException?) - throw new TypeError("Tried to dispatch a dispatching event"); - } + return !eventImpl.defaultPrevented; +} - return eventTargetHelpers.dispatch(this_, event); - } +/** Inner invoking of the event listeners where the resolved listeners are + * called. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke */ +function innerInvokeEventListeners( + eventImpl: Event, + targetListeners: Record<string, Listener[]> +): boolean { + let found = false; - get [Symbol.toStringTag](): string { - return "EventTarget"; + const { type } = eventImpl; + + if (!targetListeners || !targetListeners[type]) { + return found; } -} -const eventTargetHelpers = { - // https://dom.spec.whatwg.org/#concept-event-dispatch - dispatch( - targetImpl: EventTarget, - eventImpl: domTypes.Event, - targetOverride?: domTypes.EventTarget - ): boolean { - let clearTargets = false; - let activationTarget = null; + // Copy event listeners before iterating since the list can be modified during the iteration. + const handlers = targetListeners[type].slice(); + + for (let i = 0; i < handlers.length; i++) { + const listener = handlers[i]; + + let capture, once, passive; + if (typeof listener.options === "boolean") { + capture = listener.options; + once = false; + passive = false; + } else { + capture = listener.options.capture; + once = listener.options.once; + passive = listener.options.passive; + } - eventImpl.dispatched = true; + // Check if the event listener has been removed since the listeners has been cloned. + if (!targetListeners[type].includes(listener)) { + continue; + } - targetOverride = targetOverride || targetImpl; - let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl); + found = true; if ( - targetImpl !== relatedTarget || - targetImpl === eventImpl.relatedTarget + (eventImpl.eventPhase === Event.CAPTURING_PHASE && !capture) || + (eventImpl.eventPhase === Event.BUBBLING_PHASE && capture) ) { - const touchTargets: domTypes.EventTarget[] = []; - - eventTargetHelpers.appendToEventPath( - eventImpl, - targetImpl, - targetOverride, - relatedTarget, - touchTargets, - false - ); - - const isActivationEvent = eventImpl.type === "click"; + continue; + } - if (isActivationEvent && targetImpl[eventTargetHasActivationBehavior]) { - activationTarget = targetImpl; - } + if (once) { + targetListeners[type].splice(targetListeners[type].indexOf(listener), 1); + } - let slotInClosedTree = false; - let slotable = - isSlotable(targetImpl) && targetImpl[eventTargetAssignedSlot] - ? targetImpl - : null; - let parent = getEventTargetParent(targetImpl, eventImpl); - - // Populate event path - // https://dom.spec.whatwg.org/#event-path - while (parent !== null) { - if (slotable !== null) { - slotable = null; - - const parentRoot = getRoot(parent); - if ( - isShadowRoot(parentRoot) && - parentRoot && - parentRoot[domTypes.eventTargetMode] === "closed" - ) { - slotInClosedTree = true; - } - } + if (passive) { + setInPassiveListener(eventImpl, true); + } - relatedTarget = retarget(eventImpl.relatedTarget, parent); + if (typeof listener.callback === "object") { + if (typeof listener.callback.handleEvent === "function") { + listener.callback.handleEvent(eventImpl); + } + } else { + listener.callback.call(eventImpl.currentTarget, eventImpl); + } - if ( - isNode(parent) && - isShadowInclusiveAncestor(getRoot(targetImpl), parent) - ) { - eventTargetHelpers.appendToEventPath( - eventImpl, - parent, - null, - relatedTarget, - touchTargets, - slotInClosedTree - ); - } else if (parent === relatedTarget) { - parent = null; - } else { - targetImpl = parent; - - if ( - isActivationEvent && - activationTarget === null && - targetImpl[eventTargetHasActivationBehavior] - ) { - activationTarget = targetImpl; - } - - eventTargetHelpers.appendToEventPath( - eventImpl, - parent, - targetImpl, - relatedTarget, - touchTargets, - slotInClosedTree - ); - } + setInPassiveListener(eventImpl, false); - if (parent !== null) { - parent = getEventTargetParent(parent, eventImpl); - } + if (getStopImmediatePropagation(eventImpl)) { + return found; + } + } - slotInClosedTree = false; - } + return found; +} - let clearTargetsTupleIndex = -1; - for ( - let i = eventImpl.path.length - 1; - i >= 0 && clearTargetsTupleIndex === -1; - i-- - ) { - if (eventImpl.path[i].target !== null) { - clearTargetsTupleIndex = i; - } - } - const clearTargetsTuple = eventImpl.path[clearTargetsTupleIndex]; +/** Invokes the listeners on a given event path with the supplied event. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-listener-invoke */ +function invokeEventListeners(tuple: EventPath, eventImpl: Event): void { + const path = getPath(eventImpl); + const tupleIndex = path.indexOf(tuple); + for (let i = tupleIndex; i >= 0; i--) { + const t = path[i]; + if (t.target) { + setTarget(eventImpl, t.target); + break; + } + } - clearTargets = - (isNode(clearTargetsTuple.target) && - isShadowRoot(getRoot(clearTargetsTuple.target))) || - (isNode(clearTargetsTuple.relatedTarget) && - isShadowRoot(getRoot(clearTargetsTuple.relatedTarget))); + setRelatedTarget(eventImpl, tuple.relatedTarget); - eventImpl.eventPhase = domTypes.EventPhase.CAPTURING_PHASE; + if (eventImpl.cancelBubble) { + return; + } - for (let i = eventImpl.path.length - 1; i >= 0; --i) { - const tuple = eventImpl.path[i]; + setCurrentTarget(eventImpl, tuple.item); - if (tuple.target === null) { - eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl); - } - } + innerInvokeEventListeners(eventImpl, getListeners(tuple.item)); +} - for (let i = 0; i < eventImpl.path.length; i++) { - const tuple = eventImpl.path[i]; +function normalizeAddEventHandlerOptions( + options: boolean | AddEventListenerOptions | undefined +): AddEventListenerOptions { + if (typeof options === "boolean" || typeof options === "undefined") { + return { + capture: Boolean(options), + once: false, + passive: false, + }; + } else { + return options; + } +} - if (tuple.target !== null) { - eventImpl.eventPhase = domTypes.EventPhase.AT_TARGET; - } else { - eventImpl.eventPhase = domTypes.EventPhase.BUBBLING_PHASE; - } +function normalizeEventHandlerOptions( + options: boolean | EventListenerOptions | undefined +): EventListenerOptions { + if (typeof options === "boolean" || typeof options === "undefined") { + return { + capture: Boolean(options), + }; + } else { + return options; + } +} - if ( - (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE && - eventImpl.bubbles) || - eventImpl.eventPhase === domTypes.EventPhase.AT_TARGET - ) { - eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl); - } - } +/** Retarget the target following the spec logic. + * + * Ref: https://dom.spec.whatwg.org/#retarget */ +function retarget(a: EventTarget | null, b: EventTarget): EventTarget | null { + while (true) { + if (!isNode(a)) { + return a; } - eventImpl.eventPhase = domTypes.EventPhase.NONE; + const aRoot = a.getRootNode(); - eventImpl.currentTarget = null; - eventImpl.path = []; - eventImpl.dispatched = false; - eventImpl.cancelBubble = false; - eventImpl.cancelBubbleImmediately = false; + if (aRoot) { + if ( + !isShadowRoot(aRoot) || + (isNode(b) && isShadowInclusiveAncestor(aRoot, b)) + ) { + return a; + } - if (clearTargets) { - eventImpl.target = null; - eventImpl.relatedTarget = null; + a = getHost(aRoot); } + } +} - // TODO: invoke activation targets if HTML nodes will be implemented - // if (activationTarget !== null) { - // if (!eventImpl.defaultPrevented) { - // activationTarget._activationBehavior(); - // } - // } - - return !eventImpl.defaultPrevented; - }, - - // https://dom.spec.whatwg.org/#concept-event-listener-invoke - invokeEventListeners( - targetImpl: EventTarget, - tuple: domTypes.EventPath, - eventImpl: domTypes.Event - ): void { - const tupleIndex = eventImpl.path.indexOf(tuple); - for (let i = tupleIndex; i >= 0; i--) { - const t = eventImpl.path[i]; - if (t.target) { - eventImpl.target = t.target; - break; - } - } +// Non-public state information for an event target that needs to held onto. +// Some of the information should be moved to other entities (like Node, +// ShowRoot, UIElement, etc.). +interface EventTargetData { + assignedSlot: boolean; + hasActivationBehavior: boolean; + host: EventTarget | null; + listeners: Record<string, Listener[]>; + mode: string; +} - eventImpl.relatedTarget = tuple.relatedTarget; +interface Listener { + callback: EventListenerOrEventListenerObject; + options: AddEventListenerOptions; +} - if (eventImpl.cancelBubble) { - return; - } +// Accessors for non-public data - eventImpl.currentTarget = tuple.item; +export const eventTargetData = new WeakMap<EventTarget, EventTargetData>(); - eventTargetHelpers.innerInvokeEventListeners( - targetImpl, - eventImpl, - tuple.item[domTypes.eventTargetListeners] - ); - }, +function getAssignedSlot(target: EventTarget): boolean { + return Boolean(eventTargetData.get(target as EventTarget)?.assignedSlot); +} - // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke - innerInvokeEventListeners( - targetImpl: EventTarget, - eventImpl: domTypes.Event, - targetListeners: { [type in string]: domTypes.EventTargetListener[] } - ): boolean { - let found = false; +function getHasActivationBehavior(target: EventTarget): boolean { + return Boolean( + eventTargetData.get(target as EventTarget)?.hasActivationBehavior + ); +} - const { type } = eventImpl; +function getHost(target: EventTarget): EventTarget | null { + return eventTargetData.get(target as EventTarget)?.host ?? null; +} - if (!targetListeners || !targetListeners[type]) { - return found; - } +function getListeners(target: EventTarget): Record<string, Listener[]> { + return eventTargetData.get(target as EventTarget)?.listeners ?? {}; +} - // Copy event listeners before iterating since the list can be modified during the iteration. - const handlers = targetListeners[type].slice(); +function getMode(target: EventTarget): string | null { + return eventTargetData.get(target as EventTarget)?.mode ?? null; +} - for (let i = 0; i < handlers.length; i++) { - const listener = handlers[i]; +export function getDefaultTargetData(): Readonly<EventTargetData> { + return { + assignedSlot: false, + hasActivationBehavior: false, + host: null, + listeners: Object.create(null), + mode: "", + }; +} - let capture, once, passive; - if (typeof listener.options === "boolean") { - capture = listener.options; - once = false; - passive = false; - } else { - capture = listener.options.capture; - once = listener.options.once; - passive = listener.options.passive; - } +export class EventTargetImpl implements EventTarget { + constructor() { + eventTargetData.set(this, getDefaultTargetData()); + } - // Check if the event listener has been removed since the listeners has been cloned. - if (!targetListeners[type].includes(listener)) { - continue; - } + public addEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: AddEventListenerOptions | boolean + ): void { + requiredArguments("EventTarget.addEventListener", arguments.length, 2); + if (callback === null) { + return; + } + + options = normalizeAddEventHandlerOptions(options); + const { listeners } = eventTargetData.get(this ?? globalThis)!; - found = true; + if (!(type in listeners)) { + listeners[type] = []; + } + for (const listener of listeners[type]) { if ( - (eventImpl.eventPhase === domTypes.EventPhase.CAPTURING_PHASE && - !capture) || - (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE && capture) + ((typeof listener.options === "boolean" && + listener.options === options.capture) || + (typeof listener.options === "object" && + listener.options.capture === options.capture)) && + listener.callback === callback ) { - continue; + return; } + } - if (once) { - targetListeners[type].splice( - targetListeners[type].indexOf(listener), - 1 - ); - } + listeners[type].push({ callback, options }); + } - if (passive) { - eventImpl.inPassiveListener = true; - } + public removeEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: EventListenerOptions | boolean + ): void { + requiredArguments("EventTarget.removeEventListener", arguments.length, 2); - try { - if (typeof listener.callback === "object") { - if (typeof listener.callback.handleEvent === "function") { - listener.callback.handleEvent(eventImpl); - } - } else { - listener.callback.call(eventImpl.currentTarget, eventImpl); - } - } catch (error) { - // TODO(bartlomieju): very likely that different error - // should be thrown here (DOMException?) - throw new Error(error.message); - } + const listeners = eventTargetData.get(this ?? globalThis)!.listeners; + if (callback !== null && type in listeners) { + listeners[type] = listeners[type].filter( + (listener) => listener.callback !== callback + ); + } else if (callback === null || !listeners[type]) { + return; + } - eventImpl.inPassiveListener = false; + options = normalizeEventHandlerOptions(options); - if (eventImpl.cancelBubbleImmediately) { - return found; + for (let i = 0; i < listeners[type].length; ++i) { + const listener = listeners[type][i]; + if ( + ((typeof listener.options === "boolean" && + listener.options === options.capture) || + (typeof listener.options === "object" && + listener.options.capture === options.capture)) && + listener.callback === callback + ) { + listeners[type].splice(i, 1); + break; } } + } - return found; - }, - - normalizeAddEventHandlerOptions( - options: boolean | domTypes.AddEventListenerOptions | undefined - ): domTypes.AddEventListenerOptions { - if (typeof options === "boolean" || typeof options === "undefined") { - const returnValue: domTypes.AddEventListenerOptions = { - capture: Boolean(options), - once: false, - passive: false, - }; - - return returnValue; - } else { - return options; + public dispatchEvent(event: Event): boolean { + requiredArguments("EventTarget.dispatchEvent", arguments.length, 1); + const self = this ?? globalThis; + + const listeners = eventTargetData.get(self)!.listeners; + if (!(event.type in listeners)) { + return true; } - }, - normalizeEventHandlerOptions( - options: boolean | domTypes.EventListenerOptions | undefined - ): domTypes.EventListenerOptions { - if (typeof options === "boolean" || typeof options === "undefined") { - const returnValue: domTypes.EventListenerOptions = { - capture: Boolean(options), - }; + if (getDispatched(event)) { + throw new DOMException("Invalid event state.", "InvalidStateError"); + } - return returnValue; - } else { - return options; + if (event.eventPhase !== Event.NONE) { + throw new DOMException("Invalid event state.", "InvalidStateError"); } - }, - - // https://dom.spec.whatwg.org/#concept-event-path-append - appendToEventPath( - eventImpl: domTypes.Event, - target: domTypes.EventTarget, - targetOverride: domTypes.EventTarget | null, - relatedTarget: domTypes.EventTarget | null, - touchTargets: domTypes.EventTarget[], - slotInClosedTree: boolean - ): void { - const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target)); - const rootOfClosedTree = - isShadowRoot(target) && target[domTypes.eventTargetMode] === "closed"; - - eventImpl.path.push({ - item: target, - itemInShadowTree, - target: targetOverride, - relatedTarget, - touchTargetList: touchTargets, - rootOfClosedTree, - slotInClosedTree, - }); - }, -}; - -Reflect.defineProperty(EventTarget.prototype, "addEventListener", { - enumerable: true, -}); -Reflect.defineProperty(EventTarget.prototype, "removeEventListener", { - enumerable: true, -}); -Reflect.defineProperty(EventTarget.prototype, "dispatchEvent", { - enumerable: true, -}); + + return dispatch(self, event); + } + + get [Symbol.toStringTag](): string { + return "EventTarget"; + } + + protected getParent(_event: Event): EventTarget | null { + return null; + } +} + +defineEnumerableProps(EventTargetImpl, [ + "addEventListener", + "removeEventListener", + "dispatchEvent", +]); |