diff --git a/cjs/interface/document.js b/cjs/interface/document.js index c75eb57b..01fdc8e3 100644 --- a/cjs/interface/document.js +++ b/cjs/interface/document.js @@ -159,6 +159,13 @@ class Document extends NonElementParentNode { get isConnected() { return true; } + /** + * @protected + */ + _getParent() { + return this[EVENT_TARGET]; + } + createAttribute(name) { return new Attr(this, name); } createComment(textContent) { return new Comment(this, textContent); } createDocumentFragment() { return new DocumentFragment(this); } diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index cc94df79..063a8b82 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -1,12 +1,50 @@ 'use strict'; // https://dom.spec.whatwg.org/#interface-eventtarget -const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@ungap/event-target')); +const wm = new WeakMap(); + +function dispatch({ target, listener}) { + if (typeof listener === 'function') { + listener.call(target, this); + } else { + listener.handleEvent(this); + } + return this._stopImmediatePropagationFlag; +} + +function invokeListeners({currentTarget, target}) { + const map = wm.get(currentTarget); + if (map && map.has(this.type)) { + const listeners = map.get(this.type); + if (currentTarget === target) { + this.eventPhase = this.AT_TARGET; + } else { + this.eventPhase = this.BUBBLING_PHASE; + } + + this.currentTarget = currentTarget; + this.target = target; + for (const [listener, options] of listeners) { + if (options && options.once) + listeners.delete(listener); + if (dispatch.call(this, {target: this, listener})) + break; + } + delete this.currentTarget; + delete this.target; + return this.cancelBubble; + } +} + /** * @implements globalThis.EventTarget */ -class DOMEventTarget extends EventTarget { +class DOMEventTarget { + + constructor() { + wm.set(this, new Map); + } /** * @protected @@ -15,24 +53,39 @@ class DOMEventTarget extends EventTarget { return null; } + addEventListener(type, listener, options) { + const map = wm.get(this); + if (!map.has(type)) + map.set(type, new Map); + map.get(type).set(listener, options); + } + + removeEventListener(type, listener) { + const map = wm.get(this); + if (map.has(type)) { + const listeners = map.get(type); + if (listeners.delete(listener) && !listeners.size) { + map.delete(type); + } + } + } + dispatchEvent(event) { - const dispatched = super.dispatchEvent(event); + let node = this; + event.eventPhase = event.CAPTURING_PHASE; // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event.cancelBubble) { - const parent = this._getParent(); - if (parent && parent.dispatchEvent) { - const options = { - bubbles: event.bubbles, - cancelable: event.cancelable, - composed: event.composed, - }; - // in Node 16.5 the same event can't be used for another dispatch - return parent.dispatchEvent(new event.constructor(event.type, options)); - } + while (node) { + if (node.dispatchEvent) + event._path.push({currentTarget: node, target: this}); + node = event.bubbles && node._getParent && node._getParent(); } - return dispatched; + event._path.some(invokeListeners, event); + event._path = []; + event.eventPhase = event.NONE; + return !event.defaultPrevented; } + } exports.EventTarget = DOMEventTarget; diff --git a/cjs/interface/event.js b/cjs/interface/event.js index ae77cef7..70ed25a6 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -4,18 +4,19 @@ /* c8 ignore start */ // Node 15 has Event but 14 and 12 don't - const BUBBLING_PHASE = 3; +const AT_TARGET = 2; const CAPTURING_PHASE = 1; +const NONE = 0; /** * @implements globalThis.Event */ -const GlobalEvent = typeof Event === 'function' ? - Event : - class Event { +class GlobalEvent { static get BUBBLING_PHASE() { return BUBBLING_PHASE; } + static get AT_TARGET() { return AT_TARGET; } static get CAPTURING_PHASE() { return CAPTURING_PHASE; } + static get NONE() { return NONE; } constructor(type, eventInitDict = {}) { this.type = type; @@ -23,47 +24,38 @@ const GlobalEvent = typeof Event === 'function' ? this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; - this.eventPhase = this.BUBBLING_PHASE; + this.eventPhase = this.NONE; this.timeStamp = Date.now(); this.defaultPrevented = false; this.originalTarget = null; this.returnValue = null; this.srcElement = null; this.target = null; + this._path = []; } get BUBBLING_PHASE() { return BUBBLING_PHASE; } + get AT_TARGET() { return AT_TARGET; } get CAPTURING_PHASE() { return CAPTURING_PHASE; } + get NONE() { return NONE; } preventDefault() { this.defaultPrevented = true; } - // TODO: what do these do in native NodeJS Event ? + // simplified implementation, should be https://dom.spec.whatwg.org/#dom-event-composedpath + composedPath() { + return this._path; + } + stopPropagation() { this.cancelBubble = true; } stopImmediatePropagation() { + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } - }; - - - -/** - * @implements globalThis.Event - */ -class DOMEvent extends GlobalEvent { - // specs: "set this’s stop propagation flag and this’s stop immediate propagation flag" - // https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation - // but Node don't do that so for now we extend it - stopImmediatePropagation() { - super.stopPropagation(); - if (typeof super.stopImmediatePropagation === 'function') - super.stopImmediatePropagation(); - } } - -exports.Event = DOMEvent; +exports.Event = GlobalEvent; /* c8 ignore stop */ diff --git a/esm/interface/document.js b/esm/interface/document.js index dbc32390..9bdc0b10 100644 --- a/esm/interface/document.js +++ b/esm/interface/document.js @@ -159,6 +159,13 @@ export class Document extends NonElementParentNode { get isConnected() { return true; } + /** + * @protected + */ + _getParent() { + return this[EVENT_TARGET]; + } + createAttribute(name) { return new Attr(this, name); } createComment(textContent) { return new Comment(this, textContent); } createDocumentFragment() { return new DocumentFragment(this); } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index c07a092e..25edf24e 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -1,11 +1,49 @@ // https://dom.spec.whatwg.org/#interface-eventtarget -import EventTarget from '@ungap/event-target'; +const wm = new WeakMap(); + +function dispatch({ target, listener}) { + if (typeof listener === 'function') { + listener.call(target, this); + } else { + listener.handleEvent(this); + } + return this._stopImmediatePropagationFlag; +} + +function invokeListeners({currentTarget, target}) { + const map = wm.get(currentTarget); + if (map && map.has(this.type)) { + const listeners = map.get(this.type); + if (currentTarget === target) { + this.eventPhase = this.AT_TARGET; + } else { + this.eventPhase = this.BUBBLING_PHASE; + } + + this.currentTarget = currentTarget; + this.target = target; + for (const [listener, options] of listeners) { + if (options && options.once) + listeners.delete(listener); + if (dispatch.call(this, {target: this, listener})) + break; + } + delete this.currentTarget; + delete this.target; + return this.cancelBubble; + } +} + /** * @implements globalThis.EventTarget */ -class DOMEventTarget extends EventTarget { +class DOMEventTarget { + + constructor() { + wm.set(this, new Map); + } /** * @protected @@ -14,24 +52,39 @@ class DOMEventTarget extends EventTarget { return null; } + addEventListener(type, listener, options) { + const map = wm.get(this); + if (!map.has(type)) + map.set(type, new Map); + map.get(type).set(listener, options); + } + + removeEventListener(type, listener) { + const map = wm.get(this); + if (map.has(type)) { + const listeners = map.get(type); + if (listeners.delete(listener) && !listeners.size) { + map.delete(type); + } + } + } + dispatchEvent(event) { - const dispatched = super.dispatchEvent(event); + let node = this; + event.eventPhase = event.CAPTURING_PHASE; // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event.cancelBubble) { - const parent = this._getParent(); - if (parent && parent.dispatchEvent) { - const options = { - bubbles: event.bubbles, - cancelable: event.cancelable, - composed: event.composed, - }; - // in Node 16.5 the same event can't be used for another dispatch - return parent.dispatchEvent(new event.constructor(event.type, options)); - } + while (node) { + if (node.dispatchEvent) + event._path.push({currentTarget: node, target: this}); + node = event.bubbles && node._getParent && node._getParent(); } - return dispatched; + event._path.some(invokeListeners, event); + event._path = []; + event.eventPhase = event.NONE; + return !event.defaultPrevented; } + } export { DOMEventTarget as EventTarget }; diff --git a/esm/interface/event.js b/esm/interface/event.js index d2675793..d26705cb 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -3,18 +3,19 @@ /* c8 ignore start */ // Node 15 has Event but 14 and 12 don't - const BUBBLING_PHASE = 3; +const AT_TARGET = 2; const CAPTURING_PHASE = 1; +const NONE = 0; /** * @implements globalThis.Event */ -const GlobalEvent = typeof Event === 'function' ? - Event : - class Event { +class GlobalEvent { static get BUBBLING_PHASE() { return BUBBLING_PHASE; } + static get AT_TARGET() { return AT_TARGET; } static get CAPTURING_PHASE() { return CAPTURING_PHASE; } + static get NONE() { return NONE; } constructor(type, eventInitDict = {}) { this.type = type; @@ -22,47 +23,38 @@ const GlobalEvent = typeof Event === 'function' ? this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; - this.eventPhase = this.BUBBLING_PHASE; + this.eventPhase = this.NONE; this.timeStamp = Date.now(); this.defaultPrevented = false; this.originalTarget = null; this.returnValue = null; this.srcElement = null; this.target = null; + this._path = []; } get BUBBLING_PHASE() { return BUBBLING_PHASE; } + get AT_TARGET() { return AT_TARGET; } get CAPTURING_PHASE() { return CAPTURING_PHASE; } + get NONE() { return NONE; } preventDefault() { this.defaultPrevented = true; } - // TODO: what do these do in native NodeJS Event ? + // simplified implementation, should be https://dom.spec.whatwg.org/#dom-event-composedpath + composedPath() { + return this._path; + } + stopPropagation() { this.cancelBubble = true; } stopImmediatePropagation() { + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } - }; - - - -/** - * @implements globalThis.Event - */ -class DOMEvent extends GlobalEvent { - // specs: "set this’s stop propagation flag and this’s stop immediate propagation flag" - // https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation - // but Node don't do that so for now we extend it - stopImmediatePropagation() { - super.stopPropagation(); - if (typeof super.stopImmediatePropagation === 'function') - super.stopImmediatePropagation(); - } } - -export {DOMEvent as Event}; +export {GlobalEvent as Event}; /* c8 ignore stop */ diff --git a/package-lock.json b/package-lock.json index 7788b342..8b75eafd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,11 +5,9 @@ "requires": true, "packages": { "": { - "name": "linkedom", "version": "0.12.1", "license": "ISC", "dependencies": { - "@ungap/event-target": "^0.2.3", "css-select": "^4.1.3", "cssom": "^0.5.0", "html-escaper": "^3.0.3", @@ -300,11 +298,6 @@ "@types/node": "*" } }, - "node_modules/@ungap/event-target": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@ungap/event-target/-/event-target-0.2.3.tgz", - "integrity": "sha512-7Bz0qdvxNGV9n0f+xcMKU7wsEfK6PNzo8IdAcOiBgMNyCuU0Mk9dv0Hbd/Kgr+MFFfn4xLHFbuOt820egT5qEA==" - }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -2275,11 +2268,6 @@ "@types/node": "*" } }, - "@ungap/event-target": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@ungap/event-target/-/event-target-0.2.3.tgz", - "integrity": "sha512-7Bz0qdvxNGV9n0f+xcMKU7wsEfK6PNzo8IdAcOiBgMNyCuU0Mk9dv0Hbd/Kgr+MFFfn4xLHFbuOt820egT5qEA==" - }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", diff --git a/package.json b/package.json index b5eb2391..c4d27fa8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "./worker": "./worker.js" }, "dependencies": { - "@ungap/event-target": "^0.2.3", "css-select": "^4.1.3", "cssom": "^0.5.0", "html-escaper": "^3.0.3", diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 4e0d67a3..37d80dd3 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -2,7 +2,7 @@ const assert = require('../assert.js').for('EventTarget'); const { parseHTML } = global[Symbol.for('linkedom')]; -const { Event, document, EventTarget } = parseHTML( +const { Event, document, window, EventTarget } = parseHTML( '
', ); @@ -16,8 +16,10 @@ const basicHandler = () => { const eventTarget = new EventTarget(); eventTarget.addEventListener('foo', basicHandler); +eventTarget.addEventListener('foo', basicHandler); +eventTarget.addEventListener('foo', () => {}); eventTarget.dispatchEvent(new Event('foo')); -assert(callCount, 1, 'basicHandler should have been called'); +assert(callCount, 1, 'basicHandler should have been called once'); assert( eventTarget.dispatchEvent(new Event('click')), @@ -27,6 +29,7 @@ assert( assert(callCount, 1, 'Dispatching an event type should only call appropriate listeners'); eventTarget.removeEventListener('foo', basicHandler); +eventTarget.removeEventListener('click', basicHandler); eventTarget.dispatchEvent(new Event('foo')); assert(callCount, 1, 'basicHandler should not have been called after being removed'); @@ -34,21 +37,114 @@ assert(eventTarget._getParent(), null, 'getParent should return null'); // check propagation now +const BUBBLING_PHASE = 3; +const AT_TARGET = 2; + callCount = 0; const buttonTarget = document.getElementById('buttonTarget'); const containerTarget = document.getElementById('container'); const bodyTarget = document; -buttonTarget.addEventListener('click', basicHandler, { once: true }); -containerTarget.addEventListener('click', basicHandler, { once: true }); -bodyTarget.addEventListener('click', basicHandler, { once: true }); -buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); -assert(callCount, 3, 'Event bubbling, listener should be called 3 times'); +buttonTarget.addEventListener( + 'click', + event => { + basicHandler(); + assert( + event.target, + buttonTarget, + 'Event bubbling, target should be the button' + ); + assert( + event.currentTarget, + buttonTarget, + 'Event bubbling, current target should be the button' + ); + assert( + event.composedPath().length, + 5, + 'Event bubbling, composed path should have 5 EventTarget' + ); + assert( + event.eventPhase, + AT_TARGET, + 'Event bubbling, eventPhase should be AT_TARGET' + ); + }, + { once: true } +); + +containerTarget.addEventListener('click', event => { + basicHandler(); + assert( + event.target, + buttonTarget, + 'Event bubbling, target should be the button' + ); + assert( + event.currentTarget, + containerTarget, + 'Event bubbling, current target should be the container' + ); + assert( + event.eventPhase, + BUBBLING_PHASE, + 'Event bubbling, eventPhase should be BUBBLING_PHASE' + ); +}, { once: true }); + +containerTarget.addEventListener('click', () => { + basicHandler(); +}, { once: true }); + +bodyTarget.addEventListener('click', event => { + basicHandler(); + assert( + event.target, + buttonTarget, + 'Event bubbling, target should be the button' + ); + assert( + event.currentTarget, + bodyTarget, + 'Event bubbling, current target should be the body' + ); +}, { once: true }); + + +document.addEventListener('click', event => { + basicHandler(); + assert( + event.target, + buttonTarget, + 'Event bubbling, target should be the button' + ); + assert( + event.currentTarget, + document, + 'Event bubbling, current target should be the document' + ); +}, { once: true }); + +window.addEventListener('click', event => { + basicHandler(); + assert( + event.target, + buttonTarget, + 'Event bubbling, target should be the button' + ); + assert( + event.currentTarget !== buttonTarget, + true, + 'Event bubbling, current target should be the window' + ); +}, { once: true }); +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 6, 'Event bubbling, listener should be called 6 times'); // ensure once removed listeners buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); -assert(callCount, 3, 'listeners should only have been called once then removed'); +assert(callCount, 6, 'listeners should only have been called once then removed'); // check no bubbling callCount = 0; diff --git a/types/interface/custom-event.d.ts b/types/interface/custom-event.d.ts index 07ab20a1..5e12d94a 100644 --- a/types/interface/custom-event.d.ts +++ b/types/interface/custom-event.d.ts @@ -3,8 +3,35 @@ export { GlobalCustomEvent as CustomEvent }; * @implements globalThis.CustomEvent */ declare const GlobalCustomEvent: { + new (type: string, eventInitDict?: CustomEventInit): CustomEvent; + prototype: CustomEvent; +} | { new (type: any, eventInitDict?: {}): { detail: any; + type: any; + bubbles: boolean; + cancelBubble: boolean; + _stopImmediatePropagationFlag: boolean; + cancelable: boolean; + eventPhase: number; + timeStamp: number; + defaultPrevented: boolean; + originalTarget: any; + returnValue: any; + srcElement: any; + target: any; + _path: any[]; + readonly BUBBLING_PHASE: number; + readonly AT_TARGET: number; + readonly CAPTURING_PHASE: number; + readonly NONE: number; + preventDefault(): void; + composedPath(): any[]; + stopPropagation(): void; stopImmediatePropagation(): void; }; + readonly BUBBLING_PHASE: number; + readonly AT_TARGET: number; + readonly CAPTURING_PHASE: number; + readonly NONE: number; }; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index bec0d321..e34a1f03 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -7,5 +7,7 @@ declare class DOMEventTarget implements globalThis.EventTarget { * @protected */ protected _getParent(): any; - dispatchEvent(event: any): any; + addEventListener(type: any, listener: any, options: any): void; + removeEventListener(type: any, listener: any): void; + dispatchEvent(event: any): boolean; } diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index ef0e3744..2e49eee5 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,7 +1,32 @@ -export { DOMEvent as Event }; +export { GlobalEvent as Event }; /** * @implements globalThis.Event */ -declare class DOMEvent implements globalThis.Event { +declare class GlobalEvent implements globalThis.Event { + static get BUBBLING_PHASE(): number; + static get AT_TARGET(): number; + static get CAPTURING_PHASE(): number; + static get NONE(): number; + constructor(type: any, eventInitDict?: {}); + type: any; + bubbles: boolean; + cancelBubble: boolean; + _stopImmediatePropagationFlag: boolean; + cancelable: boolean; + eventPhase: number; + timeStamp: number; + defaultPrevented: boolean; + originalTarget: any; + returnValue: any; + srcElement: any; + target: any; + _path: any[]; + get BUBBLING_PHASE(): number; + get AT_TARGET(): number; + get CAPTURING_PHASE(): number; + get NONE(): number; + preventDefault(): void; + composedPath(): any[]; + stopPropagation(): void; stopImmediatePropagation(): void; } diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 30b44b36..cc58ef88 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -184,7 +184,9 @@ export function ImageClass(ownerDocument: any): { _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): any; + addEventListener(type: any, listener: any, options: any): void; + removeEventListener(type: any, listener: any): void; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/input-event.d.ts b/types/interface/input-event.d.ts index 6e94aee8..ae6ef89f 100644 --- a/types/interface/input-event.d.ts +++ b/types/interface/input-event.d.ts @@ -2,7 +2,6 @@ * @implements globalThis.InputEvent */ export class InputEvent extends Event implements globalThis.InputEvent { - constructor(type: any, inputEventInit?: {}); inputType: any; data: any; dataTransfer: any; diff --git a/worker.js b/worker.js index d2c6fda6..9228975f 100644 --- a/worker.js +++ b/worker.js @@ -9177,85 +9177,52 @@ export const nullableAttribute = { }; */ -/*! (c) Andrea Giammarchi - ISC */ -var self$1 = {}; -try { - self$1.EventTarget = (new EventTarget).constructor; -} catch(EventTarget) { - (function (Object, wm) { - var create = Object.create; - var defineProperty = Object.defineProperty; - var proto = EventTarget.prototype; - define(proto, 'addEventListener', function (type, listener, options) { - for (var - secret = wm.get(this), - listeners = secret[type] || (secret[type] = []), - i = 0, length = listeners.length; i < length; i++ - ) { - if (listeners[i].listener === listener) - return; - } - listeners.push({target: this, listener: listener, options: options}); - }); - define(proto, 'dispatchEvent', function (event) { - var secret = wm.get(this); - var listeners = secret[event.type]; - if (listeners) { - define(event, 'target', this); - define(event, 'currentTarget', this); - listeners.slice(0).some(dispatch, event); - delete event.currentTarget; - delete event.target; - } - return true; - }); - define(proto, 'removeEventListener', function (type, listener) { - for (var - secret = wm.get(this), - /* istanbul ignore next */ - listeners = secret[type] || (secret[type] = []), - i = 0, length = listeners.length; i < length; i++ - ) { - if (listeners[i].listener === listener) { - listeners.splice(i, 1); - return; - } - } - }); - self$1.EventTarget = EventTarget; - function EventTarget() { wm.set(this, create(null)); - } - function define(target, name, value) { - defineProperty( - target, - name, - { - configurable: true, - writable: true, - value: value - } - ); +// https://dom.spec.whatwg.org/#interface-eventtarget + +const wm = new WeakMap(); + +function dispatch({ target, listener}) { + if (typeof listener === 'function') { + listener.call(target, this); + } else { + listener.handleEvent(this); + } + return this._stopImmediatePropagationFlag; +} + +function invokeListeners({currentTarget, target}) { + const map = wm.get(currentTarget); + if (map && map.has(this.type)) { + const listeners = map.get(this.type); + if (currentTarget === target) { + this.eventPhase = this.AT_TARGET; + } else { + this.eventPhase = this.BUBBLING_PHASE; } - function dispatch(info) { - var options = info.options; + + this.currentTarget = currentTarget; + this.target = target; + for (const [listener, options] of listeners) { if (options && options.once) - info.target.removeEventListener(this.type, info.listener); - if (typeof info.listener === 'function') - info.listener.call(info.target, this); - else - info.listener.handleEvent(this); - return this._stopImmediatePropagationFlag; + listeners.delete(listener); + if (dispatch.call(this, {target: this, listener})) + break; } - }(Object, new WeakMap)); + delete this.currentTarget; + delete this.target; + return this.cancelBubble; + } } -var EventTarget$1 = self$1.EventTarget; -// https://dom.spec.whatwg.org/#interface-eventtarget /** * @implements globalThis.EventTarget */ -class DOMEventTarget extends EventTarget$1 { +class DOMEventTarget { + + constructor() { + wm.set(this, new Map); + } /** * @protected @@ -9264,24 +9231,39 @@ class DOMEventTarget extends EventTarget$1 { return null; } + addEventListener(type, listener, options) { + const map = wm.get(this); + if (!map.has(type)) + map.set(type, new Map); + map.get(type).set(listener, options); + } + + removeEventListener(type, listener) { + const map = wm.get(this); + if (map.has(type)) { + const listeners = map.get(type); + if (listeners.delete(listener) && !listeners.size) { + map.delete(type); + } + } + } + dispatchEvent(event) { - const dispatched = super.dispatchEvent(event); + let node = this; + event.eventPhase = event.CAPTURING_PHASE; // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event.cancelBubble) { - const parent = this._getParent(); - if (parent && parent.dispatchEvent) { - const options = { - bubbles: event.bubbles, - cancelable: event.cancelable, - composed: event.composed, - }; - // in Node 16.5 the same event can't be used for another dispatch - return parent.dispatchEvent(new event.constructor(event.type, options)); - } + while (node) { + if (node.dispatchEvent) + event._path.push({currentTarget: node, target: this}); + node = event.bubbles && node._getParent && node._getParent(); } - return dispatched; + event._path.some(invokeListeners, event); + event._path = []; + event.eventPhase = event.NONE; + return !event.defaultPrevented; } + } // https://dom.spec.whatwg.org/#interface-nodelist @@ -12297,18 +12279,19 @@ function push(value, key) { /* c8 ignore start */ // Node 15 has Event but 14 and 12 don't - const BUBBLING_PHASE = 3; +const AT_TARGET = 2; const CAPTURING_PHASE = 1; +const NONE = 0; /** * @implements globalThis.Event */ -const GlobalEvent = typeof Event === 'function' ? - Event : - class Event { +class GlobalEvent { static get BUBBLING_PHASE() { return BUBBLING_PHASE; } + static get AT_TARGET() { return AT_TARGET; } static get CAPTURING_PHASE() { return CAPTURING_PHASE; } + static get NONE() { return NONE; } constructor(type, eventInitDict = {}) { this.type = type; @@ -12316,44 +12299,36 @@ const GlobalEvent = typeof Event === 'function' ? this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; - this.eventPhase = this.BUBBLING_PHASE; + this.eventPhase = this.NONE; this.timeStamp = Date.now(); this.defaultPrevented = false; this.originalTarget = null; this.returnValue = null; this.srcElement = null; this.target = null; + this._path = []; } get BUBBLING_PHASE() { return BUBBLING_PHASE; } + get AT_TARGET() { return AT_TARGET; } get CAPTURING_PHASE() { return CAPTURING_PHASE; } + get NONE() { return NONE; } preventDefault() { this.defaultPrevented = true; } - // TODO: what do these do in native NodeJS Event ? + // simplified implementation, should be https://dom.spec.whatwg.org/#dom-event-composedpath + composedPath() { + return this._path; + } + stopPropagation() { this.cancelBubble = true; } stopImmediatePropagation() { + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } - }; - - - -/** - * @implements globalThis.Event - */ -class DOMEvent extends GlobalEvent { - // specs: "set this’s stop propagation flag and this’s stop immediate propagation flag" - // https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation - // but Node don't do that so for now we extend it - stopImmediatePropagation() { - super.stopPropagation(); - if (typeof super.stopImmediatePropagation === 'function') - super.stopImmediatePropagation(); - } } /* c8 ignore stop */ @@ -12554,7 +12529,7 @@ class Element$1 extends ParentNode { return new Proxy(attributes, attributesHandler); } - focus() { this.dispatchEvent(new DOMEvent('focus')); } + focus() { this.dispatchEvent(new GlobalEvent('focus')); } getAttribute(name) { if (name === 'class') @@ -12990,8 +12965,8 @@ class HTMLElement extends Element$1 { offsetWidth */ - blur() { this.dispatchEvent(new DOMEvent('blur')); } - click() { this.dispatchEvent(new DOMEvent('click')); } + blur() { this.dispatchEvent(new GlobalEvent('blur')); } + click() { this.dispatchEvent(new GlobalEvent('click')); } // Boolean getters get accessKeyLabel() { @@ -16240,7 +16215,7 @@ const Mime = { */ const GlobalCustomEvent = typeof CustomEvent === 'function' ? CustomEvent : - class CustomEvent extends DOMEvent { + class CustomEvent extends GlobalEvent { constructor(type, eventInitDict = {}) { super(type, eventInitDict); this.detail = eventInitDict.detail; @@ -16254,7 +16229,7 @@ const GlobalCustomEvent = typeof CustomEvent === 'function' ? /** * @implements globalThis.InputEvent */ -class InputEvent extends DOMEvent { +class InputEvent extends GlobalEvent { constructor(type, inputEventInit = {}) { super(type, inputEventInit); this.inputType = inputEventInit.inputType; @@ -16440,7 +16415,7 @@ const globalExports = assign( HTMLClasses, { CustomEvent: GlobalCustomEvent, - Event: DOMEvent, + Event: GlobalEvent, EventTarget: DOMEventTarget, InputEvent, NamedNodeMap, @@ -16553,6 +16528,13 @@ class Document$1 extends NonElementParentNode { get isConnected() { return true; } + /** + * @protected + */ + _getParent() { + return this[EVENT_TARGET]; + } + createAttribute(name) { return new Attr$1(this, name); } createComment(textContent) { return new Comment$1(this, textContent); } createDocumentFragment() { return new DocumentFragment$1(this); } @@ -16568,7 +16550,7 @@ class Document$1 extends NonElementParentNode { createNodeIterator(root, whatToShow = -1) { return this.createTreeWalker(root, whatToShow); } createEvent(name) { - const event = create$1(name === 'Event' ? new DOMEvent('') : new GlobalCustomEvent('')); + const event = create$1(name === 'Event' ? new GlobalEvent('') : new GlobalCustomEvent('')); event.initEvent = event.initCustomEvent = ( type, canBubble = false, @@ -16923,4 +16905,4 @@ function Document() { setPrototypeOf(Document, Document$1).prototype = Document$1.prototype; -export { Attr, CharacterData, Comment, GlobalCustomEvent as CustomEvent, DOMParser, Document, DocumentFragment, DocumentType, Element, DOMEvent as Event, DOMEventTarget as EventTarget, Facades, HTMLAnchorElement, HTMLAreaElement, HTMLAudioElement, HTMLBRElement, HTMLBaseElement, HTMLBodyElement, HTMLButtonElement, HTMLCanvasElement, HTMLClasses, HTMLDListElement, HTMLDataElement, HTMLDataListElement, HTMLDetailsElement, HTMLDirectoryElement, HTMLDivElement, HTMLElement, HTMLEmbedElement, HTMLFieldSetElement, HTMLFontElement, HTMLFormElement, HTMLFrameElement, HTMLFrameSetElement, HTMLHRElement, HTMLHeadElement, HTMLHeadingElement, HTMLHtmlElement, HTMLIFrameElement, HTMLImageElement, HTMLInputElement, HTMLLIElement, HTMLLabelElement, HTMLLegendElement, HTMLLinkElement, HTMLMapElement, HTMLMarqueeElement, HTMLMediaElement, HTMLMenuElement, HTMLMetaElement, HTMLMeterElement, HTMLModElement, HTMLOListElement, HTMLObjectElement, HTMLOptGroupElement, HTMLOptionElement, HTMLOutputElement, HTMLParagraphElement, HTMLParamElement, HTMLPictureElement, HTMLPreElement, HTMLProgressElement, HTMLQuoteElement, HTMLScriptElement, HTMLSelectElement, HTMLSlotElement, HTMLSourceElement, HTMLSpanElement, HTMLStyleElement, HTMLTableCaptionElement, HTMLTableCellElement, HTMLTableElement, HTMLTableRowElement, HTMLTemplateElement, HTMLTextAreaElement, HTMLTimeElement, HTMLTitleElement, HTMLTrackElement, HTMLUListElement, HTMLUnknownElement, HTMLVideoElement, InputEvent, Node, NodeList, SVGElement, ShadowRoot, Text, illegalConstructor, parseHTML, parseJSON, toJSON }; +export { Attr, CharacterData, Comment, GlobalCustomEvent as CustomEvent, DOMParser, Document, DocumentFragment, DocumentType, Element, GlobalEvent as Event, DOMEventTarget as EventTarget, Facades, HTMLAnchorElement, HTMLAreaElement, HTMLAudioElement, HTMLBRElement, HTMLBaseElement, HTMLBodyElement, HTMLButtonElement, HTMLCanvasElement, HTMLClasses, HTMLDListElement, HTMLDataElement, HTMLDataListElement, HTMLDetailsElement, HTMLDirectoryElement, HTMLDivElement, HTMLElement, HTMLEmbedElement, HTMLFieldSetElement, HTMLFontElement, HTMLFormElement, HTMLFrameElement, HTMLFrameSetElement, HTMLHRElement, HTMLHeadElement, HTMLHeadingElement, HTMLHtmlElement, HTMLIFrameElement, HTMLImageElement, HTMLInputElement, HTMLLIElement, HTMLLabelElement, HTMLLegendElement, HTMLLinkElement, HTMLMapElement, HTMLMarqueeElement, HTMLMediaElement, HTMLMenuElement, HTMLMetaElement, HTMLMeterElement, HTMLModElement, HTMLOListElement, HTMLObjectElement, HTMLOptGroupElement, HTMLOptionElement, HTMLOutputElement, HTMLParagraphElement, HTMLParamElement, HTMLPictureElement, HTMLPreElement, HTMLProgressElement, HTMLQuoteElement, HTMLScriptElement, HTMLSelectElement, HTMLSlotElement, HTMLSourceElement, HTMLSpanElement, HTMLStyleElement, HTMLTableCaptionElement, HTMLTableCellElement, HTMLTableElement, HTMLTableRowElement, HTMLTemplateElement, HTMLTextAreaElement, HTMLTimeElement, HTMLTitleElement, HTMLTrackElement, HTMLUListElement, HTMLUnknownElement, HTMLVideoElement, InputEvent, Node, NodeList, SVGElement, ShadowRoot, Text, illegalConstructor, parseHTML, parseJSON, toJSON };