Skip to content

Instantly share code, notes, and snippets.

@uhop
Created May 19, 2023 21:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save uhop/5dc47a31a4de9a99663d749c966ecaf1 to your computer and use it in GitHub Desktop.
Save uhop/5dc47a31a4de9a99663d749c966ecaf1 to your computer and use it in GitHub Desktop.
The ES6 facelift for the venerable on() from https://github.com/clubajax/on
// a facelift of the venerable on.js: slightly modified for ES6, removed dead browsers
// copied from https://github.com/clubajax/on, used under the MIT license
// internal utilities
const getNodeById = id => {
const node = document.getElementById(id);
if (!node) {
console.error('`on` Could not find:', id);
}
return node;
};
const getDoc = node => {
if (node === document || node === window) return document;
if (node.nodeType === 9) return node; // Node.DOCUMENT_NODE
return node.ownerDocument;
};
const closest = (element, selector, parent) => {
while (element) {
if (element.matches?.(selector)) return element;
if (element === parent) break;
element = element.parentElement;
}
return null;
};
const closestFilter = (element, selector) => e => closest(e.target, selector, element);
const makeCallback = (node, filter, handler) => {
if (!filter || !handler) return filter || handler;
if (typeof filter === 'string') {
filter = closestFilter(node, filter);
}
return e => {
const result = filter(e);
if (result) {
e.filteredTarget = result;
handler(e, result);
}
};
};
// public functions and properties
export const onDomEvent = (node, eventName, callback) => {
node.addEventListener(eventName, callback, false);
return {
state: 'resumed',
remove() {
node.removeEventListener(eventName, callback, false);
node = callback = null;
this.remove = this.pause = this.resume = function () {};
this.state = 'removed';
},
pause() {
node.removeEventListener(eventName, callback, false);
this.state = 'paused';
},
resume() {
node.addEventListener(eventName, callback, false);
this.state = 'resumed';
}
};
};
export const makeMultiHandle = handles => ({
state: 'resumed',
remove() {
handles.forEach(h => {
// allow for a simple function in the list
if (h.remove) {
h.remove();
} else if (typeof h === 'function') {
h();
}
});
handles = [];
this.remove = this.pause = this.resume = () => {};
this.state = 'removed';
},
pause() {
handles.forEach(h => h.pause && h.pause());
this.state = 'paused';
},
resume() {
handles.forEach(h => h.resume && h.resume());
this.state = 'resumed';
}
});
// registered functional events
export const events = {}; // initialized below
// helpers
const onImageLoad = (node, callback) =>
makeMultiHandle([
onDomEvent(node, 'load', e => {
const interval = setInterval(() => {
if (node.naturalWidth || node.naturalHeight) {
clearInterval(interval);
e.width = e.naturalWidth = node.naturalWidth;
e.height = e.naturalHeight = node.naturalHeight;
callback(e);
}
}, 100);
handle.remove();
}),
on(node, 'error', callback)
]);
const onKeyEvent = (keyEventName, re) => (node, callback) =>
on(node, keyEventName, e => {
if (re.test(e.key)) {
callback(e);
}
});
// wheel helpers
const hasWheel = 'onwheel' in document;
const FACTOR = navigator.userAgent.indexOf('Windows') > -1 ? 10 : 0.1;
let XLR8 = 0,
mouseWheelHandle;
// normalizes all browsers' events to a standard:
// delta, wheelY, wheelX
// also adds acceleration and deceleration to make
// Mac and Windows behave similarly
const normalizeWheelEvent = callback => e => {
XLR8 += FACTOR;
let deltaY = Math.max(-1, Math.min(1, e.wheelDeltaY || e.deltaY));
const deltaX = Math.max(-10, Math.min(10, e.wheelDeltaX || e.deltaX));
deltaY = deltaY <= 0 ? deltaY - XLR8 : deltaY + XLR8;
e.delta = deltaY;
e.wheelY = deltaY;
e.wheelX = deltaX;
clearTimeout(mouseWheelHandle);
mouseWheelHandle = setTimeout(function () {
XLR8 = 0;
}, 300);
callback(e);
};
// main function
export const on = (node, eventName, filter, handler) => {
// normalize parameters
if (typeof node === 'string') {
node = getNodeById(node);
}
// prepare a callback
let callback = makeCallback(node, filter, handler);
// functional event
if (typeof eventName === 'function') {
return eventName(node, callback);
}
// special case: keydown/keyup with a list of expected keys
// TODO: consider replacing with an explicit event function:
// var h = on(node, onKeyEvent('keyup', /Enter,Esc/), callback);
const keyEvent = /^(keyup|keydown):(.+)$/.exec(eventName);
if (keyEvent) {
return onKeyEvent(keyEvent[1], new RegExp(keyEvent[2].split(',').join('|')))(node, callback);
}
// handle multiple event types, like: on(node, 'mouseup, mousedown', callback);
if (/,/.test(eventName)) {
return makeMultiHandle(
eventName
.split(',')
.map(name => name.trim())
.filter(name => name)
.map(name => on(node, name, callback))
);
}
// handle registered functional events
if (typeof events[eventName] == 'function') return events[eventName](node, callback);
// special case: loading an image
if (eventName === 'load' && node.tagName.toLowerCase() === 'img') return onImageLoad(node, callback);
// special case: mousewheel
if (eventName === 'wheel') {
// pass through, but first curry callback to wheel events
callback = normalizeWheelEvent(callback);
if (!hasWheel) {
// old Firefox, old IE, Chrome
return makeMultiHandle([on(node, 'DOMMouseScroll', callback), on(node, 'mousewheel', callback)]);
}
}
// default case
return onDomEvent(node, eventName, callback);
};
export default on;
// public functions
export const once = (node, eventName, filter, callback) => {
let h;
if (filter && callback) {
h = on(node, eventName, filter, () => {
callback.apply(window, arguments);
h.remove();
});
} else {
h = on(node, eventName, () => {
filter.apply(window, arguments);
h.remove();
});
}
return h;
};
const INVALID_PROPS = {isTrusted: 1};
const mix = (object, value) => {
if (!value) return object;
if (typeof value != 'object') {
object.value = value;
return object;
}
return Object.entries(value).reduce(
(object, [key, value]) => (INVALID_PROPS[key] != 1 && (object[key] = value), object),
object
);
};
export const emit = (node, eventName, value) => {
node = typeof node === 'string' ? getNodeById(node) : node;
const event = getDoc(node).createEvent('HTMLEvents');
event.initEvent(eventName, true, true); // event type, bubbling, cancelable
return node.dispatchEvent(mix(event, value));
};
export const fire = (node, eventName, eventDetail, bubbles) => {
node = typeof node === 'string' ? getNodeById(node) : node;
var event = getDoc(node).createEvent('CustomEvent');
event.initCustomEvent(eventName, !!bubbles, true, eventDetail); // event type, bubbling, cancelable, value
return node.dispatchEvent(event);
};
// add public properties
Object.assign(on, {once, emit, fire, makeMultiHandle, onDomEvent, closest, events});
// synthetic events
Object.assign(events, {
// handle click and Enter
button: (node, callback) => makeMultiHandle([on(node, 'click', callback), on(node, 'keyup:Enter', callback)]),
// custom - used for popups 'n stuff
clickoff: (node, callback) => {
// important note!
// starts paused
//
const nodeDoc = node.ownerDocument.documentElement,
bHandle = makeMultiHandle([
on(nodeDoc, 'click', e => {
let target = e.target;
if (target.nodeType !== 1) {
// Node.ELEMENT_NODE
target = target.parentNode;
}
if (target && !node.contains(target)) {
callback(e);
}
}),
on(nodeDoc, 'keyup', e => e.key === 'Escape' && callback(e))
]),
handle = {
state: 'resumed',
resume() {
setTimeout(() => bHandle.resume(), 100);
this.state = 'resumed';
},
pause() {
bHandle.pause();
this.state = 'paused';
},
remove() {
bHandle.remove();
this.state = 'removed';
}
};
handle.pause();
return handle;
}
});
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 5//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Test on</title>
<style>
html,
body {
margin: 10px;
padding: 0;
background: #fff;
font-family: sans-serif;
}
#modal,
#mousewheeltest,
#rwd {
padding: 10px;
position: absolute;
right: 20px;
top: 50px;
width: 180px;
height: 180px;
border: 1px solid #666;
}
#modal {
right: 240px;
}
#rwd {
top: 270px;
height: 50px;
}
p {
font-style: italic;
}
#css {
background: #fffed7;
border: 1px solid #f5ff95;
padding: 10px;
}
#css span {
display: inline-block;
background: #c7c9ff;
padding: 10px;
}
#css a {
background: #ffe3df;
padding: 3px;
}
</style>
</head>
<body>
<h1>on v2.1 test</h1>
<p>Results are in the console.</p>
<p>Test Key Events</p>
<input id="inp" />
<p>Test Multi Key Events</p>
<input id="minp" />
<p>Test simple connections</p>
<button id="b1">clicker</button>
<button id="b2">pause clicker</button>
<button id="b3">resume clicker</button>
<button id="b4">cancel clicker</button>
<p>Test Button Event</p>
<button id="b5">Click or Enter</button>
<p>Test css selectors</p>
<div id="css">
<span class="tab"><a href="#">Tab 1</a></span>
<span class="tab"><a href="#">Tab 2</a></span>
<span class="tab"><a href="#">Tab 3</a></span>
</div>
<div id="mousewheeltest">Test mousewheel</div>
<div id="modal">Modal. Click in me, click out of me, mouse in and out of me.</div>
<script type="module">
import on from './on.mjs';
console.log('on loaded');
var h1,
h2,
o,
clickoff,
tabs = document.querySelectorAll('.tab'),
wheelNode = document.getElementById('mousewheeltest'),
modal = document.getElementById('modal');
// test mouse wheel normalization
on(wheelNode, 'wheel', function (e) {
wheelNode.innerHTML =
'delta: ' + e.delta.toFixed(2) + '<br>wheelY: ' + e.wheelY.toFixed(2) + '<br>wheelX: ' + e.wheelX.toFixed(2);
});
// test modal clickoff special function
clickoff = on(modal, 'clickoff', function () {
modal.innerHTML = 'modal clickoff';
});
clickoff.resume();
// test mouseenter/leave
// should be standard in all browsers
on('modal', 'mouseenter', function () {
modal.innerHTML = 'modal enter';
});
on('modal', 'mouseleave', function () {
modal.innerHTML = 'modal leave';
});
// test CSS filters
on('css', 'click', '.tab', function (e, filteredTarget) {
console.log('clicked tab', e.filteredTarget);
});
// test pause/remove handles
h1 = on('b1', 'click', function () {
console.log('[clicker]');
});
on('b2', 'click', function () {
h1.pause();
console.log('pause!');
});
on('b3', 'click', function () {
h1.resume();
console.log('resume!');
});
on('b4', 'click', function () {
h1.remove();
console.log('remove!');
});
// test button
on('b5', 'button', function () {
console.log('Exec Button');
});
// test key handling
// expects the use of polyfill
on('inp', 'keydown', function (e) {
console.log('keydown', e.key, e.keyCode, e);
});
on('inp', 'keyup', function (e) {
//console.log('keyup', e.alphanumeric, e.key);
});
on('inp', 'keypress', function (e) {
//console.log('keypress', e.alphanumeric, e.key);
});
// test multi key handling
on('minp', 'keydown:a,b,c, ,ArrowDown,ArrowUp,ArrowLeft,ArrowRight,Enter,Escape', function (e) {
console.log('keydown', e.key);
});
function delay(fn) {
setTimeout(fn, 300);
}
function testKeys() {
delay(function () {
on.emit('inp', 'keydown', {keyCode: 65, key: 'A'});
});
}
function test() {
// test document events
on(document, 'doc-test', function () {
console.log('document.doc-test fired');
});
on.fire(document, 'doc-test');
delay(function () {
on.emit('modal', 'mouseenter');
delay(function () {
on.emit('modal', 'mouseleave');
});
});
console.log('should see [clicker] twice:');
on.emit('b1', 'click');
on.emit('b2', 'click');
on.emit('b1', 'click');
on.emit('b1', 'click');
on.emit('b3', 'click');
on.emit('b1', 'click');
on.emit('b4', 'click');
on.emit('b1', 'click');
on.emit('b1', 'click');
on.emit(tabs[0], 'click');
on.emit(tabs[1], 'click');
on.emit(tabs[2], 'click');
}
test();
testKeys();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment