import bindAll from 'lodash.bindall'; import {getEventXY} from '../lib/touch-utils'; class DragRecognizer { /* Gesture states */ static get STATE_UNIDENTIFIED () { return 'unidentified'; } static get STATE_SCROLL () { return 'scroll'; } static get STATE_DRAG () { return 'drag'; } constructor ({ onDrag = (() => {}), onDragEnd = (() => {}), touchDragAngle = 70, // Angle and distance thresholds are the same as scratch-blocks distanceThreshold = 3 }) { this._onDrag = onDrag; this._onDragEnd = onDragEnd; this._touchDragAngle = touchDragAngle; this._distanceThreshold = distanceThreshold; this._initialOffset = null; this._gestureState = DragRecognizer.STATE_UNIDENTIFIED; bindAll(this, [ 'start', 'gestureInProgress', 'reset', '_handleMove', '_handleEnd' ]); } start (event) { this._initialOffset = getEventXY(event); this._bindListeners(); } gestureInProgress () { return this._gestureState !== DragRecognizer.STATE_UNIDENTIFIED; } reset () { this._unbindListeners(); this._initialOffset = null; this._gestureState = DragRecognizer.STATE_UNIDENTIFIED; } // // Internal functions // _bindListeners () { window.addEventListener('mouseup', this._handleEnd); window.addEventListener('mousemove', this._handleMove); window.addEventListener('touchend', this._handleEnd); // touchmove must be marked as non-passive, or else it cannot prevent scrolling window.addEventListener('touchmove', this._handleMove, {passive: false}); } _unbindListeners () { window.removeEventListener('mouseup', this._handleEnd); window.removeEventListener('mousemove', this._handleMove); window.removeEventListener('touchend', this._handleEnd); window.removeEventListener('touchmove', this._handleMove, {passive: false}); } _handleMove (event) { // For gestures identified as vertical scrolls, do not process movement events if (this._isScroll()) return; const currentOffset = getEventXY(event); // Try to identify this gesture if it hasn't been identified already if (!this.gestureInProgress()) { const dx = currentOffset.x - this._initialOffset.x; const dy = currentOffset.y - this._initialOffset.y; const dragDistance = Math.sqrt((dx * dx) + (dy * dy)); if (dragDistance < this._distanceThreshold) return; // For touch moves, additionally check if the angle suggests drag vs. scroll if (event.type === 'touchmove') { // Direction goes from -180 to 180, with 0 toward the right. let angle = Math.atan2(dy, dx) / Math.PI * 180; // Fold over horizontal axis, range now 0 to 180 angle = Math.abs(angle); // Fold over vertical axis, range now 0 to 90 if (angle > 90) angle = 180 - angle; if (angle > this._touchDragAngle) { this._gestureState = DragRecognizer.STATE_SCROLL; } else { this._gestureState = DragRecognizer.STATE_DRAG; } } else { // Mouse moves are always considered drags this._gestureState = DragRecognizer.STATE_DRAG; } } if (this._isDrag()) { this._onDrag(currentOffset, this._initialOffset); event.preventDefault(); } } _handleEnd () { this.reset(); // Call the callback after reset to make sure if gestureInProgress() // is used in response, it get the correct value (i.e. no gesture in progress) this._onDragEnd(); } _isDrag () { return this._gestureState === DragRecognizer.STATE_DRAG; } _isScroll () { return this._gestureState === DragRecognizer.STATE_SCROLL; } } export default DragRecognizer;