deno/ext/web/02_event.js
2024-03-21 13:57:32 -07:00

1533 lines
35 KiB
JavaScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module follows most of the WHATWG Living Standard for the DOM logic.
// Many parts of the DOM are not implemented in Deno, but the logic for those
// parts still exists. This means you will observe a lot of strange structures
// and impossible logic branches based on what Deno currently supports.
import { core, primordials } from "ext:core/mod.js";
const {
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ArrayPrototypeUnshift,
Boolean,
Error,
FunctionPrototypeCall,
MapPrototypeGet,
MapPrototypeSet,
ObjectCreate,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectPrototypeIsPrototypeOf,
ReflectDefineProperty,
SafeArrayIterator,
SafeMap,
StringPrototypeStartsWith,
Symbol,
SymbolFor,
SymbolToStringTag,
TypeError,
} = primordials;
import * as webidl from "ext:deno_webidl/00_webidl.js";
import { DOMException } from "./01_dom_exception.js";
import { createFilteredInspectProxy } from "ext:deno_console/01_console.js";
// This should be set via setGlobalThis this is required so that if even
// user deletes globalThis it is still usable
let globalThis_;
function saveGlobalThisReference(val) {
globalThis_ = val;
}
// accessors for non runtime visible data
function getDispatched(event) {
return Boolean(event[_dispatched]);
}
function getPath(event) {
return event[_path] ?? [];
}
function getStopImmediatePropagation(event) {
return Boolean(event[_stopImmediatePropagationFlag]);
}
function setCurrentTarget(
event,
value,
) {
event[_attributes].currentTarget = value;
}
function setIsTrusted(event, value) {
event[_isTrusted] = value;
}
function setDispatched(event, value) {
event[_dispatched] = value;
}
function setEventPhase(event, value) {
event[_attributes].eventPhase = value;
}
function setInPassiveListener(event, value) {
event[_inPassiveListener] = value;
}
function setPath(event, value) {
event[_path] = value;
}
function setRelatedTarget(
event,
value,
) {
event[_attributes].relatedTarget = value;
}
function setTarget(event, value) {
event[_attributes].target = value;
}
function setStopImmediatePropagation(
event,
value,
) {
event[_stopImmediatePropagationFlag] = value;
}
const isTrusted = ObjectGetOwnPropertyDescriptor({
get isTrusted() {
return this[_isTrusted];
},
}, "isTrusted").get;
const _attributes = Symbol("[[attributes]]");
const _canceledFlag = Symbol("[[canceledFlag]]");
const _stopPropagationFlag = Symbol("[[stopPropagationFlag]]");
const _stopImmediatePropagationFlag = Symbol(
"[[stopImmediatePropagationFlag]]",
);
const _inPassiveListener = Symbol("[[inPassiveListener]]");
const _dispatched = Symbol("[[dispatched]]");
const _isTrusted = Symbol("[[isTrusted]]");
const _path = Symbol("[[path]]");
class Event {
constructor(type, eventInitDict = {}) {
// TODO(lucacasonato): remove when this interface is spec aligned
this[SymbolToStringTag] = "Event";
this[_canceledFlag] = false;
this[_stopPropagationFlag] = false;
this[_stopImmediatePropagationFlag] = false;
this[_inPassiveListener] = false;
this[_dispatched] = false;
this[_isTrusted] = false;
this[_path] = [];
webidl.requiredArguments(
arguments.length,
1,
"Failed to construct 'Event'",
);
type = webidl.converters.DOMString(
type,
"Failed to construct 'Event'",
"Argument 1",
);
this[_attributes] = {
type,
bubbles: !!eventInitDict.bubbles,
cancelable: !!eventInitDict.cancelable,
composed: !!eventInitDict.composed,
currentTarget: null,
eventPhase: Event.NONE,
target: null,
timeStamp: 0,
};
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(EventPrototype, this),
keys: EVENT_PROPS,
}),
inspectOptions,
);
}
get type() {
return this[_attributes].type;
}
get target() {
return this[_attributes].target;
}
get srcElement() {
return null;
}
set srcElement(_) {
// this member is deprecated
}
get currentTarget() {
return this[_attributes].currentTarget;
}
composedPath() {
const path = this[_path];
if (path.length === 0) {
return [];
}
if (!this.currentTarget) {
throw new Error("assertion error");
}
const composedPath = [
{
item: this.currentTarget,
itemInShadowTree: false,
relatedTarget: null,
rootOfClosedTree: false,
slotInClosedTree: false,
target: null,
touchTargetList: [],
},
];
let currentTargetIndex = 0;
let currentTargetHiddenSubtreeLevel = 0;
for (let index = path.length - 1; index >= 0; index--) {
const { item, rootOfClosedTree, slotInClosedTree } = 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 } = path[i];
if (rootOfClosedTree) {
currentHiddenLevel++;
}
if (currentHiddenLevel <= maxHiddenLevel) {
ArrayPrototypeUnshift(composedPath, {
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 < path.length; index++) {
const { item, rootOfClosedTree, slotInClosedTree } = path[index];
if (slotInClosedTree) {
currentHiddenLevel++;
}
if (currentHiddenLevel <= maxHiddenLevel) {
ArrayPrototypePush(composedPath, {
item,
itemInShadowTree: false,
relatedTarget: null,
rootOfClosedTree: false,
slotInClosedTree: false,
target: null,
touchTargetList: [],
});
}
if (rootOfClosedTree) {
currentHiddenLevel--;
if (currentHiddenLevel < maxHiddenLevel) {
maxHiddenLevel = currentHiddenLevel;
}
}
}
return ArrayPrototypeMap(composedPath, (p) => p.item);
}
get NONE() {
return Event.NONE;
}
get CAPTURING_PHASE() {
return Event.CAPTURING_PHASE;
}
get AT_TARGET() {
return Event.AT_TARGET;
}
get BUBBLING_PHASE() {
return Event.BUBBLING_PHASE;
}
static get NONE() {
return 0;
}
static get CAPTURING_PHASE() {
return 1;
}
static get AT_TARGET() {
return 2;
}
static get BUBBLING_PHASE() {
return 3;
}
get eventPhase() {
return this[_attributes].eventPhase;
}
stopPropagation() {
this[_stopPropagationFlag] = true;
}
get cancelBubble() {
return this[_stopPropagationFlag];
}
set cancelBubble(value) {
this[_stopPropagationFlag] = webidl.converters.boolean(value);
}
stopImmediatePropagation() {
this[_stopPropagationFlag] = true;
this[_stopImmediatePropagationFlag] = true;
}
get bubbles() {
return this[_attributes].bubbles;
}
get cancelable() {
return this[_attributes].cancelable;
}
get returnValue() {
return !this[_canceledFlag];
}
set returnValue(value) {
if (!webidl.converters.boolean(value)) {
this[_canceledFlag] = true;
}
}
preventDefault() {
if (this[_attributes].cancelable && !this[_inPassiveListener]) {
this[_canceledFlag] = true;
}
}
get defaultPrevented() {
return this[_canceledFlag];
}
get composed() {
return this[_attributes].composed;
}
get initialized() {
return true;
}
get timeStamp() {
return this[_attributes].timeStamp;
}
}
const EventPrototype = Event.prototype;
// Not spec compliant. The spec defines it as [LegacyUnforgeable]
// but doing so has a big performance hit
ReflectDefineProperty(Event.prototype, "isTrusted", {
enumerable: true,
get: isTrusted,
});
function defineEnumerableProps(
Ctor,
props,
) {
for (let i = 0; i < props.length; ++i) {
const prop = props[i];
ReflectDefineProperty(Ctor.prototype, prop, { enumerable: true });
}
}
const EVENT_PROPS = [
"bubbles",
"cancelable",
"composed",
"currentTarget",
"defaultPrevented",
"eventPhase",
"srcElement",
"target",
"returnValue",
"timeStamp",
"type",
];
defineEnumerableProps(Event, EVENT_PROPS);
// This is currently the only node type we are using, so instead of implementing
// the whole of the Node interface at the moment, this just gives us the one
// value to power the standards based logic
const DOCUMENT_FRAGMENT_NODE = 11;
// DOM Logic Helper functions and type guards
/** Get the parent node, for event targets that have a parent.
*
* Ref: https://dom.spec.whatwg.org/#get-the-parent */
function getParent(eventTarget) {
return isNode(eventTarget) ? eventTarget.parentNode : null;
}
function getRoot(eventTarget) {
return isNode(eventTarget)
? eventTarget.getRootNode({ composed: true })
: null;
}
function isNode(
eventTarget,
) {
return eventTarget?.nodeType !== undefined;
}
// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor
function isShadowInclusiveAncestor(
ancestor,
node,
) {
while (isNode(node)) {
if (node === ancestor) {
return true;
}
if (isShadowRoot(node)) {
node = node && getHost(node);
} else {
node = getParent(node);
}
}
return false;
}
function isShadowRoot(nodeImpl) {
return Boolean(
nodeImpl &&
isNode(nodeImpl) &&
nodeImpl.nodeType === DOCUMENT_FRAGMENT_NODE &&
getHost(nodeImpl) != null,
);
}
function isSlottable(
/* nodeImpl, */
) {
// TODO(marcosc90) currently there aren't any slottables nodes
// https://dom.spec.whatwg.org/#concept-slotable
// return isNode(nodeImpl) && ReflectHas(nodeImpl, "assignedSlot");
return false;
}
// DOM Logic functions
/** Append a path item to an event's path.
*
* Ref: https://dom.spec.whatwg.org/#concept-event-path-append
*/
function appendToEventPath(
eventImpl,
target,
targetOverride,
relatedTarget,
touchTargets,
slotInClosedTree,
) {
const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target));
const rootOfClosedTree = isShadowRoot(target) &&
getMode(target) === "closed";
ArrayPrototypePush(getPath(eventImpl), {
item: target,
itemInShadowTree,
target: targetOverride,
relatedTarget,
touchTargetList: touchTargets,
rootOfClosedTree,
slotInClosedTree,
});
}
function dispatch(
targetImpl,
eventImpl,
targetOverride,
) {
let clearTargets = false;
let activationTarget = null;
setDispatched(eventImpl, true);
targetOverride = targetOverride ?? targetImpl;
const eventRelatedTarget = eventImpl.relatedTarget;
let relatedTarget = retarget(eventRelatedTarget, targetImpl);
if (targetImpl !== relatedTarget || targetImpl === eventRelatedTarget) {
const touchTargets = [];
appendToEventPath(
eventImpl,
targetImpl,
targetOverride,
relatedTarget,
touchTargets,
false,
);
const isActivationEvent = eventImpl.type === "click";
if (isActivationEvent && getHasActivationBehavior(targetImpl)) {
activationTarget = targetImpl;
}
let slotInClosedTree = false;
let slottable = isSlottable(targetImpl) && getAssignedSlot(targetImpl)
? targetImpl
: null;
let parent = getParent(targetImpl);
// Populate event path
// https://dom.spec.whatwg.org/#event-path
while (parent !== null) {
if (slottable !== null) {
slottable = null;
const parentRoot = getRoot(parent);
if (
isShadowRoot(parentRoot) &&
parentRoot &&
getMode(parentRoot) === "closed"
) {
slotInClosedTree = true;
}
}
relatedTarget = retarget(eventRelatedTarget, parent);
if (
isNode(parent) &&
isShadowInclusiveAncestor(getRoot(targetImpl), parent)
) {
appendToEventPath(
eventImpl,
parent,
null,
relatedTarget,
touchTargets,
slotInClosedTree,
);
} else if (parent === relatedTarget) {
parent = null;
} else {
targetImpl = parent;
if (
isActivationEvent &&
activationTarget === null &&
getHasActivationBehavior(targetImpl)
) {
activationTarget = targetImpl;
}
appendToEventPath(
eventImpl,
parent,
targetImpl,
relatedTarget,
touchTargets,
slotInClosedTree,
);
}
if (parent !== null) {
parent = getParent(parent);
}
slotInClosedTree = false;
}
let clearTargetsTupleIndex = -1;
const path = getPath(eventImpl);
for (
let i = path.length - 1;
i >= 0 && clearTargetsTupleIndex === -1;
i--
) {
if (path[i].target !== null) {
clearTargetsTupleIndex = i;
}
}
const clearTargetsTuple = path[clearTargetsTupleIndex];
clearTargets = (isNode(clearTargetsTuple.target) &&
isShadowRoot(getRoot(clearTargetsTuple.target))) ||
(isNode(clearTargetsTuple.relatedTarget) &&
isShadowRoot(getRoot(clearTargetsTuple.relatedTarget)));
setEventPhase(eventImpl, Event.CAPTURING_PHASE);
for (let i = path.length - 1; i >= 0; --i) {
const tuple = path[i];
if (tuple.target === null) {
invokeEventListeners(tuple, eventImpl);
}
}
for (let i = 0; i < path.length; i++) {
const tuple = path[i];
if (tuple.target !== null) {
setEventPhase(eventImpl, Event.AT_TARGET);
} else {
setEventPhase(eventImpl, Event.BUBBLING_PHASE);
}
if (
(eventImpl.eventPhase === Event.BUBBLING_PHASE &&
eventImpl.bubbles) ||
eventImpl.eventPhase === Event.AT_TARGET
) {
invokeEventListeners(tuple, eventImpl);
}
}
}
setEventPhase(eventImpl, Event.NONE);
setCurrentTarget(eventImpl, null);
setPath(eventImpl, []);
setDispatched(eventImpl, false);
eventImpl.cancelBubble = false;
setStopImmediatePropagation(eventImpl, false);
if (clearTargets) {
setTarget(eventImpl, null);
setRelatedTarget(eventImpl, null);
}
// TODO(bartlomieju): invoke activation targets if HTML nodes will be implemented
// if (activationTarget !== null) {
// if (!eventImpl.defaultPrevented) {
// activationTarget._activationBehavior();
// }
// }
return !eventImpl.defaultPrevented;
}
/** Inner invoking of the event listeners where the resolved listeners are
* called.
*
* Ref: https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke */
function innerInvokeEventListeners(
eventImpl,
targetListeners,
) {
let found = false;
const { type } = eventImpl;
if (!targetListeners || !targetListeners[type]) {
return found;
}
let handlers = targetListeners[type];
const handlersLength = handlers.length;
// Copy event listeners before iterating since the list can be modified during the iteration.
if (handlersLength > 1) {
handlers = ArrayPrototypeSlice(targetListeners[type]);
}
for (let i = 0; i < handlersLength; 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 (!ArrayPrototypeIncludes(targetListeners[type], listener)) {
continue;
}
found = true;
if (
(eventImpl.eventPhase === Event.CAPTURING_PHASE && !capture) ||
(eventImpl.eventPhase === Event.BUBBLING_PHASE && capture)
) {
continue;
}
if (once) {
ArrayPrototypeSplice(
targetListeners[type],
ArrayPrototypeIndexOf(targetListeners[type], listener),
1,
);
}
if (passive) {
setInPassiveListener(eventImpl, true);
}
if (typeof listener.callback === "object") {
if (typeof listener.callback.handleEvent === "function") {
listener.callback.handleEvent(eventImpl);
}
} else {
FunctionPrototypeCall(
listener.callback,
eventImpl.currentTarget,
eventImpl,
);
}
setInPassiveListener(eventImpl, false);
if (getStopImmediatePropagation(eventImpl)) {
return found;
}
}
return found;
}
/** Invokes the listeners on a given event path with the supplied event.
*
* Ref: https://dom.spec.whatwg.org/#concept-event-listener-invoke */
function invokeEventListeners(tuple, eventImpl) {
const path = getPath(eventImpl);
if (path.length === 1) {
const t = path[0];
if (t.target) {
setTarget(eventImpl, t.target);
}
} else {
const tupleIndex = ArrayPrototypeIndexOf(path, tuple);
for (let i = tupleIndex; i >= 0; i--) {
const t = path[i];
if (t.target) {
setTarget(eventImpl, t.target);
break;
}
}
}
setRelatedTarget(eventImpl, tuple.relatedTarget);
if (eventImpl.cancelBubble) {
return;
}
setCurrentTarget(eventImpl, tuple.item);
try {
innerInvokeEventListeners(eventImpl, getListeners(tuple.item));
} catch (error) {
reportException(error);
}
}
function normalizeEventHandlerOptions(
options,
) {
if (typeof options === "boolean" || typeof options === "undefined") {
return {
capture: Boolean(options),
};
} else {
return options;
}
}
/** Retarget the target following the spec logic.
*
* Ref: https://dom.spec.whatwg.org/#retarget */
function retarget(a, b) {
while (true) {
if (!isNode(a)) {
return a;
}
const aRoot = a.getRootNode();
if (aRoot) {
if (
!isShadowRoot(aRoot) ||
(isNode(b) && isShadowInclusiveAncestor(aRoot, b))
) {
return a;
}
a = getHost(aRoot);
}
}
}
// Accessors for non-public data
const eventTargetData = Symbol();
function setEventTargetData(target) {
target[eventTargetData] = getDefaultTargetData();
}
function getAssignedSlot(target) {
return Boolean(target?.[eventTargetData]?.assignedSlot);
}
function getHasActivationBehavior(target) {
return Boolean(target?.[eventTargetData]?.hasActivationBehavior);
}
function getHost(target) {
return target?.[eventTargetData]?.host ?? null;
}
function getListeners(target) {
return target?.[eventTargetData]?.listeners ?? {};
}
function getMode(target) {
return target?.[eventTargetData]?.mode ?? null;
}
function listenerCount(target, type) {
return getListeners(target)?.[type]?.length ?? 0;
}
function getDefaultTargetData() {
return {
assignedSlot: false,
hasActivationBehavior: false,
host: null,
listeners: ObjectCreate(null),
mode: "",
};
}
function addEventListenerOptionsConverter(V, prefix) {
if (webidl.type(V) !== "Object") {
return { capture: !!V, once: false, passive: false };
}
const options = {
capture: !!V.capture,
once: !!V.once,
passive: !!V.passive,
};
const signal = V.signal;
if (signal !== undefined) {
options.signal = webidl.converters.AbortSignal(
signal,
prefix,
"'signal' of 'AddEventListenerOptions' (Argument 3)",
);
}
return options;
}
class EventTarget {
constructor() {
this[eventTargetData] = getDefaultTargetData();
this[webidl.brand] = webidl.brand;
}
addEventListener(
type,
callback,
options,
) {
const self = this ?? globalThis_;
webidl.assertBranded(self, EventTargetPrototype);
const prefix = "Failed to execute 'addEventListener' on 'EventTarget'";
webidl.requiredArguments(arguments.length, 2, prefix);
options = addEventListenerOptionsConverter(options, prefix);
if (callback === null) {
return;
}
const { listeners } = self[eventTargetData];
if (!listeners[type]) {
listeners[type] = [];
}
const listenerList = listeners[type];
for (let i = 0; i < listenerList.length; ++i) {
const listener = listenerList[i];
if (
((typeof listener.options === "boolean" &&
listener.options === options.capture) ||
(typeof listener.options === "object" &&
listener.options.capture === options.capture)) &&
listener.callback === callback
) {
return;
}
}
if (options?.signal) {
const signal = options?.signal;
if (signal.aborted) {
// If signal is not null and its aborted flag is set, then return.
return;
} else {
// If listener's signal is not null, then add the following abort
// abort steps to it: Remove an event listener.
signal.addEventListener("abort", () => {
self.removeEventListener(type, callback, options);
});
}
}
ArrayPrototypePush(listeners[type], { callback, options });
}
removeEventListener(
type,
callback,
options,
) {
const self = this ?? globalThis_;
webidl.assertBranded(self, EventTargetPrototype);
webidl.requiredArguments(
arguments.length,
2,
"Failed to execute 'removeEventListener' on 'EventTarget'",
);
const { listeners } = self[eventTargetData];
if (callback !== null && listeners[type]) {
listeners[type] = ArrayPrototypeFilter(
listeners[type],
(listener) => listener.callback !== callback,
);
} else if (callback === null || !listeners[type]) {
return;
}
options = normalizeEventHandlerOptions(options);
for (let i = 0; i < listeners[type].length; ++i) {
const listener = listeners[type][i];
if (
((typeof listener.options === "boolean" &&
listener.options === options.capture) ||
(typeof listener.options === "object" &&
listener.options.capture === options.capture)) &&
listener.callback === callback
) {
ArrayPrototypeSplice(listeners[type], i, 1);
break;
}
}
}
dispatchEvent(event) {
// If `this` is not present, then fallback to global scope. We don't use
// `globalThis` directly here, because it could be deleted by user.
// Instead use saved reference to global scope when the script was
// executed.
const self = this ?? globalThis_;
webidl.assertBranded(self, EventTargetPrototype);
webidl.requiredArguments(
arguments.length,
1,
"Failed to execute 'dispatchEvent' on 'EventTarget'",
);
// This is an optimization to avoid creating an event listener
// on each startup.
// Stores the flag for checking whether unload is dispatched or not.
// This prevents the recursive dispatches of unload events.
// See https://github.com/denoland/deno/issues/9201.
if (event.type === "unload" && self === globalThis_) {
globalThis_[SymbolFor("Deno.isUnloadDispatched")] = true;
}
const { listeners } = self[eventTargetData];
if (!listeners[event.type]) {
setTarget(event, this);
return true;
}
if (getDispatched(event)) {
throw new DOMException("Invalid event state.", "InvalidStateError");
}
if (event.eventPhase !== Event.NONE) {
throw new DOMException("Invalid event state.", "InvalidStateError");
}
return dispatch(self, event);
}
getParent(_event) {
return null;
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return `${this.constructor.name} ${inspect({}, inspectOptions)}`;
}
}
webidl.configureInterface(EventTarget);
const EventTargetPrototype = EventTarget.prototype;
defineEnumerableProps(EventTarget, [
"addEventListener",
"removeEventListener",
"dispatchEvent",
]);
class ErrorEvent extends Event {
#message = "";
#filename = "";
#lineno = "";
#colno = "";
#error = "";
get message() {
return this.#message;
}
get filename() {
return this.#filename;
}
get lineno() {
return this.#lineno;
}
get colno() {
return this.#colno;
}
get error() {
return this.#error;
}
constructor(
type,
{
bubbles,
cancelable,
composed,
message = "",
filename = "",
lineno = 0,
colno = 0,
error,
} = {},
) {
super(type, {
bubbles: bubbles,
cancelable: cancelable,
composed: composed,
});
this.#message = message;
this.#filename = filename;
this.#lineno = lineno;
this.#colno = colno;
this.#error = error;
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(ErrorEventPrototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"message",
"filename",
"lineno",
"colno",
"error",
],
}),
inspectOptions,
);
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "ErrorEvent";
}
const ErrorEventPrototype = ErrorEvent.prototype;
defineEnumerableProps(ErrorEvent, [
"message",
"filename",
"lineno",
"colno",
"error",
]);
class CloseEvent extends Event {
#wasClean = "";
#code = "";
#reason = "";
get wasClean() {
return this.#wasClean;
}
get code() {
return this.#code;
}
get reason() {
return this.#reason;
}
constructor(type, {
bubbles,
cancelable,
composed,
wasClean = false,
code = 0,
reason = "",
} = {}) {
super(type, {
bubbles: bubbles,
cancelable: cancelable,
composed: composed,
});
this.#wasClean = wasClean;
this.#code = code;
this.#reason = reason;
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(CloseEventPrototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"wasClean",
"code",
"reason",
],
}),
inspectOptions,
);
}
}
const CloseEventPrototype = CloseEvent.prototype;
class MessageEvent extends Event {
get source() {
return null;
}
constructor(type, eventInitDict) {
super(type, {
bubbles: eventInitDict?.bubbles ?? false,
cancelable: eventInitDict?.cancelable ?? false,
composed: eventInitDict?.composed ?? false,
});
this.data = eventInitDict?.data ?? null;
this.ports = eventInitDict?.ports ?? [];
this.origin = eventInitDict?.origin ?? "";
this.lastEventId = eventInitDict?.lastEventId ?? "";
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(MessageEventPrototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"data",
"origin",
"lastEventId",
],
}),
inspectOptions,
);
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "CloseEvent";
}
const MessageEventPrototype = MessageEvent.prototype;
class CustomEvent extends Event {
#detail = null;
constructor(type, eventInitDict = {}) {
super(type, eventInitDict);
webidl.requiredArguments(
arguments.length,
1,
"Failed to construct 'CustomEvent'",
);
const { detail } = eventInitDict;
this.#detail = detail;
}
get detail() {
return this.#detail;
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(CustomEventPrototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"detail",
],
}),
inspectOptions,
);
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "CustomEvent";
}
const CustomEventPrototype = CustomEvent.prototype;
ReflectDefineProperty(CustomEvent.prototype, "detail", {
enumerable: true,
});
// ProgressEvent could also be used in other DOM progress event emits.
// Current use is for FileReader.
class ProgressEvent extends Event {
constructor(type, eventInitDict = {}) {
super(type, eventInitDict);
this.lengthComputable = eventInitDict?.lengthComputable ?? false;
this.loaded = eventInitDict?.loaded ?? 0;
this.total = eventInitDict?.total ?? 0;
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(ProgressEventPrototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"lengthComputable",
"loaded",
"total",
],
}),
inspectOptions,
);
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "ProgressEvent";
}
const ProgressEventPrototype = ProgressEvent.prototype;
class PromiseRejectionEvent extends Event {
#promise = null;
#reason = null;
get promise() {
return this.#promise;
}
get reason() {
return this.#reason;
}
constructor(
type,
{
bubbles,
cancelable,
composed,
promise,
reason,
} = {},
) {
super(type, {
bubbles: bubbles,
cancelable: cancelable,
composed: composed,
});
this.#promise = promise;
this.#reason = reason;
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(
PromiseRejectionEventPrototype,
this,
),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"promise",
"reason",
],
}),
inspectOptions,
);
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "PromiseRejectionEvent";
}
const PromiseRejectionEventPrototype = PromiseRejectionEvent.prototype;
defineEnumerableProps(PromiseRejectionEvent, [
"promise",
"reason",
]);
const _eventHandlers = Symbol("eventHandlers");
function makeWrappedHandler(handler, isSpecialErrorEventHandler) {
function wrappedHandler(evt) {
if (typeof wrappedHandler.handler !== "function") {
return;
}
if (
isSpecialErrorEventHandler &&
ObjectPrototypeIsPrototypeOf(ErrorEventPrototype, evt) &&
evt.type === "error"
) {
const ret = FunctionPrototypeCall(
wrappedHandler.handler,
this,
evt.message,
evt.filename,
evt.lineno,
evt.colno,
evt.error,
);
if (ret === true) {
evt.preventDefault();
}
return;
}
return FunctionPrototypeCall(wrappedHandler.handler, this, evt);
}
wrappedHandler.handler = handler;
return wrappedHandler;
}
// `init` is an optional function that will be called the first time that the
// event handler property is set. It will be called with the object on which
// the property is set as its argument.
// `isSpecialErrorEventHandler` can be set to true to opt into the special
// behavior of event handlers for the "error" event in a global scope.
function defineEventHandler(
emitter,
name,
init = undefined,
isSpecialErrorEventHandler = false,
) {
// HTML specification section 8.1.7.1
ObjectDefineProperty(emitter, `on${name}`, {
get() {
if (!this[_eventHandlers]) {
return null;
}
return MapPrototypeGet(this[_eventHandlers], name)?.handler ?? null;
},
set(value) {
// All three Web IDL event handler types are nullable callback functions
// with the [LegacyTreatNonObjectAsNull] extended attribute, meaning
// anything other than an object is treated as null.
if (typeof value !== "object" && typeof value !== "function") {
value = null;
}
if (!this[_eventHandlers]) {
this[_eventHandlers] = new SafeMap();
}
let handlerWrapper = MapPrototypeGet(this[_eventHandlers], name);
if (handlerWrapper) {
handlerWrapper.handler = value;
} else if (value !== null) {
handlerWrapper = makeWrappedHandler(
value,
isSpecialErrorEventHandler,
);
this.addEventListener(name, handlerWrapper);
init?.(this);
}
MapPrototypeSet(this[_eventHandlers], name, handlerWrapper);
},
configurable: true,
enumerable: true,
});
}
let reportExceptionStackedCalls = 0;
// https://html.spec.whatwg.org/#report-the-exception
function reportException(error) {
reportExceptionStackedCalls++;
const jsError = core.destructureError(error);
const message = jsError.exceptionMessage;
let filename = "";
let lineno = 0;
let colno = 0;
if (jsError.frames.length > 0) {
filename = jsError.frames[0].fileName;
lineno = jsError.frames[0].lineNumber;
colno = jsError.frames[0].columnNumber;
} else {
const jsError = core.destructureError(new Error());
const frames = jsError.frames;
for (let i = 0; i < frames.length; ++i) {
const frame = frames[i];
if (
typeof frame.fileName == "string" &&
!StringPrototypeStartsWith(frame.fileName, "ext:")
) {
filename = frame.fileName;
lineno = frame.lineNumber;
colno = frame.columnNumber;
break;
}
}
}
const event = new ErrorEvent("error", {
cancelable: true,
message,
filename,
lineno,
colno,
error,
});
// Avoid recursing `reportException()` via error handlers more than once.
if (reportExceptionStackedCalls > 1 || globalThis_.dispatchEvent(event)) {
core.reportUnhandledException(error);
}
reportExceptionStackedCalls--;
}
function checkThis(thisArg) {
if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis_) {
throw new TypeError("Illegal invocation");
}
}
// https://html.spec.whatwg.org/#dom-reporterror
function reportError(error) {
checkThis(this);
const prefix = "Failed to execute 'reportError'";
webidl.requiredArguments(arguments.length, 1, prefix);
reportException(error);
}
export {
CloseEvent,
CustomEvent,
defineEventHandler,
dispatch,
ErrorEvent,
Event,
EventTarget,
listenerCount,
MessageEvent,
ProgressEvent,
PromiseRejectionEvent,
reportError,
reportException,
saveGlobalThisReference,
setEventTargetData,
setIsTrusted,
setTarget,
};