mirror of
https://github.com/denoland/deno.git
synced 2024-12-31 11:34:15 -05:00
add EventTarget implementation (#2377)
This commit is contained in:
parent
73ac5f89f0
commit
9fd4096235
7 changed files with 759 additions and 50 deletions
|
@ -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",
|
||||
|
|
|
@ -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>;
|
||||
|
|
83
js/dom_util.ts
Normal file
83
js/dom_util.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
109
js/event.ts
109
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 });
|
||||
|
|
|
@ -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);
|
||||
} else {
|
||||
(stackElement as domTypes.EventListener).call(this, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return !event.defaultPrevented;
|
||||
|
||||
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 {
|
||||
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 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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue