From 9fd4096235a308a0d405888ef808d6c665bef355 Mon Sep 17 00:00:00 2001 From: Adam Conrad <422184+acconrad@users.noreply.github.com> Date: Mon, 27 May 2019 13:20:34 +0000 Subject: [PATCH] add EventTarget implementation (#2377) --- cli/BUILD.gn | 1 + js/dom_types.ts | 54 ++-- js/dom_util.ts | 83 ++++++ js/event.ts | 109 +++++++- js/event_target.ts | 543 ++++++++++++++++++++++++++++++++++++++-- js/event_target_test.ts | 15 +- js/globals.ts | 2 + 7 files changed, 758 insertions(+), 49 deletions(-) create mode 100644 js/dom_util.ts diff --git a/cli/BUILD.gn b/cli/BUILD.gn index 8e7e5c3051b75..7887624e21004 100644 --- a/cli/BUILD.gn +++ b/cli/BUILD.gn @@ -78,6 +78,7 @@ ts_sources = [ "../js/dispatch.ts", "../js/dispatch_minimal.ts", "../js/dom_types.ts", + "../js/dom_util.ts", "../js/errors.ts", "../js/event.ts", "../js/event_target.ts", diff --git a/js/dom_types.ts b/js/dom_types.ts index 7e419850678b0..2f855aeaa7dad 100644 --- a/js/dom_types.ts +++ b/js/dom_types.ts @@ -41,9 +41,6 @@ type ReferrerPolicy = | "unsafe-url"; export type BlobPart = BufferSource | Blob | string; export type FormDataEntryValue = DomFile | string; -export type EventListenerOrEventListenerObject = - | EventListener - | EventListenerObject; export interface DomIterable { keys(): IterableIterator; @@ -67,16 +64,27 @@ interface AbortSignalEventMap { abort: ProgressEvent; } +// https://dom.spec.whatwg.org/#node +export enum NodeType { + ELEMENT_NODE = 1, + TEXT_NODE = 3, + DOCUMENT_FRAGMENT_NODE = 11 +} + export interface EventTarget { + host: EventTarget | null; + listeners: { [type in string]: EventListener[] }; + mode: string; + nodeType: NodeType; addEventListener( type: string, - listener: EventListenerOrEventListenerObject | null, + callback: (event: Event) => void | null, options?: boolean | AddEventListenerOptions ): void; - dispatchEvent(evt: Event): boolean; + dispatchEvent(event: Event): boolean; removeEventListener( type: string, - listener?: EventListenerOrEventListenerObject | null, + callback?: (event: Event) => void | null, options?: EventListenerOptions | boolean ): void; } @@ -135,7 +143,9 @@ export interface URLSearchParams { } export interface EventListener { - (evt: Event): void; + handleEvent(event: Event): void; + readonly callback: (event: Event) => void | null; + readonly options: boolean | AddEventListenerOptions; } export interface EventInit { @@ -167,11 +177,11 @@ export interface EventPath { export interface Event { readonly type: string; - readonly target: EventTarget | null; - readonly currentTarget: EventTarget | null; + target: EventTarget | null; + currentTarget: EventTarget | null; composedPath(): EventPath[]; - readonly eventPhase: number; + eventPhase: number; stopPropagation(): void; stopImmediatePropagation(): void; @@ -182,8 +192,16 @@ export interface Event { readonly defaultPrevented: boolean; readonly composed: boolean; - readonly isTrusted: boolean; + isTrusted: boolean; readonly timeStamp: Date; + + dispatched: boolean; + readonly initialized: boolean; + inPassiveListener: boolean; + cancelBubble: boolean; + cancelBubbleImmediately: boolean; + path: EventPath[]; + relatedTarget: EventTarget | null; } export interface CustomEvent extends Event { @@ -217,12 +235,12 @@ interface ProgressEvent extends Event { } export interface EventListenerOptions { - capture?: boolean; + capture: boolean; } export interface AddEventListenerOptions extends EventListenerOptions { - once?: boolean; - passive?: boolean; + once: boolean; + passive: boolean; } interface AbortSignal extends EventTarget { @@ -235,7 +253,7 @@ interface AbortSignal extends EventTarget { ): void; addEventListener( type: string, - listener: EventListenerOrEventListenerObject, + listener: EventListener, options?: boolean | AddEventListenerOptions ): void; removeEventListener( @@ -245,7 +263,7 @@ interface AbortSignal extends EventTarget { ): void; removeEventListener( type: string, - listener: EventListenerOrEventListenerObject, + listener: EventListener, options?: boolean | EventListenerOptions ): void; } @@ -257,10 +275,6 @@ export interface ReadableStream { tee(): [ReadableStream, ReadableStream]; } -export interface EventListenerObject { - handleEvent(evt: Event): void; -} - export interface ReadableStreamReader { cancel(): Promise; read(): Promise; diff --git a/js/dom_util.ts b/js/dom_util.ts new file mode 100644 index 0000000000000..2f22a4b51508e --- /dev/null +++ b/js/dom_util.ts @@ -0,0 +1,83 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +// Utility functions for DOM nodes +import * as domTypes from "./dom_types"; + +export function isNode(nodeImpl: domTypes.EventTarget | null): boolean { + return Boolean(nodeImpl && "nodeType" in nodeImpl); +} + +export function isShadowRoot(nodeImpl: domTypes.EventTarget | null): boolean { + return Boolean( + nodeImpl && + nodeImpl.nodeType === domTypes.NodeType.DOCUMENT_FRAGMENT_NODE && + "host" in nodeImpl + ); +} + +export function isSlotable(nodeImpl: domTypes.EventTarget | null): boolean { + return Boolean( + nodeImpl && + (nodeImpl.nodeType === domTypes.NodeType.ELEMENT_NODE || + nodeImpl.nodeType === domTypes.NodeType.TEXT_NODE) + ); +} + +// https://dom.spec.whatwg.org/#node-trees +// const domSymbolTree = Symbol("DOM Symbol Tree"); + +// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor +export function isShadowInclusiveAncestor( + ancestor: domTypes.EventTarget | null, + node: domTypes.EventTarget | null +): boolean { + while (isNode(node)) { + if (node === ancestor) { + return true; + } + + if (isShadowRoot(node)) { + node = node && node.host; + } else { + node = null; // domSymbolTree.parent(node); + } + } + + return false; +} + +export function getRoot( + node: domTypes.EventTarget | null +): domTypes.EventTarget | null { + let root = node; + + // for (const ancestor of domSymbolTree.ancestorsIterator(node)) { + // root = ancestor; + // } + + return root; +} + +// https://dom.spec.whatwg.org/#retarget +export function retarget( + a: domTypes.EventTarget | null, + b: domTypes.EventTarget +): domTypes.EventTarget | null { + while (true) { + if (!isNode(a)) { + return a; + } + + const aRoot = getRoot(a); + + if (aRoot) { + if ( + !isShadowRoot(aRoot) || + (isNode(b) && isShadowInclusiveAncestor(aRoot, b)) + ) { + return a; + } + + a = aRoot.host; + } + } +} diff --git a/js/event.ts b/js/event.ts index dc39a3c83ee81..92d2b5fef13ac 100644 --- a/js/event.ts +++ b/js/event.ts @@ -21,7 +21,7 @@ export class EventInit implements domTypes.EventInit { export class Event implements domTypes.Event { // Each event has the following associated flags private _canceledFlag = false; - private dispatchedFlag = false; + private _dispatchedFlag = false; private _initializedFlag = false; private _inPassiveListenerFlag = false; private _stopImmediatePropagationFlag = false; @@ -42,6 +42,7 @@ export class Event implements domTypes.Event { currentTarget: null, eventPhase: domTypes.EventPhase.NONE, isTrusted: false, + relatedTarget: null, target: null, timeStamp: Date.now() }); @@ -55,10 +56,18 @@ export class Event implements domTypes.Event { return this._stopPropagationFlag; } + set cancelBubble(value: boolean) { + this._stopPropagationFlag = value; + } + get cancelBubbleImmediately(): boolean { return this._stopImmediatePropagationFlag; } + set cancelBubbleImmediately(value: boolean) { + this._stopImmediatePropagationFlag = value; + } + get cancelable(): boolean { return getPrivateValue(this, eventAttributes, "cancelable"); } @@ -71,30 +80,125 @@ export class Event implements domTypes.Event { return getPrivateValue(this, eventAttributes, "currentTarget"); } + set currentTarget(value: domTypes.EventTarget) { + eventAttributes.set(this, { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: value, + eventPhase: this.eventPhase, + isTrusted: this.isTrusted, + relatedTarget: this.relatedTarget, + target: this.target, + timeStamp: this.timeStamp + }); + } + get defaultPrevented(): boolean { return this._canceledFlag; } get dispatched(): boolean { - return this.dispatchedFlag; + return this._dispatchedFlag; + } + + set dispatched(value: boolean) { + this._dispatchedFlag = value; } get eventPhase(): number { return getPrivateValue(this, eventAttributes, "eventPhase"); } + set eventPhase(value: number) { + eventAttributes.set(this, { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: value, + isTrusted: this.isTrusted, + relatedTarget: this.relatedTarget, + target: this.target, + timeStamp: this.timeStamp + }); + } + get initialized(): boolean { return this._initializedFlag; } + set inPassiveListener(value: boolean) { + this._inPassiveListenerFlag = value; + } + get isTrusted(): boolean { return getPrivateValue(this, eventAttributes, "isTrusted"); } + set isTrusted(value: boolean) { + eventAttributes.set(this, { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: this.eventPhase, + isTrusted: value, + relatedTarget: this.relatedTarget, + target: this.target, + timeStamp: this.timeStamp + }); + } + + get path(): domTypes.EventPath[] { + return this._path; + } + + set path(value: domTypes.EventPath[]) { + this._path = value; + } + + get relatedTarget(): domTypes.EventTarget { + return getPrivateValue(this, eventAttributes, "relatedTarget"); + } + + set relatedTarget(value: domTypes.EventTarget) { + eventAttributes.set(this, { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: this.eventPhase, + isTrusted: this.isTrusted, + relatedTarget: value, + target: this.target, + timeStamp: this.timeStamp + }); + } + get target(): domTypes.EventTarget { return getPrivateValue(this, eventAttributes, "target"); } + set target(value: domTypes.EventTarget) { + eventAttributes.set(this, { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: this.eventPhase, + isTrusted: this.isTrusted, + relatedTarget: this.relatedTarget, + target: value, + timeStamp: this.timeStamp + }); + } + get timeStamp(): Date { return getPrivateValue(this, eventAttributes, "timeStamp"); } @@ -257,6 +361,7 @@ Reflect.defineProperty(Event.prototype, "currentTarget", { enumerable: true }); Reflect.defineProperty(Event.prototype, "defaultPrevented", { enumerable: true }); +Reflect.defineProperty(Event.prototype, "dispatched", { enumerable: true }); Reflect.defineProperty(Event.prototype, "eventPhase", { enumerable: true }); Reflect.defineProperty(Event.prototype, "isTrusted", { enumerable: true }); Reflect.defineProperty(Event.prototype, "target", { enumerable: true }); diff --git a/js/event_target.ts b/js/event_target.ts index ba086d5445e20..bb1166237c4f3 100644 --- a/js/event_target.ts +++ b/js/event_target.ts @@ -1,40 +1,184 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import * as domTypes from "./dom_types"; -import { requiredArguments, hasOwnProperty } from "./util"; +import { DenoError, ErrorKind } from "./errors"; +import { hasOwnProperty, requiredArguments } from "./util"; +import { + getRoot, + isNode, + isShadowRoot, + isShadowInclusiveAncestor, + isSlotable, + retarget +} from "./dom_util"; + +// 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; +} + +export class EventListenerOptions implements domTypes.EventListenerOptions { + _capture = false; + + constructor({ capture = false } = {}) { + this._capture = capture; + } + + get capture(): boolean { + return this._capture; + } +} + +export class AddEventListenerOptions extends EventListenerOptions + implements domTypes.AddEventListenerOptions { + _passive = false; + _once = false; + + constructor({ capture = false, passive = false, once = false } = {}) { + super({ capture }); + this._passive = passive; + this._once = once; + } + + get passive(): boolean { + return this._passive; + } + + get once(): boolean { + return this._once; + } +} + +export class EventListener implements domTypes.EventListener { + allEvents: domTypes.Event[] = []; + atEvents: domTypes.Event[] = []; + bubbledEvents: domTypes.Event[] = []; + capturedEvents: domTypes.Event[] = []; + + private _callback: (event: domTypes.Event) => void | null; + private _options: boolean | domTypes.AddEventListenerOptions = false; + + constructor( + callback: (event: domTypes.Event) => void | null, + options: boolean | domTypes.AddEventListenerOptions + ) { + this._callback = callback; + this._options = options; + } + + public handleEvent(event: domTypes.Event): void { + this.allEvents.push(event); + + switch (event.eventPhase) { + case domTypes.EventPhase.CAPTURING_PHASE: + this.capturedEvents.push(event); + break; + case domTypes.EventPhase.AT_TARGET: + this.atEvents.push(event); + break; + case domTypes.EventPhase.BUBBLING_PHASE: + this.bubbledEvents.push(event); + break; + default: + throw new Error("Unspecified event phase"); + } + + this._callback(event); + } + + get callback(): (event: domTypes.Event) => void | null { + return this._callback; + } + + get options(): domTypes.AddEventListenerOptions | boolean { + return this._options; + } +} -/* TODO: This is an incomplete implementation to provide functionality - * for Event. A proper spec is still required for a proper Web API. - */ export class EventTarget implements domTypes.EventTarget { - public listeners: { - [type in string]: domTypes.EventListenerOrEventListenerObject[] - } = {}; + public host: domTypes.EventTarget | null = null; + public listeners: { [type in string]: domTypes.EventListener[] } = {}; + public mode = ""; + public nodeType: domTypes.NodeType = domTypes.NodeType.DOCUMENT_FRAGMENT_NODE; + private _assignedSlot = false; + private _hasActivationBehavior = false; public addEventListener( type: string, - listener: domTypes.EventListenerOrEventListenerObject | null, - _options?: boolean | domTypes.AddEventListenerOptions + callback: (event: domTypes.Event) => void | null, + options?: domTypes.AddEventListenerOptions | boolean ): void { requiredArguments("EventTarget.addEventListener", arguments.length, 2); + const normalizedOptions: domTypes.AddEventListenerOptions = this._normalizeAddEventHandlerOptions( + options + ); + + if (callback === null) { + return; + } + if (!hasOwnProperty(this.listeners, type)) { this.listeners[type] = []; } - if (listener !== null) { - this.listeners[type].push(listener); + + for (let i = 0; i < this.listeners[type].length; ++i) { + const listener = this.listeners[type][i]; + if ( + ((typeof listener.options === "boolean" && + listener.options === normalizedOptions.capture) || + (typeof listener.options === "object" && + listener.options.capture === normalizedOptions.capture)) && + listener.callback === callback + ) { + return; + } } + + this.listeners[type].push(new EventListener(callback, normalizedOptions)); } public removeEventListener( type: string, - callback: domTypes.EventListenerOrEventListenerObject | null, - _options?: domTypes.EventListenerOptions | boolean + callback: (event: domTypes.Event) => void | null, + options?: domTypes.EventListenerOptions | boolean ): void { requiredArguments("EventTarget.removeEventListener", arguments.length, 2); if (hasOwnProperty(this.listeners, type) && callback !== null) { this.listeners[type] = this.listeners[type].filter( - (listener): boolean => listener !== callback + (listener): boolean => listener.callback !== callback ); } + + const normalizedOptions: domTypes.EventListenerOptions = this._normalizeEventHandlerOptions( + options + ); + + if (callback === null) { + // Optimization, not in the spec. + return; + } + + if (!this.listeners[type]) { + return; + } + + for (let i = 0; i < this.listeners[type].length; ++i) { + const listener = this.listeners[type][i]; + + if ( + ((typeof listener.options === "boolean" && + listener.options === normalizedOptions.capture) || + (typeof listener.options === "object" && + listener.options.capture === normalizedOptions.capture)) && + listener.callback === callback + ) { + this.listeners[type].splice(i, 1); + break; + } + } } public dispatchEvent(event: domTypes.Event): boolean { @@ -42,19 +186,378 @@ export class EventTarget implements domTypes.EventTarget { if (!hasOwnProperty(this.listeners, event.type)) { return true; } - const stack = this.listeners[event.type].slice(); - for (const stackElement of stack) { - if ((stackElement as domTypes.EventListenerObject).handleEvent) { - (stackElement as domTypes.EventListenerObject).handleEvent(event); + if (event.dispatched || !event.initialized) { + throw new DenoError( + ErrorKind.InvalidData, + "Tried to dispatch an uninitialized event" + ); + } + + if (event.eventPhase !== domTypes.EventPhase.NONE) { + throw new DenoError( + ErrorKind.InvalidData, + "Tried to dispatch a dispatching event" + ); + } + + event.isTrusted = false; + + return this._dispatch(event); + } + + // https://dom.spec.whatwg.org/#concept-event-dispatch + _dispatch( + eventImpl: domTypes.Event, + targetOverride?: domTypes.EventTarget + ): boolean { + let targetImpl = this; + let clearTargets = false; + let activationTarget = null; + + eventImpl.dispatched = true; + + targetOverride = targetOverride || targetImpl; + let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl); + + if ( + targetImpl !== relatedTarget || + targetImpl === eventImpl.relatedTarget + ) { + const touchTargets: domTypes.EventTarget[] = []; + + this._appendToEventPath( + eventImpl, + targetImpl, + targetOverride, + relatedTarget, + touchTargets, + false + ); + + const isActivationEvent = eventImpl.type === "click"; + + if (isActivationEvent && targetImpl._hasActivationBehavior) { + activationTarget = targetImpl; + } + + let slotInClosedTree = false; + let slotable = + isSlotable(targetImpl) && targetImpl._assignedSlot ? 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.mode === "closed" + ) { + slotInClosedTree = true; + } + } + + relatedTarget = retarget(eventImpl.relatedTarget, parent); + + if ( + isNode(parent) && + isShadowInclusiveAncestor(getRoot(targetImpl), parent) + ) { + this._appendToEventPath( + eventImpl, + parent, + null, + relatedTarget, + touchTargets, + slotInClosedTree + ); + } else if (parent === relatedTarget) { + parent = null; + } else { + targetImpl = parent; + + if ( + isActivationEvent && + activationTarget === null && + targetImpl._hasActivationBehavior + ) { + activationTarget = targetImpl; + } + + this._appendToEventPath( + eventImpl, + parent, + targetImpl, + relatedTarget, + touchTargets, + slotInClosedTree + ); + } + + if (parent !== null) { + parent = getEventTargetParent(parent, eventImpl); + } + + slotInClosedTree = false; + } + + 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]; + + clearTargets = + (isNode(clearTargetsTuple.target) && + isShadowRoot(getRoot(clearTargetsTuple.target))) || + (isNode(clearTargetsTuple.relatedTarget) && + isShadowRoot(getRoot(clearTargetsTuple.relatedTarget))); + + eventImpl.eventPhase = domTypes.EventPhase.CAPTURING_PHASE; + + for (let i = eventImpl.path.length - 1; i >= 0; --i) { + const tuple = eventImpl.path[i]; + + if (tuple.target === null) { + this._invokeEventListeners(tuple, eventImpl); + } + } + + for (let i = 0; i < eventImpl.path.length; i++) { + const tuple = eventImpl.path[i]; + + if (tuple.target !== null) { + eventImpl.eventPhase = domTypes.EventPhase.AT_TARGET; + } else { + eventImpl.eventPhase = domTypes.EventPhase.BUBBLING_PHASE; + } + + if ( + (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE && + eventImpl.bubbles) || + eventImpl.eventPhase === domTypes.EventPhase.AT_TARGET + ) { + this._invokeEventListeners(tuple, eventImpl); + } + } + } + + eventImpl.eventPhase = domTypes.EventPhase.NONE; + + eventImpl.currentTarget = null; + eventImpl.path = []; + eventImpl.dispatched = false; + eventImpl.cancelBubble = false; + eventImpl.cancelBubbleImmediately = false; + + if (clearTargets) { + eventImpl.target = null; + eventImpl.relatedTarget = null; + } + + // 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( + 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; + } + } + + eventImpl.relatedTarget = tuple.relatedTarget; + + if (eventImpl.cancelBubble) { + return; + } + + eventImpl.currentTarget = tuple.item; + + this._innerInvokeEventListeners(eventImpl, tuple.item.listeners); + } + + // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke + _innerInvokeEventListeners( + eventImpl: domTypes.Event, + targetListeners: { [type in string]: domTypes.EventListener[] } + ): boolean { + let found = false; + + const { type } = eventImpl; + + if (!targetListeners || !targetListeners[type]) { + return found; + } + + // 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 { - (stackElement as domTypes.EventListener).call(this, event); + capture = listener.options.capture; + once = listener.options.once; + passive = listener.options.passive; + } + + // Check if the event listener has been removed since the listeners has been cloned. + if (!targetListeners[type].includes(listener)) { + continue; + } + + found = true; + + if ( + (eventImpl.eventPhase === domTypes.EventPhase.CAPTURING_PHASE && + !capture) || + (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE && capture) + ) { + continue; + } + + if (once) { + targetListeners[type].splice( + targetListeners[type].indexOf(listener), + 1 + ); + } + + if (passive) { + eventImpl.inPassiveListener = true; + } + + try { + if (listener.callback && typeof listener.handleEvent === "function") { + listener.handleEvent(eventImpl); + } + } catch (error) { + throw new DenoError(ErrorKind.Interrupted, error.message); + } + + eventImpl.inPassiveListener = false; + + if (eventImpl.cancelBubbleImmediately) { + return found; } } - return !event.defaultPrevented; + + 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; + } + } + + _normalizeEventHandlerOptions( + options: boolean | domTypes.EventListenerOptions | undefined + ): domTypes.EventListenerOptions { + if (typeof options === "boolean" || typeof options === "undefined") { + const returnValue: domTypes.EventListenerOptions = { + capture: Boolean(options) + }; + + return returnValue; + } else { + return options; + } + } + + // 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.mode === "closed"; + + eventImpl.path.push({ + item: target, + itemInShadowTree, + target: targetOverride, + relatedTarget, + touchTargetList: touchTargets, + rootOfClosedTree, + slotInClosedTree + }); } get [Symbol.toStringTag](): string { return "EventTarget"; } } + +/** Built-in objects providing `get` methods for our + * interceptable JavaScript operations. + */ +Reflect.defineProperty(EventTarget.prototype, "host", { + enumerable: true, + writable: true +}); +Reflect.defineProperty(EventTarget.prototype, "listeners", { + enumerable: true, + writable: true +}); +Reflect.defineProperty(EventTarget.prototype, "mode", { + enumerable: true, + writable: true +}); +Reflect.defineProperty(EventTarget.prototype, "nodeType", { + enumerable: true, + writable: true +}); +Reflect.defineProperty(EventTarget.prototype, "addEventListener", { + enumerable: true +}); +Reflect.defineProperty(EventTarget.prototype, "removeEventListener", { + enumerable: true +}); +Reflect.defineProperty(EventTarget.prototype, "dispatchEvent", { + enumerable: true +}); diff --git a/js/event_target_test.ts b/js/event_target_test.ts index 6710b6e6890fb..34c486b9f38a6 100644 --- a/js/event_target_test.ts +++ b/js/event_target_test.ts @@ -14,10 +14,10 @@ test(function constructedEventTargetCanBeUsedAsExpected(): void { const event = new Event("foo", { bubbles: true, cancelable: false }); let callCount = 0; - function listener(e): void { + const listener = (e): void => { assertEquals(e, event); ++callCount; - } + }; target.addEventListener("foo", listener); @@ -47,9 +47,9 @@ test(function anEventTargetCanBeSubclassed(): void { new Event("foo", { bubbles: true, cancelable: false }); let callCount = 0; - function listener(): void { + const listener = (): void => { ++callCount; - } + }; target.on("foo", listener); assertEquals(callCount, 0); @@ -70,10 +70,10 @@ test(function constructedEventTargetUseObjectPrototype(): void { const event = new Event("toString", { bubbles: true, cancelable: false }); let callCount = 0; - function listener(e): void { + const listener = (e): void => { assertEquals(e, event); ++callCount; - } + }; target.addEventListener("toString", listener); @@ -102,7 +102,8 @@ test(function dispatchEventShouldNotThrowError(): void { bubbles: true, cancelable: false }); - target.addEventListener("hasOwnProperty", (): void => {}); + const listener = (): void => {}; + target.addEventListener("hasOwnProperty", listener); target.dispatchEvent(event); } catch { hasThrown = true; diff --git a/js/globals.ts b/js/globals.ts index c8289d3055ef3..5fc264a59e310 100644 --- a/js/globals.ts +++ b/js/globals.ts @@ -95,6 +95,8 @@ window.EventInit = event.EventInit; export type EventInit = event.EventInit; window.Event = event.Event; export type Event = event.Event; +window.EventListener = eventTarget.EventListener; +export type EventListener = eventTarget.EventListener; window.EventTarget = eventTarget.EventTarget; export type EventTarget = eventTarget.EventTarget; window.URL = url.URL;