@adoratorio/hermes
Version:
A JS utility for scroll events normalization and management
245 lines • 9.32 kB
JavaScript
import { MODE, EVENTS, KEYCODE, } from './declarations';
import { normalizeWheelDelta, normalizeKeyDelta, getTouch } from './utils';
class Hermes {
static MODE = MODE;
static EVENTS = EVENTS;
static KEYCODE = KEYCODE;
options;
handler = () => { };
listening = true;
binded = false;
prevTouchPosition = { x: 0, y: 0 };
prevTouchTime = 0;
speed = { x: 0, y: 0 };
lastScrollPosition = { x: 0, y: 0 };
touchPointId = 0;
constructor(options) {
const defaults = {
mode: Hermes.MODE.VIRTUAL,
events: [
Hermes.EVENTS.WHEEL,
Hermes.EVENTS.TOUCH,
Hermes.EVENTS.KEYS,
],
root: document.querySelector('.hermes-container'),
passive: true,
emitGlobal: false,
touchClass: '.prevent-touch',
touchMultiplier: 2,
keyMultiplier: 1,
};
this.options = { ...defaults, ...options };
if ((options.mode === Hermes.MODE.VIRTUAL || options.mode === Hermes.MODE.NATIVE)
&& typeof options.root === 'undefined') {
throw new Error('Container cannot be undefined');
}
}
bind() {
this.binded = true;
if (this.options.mode === Hermes.MODE.VIRTUAL) {
this.options.events.forEach((event) => {
switch (true) {
case event === 'wheel':
this.options.root.addEventListener('wheel', this.wheel, { passive: this.options.passive });
this.options.root.addEventListener('mousewheel', this.wheel, { passive: this.options.passive });
break;
case event === 'touch':
this.options.root.addEventListener('touchstart', this.touchStart, { passive: this.options.passive });
this.options.root.addEventListener('touchend', this.touchEnd, { passive: this.options.passive });
this.options.root.addEventListener('touchcancel', this.touchEnd, { passive: this.options.passive });
break;
case event === 'keys':
this.options.root.addEventListener('keydown', this.keydownAll);
break;
case event === 'spacebar' && this.options.events.indexOf(Hermes.EVENTS.KEYS) < 0:
this.options.root.addEventListener('keydown', this.keydownSpacebar);
break;
case event === 'arrows' && this.options.events.indexOf(Hermes.EVENTS.KEYS) < 0:
this.options.root.addEventListener('keydown', this.keydownArrows);
break;
default:
console.warn(`'${event}' is not recognized`);
}
});
}
else if (this.options.mode === Hermes.MODE.NATIVE) {
this.options.root.addEventListener('scroll', this.scroll, { passive: this.options.passive });
const e = this.options.root;
const w = this.options.root;
this.lastScrollPosition = {
x: (w.pageXOffset || e.scrollLeft || 0),
y: (w.pageYOffset || e.scrollTop || 0)
};
}
else {
console.warn(`'${this.options.mode}' is not a supported mode`);
}
}
unbind() {
this.options.root.removeEventListener('wheel', this.wheel);
this.options.root.removeEventListener('mousewheel', this.wheel);
this.options.root.removeEventListener('touchstart', this.touchStart);
this.options.root.removeEventListener('touchend', this.touchEnd);
this.options.root.removeEventListener('touchcancel', this.touchEnd);
this.options.root.removeEventListener('touchmove', this.touchMove);
this.options.root.removeEventListener('keydown', this.keydownAll);
this.options.root.removeEventListener('keydown', this.keydownSpacebar);
this.options.root.removeEventListener('keydown', this.keydownArrows);
if (this.options.mode === Hermes.MODE.NATIVE) {
this.options.root.removeEventListener('scroll', this.scroll);
}
this.binded = false;
}
wheel = (event) => {
const customEvent = {
type: Hermes.EVENTS.WHEEL,
delta: normalizeWheelDelta(event),
originalEvent: event,
};
this.callHandler(customEvent);
};
scroll = (event) => {
const e = this.options.root;
const w = this.options.root;
const delta = {
x: (w.pageXOffset || e.scrollLeft || 0) - this.lastScrollPosition.x,
y: (w.pageYOffset || e.scrollTop || 0) - this.lastScrollPosition.y,
};
this.lastScrollPosition = {
x: (w.pageXOffset || e.scrollLeft || 0),
y: (w.pageYOffset || e.scrollTop || 0),
};
const customEvent = {
type: Hermes.EVENTS.SCROLL,
delta,
originalEvent: event,
};
this.callHandler(customEvent);
};
keydownAll = (event) => {
if (event.target.tagName === 'INPUT')
return;
if (event.target.tagName === 'TEXTAREA')
return;
if (event.target.isContentEditable)
return;
const customEvent = {
type: Hermes.EVENTS.KEYS,
delta: normalizeKeyDelta(event.keyCode, this.options.keyMultiplier),
originalEvent: event,
};
this.callHandler(customEvent);
};
keydownSpacebar = (event) => {
if (event.target.tagName === 'INPUT')
return;
if (event.target.tagName === 'TEXTAREA')
return;
if (event.target.isContentEditable)
return;
const customEvent = {
type: Hermes.EVENTS.SPACEBAR,
delta: normalizeKeyDelta(event.keyCode, this.options.keyMultiplier),
originalEvent: event,
};
this.callHandler(customEvent);
};
keydownArrows = (event) => {
if (event.target.tagName === 'INPUT')
return;
if (event.target.tagName === 'TEXTAREA')
return;
if (event.target.isContentEditable)
return;
const customEvent = {
type: Hermes.EVENTS.ARROWS,
delta: normalizeKeyDelta(event.keyCode, this.options.keyMultiplier),
originalEvent: event,
};
this.callHandler(customEvent);
};
touchStart = (event) => {
if (this.touchPointId !== 0 || !event.touches[0])
return;
this.touchPointId = event.touches[0].identifier;
this.options.root.addEventListener('touchmove', this.touchMove);
this.prevTouchPosition = {
x: event.touches[0].clientX,
y: event.touches[0].clientY,
};
};
touchMove = (event) => {
const touchPoint = getTouch(event.touches, this.touchPointId);
if (touchPoint === undefined)
return;
const delta = {
x: -(touchPoint.clientX - this.prevTouchPosition.x) * this.options.touchMultiplier,
y: -(touchPoint.clientY - this.prevTouchPosition.y) * this.options.touchMultiplier,
};
this.prevTouchPosition = {
x: touchPoint.clientX,
y: touchPoint.clientY,
};
const customEvent = {
type: Hermes.EVENTS.TOUCH,
delta,
originalEvent: event,
};
const now = performance.now();
const deltaT = now - this.prevTouchTime;
if (deltaT !== 0) {
const speed = {
x: delta.x / deltaT * 16,
y: delta.y / deltaT * 16,
};
this.speed = {
x: speed.x * 0.9 + this.speed.x * 0.1,
y: speed.y * 0.9 + this.speed.y * 0.1,
};
}
this.prevTouchTime = now;
this.callHandler(customEvent);
};
touchEnd = (event) => {
if (getTouch(event.changedTouches, this.touchPointId) === undefined)
return;
this.touchPointId = 0;
const customEvent = {
type: Hermes.EVENTS.TOUCH,
delta: this.speed,
originalEvent: event,
};
this.callHandler(customEvent);
this.options.root.removeEventListener('touchmove', this.touchMove);
};
callHandler = (event) => {
if (this.listening) {
this.handler(event);
if (this.options.emitGlobal) {
const eventInit = {};
eventInit.detail = event;
const customEvent = new CustomEvent(`hermes-${event.type}`, eventInit);
window.dispatchEvent(customEvent);
}
}
};
on(handler) {
if (this.binded)
throw new Error('A handler is already binded');
this.handler = handler;
this.unbind();
this.bind();
}
off() {
this.handler = () => { };
this.unbind();
}
destroy() {
this.off();
}
set listen(listening) {
this.listening = listening;
}
}
export default Hermes;
//# sourceMappingURL=index.js.map