Created
October 26, 2020 17:22
-
-
Save clshortfuse/820fdeee78e8073a3ca07d761595ef61 to your computer and use it in GitHub Desktop.
EventTarget & CustomEvent Shims
This file contains hidden or 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
/** | |
* @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; |
This file contains hidden or 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
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