summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/dom_types.ts54
-rw-r--r--js/dom_util.ts83
-rw-r--r--js/event.ts109
-rw-r--r--js/event_target.ts543
-rw-r--r--js/event_target_test.ts15
-rw-r--r--js/globals.ts2
6 files changed, 757 insertions, 49 deletions
diff --git a/js/dom_types.ts b/js/dom_types.ts
index 7e4198506..2f855aeaa 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<K, V> {
keys(): IterableIterator<K>;
@@ -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<K extends keyof AbortSignalEventMap>(
@@ -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<void>;
read(): Promise<any>;
diff --git a/js/dom_util.ts b/js/dom_util.ts
new file mode 100644
index 000000000..2f22a4b51
--- /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 dc39a3c83..92d2b5fef 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 ba086d544..bb1166237 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 6710b6e68..34c486b9f 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 c8289d305..5fc264a59 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;