From d8c5fa392fac4a8b26228c9cdc769a0c2bb0e1d3 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Sun, 18 Jul 2021 12:55:43 +0100 Subject: [PATCH 01/27] Extend EventTarget to support event bubbling + tests - add getParent method to Node returning parentNode - on dispatch trigger parent dispatchEvent - unit tests --- cjs/interface/event-target.js | 20 +++++++- cjs/interface/event.js | 12 ++++- cjs/interface/node.js | 4 ++ esm/interface/event-target.js | 20 +++++++- esm/interface/event.js | 12 ++++- esm/interface/node.js | 4 ++ test/interface/event-target.js | 77 +++++++++++++++++++++++++++++++ types/interface/document.d.ts | 5 +- types/interface/event-target.d.ts | 9 +++- types/interface/event.d.ts | 2 + types/interface/image.d.ts | 2 + types/interface/node.d.ts | 4 +- types/shared/node.d.ts | 2 +- 13 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 test/interface/event-target.js diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 1fef92a5..10f0f039 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -6,4 +6,22 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* /** * @implements globalThis.EventTarget */ -exports.EventTarget = EventTarget; +class DOMEventTarget extends EventTarget { + getParent() { + return null; + } + + dispatchEvent(event) { + super.dispatchEvent(event); + + // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); + } + return true; + } +} + +exports.EventTarget = DOMEventTarget; diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 8b1dccf1..05080d17 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,6 +20,8 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; + this._stopPropagationFlag = false; + this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; this.timeStamp = Date.now(); @@ -36,8 +38,14 @@ const GlobalEvent = typeof Event === 'function' ? preventDefault() { this.defaultPrevented = true; } // TODO: what do these do in native NodeJS Event ? - stopPropagation() {} - stopImmediatePropagation() {} + stopPropagation() { + this._stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this._stopPropagationFlag = true; + this._stopImmediatePropagationFlag = true; + } }; exports.Event = GlobalEvent; diff --git a/cjs/interface/node.js b/cjs/interface/node.js index c3b38a58..7a165b4c 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,6 +142,10 @@ class Node extends EventTarget { return false; } + getParent() { + return this.parentNode; + } + getRootNode() { let root = this; while (root.parentNode) diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index 5d1a0a89..b6e9454d 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -5,4 +5,22 @@ import EventTarget from '@ungap/event-target'; /** * @implements globalThis.EventTarget */ -export {EventTarget}; +class DOMEventTarget extends EventTarget { + getParent() { + return null; + } + + dispatchEvent(event) { + super.dispatchEvent(event); + + // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); + } + return true; + } +} + +export { DOMEventTarget as EventTarget }; diff --git a/esm/interface/event.js b/esm/interface/event.js index 848964be..c022c467 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,6 +19,8 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; + this._stopPropagationFlag = false; + this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; this.timeStamp = Date.now(); @@ -35,8 +37,14 @@ const GlobalEvent = typeof Event === 'function' ? preventDefault() { this.defaultPrevented = true; } // TODO: what do these do in native NodeJS Event ? - stopPropagation() {} - stopImmediatePropagation() {} + stopPropagation() { + this._stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this._stopPropagationFlag = true; + this._stopImmediatePropagationFlag = true; + } }; export {GlobalEvent as Event}; diff --git a/esm/interface/node.js b/esm/interface/node.js index 38bf20db..edbba106 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,6 +141,10 @@ export class Node extends EventTarget { return false; } + getParent() { + return this.parentNode; + } + getRootNode() { let root = this; while (root.parentNode) diff --git a/test/interface/event-target.js b/test/interface/event-target.js new file mode 100644 index 00000000..6ea4c615 --- /dev/null +++ b/test/interface/event-target.js @@ -0,0 +1,77 @@ +const assert = require('../assert.js').for('EventTarget'); + +const { parseHTML } = global[Symbol.for('linkedom')]; + +const { Event, document, EventTarget } = parseHTML( + '
', +); + +// check basics + +let callCount = 0; +const basicHandler = () => { + callCount++; +}; + + +const eventTarget = new EventTarget(); +eventTarget.addEventListener('foo', basicHandler); +eventTarget.dispatchEvent(new Event('foo')); +assert(callCount, 1, 'basicHandler should have been called'); + +assert( + eventTarget.dispatchEvent(new Event('click')), + true, + 'Dispatching an event type with no handlers should return true', +); +assert(callCount, 1, 'Dispatching an event type should only call appropriate listeners'); + +eventTarget.removeEventListener('foo', basicHandler); +eventTarget.dispatchEvent(new Event('foo')); +assert(callCount, 1, 'basicHandler should not have been called after being removed'); + +assert(eventTarget.getParent(), null, 'getParent should return null'); + + +// check propagation now +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'); + + +// ensure once removed listeners +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 3, 'listeners should only have been called once then removed'); + +// check no bubbling +callCount = 0; +buttonTarget.addEventListener('click', basicHandler, { once: true }); +containerTarget.addEventListener('click', basicHandler, { once: true }); +bodyTarget.addEventListener('click', basicHandler, { once: true }); + +buttonTarget.dispatchEvent(new Event('click', { bubbles: false })); +assert(callCount, 1, 'Expect listener to be called once'); + +// check stop propagation +buttonTarget.addEventListener( + 'click', + (event) => { + event.stopPropagation(); + callCount++; + }, + { + once: true, + }, +); +containerTarget.addEventListener('click', basicHandler, { once: true }); + +callCount = 0; +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index c1e1cd36..08943f9b 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -221,8 +221,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; + getParent(): any; getRootNode(): any; [PREV]: any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; @@ -233,7 +235,7 @@ export class Document extends NonElementParentNode implements globalThis.Documen readonly DOCUMENT_FRAGMENT_NODE: number; readonly DOCUMENT_TYPE_NODE: number; }; - [EVENT_TARGET]: any; + [EVENT_TARGET]: EventTarget; } import { NonElementParentNode } from "../mixin/non-element-parent-node.js"; import { DocumentType } from "./document-type.js"; @@ -253,3 +255,4 @@ import { DOCTYPE } from "../shared/symbols.js"; import { DOM_PARSER } from "../shared/symbols.js"; import { IMAGE } from "../shared/symbols.js"; import { EVENT_TARGET } from "../shared/symbols.js"; +import { EventTarget } from "./event-target.js"; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 62856078..6cc0ee8b 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -1 +1,8 @@ -export { EventTarget }; +export { DOMEventTarget as EventTarget }; +/** + * @implements globalThis.EventTarget + */ +declare class DOMEventTarget implements globalThis.EventTarget { + getParent(): any; + dispatchEvent(event: any): boolean; +} diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 841d3ac3..d60cde00 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -13,6 +13,8 @@ declare const GlobalEvent: { new (type: any, eventInitDict?: {}): { type: any; bubbles: boolean; + _stopPropagationFlag: boolean; + _stopImmediatePropagationFlag: boolean; cancelable: boolean; eventPhase: number; timeStamp: number; diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 8dc96533..2396b272 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,8 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; + getParent(): any; getRootNode(): any; [PREV]: any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/node.d.ts b/types/interface/node.d.ts index ad40d309..1378e566 100644 --- a/types/interface/node.d.ts +++ b/types/interface/node.d.ts @@ -1,7 +1,7 @@ /** * @implements globalThis.Node */ -export class Node implements globalThis.Node { +export class Node extends EventTarget implements globalThis.Node { static get ELEMENT_NODE(): number; static get ATTRIBUTE_NODE(): number; static get TEXT_NODE(): number; @@ -42,7 +42,6 @@ export class Node implements globalThis.Node { appendChild(): void; replaceChild(): void; removeChild(): void; - toString(): string; hasChildNodes(): boolean; isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; @@ -51,6 +50,7 @@ export class Node implements globalThis.Node { [NEXT]: any; [PREV]: any; } +import { EventTarget } from "./event-target.js"; import { NodeList } from "./node-list.js"; import { NEXT } from "../shared/symbols.js"; import { PREV } from "../shared/symbols.js"; diff --git a/types/shared/node.d.ts b/types/shared/node.d.ts index 4b4d8655..75a39306 100644 --- a/types/shared/node.d.ts +++ b/types/shared/node.d.ts @@ -6,7 +6,7 @@ export function parentElement({ parentNode }: { parentNode: any; }): any; export function previousSibling({ [PREV]: prev }: { - "__@PREV@37916": any; + "__@PREV@38087": any; }): any; export function nextSibling(node: any): any; import { PREV } from "./symbols.js"; From 9967ace3e06dda757495025b31858736e1b61f58 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 10:32:45 +0100 Subject: [PATCH 02/27] Event bubbling: unit tests, method name and function return - method renamed to "_getParent" - dispatchEvent return value now take EventTarget.dispatchEvent return into account as well as parent.dispatchEvent - "stopImmediatePropagation" now tested in unit tests. Todo: when ungap/event-target support "_stopImmediatePropagationFlag" more event listeners could be added to `buttonTarget` to ensure full behaviour is working. --- cjs/interface/event-target.js | 12 ++++++------ cjs/interface/node.js | 2 +- esm/interface/event-target.js | 12 ++++++------ esm/interface/node.js | 2 +- test/interface/event-target.js | 19 ++++++++++++++++++- types/interface/document.d.ts | 4 ++-- types/interface/event-target.d.ts | 4 ++-- types/interface/image.d.ts | 4 ++-- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 10f0f039..4a24bb60 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,20 +7,20 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 7a165b4c..2221d3c2 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,7 +142,7 @@ class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index b6e9454d..eb8ffa34 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,20 +6,20 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/esm/interface/node.js b/esm/interface/node.js index edbba106..dca4dce9 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,7 +141,7 @@ export class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 6ea4c615..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -30,7 +30,7 @@ eventTarget.removeEventListener('foo', basicHandler); eventTarget.dispatchEvent(new Event('foo')); assert(callCount, 1, 'basicHandler should not have been called after being removed'); -assert(eventTarget.getParent(), null, 'getParent should return null'); +assert(eventTarget._getParent(), null, 'getParent should return null'); // check propagation now @@ -75,3 +75,20 @@ containerTarget.addEventListener('click', basicHandler, { once: true }); callCount = 0; buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); + +// check stop immediate propagation +buttonTarget.addEventListener( + 'click', + (event) => { + event.stopImmediatePropagation(); + callCount++; + }, + { + once: true, + }, +); +containerTarget.addEventListener('click', basicHandler, { once: true }); + +callCount = 0; +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index 08943f9b..fcf3a648 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -221,10 +221,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 6cc0ee8b..8ba5b491 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,6 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - getParent(): any; - dispatchEvent(event: any): boolean; + _getParent(): any; + dispatchEvent(event: any): any; } diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 2396b272..46d2f045 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,10 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; From fe0b8461f4f083b234a0d19268ecccb101788bf2 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 11:09:58 +0100 Subject: [PATCH 03/27] Event bubbling: _getParent protected used "protected" here instead of "private", because Node extend EventTarget and needs to overwrite "_getParent" while still allowing "dispatchEvent" to access it. --- cjs/interface/event-target.js | 4 ++++ cjs/interface/node.js | 3 +++ esm/interface/event-target.js | 4 ++++ esm/interface/node.js | 3 +++ types/interface/event-target.d.ts | 5 ++++- 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 4a24bb60..5f220ce0 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,6 +7,10 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 2221d3c2..a5f09e59 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,6 +142,9 @@ class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index eb8ffa34..84a14d76 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,6 +6,10 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/esm/interface/node.js b/esm/interface/node.js index dca4dce9..60c03c95 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,6 +141,9 @@ export class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 8ba5b491..bec0d321 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,9 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - _getParent(): any; + /** + * @protected + */ + protected _getParent(): any; dispatchEvent(event: any): any; } From 56a677b106a9774adc29af5a66eafc06d20488da Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 13:05:05 +0100 Subject: [PATCH 04/27] Event bubbling: fix for Node 16.5 - Node 16.5 will throw if we try to dispatch the current event to the parent. So instead we need to create a new event with the same options. - replaced "_stopPropagationFlag" by "cancelBubble" to be consistent with Node implementation of Event - currently Node don't handle bubbling, but also don't properly set "_stopPropagationFlag" when calling "stopImmediatePropagation" creating a problem and inconsistency for us. => should we extend the Node Event object to ensure the proper behaviour? --- cjs/interface/event-target.js | 13 ++++++++++--- cjs/interface/event.js | 6 +++--- esm/interface/event-target.js | 13 ++++++++++--- esm/interface/event.js | 6 +++--- test/interface/event-target.js | 7 +++++++ types/interface/event.d.ts | 2 +- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 5f220ce0..cc94df79 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -19,10 +19,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 05080d17..fd23fd54 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,7 +20,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -39,11 +39,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index 84a14d76..c07a092e 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -18,10 +18,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/esm/interface/event.js b/esm/interface/event.js index c022c467..15b86751 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,7 +19,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -38,11 +38,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 5307b535..7b516132 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,10 +77,17 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation +// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them +const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); + if (isNode16) { + event.stopPropagation(); + } callCount++; }, { diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index d60cde00..a34d4baa 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -13,7 +13,7 @@ declare const GlobalEvent: { new (type: any, eventInitDict?: {}): { type: any; bubbles: boolean; - _stopPropagationFlag: boolean; + cancelBubble: boolean; _stopImmediatePropagationFlag: boolean; cancelable: boolean; eventPhase: number; From f546696c542001aa14344682a5d832bd916e425c Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 17:07:58 +0100 Subject: [PATCH 05/27] Event bubbling: extend Event to fix Node's stopImmediatePropagation --- cjs/interface/event.js | 16 +++++++++++-- esm/interface/event.js | 16 +++++++++++-- test/interface/event-target.js | 7 ------ types/interface/custom-event.d.ts | 1 + types/interface/event.d.ts | 38 ++++--------------------------- types/interface/input-event.d.ts | 3 ++- 6 files changed, 35 insertions(+), 46 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index fd23fd54..a8ac07f7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -43,11 +43,23 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -exports.Event = GlobalEvent; + + + 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(); + super.stopImmediatePropagation(); + } + } + + +exports.Event = DOMEvent; /* c8 ignore stop */ diff --git a/esm/interface/event.js b/esm/interface/event.js index 15b86751..577cd8ba 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -42,11 +42,23 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -export {GlobalEvent as 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(); + super.stopImmediatePropagation(); + } + } + + +export {DOMEvent as Event}; /* c8 ignore stop */ diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 7b516132..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,17 +77,10 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation -// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them -const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); - if (isNode16) { - event.stopPropagation(); - } callCount++; }, { diff --git a/types/interface/custom-event.d.ts b/types/interface/custom-event.d.ts index 1e947174..07ab20a1 100644 --- a/types/interface/custom-event.d.ts +++ b/types/interface/custom-event.d.ts @@ -5,5 +5,6 @@ export { GlobalCustomEvent as CustomEvent }; declare const GlobalCustomEvent: { new (type: any, eventInitDict?: {}): { detail: any; + stopImmediatePropagation(): void; }; }; diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index a34d4baa..57219d1e 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,34 +1,4 @@ -export { GlobalEvent as Event }; -/** - * @implements globalThis.Event - */ -declare const GlobalEvent: { - new (type: string, eventInitDict?: EventInit): Event; - prototype: Event; - readonly AT_TARGET: number; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - readonly NONE: number; -} | { - new (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; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - preventDefault(): void; - stopPropagation(): void; - stopImmediatePropagation(): void; - }; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; -}; +export { DOMEvent as Event }; +declare class DOMEvent { + stopImmediatePropagation(): void; +} diff --git a/types/interface/input-event.d.ts b/types/interface/input-event.d.ts index 40114611..6e94aee8 100644 --- a/types/interface/input-event.d.ts +++ b/types/interface/input-event.d.ts @@ -1,7 +1,7 @@ /** * @implements globalThis.InputEvent */ -export class InputEvent implements globalThis.InputEvent { +export class InputEvent extends Event implements globalThis.InputEvent { constructor(type: any, inputEventInit?: {}); inputType: any; data: any; @@ -9,3 +9,4 @@ export class InputEvent implements globalThis.InputEvent { isComposing: any; ranges: any; } +import { Event } from "./event.js"; From 8e4f67c37db1fce38ba4366741713fdb032dcd69 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 18:00:44 +0100 Subject: [PATCH 06/27] Event bubbling: implement comment + super method check --- cjs/interface/event.js | 8 ++++++-- esm/interface/event.js | 8 ++++++-- types/interface/event.d.ts | 5 ++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index a8ac07f7..ae77cef7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -49,13 +49,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/esm/interface/event.js b/esm/interface/event.js index 577cd8ba..d2675793 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -48,13 +48,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 57219d1e..ef0e3744 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,4 +1,7 @@ export { DOMEvent as Event }; -declare class DOMEvent { +/** + * @implements globalThis.Event + */ +declare class DOMEvent implements globalThis.Event { stopImmediatePropagation(): void; } From 57ae688de6660705a7e05fd4f575adc291e3a47c Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Sun, 18 Jul 2021 12:55:43 +0100 Subject: [PATCH 07/27] Extend EventTarget to support event bubbling + tests - add getParent method to Node returning parentNode - on dispatch trigger parent dispatchEvent - unit tests --- cjs/interface/event-target.js | 20 +++++++- cjs/interface/event.js | 12 ++++- cjs/interface/node.js | 4 ++ esm/interface/event-target.js | 20 +++++++- esm/interface/event.js | 12 ++++- esm/interface/node.js | 4 ++ test/interface/event-target.js | 77 +++++++++++++++++++++++++++++++ types/interface/document.d.ts | 5 +- types/interface/event-target.d.ts | 9 +++- types/interface/event.d.ts | 2 + types/interface/image.d.ts | 2 + types/interface/node.d.ts | 4 +- 12 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 test/interface/event-target.js diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 1fef92a5..10f0f039 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -6,4 +6,22 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* /** * @implements globalThis.EventTarget */ -exports.EventTarget = EventTarget; +class DOMEventTarget extends EventTarget { + getParent() { + return null; + } + + dispatchEvent(event) { + super.dispatchEvent(event); + + // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); + } + return true; + } +} + +exports.EventTarget = DOMEventTarget; diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 8b1dccf1..05080d17 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,6 +20,8 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; + this._stopPropagationFlag = false; + this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; this.timeStamp = Date.now(); @@ -36,8 +38,14 @@ const GlobalEvent = typeof Event === 'function' ? preventDefault() { this.defaultPrevented = true; } // TODO: what do these do in native NodeJS Event ? - stopPropagation() {} - stopImmediatePropagation() {} + stopPropagation() { + this._stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this._stopPropagationFlag = true; + this._stopImmediatePropagationFlag = true; + } }; exports.Event = GlobalEvent; diff --git a/cjs/interface/node.js b/cjs/interface/node.js index c3b38a58..7a165b4c 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,6 +142,10 @@ class Node extends EventTarget { return false; } + getParent() { + return this.parentNode; + } + getRootNode() { let root = this; while (root.parentNode) diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index 5d1a0a89..b6e9454d 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -5,4 +5,22 @@ import EventTarget from '@ungap/event-target'; /** * @implements globalThis.EventTarget */ -export {EventTarget}; +class DOMEventTarget extends EventTarget { + getParent() { + return null; + } + + dispatchEvent(event) { + super.dispatchEvent(event); + + // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); + } + return true; + } +} + +export { DOMEventTarget as EventTarget }; diff --git a/esm/interface/event.js b/esm/interface/event.js index 848964be..c022c467 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,6 +19,8 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; + this._stopPropagationFlag = false; + this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; this.timeStamp = Date.now(); @@ -35,8 +37,14 @@ const GlobalEvent = typeof Event === 'function' ? preventDefault() { this.defaultPrevented = true; } // TODO: what do these do in native NodeJS Event ? - stopPropagation() {} - stopImmediatePropagation() {} + stopPropagation() { + this._stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this._stopPropagationFlag = true; + this._stopImmediatePropagationFlag = true; + } }; export {GlobalEvent as Event}; diff --git a/esm/interface/node.js b/esm/interface/node.js index 38bf20db..edbba106 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,6 +141,10 @@ export class Node extends EventTarget { return false; } + getParent() { + return this.parentNode; + } + getRootNode() { let root = this; while (root.parentNode) diff --git a/test/interface/event-target.js b/test/interface/event-target.js new file mode 100644 index 00000000..6ea4c615 --- /dev/null +++ b/test/interface/event-target.js @@ -0,0 +1,77 @@ +const assert = require('../assert.js').for('EventTarget'); + +const { parseHTML } = global[Symbol.for('linkedom')]; + +const { Event, document, EventTarget } = parseHTML( + '
', +); + +// check basics + +let callCount = 0; +const basicHandler = () => { + callCount++; +}; + + +const eventTarget = new EventTarget(); +eventTarget.addEventListener('foo', basicHandler); +eventTarget.dispatchEvent(new Event('foo')); +assert(callCount, 1, 'basicHandler should have been called'); + +assert( + eventTarget.dispatchEvent(new Event('click')), + true, + 'Dispatching an event type with no handlers should return true', +); +assert(callCount, 1, 'Dispatching an event type should only call appropriate listeners'); + +eventTarget.removeEventListener('foo', basicHandler); +eventTarget.dispatchEvent(new Event('foo')); +assert(callCount, 1, 'basicHandler should not have been called after being removed'); + +assert(eventTarget.getParent(), null, 'getParent should return null'); + + +// check propagation now +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'); + + +// ensure once removed listeners +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 3, 'listeners should only have been called once then removed'); + +// check no bubbling +callCount = 0; +buttonTarget.addEventListener('click', basicHandler, { once: true }); +containerTarget.addEventListener('click', basicHandler, { once: true }); +bodyTarget.addEventListener('click', basicHandler, { once: true }); + +buttonTarget.dispatchEvent(new Event('click', { bubbles: false })); +assert(callCount, 1, 'Expect listener to be called once'); + +// check stop propagation +buttonTarget.addEventListener( + 'click', + (event) => { + event.stopPropagation(); + callCount++; + }, + { + once: true, + }, +); +containerTarget.addEventListener('click', basicHandler, { once: true }); + +callCount = 0; +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index 05db549e..f5025f02 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -219,8 +219,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; + getParent(): any; getRootNode(): any; [PREV]: any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; @@ -231,7 +233,7 @@ export class Document extends NonElementParentNode implements globalThis.Documen readonly DOCUMENT_FRAGMENT_NODE: number; readonly DOCUMENT_TYPE_NODE: number; }; - [EVENT_TARGET]: any; + [EVENT_TARGET]: EventTarget; } import { NonElementParentNode } from "../mixin/non-element-parent-node.js"; import { DocumentType } from "./document-type.js"; @@ -251,3 +253,4 @@ import { DOCTYPE } from "../shared/symbols.js"; import { DOM_PARSER } from "../shared/symbols.js"; import { IMAGE } from "../shared/symbols.js"; import { EVENT_TARGET } from "../shared/symbols.js"; +import { EventTarget } from "./event-target.js"; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 62856078..6cc0ee8b 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -1 +1,8 @@ -export { EventTarget }; +export { DOMEventTarget as EventTarget }; +/** + * @implements globalThis.EventTarget + */ +declare class DOMEventTarget implements globalThis.EventTarget { + getParent(): any; + dispatchEvent(event: any): boolean; +} diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 841d3ac3..d60cde00 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -13,6 +13,8 @@ declare const GlobalEvent: { new (type: any, eventInitDict?: {}): { type: any; bubbles: boolean; + _stopPropagationFlag: boolean; + _stopImmediatePropagationFlag: boolean; cancelable: boolean; eventPhase: number; timeStamp: number; diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 8dc96533..2396b272 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,8 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; + getParent(): any; getRootNode(): any; [PREV]: any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/node.d.ts b/types/interface/node.d.ts index ad40d309..1378e566 100644 --- a/types/interface/node.d.ts +++ b/types/interface/node.d.ts @@ -1,7 +1,7 @@ /** * @implements globalThis.Node */ -export class Node implements globalThis.Node { +export class Node extends EventTarget implements globalThis.Node { static get ELEMENT_NODE(): number; static get ATTRIBUTE_NODE(): number; static get TEXT_NODE(): number; @@ -42,7 +42,6 @@ export class Node implements globalThis.Node { appendChild(): void; replaceChild(): void; removeChild(): void; - toString(): string; hasChildNodes(): boolean; isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; @@ -51,6 +50,7 @@ export class Node implements globalThis.Node { [NEXT]: any; [PREV]: any; } +import { EventTarget } from "./event-target.js"; import { NodeList } from "./node-list.js"; import { NEXT } from "../shared/symbols.js"; import { PREV } from "../shared/symbols.js"; From 64a74d4282717db52bc449478491c4f54ab335a5 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 10:32:45 +0100 Subject: [PATCH 08/27] Event bubbling: unit tests, method name and function return - method renamed to "_getParent" - dispatchEvent return value now take EventTarget.dispatchEvent return into account as well as parent.dispatchEvent - "stopImmediatePropagation" now tested in unit tests. Todo: when ungap/event-target support "_stopImmediatePropagationFlag" more event listeners could be added to `buttonTarget` to ensure full behaviour is working. --- cjs/interface/event-target.js | 12 ++++++------ cjs/interface/node.js | 2 +- esm/interface/event-target.js | 12 ++++++------ esm/interface/node.js | 2 +- test/interface/event-target.js | 19 ++++++++++++++++++- types/interface/document.d.ts | 4 ++-- types/interface/event-target.d.ts | 4 ++-- types/interface/image.d.ts | 4 ++-- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 10f0f039..4a24bb60 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,20 +7,20 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 7a165b4c..2221d3c2 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,7 +142,7 @@ class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index b6e9454d..eb8ffa34 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,20 +6,20 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/esm/interface/node.js b/esm/interface/node.js index edbba106..dca4dce9 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,7 +141,7 @@ export class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 6ea4c615..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -30,7 +30,7 @@ eventTarget.removeEventListener('foo', basicHandler); eventTarget.dispatchEvent(new Event('foo')); assert(callCount, 1, 'basicHandler should not have been called after being removed'); -assert(eventTarget.getParent(), null, 'getParent should return null'); +assert(eventTarget._getParent(), null, 'getParent should return null'); // check propagation now @@ -75,3 +75,20 @@ containerTarget.addEventListener('click', basicHandler, { once: true }); callCount = 0; buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); + +// check stop immediate propagation +buttonTarget.addEventListener( + 'click', + (event) => { + event.stopImmediatePropagation(); + callCount++; + }, + { + once: true, + }, +); +containerTarget.addEventListener('click', basicHandler, { once: true }); + +callCount = 0; +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index f5025f02..d42014d8 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -219,10 +219,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 6cc0ee8b..8ba5b491 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,6 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - getParent(): any; - dispatchEvent(event: any): boolean; + _getParent(): any; + dispatchEvent(event: any): any; } diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 2396b272..46d2f045 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,10 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; From 023cc311b44a4b1062a69e36f06733069bfeaa3a Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 11:09:58 +0100 Subject: [PATCH 09/27] Event bubbling: _getParent protected used "protected" here instead of "private", because Node extend EventTarget and needs to overwrite "_getParent" while still allowing "dispatchEvent" to access it. --- cjs/interface/event-target.js | 4 ++++ cjs/interface/node.js | 3 +++ esm/interface/event-target.js | 4 ++++ esm/interface/node.js | 3 +++ types/interface/event-target.d.ts | 5 ++++- 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 4a24bb60..5f220ce0 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,6 +7,10 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 2221d3c2..a5f09e59 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,6 +142,9 @@ class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index eb8ffa34..84a14d76 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,6 +6,10 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/esm/interface/node.js b/esm/interface/node.js index dca4dce9..60c03c95 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,6 +141,9 @@ export class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 8ba5b491..bec0d321 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,9 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - _getParent(): any; + /** + * @protected + */ + protected _getParent(): any; dispatchEvent(event: any): any; } From 3754826d78da23bd8030db752e695168e7424533 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 13:05:05 +0100 Subject: [PATCH 10/27] Event bubbling: fix for Node 16.5 - Node 16.5 will throw if we try to dispatch the current event to the parent. So instead we need to create a new event with the same options. - replaced "_stopPropagationFlag" by "cancelBubble" to be consistent with Node implementation of Event - currently Node don't handle bubbling, but also don't properly set "_stopPropagationFlag" when calling "stopImmediatePropagation" creating a problem and inconsistency for us. => should we extend the Node Event object to ensure the proper behaviour? --- cjs/interface/event-target.js | 13 ++++++++++--- cjs/interface/event.js | 6 +++--- esm/interface/event-target.js | 13 ++++++++++--- esm/interface/event.js | 6 +++--- test/interface/event-target.js | 7 +++++++ types/interface/event.d.ts | 2 +- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 5f220ce0..cc94df79 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -19,10 +19,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 05080d17..fd23fd54 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,7 +20,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -39,11 +39,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index 84a14d76..c07a092e 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -18,10 +18,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/esm/interface/event.js b/esm/interface/event.js index c022c467..15b86751 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,7 +19,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -38,11 +38,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 5307b535..7b516132 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,10 +77,17 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation +// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them +const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); + if (isNode16) { + event.stopPropagation(); + } callCount++; }, { diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index d60cde00..a34d4baa 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -13,7 +13,7 @@ declare const GlobalEvent: { new (type: any, eventInitDict?: {}): { type: any; bubbles: boolean; - _stopPropagationFlag: boolean; + cancelBubble: boolean; _stopImmediatePropagationFlag: boolean; cancelable: boolean; eventPhase: number; From 88d4cd8d5f595eb2ce83bb5cb9b00c157e7dd147 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 17:07:58 +0100 Subject: [PATCH 11/27] Event bubbling: extend Event to fix Node's stopImmediatePropagation --- cjs/interface/event.js | 16 +++++++++++-- esm/interface/event.js | 16 +++++++++++-- test/interface/event-target.js | 7 ------ types/interface/custom-event.d.ts | 1 + types/interface/event.d.ts | 38 ++++--------------------------- types/interface/input-event.d.ts | 3 ++- 6 files changed, 35 insertions(+), 46 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index fd23fd54..a8ac07f7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -43,11 +43,23 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -exports.Event = GlobalEvent; + + + 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(); + super.stopImmediatePropagation(); + } + } + + +exports.Event = DOMEvent; /* c8 ignore stop */ diff --git a/esm/interface/event.js b/esm/interface/event.js index 15b86751..577cd8ba 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -42,11 +42,23 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -export {GlobalEvent as 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(); + super.stopImmediatePropagation(); + } + } + + +export {DOMEvent as Event}; /* c8 ignore stop */ diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 7b516132..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,17 +77,10 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation -// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them -const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); - if (isNode16) { - event.stopPropagation(); - } callCount++; }, { diff --git a/types/interface/custom-event.d.ts b/types/interface/custom-event.d.ts index 1e947174..07ab20a1 100644 --- a/types/interface/custom-event.d.ts +++ b/types/interface/custom-event.d.ts @@ -5,5 +5,6 @@ export { GlobalCustomEvent as CustomEvent }; declare const GlobalCustomEvent: { new (type: any, eventInitDict?: {}): { detail: any; + stopImmediatePropagation(): void; }; }; diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index a34d4baa..57219d1e 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,34 +1,4 @@ -export { GlobalEvent as Event }; -/** - * @implements globalThis.Event - */ -declare const GlobalEvent: { - new (type: string, eventInitDict?: EventInit): Event; - prototype: Event; - readonly AT_TARGET: number; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - readonly NONE: number; -} | { - new (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; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - preventDefault(): void; - stopPropagation(): void; - stopImmediatePropagation(): void; - }; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; -}; +export { DOMEvent as Event }; +declare class DOMEvent { + stopImmediatePropagation(): void; +} diff --git a/types/interface/input-event.d.ts b/types/interface/input-event.d.ts index 40114611..6e94aee8 100644 --- a/types/interface/input-event.d.ts +++ b/types/interface/input-event.d.ts @@ -1,7 +1,7 @@ /** * @implements globalThis.InputEvent */ -export class InputEvent implements globalThis.InputEvent { +export class InputEvent extends Event implements globalThis.InputEvent { constructor(type: any, inputEventInit?: {}); inputType: any; data: any; @@ -9,3 +9,4 @@ export class InputEvent implements globalThis.InputEvent { isComposing: any; ranges: any; } +import { Event } from "./event.js"; From 26545dc51a9cf63f4637f4be531ffa1328ff5c3f Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 18:00:44 +0100 Subject: [PATCH 12/27] Event bubbling: implement comment + super method check --- cjs/interface/event.js | 8 ++++++-- esm/interface/event.js | 8 ++++++-- types/interface/event.d.ts | 5 ++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index a8ac07f7..ae77cef7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -49,13 +49,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/esm/interface/event.js b/esm/interface/event.js index 577cd8ba..d2675793 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -48,13 +48,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 57219d1e..ef0e3744 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,4 +1,7 @@ export { DOMEvent as Event }; -declare class DOMEvent { +/** + * @implements globalThis.Event + */ +declare class DOMEvent implements globalThis.Event { stopImmediatePropagation(): void; } From 495587ac7d9dc209f8598936c4c0967d320ae963 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Tue, 20 Jul 2021 21:05:29 +0100 Subject: [PATCH 13/27] Event bubbling: package update + improved test --- package.json | 2 +- test/interface/event-target.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cca71630..c69ebae0 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "./worker": "./worker.js" }, "dependencies": { - "@ungap/event-target": "^0.2.2", + "@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 5307b535..4e0d67a3 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,6 +77,7 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation +buttonTarget.addEventListener('click', () => callCount++, { once: true }); buttonTarget.addEventListener( 'click', (event) => { @@ -87,8 +88,9 @@ buttonTarget.addEventListener( once: true, }, ); +buttonTarget.addEventListener('click', () => callCount++, { once: true }); containerTarget.addEventListener('click', basicHandler, { once: true }); callCount = 0; buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); -assert(callCount, 1, 'listener should be called once before stopping bubbling'); +assert(callCount, 2, '2 listeners should be called before stopping'); From 88824dcb6084d99e7f849893456887fb033d3f17 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Sun, 18 Jul 2021 12:55:43 +0100 Subject: [PATCH 14/27] Extend EventTarget to support event bubbling + tests - add getParent method to Node returning parentNode - on dispatch trigger parent dispatchEvent - unit tests --- cjs/interface/event-target.js | 20 +++++++- cjs/interface/event.js | 12 ++++- cjs/interface/node.js | 4 ++ esm/interface/event-target.js | 20 +++++++- esm/interface/event.js | 12 ++++- esm/interface/node.js | 4 ++ test/interface/event-target.js | 77 +++++++++++++++++++++++++++++++ types/interface/document.d.ts | 5 +- types/interface/event-target.d.ts | 9 +++- types/interface/event.d.ts | 2 + types/interface/image.d.ts | 2 + types/interface/node.d.ts | 4 +- types/shared/node.d.ts | 2 +- 13 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 test/interface/event-target.js diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 1fef92a5..10f0f039 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -6,4 +6,22 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* /** * @implements globalThis.EventTarget */ -exports.EventTarget = EventTarget; +class DOMEventTarget extends EventTarget { + getParent() { + return null; + } + + dispatchEvent(event) { + super.dispatchEvent(event); + + // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); + } + return true; + } +} + +exports.EventTarget = DOMEventTarget; diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 8b1dccf1..05080d17 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,6 +20,8 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; + this._stopPropagationFlag = false; + this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; this.timeStamp = Date.now(); @@ -36,8 +38,14 @@ const GlobalEvent = typeof Event === 'function' ? preventDefault() { this.defaultPrevented = true; } // TODO: what do these do in native NodeJS Event ? - stopPropagation() {} - stopImmediatePropagation() {} + stopPropagation() { + this._stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this._stopPropagationFlag = true; + this._stopImmediatePropagationFlag = true; + } }; exports.Event = GlobalEvent; diff --git a/cjs/interface/node.js b/cjs/interface/node.js index c3b38a58..7a165b4c 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,6 +142,10 @@ class Node extends EventTarget { return false; } + getParent() { + return this.parentNode; + } + getRootNode() { let root = this; while (root.parentNode) diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index 5d1a0a89..b6e9454d 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -5,4 +5,22 @@ import EventTarget from '@ungap/event-target'; /** * @implements globalThis.EventTarget */ -export {EventTarget}; +class DOMEventTarget extends EventTarget { + getParent() { + return null; + } + + dispatchEvent(event) { + super.dispatchEvent(event); + + // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); + } + return true; + } +} + +export { DOMEventTarget as EventTarget }; diff --git a/esm/interface/event.js b/esm/interface/event.js index 848964be..c022c467 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,6 +19,8 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; + this._stopPropagationFlag = false; + this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; this.timeStamp = Date.now(); @@ -35,8 +37,14 @@ const GlobalEvent = typeof Event === 'function' ? preventDefault() { this.defaultPrevented = true; } // TODO: what do these do in native NodeJS Event ? - stopPropagation() {} - stopImmediatePropagation() {} + stopPropagation() { + this._stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this._stopPropagationFlag = true; + this._stopImmediatePropagationFlag = true; + } }; export {GlobalEvent as Event}; diff --git a/esm/interface/node.js b/esm/interface/node.js index 38bf20db..edbba106 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,6 +141,10 @@ export class Node extends EventTarget { return false; } + getParent() { + return this.parentNode; + } + getRootNode() { let root = this; while (root.parentNode) diff --git a/test/interface/event-target.js b/test/interface/event-target.js new file mode 100644 index 00000000..6ea4c615 --- /dev/null +++ b/test/interface/event-target.js @@ -0,0 +1,77 @@ +const assert = require('../assert.js').for('EventTarget'); + +const { parseHTML } = global[Symbol.for('linkedom')]; + +const { Event, document, EventTarget } = parseHTML( + '
', +); + +// check basics + +let callCount = 0; +const basicHandler = () => { + callCount++; +}; + + +const eventTarget = new EventTarget(); +eventTarget.addEventListener('foo', basicHandler); +eventTarget.dispatchEvent(new Event('foo')); +assert(callCount, 1, 'basicHandler should have been called'); + +assert( + eventTarget.dispatchEvent(new Event('click')), + true, + 'Dispatching an event type with no handlers should return true', +); +assert(callCount, 1, 'Dispatching an event type should only call appropriate listeners'); + +eventTarget.removeEventListener('foo', basicHandler); +eventTarget.dispatchEvent(new Event('foo')); +assert(callCount, 1, 'basicHandler should not have been called after being removed'); + +assert(eventTarget.getParent(), null, 'getParent should return null'); + + +// check propagation now +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'); + + +// ensure once removed listeners +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 3, 'listeners should only have been called once then removed'); + +// check no bubbling +callCount = 0; +buttonTarget.addEventListener('click', basicHandler, { once: true }); +containerTarget.addEventListener('click', basicHandler, { once: true }); +bodyTarget.addEventListener('click', basicHandler, { once: true }); + +buttonTarget.dispatchEvent(new Event('click', { bubbles: false })); +assert(callCount, 1, 'Expect listener to be called once'); + +// check stop propagation +buttonTarget.addEventListener( + 'click', + (event) => { + event.stopPropagation(); + callCount++; + }, + { + once: true, + }, +); +containerTarget.addEventListener('click', basicHandler, { once: true }); + +callCount = 0; +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index c1e1cd36..08943f9b 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -221,8 +221,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; + getParent(): any; getRootNode(): any; [PREV]: any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; @@ -233,7 +235,7 @@ export class Document extends NonElementParentNode implements globalThis.Documen readonly DOCUMENT_FRAGMENT_NODE: number; readonly DOCUMENT_TYPE_NODE: number; }; - [EVENT_TARGET]: any; + [EVENT_TARGET]: EventTarget; } import { NonElementParentNode } from "../mixin/non-element-parent-node.js"; import { DocumentType } from "./document-type.js"; @@ -253,3 +255,4 @@ import { DOCTYPE } from "../shared/symbols.js"; import { DOM_PARSER } from "../shared/symbols.js"; import { IMAGE } from "../shared/symbols.js"; import { EVENT_TARGET } from "../shared/symbols.js"; +import { EventTarget } from "./event-target.js"; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 62856078..6cc0ee8b 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -1 +1,8 @@ -export { EventTarget }; +export { DOMEventTarget as EventTarget }; +/** + * @implements globalThis.EventTarget + */ +declare class DOMEventTarget implements globalThis.EventTarget { + getParent(): any; + dispatchEvent(event: any): boolean; +} diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 841d3ac3..d60cde00 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -13,6 +13,8 @@ declare const GlobalEvent: { new (type: any, eventInitDict?: {}): { type: any; bubbles: boolean; + _stopPropagationFlag: boolean; + _stopImmediatePropagationFlag: boolean; cancelable: boolean; eventPhase: number; timeStamp: number; diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 8dc96533..2396b272 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,8 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; + getParent(): any; getRootNode(): any; [PREV]: any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/node.d.ts b/types/interface/node.d.ts index ad40d309..1378e566 100644 --- a/types/interface/node.d.ts +++ b/types/interface/node.d.ts @@ -1,7 +1,7 @@ /** * @implements globalThis.Node */ -export class Node implements globalThis.Node { +export class Node extends EventTarget implements globalThis.Node { static get ELEMENT_NODE(): number; static get ATTRIBUTE_NODE(): number; static get TEXT_NODE(): number; @@ -42,7 +42,6 @@ export class Node implements globalThis.Node { appendChild(): void; replaceChild(): void; removeChild(): void; - toString(): string; hasChildNodes(): boolean; isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; @@ -51,6 +50,7 @@ export class Node implements globalThis.Node { [NEXT]: any; [PREV]: any; } +import { EventTarget } from "./event-target.js"; import { NodeList } from "./node-list.js"; import { NEXT } from "../shared/symbols.js"; import { PREV } from "../shared/symbols.js"; diff --git a/types/shared/node.d.ts b/types/shared/node.d.ts index 4b4d8655..75a39306 100644 --- a/types/shared/node.d.ts +++ b/types/shared/node.d.ts @@ -6,7 +6,7 @@ export function parentElement({ parentNode }: { parentNode: any; }): any; export function previousSibling({ [PREV]: prev }: { - "__@PREV@37916": any; + "__@PREV@38087": any; }): any; export function nextSibling(node: any): any; import { PREV } from "./symbols.js"; From 67024dafedc9913f35461e14473f4b14a680dc53 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 10:08:36 +0100 Subject: [PATCH 15/27] Adding NamedNodeMap to global export (#85) * Adding NamedNodeMap to global export --- cjs/interface/document.js | 2 ++ cjs/shared/symbols.js | 2 +- esm/interface/document.js | 2 ++ esm/shared/symbols.js | 2 +- test/interface/named-node-map.js | 4 ++-- types/interface/document.d.ts | 4 +--- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cjs/interface/document.js b/cjs/interface/document.js index b22b6439..c75eb57b 100644 --- a/cjs/interface/document.js +++ b/cjs/interface/document.js @@ -29,6 +29,7 @@ const {EventTarget} = require('./event-target.js'); const {InputEvent} = require('./input-event.js'); const {ImageClass} = require('./image.js'); const {MutationObserverClass} = require('./mutation-observer.js'); +const {NamedNodeMap} = require('./named-node-map.js'); const {NodeList} = require('./node-list.js'); const {Range} = require('./range.js'); const {Text} = require('./text.js'); @@ -48,6 +49,7 @@ const globalExports = assign( Event, EventTarget, InputEvent, + NamedNodeMap, NodeList } ); diff --git a/cjs/shared/symbols.js b/cjs/shared/symbols.js index e493758b..39e29a0e 100644 --- a/cjs/shared/symbols.js +++ b/cjs/shared/symbols.js @@ -23,7 +23,7 @@ exports.DATASET = DATASET; const DOCTYPE = Symbol('doctype'); exports.DOCTYPE = DOCTYPE; -// ised in parser and Document to attach once a DOMParser +// used in parser and Document to attach once a DOMParser const DOM_PARSER = Symbol('DOMParser'); exports.DOM_PARSER = DOM_PARSER; diff --git a/esm/interface/document.js b/esm/interface/document.js index ac9e7903..dbc32390 100644 --- a/esm/interface/document.js +++ b/esm/interface/document.js @@ -29,6 +29,7 @@ import {EventTarget} from './event-target.js'; import {InputEvent} from './input-event.js'; import {ImageClass} from './image.js'; import {MutationObserverClass} from './mutation-observer.js'; +import {NamedNodeMap} from './named-node-map.js'; import {NodeList} from './node-list.js'; import {Range} from './range.js'; import {Text} from './text.js'; @@ -48,6 +49,7 @@ const globalExports = assign( Event, EventTarget, InputEvent, + NamedNodeMap, NodeList } ); diff --git a/esm/shared/symbols.js b/esm/shared/symbols.js index 5df1d7bd..23fee63f 100644 --- a/esm/shared/symbols.js +++ b/esm/shared/symbols.js @@ -16,7 +16,7 @@ export const DATASET = Symbol('dataset'); // used in Document to attach the DocType export const DOCTYPE = Symbol('doctype'); -// ised in parser and Document to attach once a DOMParser +// used in parser and Document to attach once a DOMParser export const DOM_PARSER = Symbol('DOMParser'); // used to reference an end node diff --git a/test/interface/named-node-map.js b/test/interface/named-node-map.js index b5f43406..c1a649d8 100644 --- a/test/interface/named-node-map.js +++ b/test/interface/named-node-map.js @@ -2,10 +2,10 @@ const assert = require('../assert.js').for('NamedNodeMap'); const {parseHTML} = global[Symbol.for('linkedom')]; -const {document} = parseHTML('
abc
'); +const {document, NamedNodeMap} = parseHTML('
abc
'); let node = document.documentElement.firstElementChild; - +assert(typeof NamedNodeMap !== 'undefined', true, 'NamedNodeMap undefined in global export'); assert(node.id, '', 'no id'); assert(!node.hasAttribute('id'), true, 'no id'); node.id = 'test'; diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index 08943f9b..f5025f02 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -180,9 +180,7 @@ export class Document extends NonElementParentNode implements globalThis.Documen setAttributeNS(_: any, name: any, value: any): void; setAttributeNodeNS(attr: any): import("../mixin/parent-node.js").NodeStruct; [CLASS_LIST]: import("../dom/token-list.js").DOMTokenList; - [DATASET]: import("../dom/string-map.js").DOMStringMap; /** - * @type {globalThis.Document['defaultView']} - */ + [DATASET]: import("../dom/string-map.js").DOMStringMap; [STYLE]: import("./css-style-declaration.js").CSSStyleDeclaration; readonly childNodes: NodeList; readonly children: NodeList; From 26a6d7f324421e6d6fbe874bf1f7b6fa01432d19 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 10:32:45 +0100 Subject: [PATCH 16/27] Event bubbling: unit tests, method name and function return - method renamed to "_getParent" - dispatchEvent return value now take EventTarget.dispatchEvent return into account as well as parent.dispatchEvent - "stopImmediatePropagation" now tested in unit tests. Todo: when ungap/event-target support "_stopImmediatePropagationFlag" more event listeners could be added to `buttonTarget` to ensure full behaviour is working. --- cjs/interface/event-target.js | 12 ++++++------ cjs/interface/node.js | 2 +- esm/interface/event-target.js | 12 ++++++------ esm/interface/node.js | 2 +- test/interface/event-target.js | 19 ++++++++++++++++++- types/interface/document.d.ts | 4 ++-- types/interface/event-target.d.ts | 4 ++-- types/interface/image.d.ts | 4 ++-- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 10f0f039..4a24bb60 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,20 +7,20 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 7a165b4c..2221d3c2 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,7 +142,7 @@ class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index b6e9454d..eb8ffa34 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,20 +6,20 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/esm/interface/node.js b/esm/interface/node.js index edbba106..dca4dce9 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,7 +141,7 @@ export class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 6ea4c615..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -30,7 +30,7 @@ eventTarget.removeEventListener('foo', basicHandler); eventTarget.dispatchEvent(new Event('foo')); assert(callCount, 1, 'basicHandler should not have been called after being removed'); -assert(eventTarget.getParent(), null, 'getParent should return null'); +assert(eventTarget._getParent(), null, 'getParent should return null'); // check propagation now @@ -75,3 +75,20 @@ containerTarget.addEventListener('click', basicHandler, { once: true }); callCount = 0; buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); + +// check stop immediate propagation +buttonTarget.addEventListener( + 'click', + (event) => { + event.stopImmediatePropagation(); + callCount++; + }, + { + once: true, + }, +); +containerTarget.addEventListener('click', basicHandler, { once: true }); + +callCount = 0; +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index f5025f02..d42014d8 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -219,10 +219,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 6cc0ee8b..8ba5b491 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,6 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - getParent(): any; - dispatchEvent(event: any): boolean; + _getParent(): any; + dispatchEvent(event: any): any; } diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 2396b272..46d2f045 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,10 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; From 86169fa1ffd9f0217be71498504f5ced33ab2562 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 11:09:58 +0100 Subject: [PATCH 17/27] Event bubbling: _getParent protected used "protected" here instead of "private", because Node extend EventTarget and needs to overwrite "_getParent" while still allowing "dispatchEvent" to access it. --- cjs/interface/event-target.js | 4 ++++ cjs/interface/node.js | 3 +++ esm/interface/event-target.js | 4 ++++ esm/interface/node.js | 3 +++ types/interface/event-target.d.ts | 5 ++++- 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 4a24bb60..5f220ce0 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,6 +7,10 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 2221d3c2..a5f09e59 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,6 +142,9 @@ class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index eb8ffa34..84a14d76 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,6 +6,10 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/esm/interface/node.js b/esm/interface/node.js index dca4dce9..60c03c95 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,6 +141,9 @@ export class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 8ba5b491..bec0d321 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,9 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - _getParent(): any; + /** + * @protected + */ + protected _getParent(): any; dispatchEvent(event: any): any; } From 96df7a8e5ac9437dd08a6167e08f36e0de0dff0a Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 13:05:05 +0100 Subject: [PATCH 18/27] Event bubbling: fix for Node 16.5 - Node 16.5 will throw if we try to dispatch the current event to the parent. So instead we need to create a new event with the same options. - replaced "_stopPropagationFlag" by "cancelBubble" to be consistent with Node implementation of Event - currently Node don't handle bubbling, but also don't properly set "_stopPropagationFlag" when calling "stopImmediatePropagation" creating a problem and inconsistency for us. => should we extend the Node Event object to ensure the proper behaviour? --- cjs/interface/event-target.js | 13 ++++++++++--- cjs/interface/event.js | 6 +++--- esm/interface/event-target.js | 13 ++++++++++--- esm/interface/event.js | 6 +++--- test/interface/event-target.js | 7 +++++++ types/interface/event.d.ts | 2 +- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 5f220ce0..cc94df79 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -19,10 +19,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 05080d17..fd23fd54 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,7 +20,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -39,11 +39,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index 84a14d76..c07a092e 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -18,10 +18,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/esm/interface/event.js b/esm/interface/event.js index c022c467..15b86751 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,7 +19,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -38,11 +38,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 5307b535..7b516132 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,10 +77,17 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation +// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them +const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); + if (isNode16) { + event.stopPropagation(); + } callCount++; }, { diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index d60cde00..a34d4baa 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -13,7 +13,7 @@ declare const GlobalEvent: { new (type: any, eventInitDict?: {}): { type: any; bubbles: boolean; - _stopPropagationFlag: boolean; + cancelBubble: boolean; _stopImmediatePropagationFlag: boolean; cancelable: boolean; eventPhase: number; From 89de5351b858d38561d06733967d644800892366 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 17:07:58 +0100 Subject: [PATCH 19/27] Event bubbling: extend Event to fix Node's stopImmediatePropagation --- cjs/interface/event.js | 16 +++++++++++-- esm/interface/event.js | 16 +++++++++++-- test/interface/event-target.js | 7 ------ types/interface/custom-event.d.ts | 1 + types/interface/event.d.ts | 38 ++++--------------------------- types/interface/input-event.d.ts | 3 ++- 6 files changed, 35 insertions(+), 46 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index fd23fd54..a8ac07f7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -43,11 +43,23 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -exports.Event = GlobalEvent; + + + 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(); + super.stopImmediatePropagation(); + } + } + + +exports.Event = DOMEvent; /* c8 ignore stop */ diff --git a/esm/interface/event.js b/esm/interface/event.js index 15b86751..577cd8ba 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -42,11 +42,23 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -export {GlobalEvent as 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(); + super.stopImmediatePropagation(); + } + } + + +export {DOMEvent as Event}; /* c8 ignore stop */ diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 7b516132..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,17 +77,10 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation -// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them -const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); - if (isNode16) { - event.stopPropagation(); - } callCount++; }, { diff --git a/types/interface/custom-event.d.ts b/types/interface/custom-event.d.ts index 1e947174..07ab20a1 100644 --- a/types/interface/custom-event.d.ts +++ b/types/interface/custom-event.d.ts @@ -5,5 +5,6 @@ export { GlobalCustomEvent as CustomEvent }; declare const GlobalCustomEvent: { new (type: any, eventInitDict?: {}): { detail: any; + stopImmediatePropagation(): void; }; }; diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index a34d4baa..57219d1e 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,34 +1,4 @@ -export { GlobalEvent as Event }; -/** - * @implements globalThis.Event - */ -declare const GlobalEvent: { - new (type: string, eventInitDict?: EventInit): Event; - prototype: Event; - readonly AT_TARGET: number; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - readonly NONE: number; -} | { - new (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; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - preventDefault(): void; - stopPropagation(): void; - stopImmediatePropagation(): void; - }; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; -}; +export { DOMEvent as Event }; +declare class DOMEvent { + stopImmediatePropagation(): void; +} diff --git a/types/interface/input-event.d.ts b/types/interface/input-event.d.ts index 40114611..6e94aee8 100644 --- a/types/interface/input-event.d.ts +++ b/types/interface/input-event.d.ts @@ -1,7 +1,7 @@ /** * @implements globalThis.InputEvent */ -export class InputEvent implements globalThis.InputEvent { +export class InputEvent extends Event implements globalThis.InputEvent { constructor(type: any, inputEventInit?: {}); inputType: any; data: any; @@ -9,3 +9,4 @@ export class InputEvent implements globalThis.InputEvent { isComposing: any; ranges: any; } +import { Event } from "./event.js"; From 5f6c890fadc3dc9a89bb123f8a05630f3f24ebbd Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 18:00:44 +0100 Subject: [PATCH 20/27] Event bubbling: implement comment + super method check --- cjs/interface/event.js | 8 ++++++-- esm/interface/event.js | 8 ++++++-- types/interface/event.d.ts | 5 ++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index a8ac07f7..ae77cef7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -49,13 +49,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/esm/interface/event.js b/esm/interface/event.js index 577cd8ba..d2675793 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -48,13 +48,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 57219d1e..ef0e3744 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,4 +1,7 @@ export { DOMEvent as Event }; -declare class DOMEvent { +/** + * @implements globalThis.Event + */ +declare class DOMEvent implements globalThis.Event { stopImmediatePropagation(): void; } From 4b1c1e61d996086c74381e1fff9f7b78d22f8bd7 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Sun, 18 Jul 2021 12:55:43 +0100 Subject: [PATCH 21/27] Extend EventTarget to support event bubbling + tests - add getParent method to Node returning parentNode - on dispatch trigger parent dispatchEvent - unit tests --- cjs/interface/event-target.js | 25 +++++++---------------- cjs/interface/event.js | 5 +++-- cjs/interface/node.js | 5 +---- esm/interface/event-target.js | 25 +++++++---------------- esm/interface/event.js | 5 +++-- esm/interface/node.js | 5 +---- test/interface/event-target.js | 19 +----------------- types/interface/document.d.ts | 4 ++-- types/interface/event-target.d.ts | 7 ++----- types/interface/event.d.ts | 33 ++++++++++++++++++++++++++++--- types/interface/image.d.ts | 4 ++-- 11 files changed, 59 insertions(+), 78 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index cc94df79..10f0f039 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,31 +7,20 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - - /** - * @protected - */ - _getParent() { + getParent() { return null; } dispatchEvent(event) { - const dispatched = super.dispatchEvent(event); + super.dispatchEvent(event); // 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)); - } + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); } - return dispatched; + return true; } } diff --git a/cjs/interface/event.js b/cjs/interface/event.js index ae77cef7..065111ee 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,7 +20,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this.cancelBubble = false; + this._stopPropagationFlag = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -39,10 +39,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this.cancelBubble = true; + this._stopPropagationFlag = true; } stopImmediatePropagation() { + this._stopPropagationFlag = true; this._stopImmediatePropagationFlag = true; } }; diff --git a/cjs/interface/node.js b/cjs/interface/node.js index a5f09e59..7a165b4c 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,10 +142,7 @@ class Node extends EventTarget { return false; } - /** - * @protected - */ - _getParent() { + getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index c07a092e..b6e9454d 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,31 +6,20 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - - /** - * @protected - */ - _getParent() { + getParent() { return null; } dispatchEvent(event) { - const dispatched = super.dispatchEvent(event); + super.dispatchEvent(event); // 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)); - } + if (event.bubbles && !event._stopPropagationFlag) { + const parent = this.getParent(); + if (parent && parent.dispatchEvent) + parent.dispatchEvent(event); } - return dispatched; + return true; } } diff --git a/esm/interface/event.js b/esm/interface/event.js index d2675793..8456f4a4 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,7 +19,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this.cancelBubble = false; + this._stopPropagationFlag = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -38,10 +38,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this.cancelBubble = true; + this._stopPropagationFlag = true; } stopImmediatePropagation() { + this._stopPropagationFlag = true; this._stopImmediatePropagationFlag = true; } }; diff --git a/esm/interface/node.js b/esm/interface/node.js index 60c03c95..edbba106 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,10 +141,7 @@ export class Node extends EventTarget { return false; } - /** - * @protected - */ - _getParent() { + getParent() { return this.parentNode; } diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 5307b535..6ea4c615 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -30,7 +30,7 @@ eventTarget.removeEventListener('foo', basicHandler); eventTarget.dispatchEvent(new Event('foo')); assert(callCount, 1, 'basicHandler should not have been called after being removed'); -assert(eventTarget._getParent(), null, 'getParent should return null'); +assert(eventTarget.getParent(), null, 'getParent should return null'); // check propagation now @@ -75,20 +75,3 @@ containerTarget.addEventListener('click', basicHandler, { once: true }); callCount = 0; buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); - -// check stop immediate propagation -buttonTarget.addEventListener( - 'click', - (event) => { - event.stopImmediatePropagation(); - callCount++; - }, - { - once: true, - }, -); -containerTarget.addEventListener('click', basicHandler, { once: true }); - -callCount = 0; -buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); -assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index d42014d8..f5025f02 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -219,10 +219,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - _getParent(): any; + getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index bec0d321..6cc0ee8b 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,9 +3,6 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - /** - * @protected - */ - protected _getParent(): any; - dispatchEvent(event: any): any; + getParent(): any; + dispatchEvent(event: any): boolean; } diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index ef0e3744..02c2f22c 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -2,6 +2,33 @@ export { DOMEvent as Event }; /** * @implements globalThis.Event */ -declare class DOMEvent implements globalThis.Event { - stopImmediatePropagation(): void; -} +declare const GlobalEvent: { + new (type: string, eventInitDict?: EventInit): Event; + prototype: Event; + readonly AT_TARGET: number; + readonly BUBBLING_PHASE: number; + readonly CAPTURING_PHASE: number; + readonly NONE: number; +} | { + new (type: any, eventInitDict?: {}): { + type: any; + bubbles: boolean; + _stopPropagationFlag: boolean; + _stopImmediatePropagationFlag: boolean; + cancelable: boolean; + eventPhase: number; + timeStamp: number; + defaultPrevented: boolean; + originalTarget: any; + returnValue: any; + srcElement: any; + target: any; + readonly BUBBLING_PHASE: number; + readonly CAPTURING_PHASE: number; + preventDefault(): void; + stopPropagation(): void; + stopImmediatePropagation(): void; + }; + readonly BUBBLING_PHASE: number; + readonly CAPTURING_PHASE: number; +}; diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 46d2f045..2396b272 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,10 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - _getParent(): any; + getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): any; + dispatchEvent(event: any): boolean; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; From bc734cfdbce60e0eeb2a60f348ff693a32623558 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 10:32:45 +0100 Subject: [PATCH 22/27] Event bubbling: unit tests, method name and function return - method renamed to "_getParent" - dispatchEvent return value now take EventTarget.dispatchEvent return into account as well as parent.dispatchEvent - "stopImmediatePropagation" now tested in unit tests. Todo: when ungap/event-target support "_stopImmediatePropagationFlag" more event listeners could be added to `buttonTarget` to ensure full behaviour is working. --- cjs/interface/event-target.js | 12 ++++++------ cjs/interface/node.js | 2 +- esm/interface/event-target.js | 12 ++++++------ esm/interface/node.js | 2 +- test/interface/event-target.js | 19 ++++++++++++++++++- types/interface/document.d.ts | 4 ++-- types/interface/event-target.d.ts | 4 ++-- types/interface/image.d.ts | 4 ++-- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 10f0f039..4a24bb60 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,20 +7,20 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 7a165b4c..2221d3c2 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,7 +142,7 @@ class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index b6e9454d..eb8ffa34 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,20 +6,20 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { - getParent() { + _getParent() { return null; } dispatchEvent(event) { - super.dispatchEvent(event); + const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (event.bubbles && !event._stopPropagationFlag) { - const parent = this.getParent(); + if (dispatched && event.bubbles && !event._stopPropagationFlag) { + const parent = this._getParent(); if (parent && parent.dispatchEvent) - parent.dispatchEvent(event); + return parent.dispatchEvent(event); } - return true; + return dispatched; } } diff --git a/esm/interface/node.js b/esm/interface/node.js index edbba106..dca4dce9 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,7 +141,7 @@ export class Node extends EventTarget { return false; } - getParent() { + _getParent() { return this.parentNode; } diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 6ea4c615..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -30,7 +30,7 @@ eventTarget.removeEventListener('foo', basicHandler); eventTarget.dispatchEvent(new Event('foo')); assert(callCount, 1, 'basicHandler should not have been called after being removed'); -assert(eventTarget.getParent(), null, 'getParent should return null'); +assert(eventTarget._getParent(), null, 'getParent should return null'); // check propagation now @@ -75,3 +75,20 @@ containerTarget.addEventListener('click', basicHandler, { once: true }); callCount = 0; buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); + +// check stop immediate propagation +buttonTarget.addEventListener( + 'click', + (event) => { + event.stopImmediatePropagation(); + callCount++; + }, + { + once: true, + }, +); +containerTarget.addEventListener('click', basicHandler, { once: true }); + +callCount = 0; +buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); +assert(callCount, 1, 'listener should be called once before stopping bubbling'); diff --git a/types/interface/document.d.ts b/types/interface/document.d.ts index f5025f02..d42014d8 100644 --- a/types/interface/document.d.ts +++ b/types/interface/document.d.ts @@ -219,10 +219,10 @@ export class Document extends NonElementParentNode implements globalThis.Documen isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 6cc0ee8b..8ba5b491 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,6 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - getParent(): any; - dispatchEvent(event: any): boolean; + _getParent(): any; + dispatchEvent(event: any): any; } diff --git a/types/interface/image.d.ts b/types/interface/image.d.ts index 2396b272..46d2f045 100644 --- a/types/interface/image.d.ts +++ b/types/interface/image.d.ts @@ -181,10 +181,10 @@ export function ImageClass(ownerDocument: any): { isSameNode(node: any): boolean; compareDocumentPosition(target: any): number; isEqualNode(node: any): boolean; - getParent(): any; + _getParent(): any; getRootNode(): any; [PREV]: any; - dispatchEvent(event: any): boolean; + dispatchEvent(event: any): any; }; readonly observedAttributes: any[]; readonly ELEMENT_NODE: number; From e941912d3fecfebb22a6c772e41c63e7ffc1aa01 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 11:09:58 +0100 Subject: [PATCH 23/27] Event bubbling: _getParent protected used "protected" here instead of "private", because Node extend EventTarget and needs to overwrite "_getParent" while still allowing "dispatchEvent" to access it. --- cjs/interface/event-target.js | 4 ++++ cjs/interface/node.js | 3 +++ esm/interface/event-target.js | 4 ++++ esm/interface/node.js | 3 +++ types/interface/event-target.d.ts | 5 ++++- 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 4a24bb60..5f220ce0 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -7,6 +7,10 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/cjs/interface/node.js b/cjs/interface/node.js index 2221d3c2..a5f09e59 100644 --- a/cjs/interface/node.js +++ b/cjs/interface/node.js @@ -142,6 +142,9 @@ class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index eb8ffa34..84a14d76 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -6,6 +6,10 @@ import EventTarget from '@ungap/event-target'; * @implements globalThis.EventTarget */ class DOMEventTarget extends EventTarget { + + /** + * @protected + */ _getParent() { return null; } diff --git a/esm/interface/node.js b/esm/interface/node.js index dca4dce9..60c03c95 100644 --- a/esm/interface/node.js +++ b/esm/interface/node.js @@ -141,6 +141,9 @@ export class Node extends EventTarget { return false; } + /** + * @protected + */ _getParent() { return this.parentNode; } diff --git a/types/interface/event-target.d.ts b/types/interface/event-target.d.ts index 8ba5b491..bec0d321 100644 --- a/types/interface/event-target.d.ts +++ b/types/interface/event-target.d.ts @@ -3,6 +3,9 @@ export { DOMEventTarget as EventTarget }; * @implements globalThis.EventTarget */ declare class DOMEventTarget implements globalThis.EventTarget { - _getParent(): any; + /** + * @protected + */ + protected _getParent(): any; dispatchEvent(event: any): any; } From 3b909c44b11a4aa3d76f1daaad0629e7e3dcf5ad Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 13:05:05 +0100 Subject: [PATCH 24/27] Event bubbling: fix for Node 16.5 - Node 16.5 will throw if we try to dispatch the current event to the parent. So instead we need to create a new event with the same options. - replaced "_stopPropagationFlag" by "cancelBubble" to be consistent with Node implementation of Event - currently Node don't handle bubbling, but also don't properly set "_stopPropagationFlag" when calling "stopImmediatePropagation" creating a problem and inconsistency for us. => should we extend the Node Event object to ensure the proper behaviour? --- cjs/interface/event-target.js | 13 ++++++++++--- cjs/interface/event.js | 6 +++--- esm/interface/event-target.js | 13 ++++++++++--- esm/interface/event.js | 6 +++--- test/interface/event-target.js | 7 +++++++ types/interface/event.d.ts | 2 +- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/cjs/interface/event-target.js b/cjs/interface/event-target.js index 5f220ce0..cc94df79 100644 --- a/cjs/interface/event-target.js +++ b/cjs/interface/event-target.js @@ -19,10 +19,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 065111ee..929b8b08 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -20,7 +20,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -39,11 +39,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/esm/interface/event-target.js b/esm/interface/event-target.js index 84a14d76..c07a092e 100644 --- a/esm/interface/event-target.js +++ b/esm/interface/event-target.js @@ -18,10 +18,17 @@ class DOMEventTarget extends EventTarget { const dispatched = super.dispatchEvent(event); // intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path - if (dispatched && event.bubbles && !event._stopPropagationFlag) { + if (dispatched && event.bubbles && !event.cancelBubble) { const parent = this._getParent(); - if (parent && parent.dispatchEvent) - return parent.dispatchEvent(event); + 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)); + } } return dispatched; } diff --git a/esm/interface/event.js b/esm/interface/event.js index 8456f4a4..0de75939 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -19,7 +19,7 @@ const GlobalEvent = typeof Event === 'function' ? constructor(type, eventInitDict = {}) { this.type = type; this.bubbles = !!eventInitDict.bubbles; - this._stopPropagationFlag = false; + this.cancelBubble = false; this._stopImmediatePropagationFlag = false; this.cancelable = !!eventInitDict.cancelable; this.eventPhase = this.BUBBLING_PHASE; @@ -38,11 +38,11 @@ const GlobalEvent = typeof Event === 'function' ? // TODO: what do these do in native NodeJS Event ? stopPropagation() { - this._stopPropagationFlag = true; + this.cancelBubble = true; } stopImmediatePropagation() { - this._stopPropagationFlag = true; + this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 5307b535..7b516132 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,10 +77,17 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation +// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them +const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); + if (isNode16) { + event.stopPropagation(); + } callCount++; }, { diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 02c2f22c..41059e34 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -13,7 +13,7 @@ declare const GlobalEvent: { new (type: any, eventInitDict?: {}): { type: any; bubbles: boolean; - _stopPropagationFlag: boolean; + cancelBubble: boolean; _stopImmediatePropagationFlag: boolean; cancelable: boolean; eventPhase: number; From feafc8caa4bec033f23354a8c7f07dc899abee07 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 17:07:58 +0100 Subject: [PATCH 25/27] Event bubbling: extend Event to fix Node's stopImmediatePropagation --- cjs/interface/event.js | 9 ++------- esm/interface/event.js | 9 ++------- test/interface/event-target.js | 7 ------- types/interface/event.d.ts | 36 +++------------------------------- 4 files changed, 7 insertions(+), 54 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index 929b8b08..a8ac07f7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -43,24 +43,19 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -/** - * @implements globalThis.Event - */ -class DOMEvent extends GlobalEvent { + 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(); + super.stopImmediatePropagation(); } } diff --git a/esm/interface/event.js b/esm/interface/event.js index 0de75939..577cd8ba 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -42,24 +42,19 @@ const GlobalEvent = typeof Event === 'function' ? } stopImmediatePropagation() { - this.stopPropagation(); this._stopImmediatePropagationFlag = true; } }; -/** - * @implements globalThis.Event - */ -class DOMEvent extends GlobalEvent { + 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(); + super.stopImmediatePropagation(); } } diff --git a/test/interface/event-target.js b/test/interface/event-target.js index 7b516132..5307b535 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,17 +77,10 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation -// specs mention for stopImmediatePropagation "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 - will check if that's a bug or expected for them -const isNode16 = Event._stopImmediatePropagationFlag !== false; buttonTarget.addEventListener( 'click', (event) => { event.stopImmediatePropagation(); - if (isNode16) { - event.stopPropagation(); - } callCount++; }, { diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 41059e34..57219d1e 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,34 +1,4 @@ export { DOMEvent as Event }; -/** - * @implements globalThis.Event - */ -declare const GlobalEvent: { - new (type: string, eventInitDict?: EventInit): Event; - prototype: Event; - readonly AT_TARGET: number; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - readonly NONE: number; -} | { - new (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; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - preventDefault(): void; - stopPropagation(): void; - stopImmediatePropagation(): void; - }; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; -}; +declare class DOMEvent { + stopImmediatePropagation(): void; +} From 05b5f21c93856a99474718018f3474a6887bd440 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Mon, 19 Jul 2021 18:00:44 +0100 Subject: [PATCH 26/27] Event bubbling: implement comment + super method check --- cjs/interface/event.js | 8 ++++++-- esm/interface/event.js | 8 ++++++-- types/interface/event.d.ts | 5 ++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cjs/interface/event.js b/cjs/interface/event.js index a8ac07f7..ae77cef7 100644 --- a/cjs/interface/event.js +++ b/cjs/interface/event.js @@ -49,13 +49,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/esm/interface/event.js b/esm/interface/event.js index 577cd8ba..d2675793 100644 --- a/esm/interface/event.js +++ b/esm/interface/event.js @@ -48,13 +48,17 @@ const GlobalEvent = typeof Event === 'function' ? - class DOMEvent extends GlobalEvent { +/** + * @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(); - super.stopImmediatePropagation(); + if (typeof super.stopImmediatePropagation === 'function') + super.stopImmediatePropagation(); } } diff --git a/types/interface/event.d.ts b/types/interface/event.d.ts index 57219d1e..ef0e3744 100644 --- a/types/interface/event.d.ts +++ b/types/interface/event.d.ts @@ -1,4 +1,7 @@ export { DOMEvent as Event }; -declare class DOMEvent { +/** + * @implements globalThis.Event + */ +declare class DOMEvent implements globalThis.Event { stopImmediatePropagation(): void; } From 45d58f89f3e3a480ecdb32140ba1343d4cf9eab3 Mon Sep 17 00:00:00 2001 From: Mickael Meausoone Date: Tue, 20 Jul 2021 21:05:29 +0100 Subject: [PATCH 27/27] Event bubbling: package update + improved test --- package.json | 2 +- test/interface/event-target.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cca71630..c69ebae0 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "./worker": "./worker.js" }, "dependencies": { - "@ungap/event-target": "^0.2.2", + "@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 5307b535..4e0d67a3 100644 --- a/test/interface/event-target.js +++ b/test/interface/event-target.js @@ -77,6 +77,7 @@ buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); assert(callCount, 1, 'listener should be called once before stopping bubbling'); // check stop immediate propagation +buttonTarget.addEventListener('click', () => callCount++, { once: true }); buttonTarget.addEventListener( 'click', (event) => { @@ -87,8 +88,9 @@ buttonTarget.addEventListener( once: true, }, ); +buttonTarget.addEventListener('click', () => callCount++, { once: true }); containerTarget.addEventListener('click', basicHandler, { once: true }); callCount = 0; buttonTarget.dispatchEvent(new Event('click', { bubbles: true })); -assert(callCount, 1, 'listener should be called once before stopping bubbling'); +assert(callCount, 2, '2 listeners should be called before stopping');