Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions cjs/interface/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ class Document extends NonElementParentNode {

get isConnected() { return true; }

/**
* @protected
*/
_getParent() {
return this[EVENT_TARGET];
Comment thread
WebReflection marked this conversation as resolved.
}

createAttribute(name) { return new Attr(this, name); }
createComment(textContent) { return new Comment(this, textContent); }
createDocumentFragment() { return new DocumentFragment(this); }
Expand Down
83 changes: 68 additions & 15 deletions cjs/interface/event-target.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
'use strict';
// https://dom.spec.whatwg.org/#interface-eventtarget

const EventTarget = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@ungap/event-target'));
const wm = new WeakMap();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of extending Node or ungap EventTarget all features are now here.
In the end I think this is better as it allow for easier changes (one repo) and doesn't let the code exposed to eventual Node changes that doesn't support needed features anyway.


function dispatch({ target, listener}) {
if (typeof listener === 'function') {
listener.call(target, this);
} else {
listener.handleEvent(this);
}
return this._stopImmediatePropagationFlag;
}

function invokeListeners({currentTarget, target}) {
const map = wm.get(currentTarget);
if (map && map.has(this.type)) {
const listeners = map.get(this.type);
if (currentTarget === target) {
this.eventPhase = this.AT_TARGET;
} else {
this.eventPhase = this.BUBBLING_PHASE;
}

this.currentTarget = currentTarget;
this.target = target;
for (const [listener, options] of listeners) {
if (options && options.once)
listeners.delete(listener);
if (dispatch.call(this, {target: this, listener}))
break;
}
delete this.currentTarget;
delete this.target;
return this.cancelBubble;
}
}


/**
* @implements globalThis.EventTarget
*/
class DOMEventTarget extends EventTarget {
class DOMEventTarget {

constructor() {
wm.set(this, new Map);
}

/**
* @protected
Expand All @@ -15,24 +53,39 @@ class DOMEventTarget extends EventTarget {
return null;
}

addEventListener(type, listener, options) {
const map = wm.get(this);
if (!map.has(type))
map.set(type, new Map);
map.get(type).set(listener, options);
}

removeEventListener(type, listener) {
const map = wm.get(this);
if (map.has(type)) {
const listeners = map.get(type);
if (listeners.delete(listener) && !listeners.size) {
map.delete(type);
}
}
}

dispatchEvent(event) {
const dispatched = super.dispatchEvent(event);
let node = this;
event.eventPhase = event.CAPTURING_PHASE;

// intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path
if (dispatched && event.bubbles && !event.cancelBubble) {
const parent = this._getParent();
if (parent && parent.dispatchEvent) {
const options = {
bubbles: event.bubbles,
cancelable: event.cancelable,
composed: event.composed,
};
// in Node 16.5 the same event can't be used for another dispatch
return parent.dispatchEvent(new event.constructor(event.type, options));
}
while (node) {
if (node.dispatchEvent)
event._path.push({currentTarget: node, target: this});
node = event.bubbles && node._getParent && node._getParent();
}
return dispatched;
event._path.some(invokeListeners, event);
event._path = [];
event.eventPhase = event.NONE;
return !event.defaultPrevented;
Copy link
Copy Markdown
Contributor Author

@mikemadest mikemadest Aug 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initially it was just returning true, but spec says

Return false if event’s canceled flag is set, and true otherwise.

https://dom.spec.whatwg.org/#concept-event-dispatch

}

}

exports.EventTarget = DOMEventTarget;
40 changes: 16 additions & 24 deletions cjs/interface/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,58 @@
/* c8 ignore start */

// Node 15 has Event but 14 and 12 don't

const BUBBLING_PHASE = 3;
const AT_TARGET = 2;
const CAPTURING_PHASE = 1;
const NONE = 0;

/**
* @implements globalThis.Event
*/
const GlobalEvent = typeof Event === 'function' ?
Event :
class Event {
class GlobalEvent {
static get BUBBLING_PHASE() { return BUBBLING_PHASE; }
static get AT_TARGET() { return AT_TARGET; }
static get CAPTURING_PHASE() { return CAPTURING_PHASE; }
static get NONE() { return NONE; }

constructor(type, eventInitDict = {}) {
this.type = type;
this.bubbles = !!eventInitDict.bubbles;
this.cancelBubble = false;
this._stopImmediatePropagationFlag = false;
this.cancelable = !!eventInitDict.cancelable;
this.eventPhase = this.BUBBLING_PHASE;
this.eventPhase = this.NONE;
this.timeStamp = Date.now();
this.defaultPrevented = false;
this.originalTarget = null;
this.returnValue = null;
this.srcElement = null;
this.target = null;
this._path = [];
}

get BUBBLING_PHASE() { return BUBBLING_PHASE; }
get AT_TARGET() { return AT_TARGET; }
get CAPTURING_PHASE() { return CAPTURING_PHASE; }
get NONE() { return NONE; }

preventDefault() { this.defaultPrevented = true; }

// TODO: what do these do in native NodeJS Event ?
// simplified implementation, should be https://dom.spec.whatwg.org/#dom-event-composedpath
composedPath() {
return this._path;
}

stopPropagation() {
this.cancelBubble = true;
}

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



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


exports.Event = DOMEvent;
exports.Event = GlobalEvent;

/* c8 ignore stop */
7 changes: 7 additions & 0 deletions esm/interface/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ export class Document extends NonElementParentNode {

get isConnected() { return true; }

/**
* @protected
*/
_getParent() {
return this[EVENT_TARGET];
}

createAttribute(name) { return new Attr(this, name); }
createComment(textContent) { return new Comment(this, textContent); }
createDocumentFragment() { return new DocumentFragment(this); }
Expand Down
83 changes: 68 additions & 15 deletions esm/interface/event-target.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
// https://dom.spec.whatwg.org/#interface-eventtarget

import EventTarget from '@ungap/event-target';
const wm = new WeakMap();

function dispatch({ target, listener}) {
if (typeof listener === 'function') {
listener.call(target, this);
} else {
listener.handleEvent(this);
}
return this._stopImmediatePropagationFlag;
}

function invokeListeners({currentTarget, target}) {
const map = wm.get(currentTarget);
if (map && map.has(this.type)) {
const listeners = map.get(this.type);
if (currentTarget === target) {
this.eventPhase = this.AT_TARGET;
} else {
this.eventPhase = this.BUBBLING_PHASE;
}

this.currentTarget = currentTarget;
this.target = target;
for (const [listener, options] of listeners) {
if (options && options.once)
listeners.delete(listener);
if (dispatch.call(this, {target: this, listener}))
break;
}
delete this.currentTarget;
delete this.target;
return this.cancelBubble;
}
}


/**
* @implements globalThis.EventTarget
*/
class DOMEventTarget extends EventTarget {
class DOMEventTarget {

constructor() {
wm.set(this, new Map);
}

/**
* @protected
Expand All @@ -14,24 +52,39 @@ class DOMEventTarget extends EventTarget {
return null;
}

addEventListener(type, listener, options) {
const map = wm.get(this);
if (!map.has(type))
map.set(type, new Map);
map.get(type).set(listener, options);
}

removeEventListener(type, listener) {
const map = wm.get(this);
if (map.has(type)) {
const listeners = map.get(type);
if (listeners.delete(listener) && !listeners.size) {
map.delete(type);
}
}
}

dispatchEvent(event) {
const dispatched = super.dispatchEvent(event);
let node = this;
event.eventPhase = event.CAPTURING_PHASE;

// intentionally simplified, specs imply way more code: https://dom.spec.whatwg.org/#event-path
if (dispatched && event.bubbles && !event.cancelBubble) {
const parent = this._getParent();
if (parent && parent.dispatchEvent) {
const options = {
bubbles: event.bubbles,
cancelable: event.cancelable,
composed: event.composed,
};
// in Node 16.5 the same event can't be used for another dispatch
return parent.dispatchEvent(new event.constructor(event.type, options));
}
while (node) {
if (node.dispatchEvent)
event._path.push({currentTarget: node, target: this});
node = event.bubbles && node._getParent && node._getParent();
}
return dispatched;
event._path.some(invokeListeners, event);
event._path = [];
event.eventPhase = event.NONE;
return !event.defaultPrevented;
}

}

export { DOMEventTarget as EventTarget };
Loading