1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-11 10:07:54 -05:00
denoland-deno/ext/web/02_event.js
Bartek Iwańczuk f9b43044ef perf: don't add unload event listener (#18082)
This commit changes how "unload" event is handled - before
this commit an event listener was added unconditionally in
the runtime bootstrapping function, which for some reason was
very expensive (0.3ms). Instead of adding an event listener,
a check was added to "dispatchEvent" function that performs
the same action (so it's only called if there's an event dispatched).
2023-03-10 13:00:11 +09:00

1537 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2018-2023 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.
const core = globalThis.Deno.core;
const ops = core.ops;
import * as webidl from "ext:deno_webidl/00_webidl.js";
import DOMException from "ext:deno_web/01_dom_exception.js";
import { createFilteredInspectProxy } from "ext:deno_console/02_console.js";
const primordials = globalThis.__bootstrap.primordials;
const {
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ArrayPrototypeUnshift,
Boolean,
DateNow,
Error,
FunctionPrototypeCall,
Map,
MapPrototypeGet,
MapPrototypeSet,
ObjectCreate,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectPrototypeIsPrototypeOf,
ReflectDefineProperty,
ReflectHas,
SafeArrayIterator,
StringPrototypeStartsWith,
Symbol,
SymbolFor,
SymbolToStringTag,
TypeError,
} = primordials;
// 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;
}
// Type guards that widen the event type
function hasRelatedTarget(
event,
) {
return ReflectHas(event, "relatedTarget");
}
const isTrusted = ObjectGetOwnPropertyDescriptor({
get isTrusted() {
return this[_isTrusted];
},
}, "isTrusted").get;
const eventInitConverter = webidl.createDictionaryConverter("EventInit", [{
key: "bubbles",
defaultValue: false,
converter: webidl.converters.boolean,
}, {
key: "cancelable",
defaultValue: false,
converter: webidl.converters.boolean,
}, {
key: "composed",
defaultValue: false,
converter: webidl.converters.boolean,
}]);
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]]");
// internal.
const _skipInternalInit = Symbol("[[skipSlowInit]]");
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] = [];
if (!eventInitDict[_skipInternalInit]) {
webidl.requiredArguments(arguments.length, 1, {
prefix: "Failed to construct 'Event'",
});
type = webidl.converters.DOMString(type, {
prefix: "Failed to construct 'Event'",
context: "Argument 1",
});
const eventInit = eventInitConverter(eventInitDict, {
prefix: "Failed to construct 'Event'",
context: "Argument 2",
});
this[_attributes] = {
type,
...eventInit,
currentTarget: null,
eventPhase: Event.NONE,
target: null,
timeStamp: DateNow(),
};
// [LegacyUnforgeable]
ReflectDefineProperty(this, "isTrusted", {
enumerable: true,
get: isTrusted,
});
} else {
this[_attributes] = {
type,
data: eventInitDict.data ?? null,
bubbles: eventInitDict.bubbles ?? false,
cancelable: eventInitDict.cancelable ?? false,
composed: eventInitDict.composed ?? false,
currentTarget: null,
eventPhase: Event.NONE,
target: null,
timeStamp: DateNow(),
};
// TODO(@littledivy): Not spec compliant but performance is hurt badly
// for users of `_skipInternalInit`.
this.isTrusted = false;
}
}
[SymbolFor("Deno.privateCustomInspect")](inspect) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(Event.prototype, this),
keys: EVENT_PROPS,
}));
}
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;
}
}
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 Boolean(eventTarget && ReflectHas(eventTarget, "nodeType"));
}
// 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 isSlotable(
nodeImpl,
) {
return Boolean(isNode(nodeImpl) && ReflectHas(nodeImpl, "assignedSlot"));
}
// 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 = hasRelatedTarget(eventImpl)
? eventImpl.relatedTarget
: null;
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 slotable = isSlotable(targetImpl) && getAssignedSlot(targetImpl)
? targetImpl
: null;
let parent = getParent(targetImpl);
// 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 &&
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;
}
// Copy event listeners before iterating since the list can be modified during the iteration.
const handlers = ArrayPrototypeSlice(targetListeners[type]);
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 (!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);
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: "",
};
}
// This is lazy loaded because there is a circular dependency with AbortSignal.
let addEventListenerOptionsConverter;
function lazyAddEventListenerOptionsConverter() {
addEventListenerOptionsConverter ??= webidl.createDictionaryConverter(
"AddEventListenerOptions",
[
{
key: "capture",
defaultValue: false,
converter: webidl.converters.boolean,
},
{
key: "passive",
defaultValue: false,
converter: webidl.converters.boolean,
},
{
key: "once",
defaultValue: false,
converter: webidl.converters.boolean,
},
{
key: "signal",
converter: webidl.converters.AbortSignal,
},
],
);
}
webidl.converters.AddEventListenerOptions = (V, opts) => {
if (webidl.type(V) !== "Object" || V === null) {
V = { capture: Boolean(V) };
}
lazyAddEventListenerOptionsConverter();
return addEventListenerOptionsConverter(V, opts);
};
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 = webidl.converters.AddEventListenerOptions(options, {
prefix,
context: "Argument 3",
});
if (callback === null) {
return;
}
const { listeners } = self[eventTargetData];
if (!(ReflectHas(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 listeners 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, {
prefix: "Failed to execute 'removeEventListener' on 'EventTarget'",
});
const { listeners } = self[eventTargetData];
if (callback !== null && ReflectHas(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, {
prefix: "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("isUnloadDispatched")] = true;
}
const { listeners } = self[eventTargetData];
if (!ReflectHas(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;
}
}
webidl.configurePrototype(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) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(ErrorEvent.prototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"message",
"filename",
"lineno",
"colno",
"error",
],
}));
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "ErrorEvent";
}
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) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(CloseEvent.prototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"wasClean",
"code",
"reason",
],
}));
}
}
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,
[_skipInternalInit]: eventInitDict?.[_skipInternalInit],
});
this.data = eventInitDict?.data ?? null;
this.ports = eventInitDict?.ports ?? [];
this.origin = eventInitDict?.origin ?? "";
this.lastEventId = eventInitDict?.lastEventId ?? "";
}
[SymbolFor("Deno.privateCustomInspect")](inspect) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(MessageEvent.prototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"data",
"origin",
"lastEventId",
],
}));
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "CloseEvent";
}
class CustomEvent extends Event {
#detail = null;
constructor(type, eventInitDict = {}) {
super(type, eventInitDict);
webidl.requiredArguments(arguments.length, 1, {
prefix: "Failed to construct 'CustomEvent'",
});
const { detail } = eventInitDict;
this.#detail = detail;
}
get detail() {
return this.#detail;
}
[SymbolFor("Deno.privateCustomInspect")](inspect) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(CustomEvent.prototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"detail",
],
}));
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "CustomEvent";
}
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) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(ProgressEvent.prototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"lengthComputable",
"loaded",
"total",
],
}));
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "ProgressEvent";
}
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) {
return inspect(createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(
PromiseRejectionEvent.prototype,
this,
),
keys: [
...new SafeArrayIterator(EVENT_PROPS),
"promise",
"reason",
],
}));
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "PromiseRejectionEvent";
}
defineEnumerableProps(PromiseRejectionEvent, [
"promise",
"reason",
]);
const _eventHandlers = Symbol("eventHandlers");
function makeWrappedHandler(handler, isSpecialErrorEventHandler) {
function wrappedHandler(evt) {
if (typeof wrappedHandler.handler !== "function") {
return;
}
if (
isSpecialErrorEventHandler &&
ObjectPrototypeIsPrototypeOf(ErrorEvent.prototype, 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 Map();
}
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)) {
ops.op_dispatch_exception(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 call 'reportError'";
webidl.requiredArguments(arguments.length, 1, { prefix });
reportException(error);
}
export {
_skipInternalInit,
CloseEvent,
CustomEvent,
defineEventHandler,
ErrorEvent,
Event,
EventTarget,
listenerCount,
MessageEvent,
ProgressEvent,
PromiseRejectionEvent,
reportError,
reportException,
saveGlobalThisReference,
setEventTargetData,
setIsTrusted,
setTarget,
};