Created
May 19, 2023 21:15
-
-
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
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
// 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; | |
} | |
}); |
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
<!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