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

Add Event and minimal EventTarget #76

Merged
merged 4 commits into from
Apr 26, 2024
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
340 changes: 340 additions & 0 deletions src/builtins/internal_js_modules/node/event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
/**
* @see: https://dom.spec.whatwg.org/#event
*/
class Event {
type;
target;
srcElement; // legacy
currentTarget;
static NONE = 0;
static CAPTURING_PHASE = 1;
static AT_TARGET = 2;
static BUBBLING_PHASE = 3;
eventPhase;
bubbles;
cancelable;
isTrusted; // Legacy Unforgeable
timeStamp;
path = [];
/**
* flags
*/
stopPropagationFlag = false;
stopImmediatePropagationFlag = false;
canceledFlag = false;
inPassiveListenerFlag = false;
composedFlag = false;
initializedFlag = false;
dispatchFlag = false;
constructor(type, eventInitDict) {
this.initializedFlag = true;
this.isTrusted = false;
this.target = null;
this.type = type;
this.bubbles = !!eventInitDict?.bubbles;
this.cancelable = !!eventInitDict?.cancelable;
this.currentTarget = null;
this.eventPhase = Event.NONE;
this.composedFlag = !!eventInitDict?.composed;
this.timeStamp = 0;
this.srcElement = null;
}
composedPath() {
/**
* 1. Let composedPath be an empty list.
*/
let composedPath = [];
/**
* 2. Let path be this’s path.
* 3. If path is empty, then return composedPath.
*/
const path = this.path;
if (path.length === 0) {
return [];
}
if (!this.currentTarget) {
throw new Error('Error in composedPath: currentTarget is not found.');
}
/**
* 4. Let currentTarget be this’s currentTarget attribute value.
* 5. Append currentTarget to composedPath..
* 6. Let currentTargetIndex be 0.
* 7. Let currentTargetHiddenSubtreeLevel be 0.
*/ 1;
composedPath.push({
item: this.currentTarget,
itemInShadowTree: false,
relatedTarget: null,
rootOfClosedTree: false,
slotInClosedTree: false,
target: null,
touchTargetList: [],
});
let currentTargetIndex = 0;
let currentTargetHiddenSubtreeLevel = 0;
/**
* 7. Let index be path’s size − 1.
* 8. While index is greater than or equal to 0:
* 9. If path[index]'s root-of-closed-tree is true, then increase currentTargetHiddenSubtreeLevel by 1.
* 9-1. If path[index]'s invocation target is currentTarget, then set currentTargetIndex to index and break.
* 9-2. If path[index]'s slot-in-closed-tree is true, then decrease currentTargetHiddenSubtreeLevel by 1.
* 9-3. Decrease index by 1.
*/
for (let i = path.length - 1; i >= 0; i--) {
const { item, rootOfClosedTree, slotInClosedTree } = path[i];
if (rootOfClosedTree)
currentTargetHiddenSubtreeLevel++;
if (item === this.currentTarget) {
currentTargetIndex = i;
break;
}
if (slotInClosedTree)
currentTargetHiddenSubtreeLevel--;
}
/**
* 10. Let currentHiddenLevel and maxHiddenLevel be currentTargetHiddenSubtreeLevel.
*/
let currentHiddenLevel = currentTargetHiddenSubtreeLevel;
let maxHiddenLevel = currentTargetHiddenSubtreeLevel;
/**
* 11. Set index to currentTargetIndex − 1.
* 12. While index is greater than or equal to 0:
* 12-1. If path[index]'s root-of-closed-tree is true, then increase currentHiddenLevel by 1.
* 12-2. If currentHiddenLevel is less than or equal to maxHiddenLevel, then prepend path[index]'s invocation target to composedPath.
* 12-3. If path[index]'s slot-in-closed-tree is true then:
* 12-3-1. Decrease currentHiddenLevel by 1.
* 12-3-2. If currentHiddenLevel is less than maxHiddenLevel, then set maxHiddenLevel to currentHiddenLevel.
* 12-4. Decrease index by 1.
*
*/
for (let i = currentTargetIndex - 1; i >= 0; i--) {
const { item, rootOfClosedTree, slotInClosedTree } = path[i];
if (rootOfClosedTree)
currentHiddenLevel++;
if (currentHiddenLevel <= maxHiddenLevel) {
composedPath.unshift({
item,
itemInShadowTree: false,
relatedTarget: null,
rootOfClosedTree: false,
slotInClosedTree: false,
target: null,
touchTargetList: [],
});
}
if (slotInClosedTree) {
currentHiddenLevel--;
if (currentHiddenLevel < maxHiddenLevel) {
maxHiddenLevel = currentHiddenLevel;
}
}
}
/**
* 13. Set currentHiddenLevel and maxHiddenLevel to currentTargetHiddenSubtreeLevel.
*/
currentHiddenLevel = currentTargetHiddenSubtreeLevel;
maxHiddenLevel = currentTargetHiddenSubtreeLevel;
/**
* 14. Set index to currentTargetIndex + 1.
* 15. While index is less than path’s size:
* 15-1. If path[index]'s slot-in-closed-tree is true, then increase currentHiddenLevel by 1.
* 15-2. If currentHiddenLevel is less than or equal to maxHiddenLevel, then append path[index]'s invocation target to composedPath.
* 15-3. If path[index]'s root-of-closed-tree is true, then:
* 15-3-1. Decrease currentHiddenLevel by 1.
* 15-3-2. If currentHiddenLevel is less than maxHiddenLevel, then set maxHiddenLevel to currentHiddenLevel.
* 15-4. Increase index by 1.
*/
for (let i = currentTargetIndex + 1; i < path.length; i++) {
const { item, rootOfClosedTree, slotInClosedTree } = path[i];
if (slotInClosedTree)
currentHiddenLevel++;
if (currentHiddenLevel <= maxHiddenLevel) {
composedPath.push({
item,
itemInShadowTree: false,
relatedTarget: null,
rootOfClosedTree: false,
slotInClosedTree: false,
target: null,
touchTargetList: [],
});
}
if (rootOfClosedTree) {
currentHiddenLevel--;
if (currentHiddenLevel < maxHiddenLevel) {
maxHiddenLevel = currentHiddenLevel;
}
}
}
/**
* 16. Return composedPath.
*/
return composedPath.map((i) => i.item);
}
/**
* The stopPropagation() method steps are to set this’s stop propagation flag.
*/
stopPropagation() {
this.stopPropagationFlag = true;
}
/**
* The cancelBubble getter steps are to return true if this’s stop propagation flag is set; otherwise false.
*/
get cancelBubble() {
return !!this.stopPropagationFlag;
}
/**
* The cancelBubble setter steps are to set this’s stop propagation flag if the given value is true; otherwise do nothing.
*/
set cancelBubble(value) {
if (value)
this.stopPropagationFlag = true;
}
/**
* The stopImmediatePropagation() method steps are to set this’s stop propagation flag and this’s stop immediate propagation flag.
*/
stopImmediatePropagation() {
this.stopImmediatePropagationFlag = true;
this.stopPropagationFlag = true;
}
/**
* The returnValue getter steps are to return false if this’s canceled flag is set; otherwise true.
*/
get returnValue() {
return !!!this.canceledFlag;
}
/**
* The returnValue setter steps are to set the canceled flag with this if the given value is false; otherwise do nothing.
*/
set returnValue(value) {
if (!value)
this.canceledFlag = true;
}
/**
* The preventDefault() method steps are to set the canceled flag with this.
*/
preventDefault() {
this.canceledFlag = true;
}
get defaultPrevented() {
return !!this.canceledFlag;
}
get composed() {
return !!this.composedFlag;
}
initEvent(type, bubbles, cancelable) { }
// other setter/getter
get dispatched() {
return this.dispatchFlag;
}
set dispatched(value) {
this.dispatchFlag = value;
}
get initialized() {
return this.initializedFlag;
}
set initialized(value) {
this.initializedFlag = value;
}
}
/**
* @see: https://dom.spec.whatwg.org/#eventtarget
*/
class EventTarget {
eventTargetData = {
listeners: Object.create(null),
};
constructor() {
// The new EventTarget() constructor steps are to do nothing.
}
addEventListener(type, callback, options) {
if (callback === null)
return;
const self = this;
// flatten options
if (options) {
options = this.flattenOptions(options);
}
else {
options = { capture: false, once: false, passive: undefined, signal: undefined };
}
const { listeners } = self.eventTargetData;
// init listeners[type]
if (!listeners[type]) {
listeners[type] = [];
}
const listenerList = listeners[type];
// check if the same callback is already added then skip
for (let i = 0; i < listenerList.length; ++i) {
const listener = listenerList[i];
const matchWithBooleanOptions = typeof listener.options === 'boolean' && listener.options === options.capture;
const matchWithObjectOptions = typeof listener.options === 'object' && listener.options.capture === options.capture;
const matchCallback = listener.callback === callback;
if ((matchWithBooleanOptions || matchWithObjectOptions) && matchCallback)
return;
}
const signal = options.signal;
// If an AbortSignal is passed for options’s signal, then the event listener will be removed when signal is aborted.
if (signal) {
if (signal.aborted) {
return;
}
else {
signal.addEventListener('abort', () => {
self.removeEventListener(type, callback, options);
});
}
}
listenerList.push({ callback, options });
}
removeEventListener(type, callback, options) {
const self = this;
const { listeners } = self.eventTargetData;
const notExistListeners = !listeners[type] || listeners[type].length === 0;
if (notExistListeners)
return;
// flatten options
if (typeof options === 'boolean' || typeof options === 'undefined') {
options = {
capture: Boolean(options),
};
}
// remove match listeners
for (let i = 0; i < listeners[type].length; ++i) {
const listener = listeners[type][i];
const matchWithBooleanOptions = typeof listener.options === 'boolean' && listener.options === options.capture;
const matchWithObjectOptions = typeof listener.options === 'object' && listener.options.capture === options.capture;
const matchCallback = listener.callback === callback;
if ((matchWithBooleanOptions || matchWithObjectOptions) && matchCallback) {
listeners[type].splice(i, 1);
break;
}
}
}
dispatchEvent(event) {
const self = this;
// If event’s dispatch flag is set, or if its initialized flag is not set, then throw an "InvalidStateError" DOMException.
if (event.dispatched || !event.initialized) {
throw new DOMException('Invalid event state.', 'InvalidStateError');
}
if (event.eventPhase !== Event.NONE) {
throw new DOMException('Invalid event state.', 'InvalidStateError');
}
return dispatch(self, event);
}
flattenOptions(options) {
if (typeof options === 'boolean') {
return { capture: options, once: false, passive: false };
}
return options;
}
}
// TODO: implement according to https://dom.spec.whatwg.org/#concept-event-dispatch
function dispatch(eventTarget, event) {
// Tentative implementation that just calls the callback
eventTarget.eventTargetData.listeners[event.type].forEach((listener) => {
listener.callback(event);
});
return true;
}
export { Event, EventTarget };
Loading