UNPKG

@adoratorio/hermes

Version:

A JS utility for scroll events normalization and management

245 lines 9.32 kB
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