happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
309 lines • 13.7 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const PropertySymbol = __importStar(require("../PropertySymbol.cjs"));
const EventPhaseEnum_js_1 = __importDefault(require("./EventPhaseEnum.cjs"));
const WindowBrowserContext_js_1 = __importDefault(require("../window/WindowBrowserContext.cjs"));
const BrowserErrorCaptureEnum_js_1 = __importDefault(require("../browser/enums/BrowserErrorCaptureEnum.cjs"));
/**
* Handles events.
*/
class EventTarget {
[PropertySymbol.listeners] = {
capturing: new Map(),
bubbling: new Map()
};
[PropertySymbol.listenerOptions] = {
capturing: new Map(),
bubbling: new Map()
};
/**
* Return a default description for the EventTarget class.
*/
get [Symbol.toStringTag]() {
return 'EventTarget';
}
/**
* Adds an event listener.
*
* @param type Event type.
* @param listener Listener.
* @param options An object that specifies characteristics about the event listener.(currently only once)
* @param options.once
* @param options.signal An AbortSignal. The listener will be removed when the given AbortSignal object's abort() method is called.
*/
addEventListener(type, listener, options) {
options = typeof options === 'boolean' ? { capture: options } : options || {};
const eventPhase = options.capture ? 'capturing' : 'bubbling';
let listeners = this[PropertySymbol.listeners][eventPhase].get(type);
let listenerOptions = this[PropertySymbol.listenerOptions][eventPhase].get(type);
if (!listeners) {
listeners = [];
listenerOptions = [];
this[PropertySymbol.listeners][eventPhase].set(type, listeners);
this[PropertySymbol.listenerOptions][eventPhase].set(type, listenerOptions);
}
if (listeners.includes(listener)) {
return;
}
listeners.push(listener);
listenerOptions.push(options);
if (options.signal && !options.signal.aborted) {
options.signal.addEventListener('abort', () => {
this.removeEventListener(type, listener);
});
}
}
/**
* Adds an event listener.
*
* @param type Event type.
* @param listener Listener.
*/
removeEventListener(type, listener) {
const bubblingListeners = this[PropertySymbol.listeners].bubbling.get(type);
if (bubblingListeners) {
const index = bubblingListeners.indexOf(listener);
if (index !== -1) {
bubblingListeners.splice(index, 1);
this[PropertySymbol.listenerOptions].bubbling.get(type).splice(index, 1);
return;
}
}
const capturingListeners = this[PropertySymbol.listeners].capturing.get(type);
if (capturingListeners) {
const index = capturingListeners.indexOf(listener);
if (index !== -1) {
capturingListeners.splice(index, 1);
this[PropertySymbol.listenerOptions].capturing.get(type).splice(index, 1);
}
}
}
/**
* Dispatches an event.
*
* @see https://www.w3.org/TR/DOM-Level-3-Events/#event-flow
* @see https://www.quirksmode.org/js/events_order.html#link4
* @param event Event.
* @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault().
*/
dispatchEvent(event) {
// The "load" event is a special case. It should not bubble up to the window from the document.
if (!event[PropertySymbol.dispatching] &&
(event[PropertySymbol.type] !== 'load' || !event[PropertySymbol.target])) {
event[PropertySymbol.dispatching] = true;
event[PropertySymbol.target] = this[PropertySymbol.proxy] || this;
this.#goThroughDispatchEventPhases(event);
event[PropertySymbol.dispatching] = false;
return !(event[PropertySymbol.cancelable] && event[PropertySymbol.defaultPrevented]);
}
this.#callDispatchEventListeners(event);
return !(event[PropertySymbol.cancelable] && event[PropertySymbol.defaultPrevented]);
}
/**
* Adds an event listener.
*
* TODO:
* Was used by with IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified.
*
* @deprecated
* @param type Event type.
* @param listener Listener.
*/
attachEvent(type, listener) {
this.addEventListener(type.replace('on', ''), listener);
}
/**
* Removes an event listener.
*
* TODO:
* Was used by IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified.
*
* @deprecated
* @param type Event type.
* @param listener Listener.
*/
detachEvent(type, listener) {
this.removeEventListener(type.replace('on', ''), listener);
}
/**
* Goes through dispatch event phases.
*
* @param event Event.
*/
#goThroughDispatchEventPhases(event) {
const composedPath = event.composedPath();
// Capturing phase
event[PropertySymbol.eventPhase] = EventPhaseEnum_js_1.default.capturing;
for (let i = composedPath.length - 1; i >= 0; i--) {
event[PropertySymbol.currentTarget] = composedPath[i];
composedPath[i].dispatchEvent(event);
if (event[PropertySymbol.propagationStopped] ||
event[PropertySymbol.immediatePropagationStopped]) {
event[PropertySymbol.eventPhase] = EventPhaseEnum_js_1.default.none;
event[PropertySymbol.currentTarget] = null;
return;
}
}
// At target phase
event[PropertySymbol.eventPhase] = EventPhaseEnum_js_1.default.atTarget;
event[PropertySymbol.currentTarget] = this[PropertySymbol.proxy] || this;
event[PropertySymbol.target].dispatchEvent(event);
// Bubbling phase
event[PropertySymbol.eventPhase] = EventPhaseEnum_js_1.default.bubbling;
if (event[PropertySymbol.bubbles] &&
!event[PropertySymbol.propagationStopped] &&
!event[PropertySymbol.immediatePropagationStopped]) {
for (let i = 1, max = composedPath.length; i < max; i++) {
event[PropertySymbol.currentTarget] = composedPath[i];
composedPath[i].dispatchEvent(event);
if (event[PropertySymbol.propagationStopped] ||
event[PropertySymbol.immediatePropagationStopped]) {
event[PropertySymbol.eventPhase] = EventPhaseEnum_js_1.default.none;
event[PropertySymbol.currentTarget] = null;
return;
}
}
}
// None phase (done)
event[PropertySymbol.eventPhase] = EventPhaseEnum_js_1.default.none;
event[PropertySymbol.currentTarget] = null;
}
/**
* Handles dispatch event listeners.
*
* @param event Event.
*/
#callDispatchEventListeners(event) {
const window = this[PropertySymbol.window];
const browserSettings = window ? new WindowBrowserContext_js_1.default(window).getSettings() : null;
const eventPhase = event.eventPhase === EventPhaseEnum_js_1.default.capturing ? 'capturing' : 'bubbling';
// We need to clone the arrays because the listeners may remove themselves while we are iterating.
const listeners = this[PropertySymbol.listeners][eventPhase].get(event.type)?.slice();
if (listeners && listeners.length) {
const listenerOptions = this[PropertySymbol.listenerOptions][eventPhase]
.get(event.type)
?.slice();
for (let i = 0, max = listeners.length; i < max; i++) {
const listener = listeners[i];
const options = listenerOptions[i];
if (options?.passive) {
event[PropertySymbol.isInPassiveEventListener] = true;
}
// We can end up in a never ending loop if the listener for the error event on Window also throws an error.
if (window &&
(this !== window || event.type !== 'error') &&
!browserSettings?.disableErrorCapturing &&
browserSettings?.errorCapture === BrowserErrorCaptureEnum_js_1.default.tryAndCatch) {
if (listener.handleEvent) {
let result;
try {
result = listener.handleEvent.call(listener, event);
}
catch (error) {
window[PropertySymbol.dispatchError](error);
}
if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
}
}
else {
let result;
try {
result = listener.call(this, event);
}
catch (error) {
window[PropertySymbol.dispatchError](error);
}
if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
}
}
}
else {
if (listener.handleEvent) {
listener.handleEvent.call(listener, event);
}
else {
listener.call(this, event);
}
}
event[PropertySymbol.isInPassiveEventListener] = false;
if (options?.once) {
// At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted,
// The value corresponding to the cloned array is not deleted. So we need to delete the value in the cloned array.
listeners.splice(i, 1);
listenerOptions.splice(i, 1);
this.removeEventListener(event.type, listener);
i--;
max--;
}
if (event[PropertySymbol.immediatePropagationStopped]) {
return;
}
}
}
if (event.eventPhase !== EventPhaseEnum_js_1.default.capturing) {
const onEventName = 'on' + event.type.toLowerCase();
const eventListener = this[onEventName];
if (typeof eventListener === 'function') {
// We can end up in a never ending loop if the listener for the error event on Window also throws an error.
if (window &&
(this !== window || event.type !== 'error') &&
!browserSettings?.disableErrorCapturing &&
browserSettings?.errorCapture === BrowserErrorCaptureEnum_js_1.default.tryAndCatch) {
let result;
try {
result = eventListener(event);
}
catch (error) {
window[PropertySymbol.dispatchError](error);
}
if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
}
}
else {
eventListener(event);
}
}
}
}
}
exports.default = EventTarget;
//# sourceMappingURL=EventTarget.cjs.map
;