// 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 "ext:deno_web/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 call '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, };