summaryrefslogtreecommitdiff
path: root/cli/js/web/event_target.ts
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/web/event_target.ts')
-rw-r--r--cli/js/web/event_target.ts901
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",
+]);