-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Event and minimal EventTarget (#76)
* feat: implements event and eventTarget * refactor: fix spelling inconsistencies for event test * fix: remove unneccesary code --------- Co-authored-by: Ubuntu <ubuntu@ip-172-31-27-237.us-west-2.compute.internal>
- Loading branch information
Showing
5 changed files
with
843 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.