File size: 4,278 Bytes
f2bee8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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;