Spaces:
Running
Running
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) { | |
if (typeof event.button === 'number' && event.button !== 0) { | |
return; | |
} | |
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; | |