Skip to content

Commit

Permalink
Add Event web API (denoland#1059)
Browse files Browse the repository at this point in the history
  • Loading branch information
acconrad authored and ry committed Jan 5, 2019
1 parent ad01085 commit f443221
Show file tree
Hide file tree
Showing 9 changed files with 502 additions and 33 deletions.
2 changes: 2 additions & 0 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ ts_sources = [
"js/dispatch.ts",
"js/dom_types.ts",
"js/errors.ts",
"js/event.ts",
"js/event_target.ts",
"js/fetch.ts",
"js/dom_file.ts",
"js/file_info.ts",
Expand Down
71 changes: 38 additions & 33 deletions js/dom_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,6 @@ export interface DomIterable<K, V> {
): void;
}

interface Element {
// TODO
}

export interface HTMLFormElement {
// TODO
}

type EndingType = "transparent" | "native";

export interface BlobPropertyBag {
Expand All @@ -72,7 +64,7 @@ interface AbortSignalEventMap {
abort: ProgressEvent;
}

interface EventTarget {
export interface EventTarget {
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -236,7 +241,7 @@ export interface ReadableStream {
getReader(): ReadableStreamReader;
}

interface EventListenerObject {
export interface EventListenerObject {
handleEvent(evt: Event): void;
}

Expand Down
252 changes: 252 additions & 0 deletions js/event.ts
Original file line number Diff line number Diff line change
@@ -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 });
Loading

0 comments on commit f443221

Please sign in to comment.