Skip to content

Instantly share code, notes, and snippets.

@clshortfuse
Created October 26, 2020 17:22
Show Gist options
  • Save clshortfuse/820fdeee78e8073a3ca07d761595ef61 to your computer and use it in GitHub Desktop.
Save clshortfuse/820fdeee78e8073a3ca07d761595ef61 to your computer and use it in GitHub Desktop.
EventTarget & CustomEvent Shims
/**
* @template T
* @typedef {Object} CustomEventShimPrivateFields<T>
* @prop {T} detail
* @prop {string} type
* @prop {any} target
* @prop {any} currentTarget
* @prop {number} eventPhase
* @prop {boolean} bubbles
* @prop {boolean} cancelable
* @prop {number} timeStamp
* @prop {boolean} composed
* @prop {boolean} stopPropagationFlag
* @prop {boolean} stopImmediatePropagationFlag
* @prop {boolean} canceledFlag
* @prop {boolean} inPassiveListenerFlag
* @prop {boolean} initializedFlag
* @prop {boolean} dispatchFlag
* @prop {EventTarget[]} path
*/
/**
* @template {any} T
* @typedef {Object} CustomEventInit<T>
* @prop {T} [detail]
* @prop {boolean} [bubbles=false]
* @prop {boolean} [cancelable=false]
* @prop {boolean} [composed=false]
*/
/**
* @template T
* @type {WeakMap<CustomEventShim<any>, CustomEventShimPrivateFields<T>>} */
export const privateCustomEventFields = new WeakMap();
/**
* @template T
* @class CustomEventShim<T>
* @implements {CustomEvent}
*/
export default class CustomEventShim {
/**
* @param {string} type
* @param {CustomEventInit<T>} eventInitDict
*/
constructor(type, eventInitDict = ({
detail: null,
bubbles: false,
cancelable: false,
composed: false,
})) {
const detail = eventInitDict.detail || null;
const bubbles = eventInitDict.bubbles || false;
const cancelable = eventInitDict.cancelable || false;
const composed = eventInitDict.composed || false;
privateCustomEventFields.set(this, {
type: '',
target: null,
currentTarget: null,
eventPhase: CustomEventShim.prototype.NONE,
timeStamp: (typeof performance !== 'undefined' && 'now' in performance ? performance.now() : Date.now()),
stopPropagationFlag: false,
stopImmediatePropagationFlag: false,
canceledFlag: false,
inPassiveListenerFlag: false,
initializedFlag: false,
dispatchFlag: false,
path: [],
bubbles,
cancelable,
composed,
detail,
});
this.initCustomEvent(type, bubbles, cancelable, detail);
}
/** @return {string} */
get type() {
return privateCustomEventFields.get(this).type;
}
/** @return {?EventTarget} */
get target() {
return privateCustomEventFields.get(this).target;
}
/** @return {?EventTarget} */
get srcElement() {
return privateCustomEventFields.get(this).target;
}
/** @return {?EventTarget} */
get currentTarget() {
return privateCustomEventFields.get(this).currentTarget;
}
/** @return {Array<EventTarget>} */
composedPath() {
return privateCustomEventFields.get(this).path;
}
/** @return {number} */
get eventPhase() {
return privateCustomEventFields.get(this).eventPhase;
}
/** @return {void} */
stopPropagation() {
privateCustomEventFields.get(this).stopPropagationFlag = true;
}
/** @return {boolean} */
get cancelBubble() {
return privateCustomEventFields.get(this).stopPropagationFlag;
}
/**
* @param {boolean} value
*/
set cancelBubble(value) {
if (value) {
privateCustomEventFields.get(this).stopPropagationFlag = true;
}
}
/** @return {void} */
stopImmediatePropagation() {
const map = privateCustomEventFields.get(this);
map.stopPropagationFlag = true;
map.stopImmediatePropagationFlag = true;
}
/** @return {boolean} */
get bubbles() {
return privateCustomEventFields.get(this).bubbles;
}
/** @return {boolean} */
get cancelable() {
return privateCustomEventFields.get(this).cancelable;
}
/** @return {boolean} */
get returnValue() {
return privateCustomEventFields.get(this).canceledFlag;
}
/** @param {boolean} value */
set returnValue(value) {
if (!value) {
privateCustomEventFields.get(this).canceledFlag = true;
}
}
/** @return {void} */
preventDefault() {
const map = privateCustomEventFields.get(this);
if (!map.inPassiveListenerFlag && map.cancelable) {
privateCustomEventFields.get(this).canceledFlag = true;
}
}
/** @return {boolean} */
get defaultPrevented() {
return privateCustomEventFields.get(this).canceledFlag;
}
/** @return {boolean} */
get composed() {
return privateCustomEventFields.get(this).composed;
}
/** @return {boolean} */
get isTrusted() {
return false;
}
/** @return {number} */
get timeStamp() {
return privateCustomEventFields.get(this).timeStamp;
}
/**
* @param {string} type
* @param {boolean} bubbles
* @param {boolean} cancelable
* @param {T} detail
*/
initCustomEvent(type, bubbles = false, cancelable = false, detail = null) {
const map = privateCustomEventFields.get(this);
if (map.dispatchFlag) {
return;
}
this.initEvent(type, bubbles, cancelable);
map.detail = detail;
}
/**
* @param {string} type
* @param {boolean} bubbles
* @param {boolean} cancelable
*/
initEvent(type, bubbles = false, cancelable = false) {
const map = privateCustomEventFields.get(this);
if (map.dispatchFlag) {
return;
}
map.initializedFlag = true;
map.stopPropagationFlag = false;
map.stopImmediatePropagationFlag = false;
map.canceledFlag = false;
map.type = type;
map.bubbles = bubbles;
map.cancelable = cancelable;
}
/** @return {T} */
get detail() {
return privateCustomEventFields.get(this).detail;
}
}
CustomEventShim.prototype.NONE = 0;
CustomEventShim.prototype.CAPTURING_PHASE = 1;
CustomEventShim.prototype.AT_TARGET = 2;
CustomEventShim.prototype.BUBBLING_PHASE = 3;
import { privateCustomEventFields } from './customevent.js';
/**
* @template T
* @typedef {import("./customevent").default<T>} CustomEventShim<T>
*/
/**
* @typedef {Object} Listener
* @prop {EventListener} callback
* @prop {AddEventListenerOptions} options
*/
/**
* @typedef {Object} EventTargetShimPrivateFields
* @prop {Map<string, Listener[]>} listeners
* @prop {function():EventTargetShim} getParent
*/
/** @type {WeakMap<any, EventTargetShimPrivateFields>} */
export const privateEventTargetFields = new WeakMap();
/**
* @param {AddEventListenerOptions | EventListenerOptions | boolean} options
* @return {AddEventListenerOptions}
*/
function flattenOptions(options) {
if (!options) {
return {
passive: false,
once: false,
capture: false,
};
}
if (options === true) {
return {
passive: false,
once: false,
capture: true,
};
}
return options;
}
/**
* @implements {EventTarget}
* @url https://dom.spec.whatwg.org/#interface-eventtarget
*/
export default class EventTargetShim {
constructor() {
privateEventTargetFields.set(this, {
listeners: new Map(),
getParent: () => null,
});
}
/**
* @param {string} type
* @param {EventListenerOrEventListenerObject} callback
* @param {AddEventListenerOptions|boolean=} options
* @return {void}
*/
addEventListener(type, callback, options) {
const flattenedOptions = flattenOptions(options);
const o = privateEventTargetFields.get(this);
const callbackFn = (typeof callback === 'function' ? callback : callback.handleEvent);
if (!o.listeners.has(type)) {
o.listeners.set(type, [{ callback: callbackFn, options: flattenedOptions }]);
} else {
const listener = o.listeners.get(type).find((l) => l.callback === callback
&& l.options.capture === flattenedOptions.capture);
if (listener) {
listener.options = flattenedOptions;
return;
}
o.listeners.get(type).push({ callback: callbackFn, options: flattenedOptions });
}
}
/**
* @param {string} type
* @param {EventListener} callback
* @param {EventListenerOptions|boolean=} options
* @return {void}
*/
removeEventListener(type, callback, options) {
const flattenedOptions = flattenOptions(options);
const o = privateEventTargetFields.get(this);
const listeners = o.listeners.get(type);
if (listeners == null) {
return;
}
let removeIndex = -1;
listeners.some((l, index) => {
if (l.callback !== callback
|| l.options.passive !== flattenedOptions.passive
|| l.options.once !== flattenedOptions.once
|| l.options.capture !== flattenedOptions.capture) {
return false;
}
removeIndex = index;
return true;
});
if (removeIndex !== -1) {
listeners.splice(removeIndex, 1);
}
if (!listeners.length) {
o.listeners.delete(type);
}
}
/**
* @param {CustomEventShim<any>} event
* @return {boolean}
*/
dispatchEvent(event) {
const pf = privateCustomEventFields.get(event);
if (!pf.initializedFlag || pf.dispatchFlag) {
throw new Error('InvalidStateError');
}
pf.dispatchFlag = true;
pf.target = this;
/** @type {EventTargetShim[]} */
pf.path = [];
/** @type {EventTargetShim} */
let pathTarget = this;
do {
pf.path.splice(0, 0, pathTarget);
const targetPf = privateEventTargetFields.get(pathTarget);
pathTarget = targetPf?.getParent?.();
} while (pathTarget);
/**
* @param {EventTargetShim[]} targets
* @param {function(Listener):boolean} listenerFilter
* @return {boolean} stopped
*/
function cycleListeners(targets, listenerFilter = () => true) {
return targets.some((target) => {
const targetPf = privateEventTargetFields.get(target);
const listeners = (targetPf.listeners.get(event.type) || []).slice();
/** @type {Listener[]} */
const removalList = [];
pf.currentTarget = target;
listeners.filter(listenerFilter).some((l) => {
pf.inPassiveListenerFlag = (l.options.passive === true);
l.callback(event);
pf.inPassiveListenerFlag = false;
if (l.options.once) {
removalList.push(l);
}
return pf.stopImmediatePropagationFlag;
});
// Manually check in case removed by a callback
removalList.forEach((l) => {
const index = listeners.indexOf(l);
if (index !== -1) {
listeners.splice(index, 1);
}
});
return pf.stopPropagationFlag;
});
}
pf.eventPhase = event.CAPTURING_PHASE;
if (!cycleListeners(pf.path.slice().reverse(), (l) => l.options.capture)) {
pf.eventPhase = event.AT_TARGET;
if (!cycleListeners([this], (l) => !l.options.capture)) {
if (event.bubbles) {
pf.eventPhase = event.BUBBLING_PHASE;
cycleListeners(pf.path.filter((t) => t !== this));
}
}
}
pf.eventPhase = event.NONE;
pf.currentTarget = null;
pf.dispatchFlag = false;
pf.stopPropagationFlag = false;
pf.stopImmediatePropagationFlag = false;
return !pf.canceledFlag;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment