diffhtml-middleware-synthetic-events
Version:
Global event delegation middleware, avoids inline events
218 lines (171 loc) • 5.84 kB
JavaScript
const globalThis = typeof global === 'object' ? global : window || {};
const binding = globalThis[Symbol.for('diffHTML')]
const { NodeCache, PATCH_TYPE, decodeEntities, createNode } = binding.Internals;
const useCapture = [
'onload', 'onunload', 'onscroll', 'onfocus', 'onblur', 'onloadstart',
'onprogress', 'onerror', 'onabort', 'onload', 'onloadend', 'onpointerenter',
'onpointerleave',
];
const { assign, defineProperty, getOwnPropertyDescriptor } = Object;
const eventNames = /** @type {string[]} */ ([]);
const handlers = new Map();
const bounded = new Set();
// Ensure we don't get user added event/properties.
const cloneDoc = typeof document !== 'undefined' ? document.cloneNode() : null;
// Fill up event names.
for (const name in cloneDoc) {
if (name.indexOf('on') === 0) {
eventNames.push(name);
}
}
class SyntheticEvent {}
/**
* @param {{ [key: string]: any }} ev
* @param {{ [key: string]: any }} ov
*/
const cloneEvent = (ev, ov = {}) => {
const newEvent = /** @type {any} */ (new SyntheticEvent());
// Copy over original event getters/setters first, will need some extra
// intelligence to ensure getters/setters work, thx @kofifus.
for (let key in ev) {
const desc = getOwnPropertyDescriptor(ev, key);
if (key === 'isTrusted') { continue; }
if (desc && desc.get) {
defineProperty(newEvent, key, desc);
}
else {
newEvent[key] = ev[key];
}
}
// Copy over overrides.
for (let key in ov) {
newEvent[key] = ov[key];
}
return newEvent;
}
const getShadowRoot = (/** @type {any} */ node) => {
while (node = node.parentNode) {
if (node.toString() === "[object ShadowRoot]") {
return node;
}
}
return false;
};
// Set up global event delegation, once clicked call the saved handlers.
const bindEventsTo = (/** @type {any} */ domNode) => {
const rootNode = getShadowRoot(domNode) || domNode.ownerDocument;
const { addEventListener } = rootNode;
if (bounded.has(rootNode)) {
return false;
}
bounded.add(rootNode);
eventNames.forEach(eventName => addEventListener(eventName.slice(2), (/** @type {any} */ ev) => {
let target = ev.target;
let eventHandler = null;
const path = ev.path ? ev.path : ev.composedPath ? ev.composedPath() : [];
// If we were unable to get the path via some kind of standard approach,
// build it up manually.
if (!path.length) {
for (let node = target; node; node = node.parentNode) {
path.push(node);
}
}
for (let i = 0; i < path.length; i++) {
const node = path[i];
if (handlers.has(node)) {
const hasEventHandler = handlers.get(node)[eventName];
if (hasEventHandler) {
eventHandler = hasEventHandler;
}
break;
}
}
const syntheticEvent = cloneEvent(ev, {
stopPropagation() {
ev.stopImmediatePropagation();
ev.stopPropagation();
},
preventDefault() { ev.preventDefault(); },
nativeEvent: ev,
});
eventHandler && eventHandler(syntheticEvent);
}, useCapture.includes(eventName) ? true : false));
}
const syntheticEvents = () => {
function syntheticEventsTask() {
return (/** @type {any} */{ patches }) => {
const { length } = patches;
let i = 0;
while (true) {
const patchType = patches[i];
if (i === length) {
break;
}
switch(patchType) {
case PATCH_TYPE.SET_ATTRIBUTE: {
const vTree = patches[i + 1];
const name = patches[i + 2];
const value = decodeEntities(patches[i + 3]);
const domNode = createNode(vTree);
const eventName = name.toLowerCase();
// Remove inline event binding from element and add to handlers.
if (eventNames.includes(eventName)) {
const handler = value;
domNode[eventName] = undefined;
const newHandlers = handlers.get(domNode) || {};
// If the value passed is a function, that's what we're looking for.
if (typeof handler === 'function') {
newHandlers[eventName] = handler;
}
// If the value passed is a string name for a global function, use
// that.
else if (typeof window[handler] === 'function') {
newHandlers[eventName] = window[handler];
}
// Remove the event association if the value passed was not a
// function.
else {
delete newHandlers[eventName];
}
handlers.set(domNode, newHandlers);
bindEventsTo(domNode);
}
i += 4;
break;
}
case PATCH_TYPE.REMOVE_ATTRIBUTE: {
const vTree = patches[i + 1];
const name = patches[i + 2];
const domNode = NodeCache.get(vTree);
const eventName = name.toLowerCase();
// Remove event binding from element and instead add to handlers.
if (eventNames.includes(eventName)) {
const newHandlers = handlers.get(domNode) || {};
delete newHandlers[eventName];
handlers.set(domNode, newHandlers);
}
i += 3;
break;
}
case PATCH_TYPE.NODE_VALUE:
case PATCH_TYPE.INSERT_BEFORE: {
i += 4;
break;
}
case PATCH_TYPE.REPLACE_CHILD: {
i += 3;
break;
}
case PATCH_TYPE.REMOVE_CHILD: {
i += 2;
break;
}
}
}
}
}
return assign(syntheticEventsTask, {
displayName: 'syntheticEventsTask',
});
};
export default syntheticEvents;