/** * Add an event listener bound to the context of the superClass. * * @param {HTMLElement} node Element to attach the event listener to. * @param {String} eventName Name of the event. * @param {String} cbName Name of the handler. */ export const EventHelpers = function(superClass) { return class extends superClass { listen(node, eventName, cbName, options) { const boundListener = this.__registerListener(cbName, node, eventName); if (eventName === 'track') { this._track(node, boundListener); return; } node.addEventListener(eventName, boundListener, options); } /** * Remove an event listener bound to the context of the superClass. * * @param {HTMLElement} node Element to attach the event listener to. * @param {String} eventName Name of the event. * @param {String} cbName Name of the handler. */ unlisten(node, eventName, cbName, options) { this.__boundEventListeners = this.__boundEventListeners || new WeakMap(); const listeners = this.__boundEventListeners.get(node); const eventKey = `${eventName}_${cbName}`; if (listeners && typeof listeners[eventKey]) { node.removeEventListener(eventName, listeners[eventKey], options); listeners[eventKey] = null; } } once(node, eventName, cbName, options) { const wrappedCbName = `__onceCb__${cbName}`; this[wrappedCbName] = (...args) => { this[cbName](...args); this.unlisten(node, eventName, wrappedCbName, options); }; this.listen(node, eventName, wrappedCbName, options); } __registerListener(cbName, node, eventName) { this.__boundEventListeners = this.__boundEventListeners || new WeakMap(); const boundListener = this[cbName].bind(this); const eventKey = `${eventName}_${cbName}`; let listeners = this.__boundEventListeners.get(node); // If there is already a handler for the event assigned we stop here. if (listeners && typeof listeners[eventKey] === 'function') return; if (!listeners) { listeners = {}; } listeners[eventKey] = boundListener; this.__boundEventListeners.set(node, listeners); return boundListener; } _track(node, boundListener) { this._boundTrackingListener = boundListener; this.listen(node, 'mousedown', '_trackMouseDown'); } _trackMouseDown(e) { this._trackInfo = { originX: e.clientX, originY: e.clientY, started: false }; this.listen(window, 'mousemove', '_trackMouseMove', true); this.once(window, 'mouseup', '_trackMouseUp'); } _trackMouseMove(e) { if (!this._trackInfo.started && trackHasMovedEnough(this._trackInfo, e.clientX, e.clientY)) { this._trackInfo.started = true; this._boundTrackingListener(new CustomEvent('track', { detail: { state: 'start', dx: e.clientX - this._trackInfo.originX, dy: e.clientY - this._trackInfo.originY }, bubbles: true, composed: true })); } if (this._trackInfo.started) { this._boundTrackingListener(new CustomEvent('track', { detail: { state: 'track', dx: e.clientX - this._trackInfo.originX, dy: e.clientY - this._trackInfo.originY }, bubbles: true, composed: true })); } } _trackMouseUp(e) { this._boundTrackingListener(new CustomEvent('track', { detail: { state: 'end', dx: e.clientX - this._trackInfo.originX, dy: e.clientY - this._trackInfo.originY }, bubbles: true, composed: true })); this.unlisten(window, 'mousemove', '_trackMouseMove', true); this._trackInfo = null; } } } const TRACK_DISTANCE = 5; function trackHasMovedEnough(info, x, y) { let dx = Math.abs(info.originX - x); let dy = Math.abs(info.originY - y); return (dx >= TRACK_DISTANCE || dy >= TRACK_DISTANCE); }