Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend EventTarget to support event bubbling #84

Merged
merged 29 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d8c5fa3
Extend EventTarget to support event bubbling + tests
Jul 18, 2021
9967ace
Event bubbling: unit tests, method name and function return
Jul 19, 2021
fe0b846
Event bubbling: _getParent protected
Jul 19, 2021
56a677b
Event bubbling: fix for Node 16.5
Jul 19, 2021
f546696
Event bubbling: extend Event to fix Node's stopImmediatePropagation
Jul 19, 2021
8e4f67c
Event bubbling: implement comment + super method check
Jul 19, 2021
57ae688
Extend EventTarget to support event bubbling + tests
Jul 18, 2021
64a74d4
Event bubbling: unit tests, method name and function return
Jul 19, 2021
023cc31
Event bubbling: _getParent protected
Jul 19, 2021
3754826
Event bubbling: fix for Node 16.5
Jul 19, 2021
88d4cd8
Event bubbling: extend Event to fix Node's stopImmediatePropagation
Jul 19, 2021
26545dc
Event bubbling: implement comment + super method check
Jul 19, 2021
495587a
Event bubbling: package update + improved test
Jul 20, 2021
3d35561
Merge branch 'event-bubbling' of https://github.com/mikemadest/linked…
Jul 20, 2021
88824dc
Extend EventTarget to support event bubbling + tests
Jul 18, 2021
67024da
Adding NamedNodeMap to global export (#85)
mikemadest Jul 19, 2021
26a6d7f
Event bubbling: unit tests, method name and function return
Jul 19, 2021
86169fa
Event bubbling: _getParent protected
Jul 19, 2021
96df7a8
Event bubbling: fix for Node 16.5
Jul 19, 2021
89de535
Event bubbling: extend Event to fix Node's stopImmediatePropagation
Jul 19, 2021
5f6c890
Event bubbling: implement comment + super method check
Jul 19, 2021
4b1c1e6
Extend EventTarget to support event bubbling + tests
Jul 18, 2021
bc734cf
Event bubbling: unit tests, method name and function return
Jul 19, 2021
e941912
Event bubbling: _getParent protected
Jul 19, 2021
3b909c4
Event bubbling: fix for Node 16.5
Jul 19, 2021
feafc8c
Event bubbling: extend Event to fix Node's stopImmediatePropagation
Jul 19, 2021
05b5f21
Event bubbling: implement comment + super method check
Jul 19, 2021
45d58f8
Event bubbling: package update + improved test
Jul 20, 2021
e7253bc
Merge branch 'event-bubbling' of https://github.com/mikemadest/linked…
Jul 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion cjs/interface/event-target.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,33 @@ const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /*
/**
* @implements globalThis.EventTarget
*/
exports.EventTarget = EventTarget;
class DOMEventTarget extends EventTarget {

/**
* @protected
*/
_getParent() {
return null;
}

dispatchEvent(event) {
const dispatched = 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));
WebReflection marked this conversation as resolved.
Show resolved Hide resolved
}
}
return dispatched;
}
}

exports.EventTarget = DOMEventTarget;
30 changes: 27 additions & 3 deletions cjs/interface/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const GlobalEvent = typeof Event === 'function' ?
constructor(type, eventInitDict = {}) {
this.type = type;
this.bubbles = !!eventInitDict.bubbles;
this.cancelBubble = false;
this._stopImmediatePropagationFlag = false;
WebReflection marked this conversation as resolved.
Show resolved Hide resolved
this.cancelable = !!eventInitDict.cancelable;
this.eventPhase = this.BUBBLING_PHASE;
this.timeStamp = Date.now();
Expand All @@ -36,10 +38,32 @@ const GlobalEvent = typeof Event === 'function' ?
preventDefault() { this.defaultPrevented = true; }

// TODO: what do these do in native NodeJS Event ?
stopPropagation() {}
stopImmediatePropagation() {}
stopPropagation() {
this.cancelBubble = true;
}

stopImmediatePropagation() {
this._stopImmediatePropagationFlag = true;
}
WebReflection marked this conversation as resolved.
Show resolved Hide resolved
};

exports.Event = 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();
if (typeof super.stopImmediatePropagation === 'function')
super.stopImmediatePropagation();
}
}
WebReflection marked this conversation as resolved.
Show resolved Hide resolved


exports.Event = DOMEvent;
WebReflection marked this conversation as resolved.
Show resolved Hide resolved

/* c8 ignore stop */
7 changes: 7 additions & 0 deletions cjs/interface/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ class Node extends EventTarget {
return false;
}

/**
* @protected
*/
_getParent() {
return this.parentNode;
}

getRootNode() {
let root = this;
while (root.parentNode)
Expand Down
31 changes: 30 additions & 1 deletion esm/interface/event-target.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,33 @@ import EventTarget from '@ungap/event-target';
/**
* @implements globalThis.EventTarget
*/
export {EventTarget};
class DOMEventTarget extends EventTarget {

/**
* @protected
*/
_getParent() {
return null;
}

dispatchEvent(event) {
const dispatched = 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));
}
}
return dispatched;
}
}

export { DOMEventTarget as EventTarget };
30 changes: 27 additions & 3 deletions esm/interface/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const GlobalEvent = typeof Event === 'function' ?
constructor(type, eventInitDict = {}) {
this.type = type;
this.bubbles = !!eventInitDict.bubbles;
this.cancelBubble = false;
this._stopImmediatePropagationFlag = false;
this.cancelable = !!eventInitDict.cancelable;
this.eventPhase = this.BUBBLING_PHASE;
this.timeStamp = Date.now();
Expand All @@ -35,10 +37,32 @@ const GlobalEvent = typeof Event === 'function' ?
preventDefault() { this.defaultPrevented = true; }

// TODO: what do these do in native NodeJS Event ?
stopPropagation() {}
stopImmediatePropagation() {}
stopPropagation() {
this.cancelBubble = true;
}

stopImmediatePropagation() {
this._stopImmediatePropagationFlag = true;
}
};

export {GlobalEvent as Event};


WebReflection marked this conversation as resolved.
Show resolved Hide resolved
/**
* @implements globalThis.Event
*/
class DOMEvent extends GlobalEvent {
// specs: "set this’s stop propagation flag and this’s stop immediate propagation flag"
// https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation
// but Node don't do that so for now we extend it
stopImmediatePropagation() {
super.stopPropagation();
if (typeof super.stopImmediatePropagation === 'function')
super.stopImmediatePropagation();
}
}


export {DOMEvent as Event};

/* c8 ignore stop */
7 changes: 7 additions & 0 deletions esm/interface/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ export class Node extends EventTarget {
return false;
}

/**
* @protected
*/
_getParent() {
WebReflection marked this conversation as resolved.
Show resolved Hide resolved
return this.parentNode;
}

getRootNode() {
let root = this;
while (root.parentNode)
Expand Down
94 changes: 94 additions & 0 deletions test/interface/event-target.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const assert = require('../assert.js').for('EventTarget');

const { parseHTML } = global[Symbol.for('linkedom')];

const { Event, document, EventTarget } = parseHTML(
'<html><div id="container"><button id="buttonTarget" type="button">Click me!</button></div></html>',
);

// 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');

// check stop immediate propagation
buttonTarget.addEventListener(
'click',
(event) => {
event.stopImmediatePropagation();
callCount++;
},
{
once: true,
},
);
containerTarget.addEventListener('click', basicHandler, { once: true });
WebReflection marked this conversation as resolved.
Show resolved Hide resolved

callCount = 0;
buttonTarget.dispatchEvent(new Event('click', { bubbles: true }));
assert(callCount, 1, 'listener should be called once before stopping bubbling');
1 change: 1 addition & 0 deletions types/interface/custom-event.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export { GlobalCustomEvent as CustomEvent };
declare const GlobalCustomEvent: {
new (type: any, eventInitDict?: {}): {
detail: any;
stopImmediatePropagation(): void;
};
};
5 changes: 4 additions & 1 deletion types/interface/document.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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): any;
};
readonly observedAttributes: any[];
readonly ELEMENT_NODE: number;
Expand All @@ -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";
Expand All @@ -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";
12 changes: 11 additions & 1 deletion types/interface/event-target.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
export { EventTarget };
export { DOMEventTarget as EventTarget };
/**
* @implements globalThis.EventTarget
*/
declare class DOMEventTarget implements globalThis.EventTarget {
/**
* @protected
*/
protected _getParent(): any;
dispatchEvent(event: any): any;
}
33 changes: 4 additions & 29 deletions types/interface/event.d.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,7 @@
export { GlobalEvent as Event };
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;
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 implements globalThis.Event {
stopImmediatePropagation(): void;
}
2 changes: 2 additions & 0 deletions types/interface/image.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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): any;
};
readonly observedAttributes: any[];
readonly ELEMENT_NODE: number;
Expand Down
3 changes: 2 additions & 1 deletion types/interface/input-event.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/**
* @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;
dataTransfer: any;
isComposing: any;
ranges: any;
}
import { Event } from "./event.js";
Loading