summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/dom_types.ts71
-rw-r--r--js/event.ts252
-rw-r--r--js/event_target.ts52
-rw-r--r--js/event_target_test.ts66
-rw-r--r--js/event_test.ts70
-rw-r--r--js/globals.ts8
-rw-r--r--js/unit_tests.ts2
-rw-r--r--js/util.ts12
8 files changed, 500 insertions, 33 deletions
diff --git a/js/dom_types.ts b/js/dom_types.ts
index 4bfb3e041..549b0057c 100644
--- a/js/dom_types.ts
+++ b/js/dom_types.ts
@@ -53,14 +53,6 @@ export interface DomIterable<K, V> {
): void;
}
-interface Element {
- // TODO
-}
-
-export interface HTMLFormElement {
- // TODO
-}
-
type EndingType = "transparent" | "native";
export interface BlobPropertyBag {
@@ -72,7 +64,7 @@ interface AbortSignalEventMap {
abort: ProgressEvent;
}
-interface EventTarget {
+export interface EventTarget {
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
@@ -140,39 +132,52 @@ export interface URLSearchParams {
): void;
}
-interface EventListener {
+export interface EventListener {
(evt: Event): void;
}
-interface EventInit {
+export interface EventInit {
bubbles?: boolean;
cancelable?: boolean;
composed?: boolean;
}
-interface Event {
+export enum EventPhase {
+ NONE = 0,
+ CAPTURING_PHASE = 1,
+ AT_TARGET = 2,
+ BUBBLING_PHASE = 3
+}
+
+export interface EventPath {
+ item: EventTarget;
+ itemInShadowTree: boolean;
+ relatedTarget: EventTarget | null;
+ rootOfClosedTree: boolean;
+ slotInClosedTree: boolean;
+ target: EventTarget | null;
+ touchTargetList: EventTarget[];
+}
+
+export interface Event {
+ readonly type: string;
+ readonly target: EventTarget | null;
+ readonly currentTarget: EventTarget | null;
+ composedPath(): EventPath[];
+
+ readonly eventPhase: number;
+
+ stopPropagation(): void;
+ stopImmediatePropagation(): void;
+
readonly bubbles: boolean;
- cancelBubble: boolean;
readonly cancelable: boolean;
- readonly composed: boolean;
- readonly currentTarget: EventTarget | null;
+ preventDefault(): void;
readonly defaultPrevented: boolean;
- readonly eventPhase: number;
+ readonly composed: boolean;
+
readonly isTrusted: boolean;
- returnValue: boolean;
- readonly srcElement: Element | null;
- readonly target: EventTarget | null;
- readonly timeStamp: number;
- readonly type: string;
- deepPath(): EventTarget[];
- initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void;
- preventDefault(): void;
- stopImmediatePropagation(): void;
- stopPropagation(): void;
- readonly AT_TARGET: number;
- readonly BUBBLING_PHASE: number;
- readonly CAPTURING_PHASE: number;
- readonly NONE: number;
+ readonly timeStamp: Date;
}
/* TODO(ry) Re-expose this interface. There is currently some interference
@@ -193,11 +198,11 @@ interface ProgressEvent extends Event {
readonly total: number;
}
-interface EventListenerOptions {
+export interface EventListenerOptions {
capture?: boolean;
}
-interface AddEventListenerOptions extends EventListenerOptions {
+export interface AddEventListenerOptions extends EventListenerOptions {
once?: boolean;
passive?: boolean;
}
@@ -236,7 +241,7 @@ export interface ReadableStream {
getReader(): ReadableStreamReader;
}
-interface EventListenerObject {
+export interface EventListenerObject {
handleEvent(evt: Event): void;
}
diff --git a/js/event.ts b/js/event.ts
new file mode 100644
index 000000000..29fd8177b
--- /dev/null
+++ b/js/event.ts
@@ -0,0 +1,252 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types";
+import { getPrivateValue } from "./util";
+
+// WeakMaps are recommended for private attributes (see MDN link below)
+// tslint:disable-next-line:max-line-length
+// https://developer.mozilla.org/en-US/docs/Archive/Add-ons/Add-on_SDK/Guides/Contributor_s_Guide/Private_Properties#Using_WeakMaps
+export const eventAttributes = new WeakMap();
+
+export class EventInit implements domTypes.EventInit {
+ bubbles = false;
+ cancelable = false;
+ composed = false;
+
+ constructor({ bubbles = false, cancelable = false, composed = false } = {}) {
+ this.bubbles = bubbles;
+ this.cancelable = cancelable;
+ this.composed = composed;
+ }
+}
+
+export class Event implements domTypes.Event {
+ // Each event has the following associated flags
+ private _canceledFlag = false;
+ private _inPassiveListenerFlag = false;
+ private _stopImmediatePropagationFlag = false;
+ private _stopPropagationFlag = false;
+
+ // Property for objects on which listeners will be invoked
+ private _path: domTypes.EventPath[] = [];
+
+ constructor(type: string, eventInitDict: domTypes.EventInit = {}) {
+ eventAttributes.set(this, {
+ type,
+ bubbles: eventInitDict.bubbles || false,
+ cancelable: eventInitDict.cancelable || false,
+ composed: eventInitDict.composed || false,
+ currentTarget: null,
+ eventPhase: domTypes.EventPhase.NONE,
+ isTrusted: false,
+ target: null,
+ timeStamp: Date.now()
+ });
+ }
+
+ get bubbles(): boolean {
+ return getPrivateValue(this, eventAttributes, "bubbles");
+ }
+
+ get cancelBubble(): boolean {
+ return this._stopPropagationFlag;
+ }
+
+ get cancelBubbleImmediately(): boolean {
+ return this._stopImmediatePropagationFlag;
+ }
+
+ get cancelable(): boolean {
+ return getPrivateValue(this, eventAttributes, "cancelable");
+ }
+
+ get composed(): boolean {
+ return getPrivateValue(this, eventAttributes, "composed");
+ }
+
+ get currentTarget(): domTypes.EventTarget {
+ return getPrivateValue(this, eventAttributes, "currentTarget");
+ }
+
+ get defaultPrevented(): boolean {
+ return this._canceledFlag;
+ }
+
+ get eventPhase(): number {
+ return getPrivateValue(this, eventAttributes, "eventPhase");
+ }
+
+ get isTrusted(): boolean {
+ return getPrivateValue(this, eventAttributes, "isTrusted");
+ }
+
+ get target(): domTypes.EventTarget {
+ return getPrivateValue(this, eventAttributes, "target");
+ }
+
+ get timeStamp(): Date {
+ return getPrivateValue(this, eventAttributes, "timeStamp");
+ }
+
+ get type(): string {
+ return getPrivateValue(this, eventAttributes, "type");
+ }
+
+ /** Returns the event’s path (objects on which listeners will be
+ * invoked). This does not include nodes in shadow trees if the
+ * shadow root was created with its ShadowRoot.mode closed.
+ *
+ * event.composedPath();
+ */
+ composedPath(): domTypes.EventPath[] {
+ if (this._path.length === 0) {
+ return [];
+ }
+
+ const composedPath: domTypes.EventPath[] = [
+ {
+ item: this.currentTarget,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: []
+ }
+ ];
+
+ let currentTargetIndex = 0;
+ let currentTargetHiddenSubtreeLevel = 0;
+
+ for (let index = this._path.length - 1; index >= 0; index--) {
+ const { item, rootOfClosedTree, slotInClosedTree } = this._path[index];
+
+ if (rootOfClosedTree) {
+ currentTargetHiddenSubtreeLevel++;
+ }
+
+ if (item === this.currentTarget) {
+ currentTargetIndex = index;
+ break;
+ }
+
+ if (slotInClosedTree) {
+ currentTargetHiddenSubtreeLevel--;
+ }
+ }
+
+ let currentHiddenLevel = currentTargetHiddenSubtreeLevel;
+ let maxHiddenLevel = currentTargetHiddenSubtreeLevel;
+
+ for (let i = currentTargetIndex - 1; i >= 0; i--) {
+ const { item, rootOfClosedTree, slotInClosedTree } = this._path[i];
+
+ if (rootOfClosedTree) {
+ currentHiddenLevel++;
+ }
+
+ if (currentHiddenLevel <= maxHiddenLevel) {
+ composedPath.unshift({
+ item,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: []
+ });
+ }
+
+ if (slotInClosedTree) {
+ currentHiddenLevel--;
+
+ if (currentHiddenLevel < maxHiddenLevel) {
+ maxHiddenLevel = currentHiddenLevel;
+ }
+ }
+ }
+
+ currentHiddenLevel = currentTargetHiddenSubtreeLevel;
+ maxHiddenLevel = currentTargetHiddenSubtreeLevel;
+
+ for (
+ let index = currentTargetIndex + 1;
+ index < this._path.length;
+ index++
+ ) {
+ const { item, rootOfClosedTree, slotInClosedTree } = this._path[index];
+
+ if (slotInClosedTree) {
+ currentHiddenLevel++;
+ }
+
+ if (currentHiddenLevel <= maxHiddenLevel) {
+ composedPath.push({
+ item,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: []
+ });
+ }
+
+ if (rootOfClosedTree) {
+ currentHiddenLevel--;
+
+ if (currentHiddenLevel < maxHiddenLevel) {
+ maxHiddenLevel = currentHiddenLevel;
+ }
+ }
+ }
+
+ return composedPath;
+ }
+
+ /** Cancels the event (if it is cancelable).
+ * See https://dom.spec.whatwg.org/#set-the-canceled-flag
+ *
+ * event.preventDefault();
+ */
+ preventDefault(): void {
+ if (this.cancelable && !this._inPassiveListenerFlag) {
+ this._canceledFlag = true;
+ }
+ }
+
+ /** Stops the propagation of events further along in the DOM.
+ *
+ * event.stopPropagation();
+ */
+ stopPropagation(): void {
+ this._stopPropagationFlag = true;
+ }
+
+ /** For this particular event, no other listener will be called.
+ * Neither those attached on the same element, nor those attached
+ * on elements which will be traversed later (in capture phase,
+ * for instance).
+ *
+ * event.stopImmediatePropagation();
+ */
+ stopImmediatePropagation(): void {
+ this._stopPropagationFlag = true;
+ this._stopImmediatePropagationFlag = true;
+ }
+}
+
+/** Built-in objects providing `get` methods for our
+ * interceptable JavaScript operations.
+ */
+Reflect.defineProperty(Event.prototype, "bubbles", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "cancelable", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "composed", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "currentTarget", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "defaultPrevented", {
+ enumerable: true
+});
+Reflect.defineProperty(Event.prototype, "eventPhase", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "isTrusted", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "target", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "timeStamp", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "type", { enumerable: true });
diff --git a/js/event_target.ts b/js/event_target.ts
new file mode 100644
index 000000000..3226fde96
--- /dev/null
+++ b/js/event_target.ts
@@ -0,0 +1,52 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types";
+
+/* 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 addEventListener(
+ type: string,
+ listener: domTypes.EventListenerOrEventListenerObject | null,
+ options?: boolean | domTypes.AddEventListenerOptions
+ ): void {
+ if (!(type in this.listeners)) {
+ this.listeners[type] = [];
+ }
+ if (listener !== null) {
+ this.listeners[type].push(listener);
+ }
+ }
+
+ public removeEventListener(
+ type: string,
+ callback: domTypes.EventListenerOrEventListenerObject | null,
+ options?: domTypes.EventListenerOptions | boolean
+ ): void {
+ if (type in this.listeners && callback !== null) {
+ this.listeners[type] = this.listeners[type].filter(
+ listener => listener !== callback
+ );
+ }
+ }
+
+ public dispatchEvent(event: domTypes.Event): boolean {
+ if (!(event.type in this.listeners)) {
+ 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);
+ } else {
+ (stackElement as domTypes.EventListener).call(this, event);
+ }
+ }
+ return !event.defaultPrevented;
+ }
+}
diff --git a/js/event_target_test.ts b/js/event_target_test.ts
new file mode 100644
index 000000000..ebb92c7f0
--- /dev/null
+++ b/js/event_target_test.ts
@@ -0,0 +1,66 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEqual } from "./test_util.ts";
+
+test(function addEventListenerTest() {
+ const document = new EventTarget();
+
+ assertEqual(document.addEventListener("x", null, false), undefined);
+ assertEqual(document.addEventListener("x", null, true), undefined);
+ assertEqual(document.addEventListener("x", null), undefined);
+});
+
+test(function constructedEventTargetCanBeUsedAsExpected() {
+ const target = new EventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ function listener(e) {
+ assertEqual(e, event);
+ ++callCount;
+ }
+
+ target.addEventListener("foo", listener);
+
+ target.dispatchEvent(event);
+ assertEqual(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEqual(callCount, 2);
+
+ target.removeEventListener("foo", listener);
+ target.dispatchEvent(event);
+ assertEqual(callCount, 2);
+});
+
+test(function anEventTargetCanBeSubclassed() {
+ class NicerEventTarget extends EventTarget {
+ on(type, listener?, options?) {
+ this.addEventListener(type, listener, options);
+ }
+
+ off(type, callback?, options?) {
+ this.removeEventListener(type, callback, options);
+ }
+ }
+
+ const target = new NicerEventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ function listener() {
+ ++callCount;
+ }
+
+ target.on("foo", listener);
+ assertEqual(callCount, 0);
+
+ target.off("foo", listener);
+ assertEqual(callCount, 0);
+});
+
+test(function removingNullEventListenerShouldSucceed() {
+ const document = new EventTarget();
+ assertEqual(document.removeEventListener("x", null, false), undefined);
+ assertEqual(document.removeEventListener("x", null, true), undefined);
+ assertEqual(document.removeEventListener("x", null), undefined);
+});
diff --git a/js/event_test.ts b/js/event_test.ts
new file mode 100644
index 000000000..3a1254956
--- /dev/null
+++ b/js/event_test.ts
@@ -0,0 +1,70 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import { test, assertEqual } from "./test_util.ts";
+
+test(function eventInitializedWithType() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEqual(event.isTrusted, false);
+ assertEqual(event.target, null);
+ assertEqual(event.currentTarget, null);
+ assertEqual(event.type, "click");
+ assertEqual(event.bubbles, false);
+ assertEqual(event.cancelable, false);
+});
+
+test(function eventInitializedWithTypeAndDict() {
+ const init = "submit";
+ const eventInitDict = new EventInit({ bubbles: true, cancelable: true });
+ const event = new Event(init, eventInitDict);
+
+ assertEqual(event.isTrusted, false);
+ assertEqual(event.target, null);
+ assertEqual(event.currentTarget, null);
+ assertEqual(event.type, "submit");
+ assertEqual(event.bubbles, true);
+ assertEqual(event.cancelable, true);
+});
+
+test(function eventComposedPathSuccess() {
+ const type = "click";
+ const event = new Event(type);
+ const composedPath = event.composedPath();
+
+ assertEqual(composedPath, []);
+});
+
+test(function eventStopPropagationSuccess() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEqual(event.cancelBubble, false);
+ event.stopPropagation();
+ assertEqual(event.cancelBubble, true);
+});
+
+test(function eventStopImmediatePropagationSuccess() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEqual(event.cancelBubble, false);
+ assertEqual(event.cancelBubbleImmediately, false);
+ event.stopImmediatePropagation();
+ assertEqual(event.cancelBubble, true);
+ assertEqual(event.cancelBubbleImmediately, true);
+});
+
+test(function eventPreventDefaultSuccess() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEqual(event.defaultPrevented, false);
+ event.preventDefault();
+ assertEqual(event.defaultPrevented, false);
+
+ const eventInitDict = new EventInit({ bubbles: true, cancelable: true });
+ const cancelableEvent = new Event(type, eventInitDict);
+ assertEqual(cancelableEvent.defaultPrevented, false);
+ cancelableEvent.preventDefault();
+ assertEqual(cancelableEvent.defaultPrevented, true);
+});
diff --git a/js/globals.ts b/js/globals.ts
index efa377e9b..663215341 100644
--- a/js/globals.ts
+++ b/js/globals.ts
@@ -10,6 +10,8 @@
import * as blob from "./blob";
import * as consoleTypes from "./console";
import * as domTypes from "./dom_types";
+import * as event from "./event";
+import * as eventTarget from "./event_target";
import * as formData from "./form_data";
import * as fetchTypes from "./fetch";
import * as headers from "./headers";
@@ -61,6 +63,12 @@ export type Blob = blob.DenoBlob;
// window.File = file.DenoFile;
// export type File = file.DenoFile;
+window.EventInit = event.EventInit;
+export type EventInit = event.EventInit;
+window.Event = event.Event;
+export type Event = event.Event;
+window.EventTarget = eventTarget.EventTarget;
+export type EventTarget = eventTarget.EventTarget;
window.URL = url.URL;
export type URL = url.URL;
window.URLSearchParams = urlSearchParams.URLSearchParams;
diff --git a/js/unit_tests.ts b/js/unit_tests.ts
index b24a156d5..73298297b 100644
--- a/js/unit_tests.ts
+++ b/js/unit_tests.ts
@@ -10,6 +10,8 @@ import "./compiler_test.ts";
import "./console_test.ts";
import "./copy_file_test.ts";
import "./dir_test.ts";
+import "./event_test.ts";
+import "./event_target_test.ts";
import "./fetch_test.ts";
// TODO The global "File" has been temporarily disabled. See the note in
// js/globals.ts
diff --git a/js/util.ts b/js/util.ts
index 58e865337..ec24ec5bd 100644
--- a/js/util.ts
+++ b/js/util.ts
@@ -151,3 +151,15 @@ export function requiredArguments(
throw new TypeError(errMsg);
}
}
+
+// Returns values from a WeakMap to emulate private properties in JavaScript
+export function getPrivateValue<
+ K extends object,
+ V extends object,
+ W extends keyof V
+>(instance: K, weakMap: WeakMap<K, V>, key: W): V[W] {
+ if (weakMap.has(instance)) {
+ return weakMap.get(instance)![key];
+ }
+ throw new TypeError("Illegal invocation");
+}