2019-06-15 22:44:54 +02:00
|
|
|
/*
|
|
|
|
Copyright 2017 Jeremy Scheff
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
|
|
or implied. See the License for the specific language governing
|
|
|
|
permissions and limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { InvalidStateError } from "./errors";
|
|
|
|
import FakeEvent from "./FakeEvent";
|
|
|
|
import { EventCallback, EventType } from "./types";
|
|
|
|
|
|
|
|
type EventTypeProp =
|
2019-06-21 19:18:36 +02:00
|
|
|
| "onabort"
|
|
|
|
| "onblocked"
|
|
|
|
| "oncomplete"
|
|
|
|
| "onerror"
|
|
|
|
| "onsuccess"
|
|
|
|
| "onupgradeneeded"
|
|
|
|
| "onversionchange";
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
interface Listener {
|
2019-06-21 19:18:36 +02:00
|
|
|
callback: EventCallback;
|
|
|
|
capture: boolean;
|
|
|
|
type: EventType;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const stopped = (event: FakeEvent, listener: Listener) => {
|
2019-06-21 19:18:36 +02:00
|
|
|
return (
|
|
|
|
event.immediatePropagationStopped ||
|
|
|
|
(event.eventPhase === event.CAPTURING_PHASE &&
|
|
|
|
listener.capture === false) ||
|
|
|
|
(event.eventPhase === event.BUBBLING_PHASE && listener.capture === true)
|
|
|
|
);
|
2019-06-15 22:44:54 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// http://www.w3.org/TR/dom/#concept-event-listener-invoke
|
|
|
|
const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => {
|
2019-06-21 19:18:36 +02:00
|
|
|
event.currentTarget = obj;
|
|
|
|
|
|
|
|
// The callback might cause obj.listeners to mutate as we traverse it.
|
|
|
|
// Take a copy of the array so that nothing sneaks in and we don't lose
|
|
|
|
// our place.
|
|
|
|
for (const listener of obj.listeners.slice()) {
|
|
|
|
if (event.type !== listener.type || stopped(event, listener)) {
|
|
|
|
continue;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
// @ts-ignore
|
|
|
|
listener.callback.call(event.currentTarget, event);
|
|
|
|
}
|
|
|
|
|
|
|
|
const typeToProp: { [key in EventType]: EventTypeProp } = {
|
|
|
|
abort: "onabort",
|
|
|
|
blocked: "onblocked",
|
|
|
|
complete: "oncomplete",
|
|
|
|
error: "onerror",
|
|
|
|
success: "onsuccess",
|
|
|
|
upgradeneeded: "onupgradeneeded",
|
|
|
|
versionchange: "onversionchange",
|
|
|
|
};
|
|
|
|
const prop = typeToProp[event.type];
|
|
|
|
if (prop === undefined) {
|
|
|
|
throw new Error(`Unknown event type: "${event.type}"`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const callback = event.currentTarget[prop];
|
|
|
|
if (callback) {
|
|
|
|
const listener = {
|
|
|
|
callback,
|
|
|
|
capture: false,
|
|
|
|
type: event.type,
|
2019-06-15 22:44:54 +02:00
|
|
|
};
|
2019-06-21 19:18:36 +02:00
|
|
|
if (!stopped(event, listener)) {
|
|
|
|
// @ts-ignore
|
|
|
|
listener.callback.call(event.currentTarget, event);
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
abstract class FakeEventTarget {
|
2019-06-21 19:18:36 +02:00
|
|
|
public readonly listeners: Listener[] = [];
|
|
|
|
|
|
|
|
// These will be overridden in individual subclasses and made not readonly
|
|
|
|
public readonly onabort: EventCallback | null | undefined;
|
|
|
|
public readonly onblocked: EventCallback | null | undefined;
|
|
|
|
public readonly oncomplete: EventCallback | null | undefined;
|
|
|
|
public readonly onerror: EventCallback | null | undefined;
|
|
|
|
public readonly onsuccess: EventCallback | null | undefined;
|
|
|
|
public readonly onupgradeneeded: EventCallback | null | undefined;
|
|
|
|
public readonly onversionchange: EventCallback | null | undefined;
|
|
|
|
|
2019-07-31 01:33:23 +02:00
|
|
|
static enableTracing: boolean = false;
|
2019-06-21 19:18:36 +02:00
|
|
|
|
|
|
|
public addEventListener(
|
|
|
|
type: EventType,
|
|
|
|
callback: EventCallback,
|
|
|
|
capture = false,
|
|
|
|
) {
|
|
|
|
this.listeners.push({
|
|
|
|
callback,
|
|
|
|
capture,
|
|
|
|
type,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public removeEventListener(
|
|
|
|
type: EventType,
|
|
|
|
callback: EventCallback,
|
|
|
|
capture = false,
|
|
|
|
) {
|
|
|
|
const i = this.listeners.findIndex(listener => {
|
|
|
|
return (
|
|
|
|
listener.type === type &&
|
|
|
|
listener.callback === callback &&
|
|
|
|
listener.capture === capture
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.listeners.splice(i, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// http://www.w3.org/TR/dom/#dispatching-events
|
|
|
|
public dispatchEvent(event: FakeEvent) {
|
|
|
|
if (event.dispatched || !event.initialized) {
|
|
|
|
throw new InvalidStateError("The object is in an invalid state.");
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
event.isTrusted = false;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
event.dispatched = true;
|
|
|
|
event.target = this;
|
|
|
|
// NOT SURE WHEN THIS SHOULD BE SET event.eventPath = [];
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
event.eventPhase = event.CAPTURING_PHASE;
|
|
|
|
if (FakeEventTarget.enableTracing) {
|
|
|
|
console.log(
|
|
|
|
`dispatching '${event.type}' event along path with ${event.eventPath.length} elements`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
for (const obj of event.eventPath) {
|
|
|
|
if (!event.propagationStopped) {
|
|
|
|
invokeEventListeners(event, obj);
|
|
|
|
}
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
event.eventPhase = event.AT_TARGET;
|
|
|
|
if (!event.propagationStopped) {
|
|
|
|
invokeEventListeners(event, event.target);
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
if (event.bubbles) {
|
|
|
|
event.eventPath.reverse();
|
|
|
|
event.eventPhase = event.BUBBLING_PHASE;
|
|
|
|
if (event.eventPath.length === 0 && event.type === "error") {
|
|
|
|
console.error("Unhandled error event: ", event.target);
|
|
|
|
}
|
|
|
|
for (const obj of event.eventPath) {
|
2019-06-15 22:44:54 +02:00
|
|
|
if (!event.propagationStopped) {
|
2019-06-21 19:18:36 +02:00
|
|
|
invokeEventListeners(event, obj);
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
event.dispatched = false;
|
|
|
|
event.eventPhase = event.NONE;
|
|
|
|
event.currentTarget = null;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
if (event.canceled) {
|
|
|
|
return false;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
return true;
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default FakeEventTarget;
|