From 9e775f4036a657d7a254f1a3587d213ec9d1657b Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Mon, 26 Feb 2018 08:48:10 -0800 Subject: [PATCH] Support disable() mid drag or rotate gesture --- src/ui/handler/drag_pan.js | 109 +++++++++++++++++------ src/ui/handler/drag_rotate.js | 88 ++++++++++++++---- test/unit/ui/handler/drag_pan.test.js | 78 ++++++++++++++++ test/unit/ui/handler/drag_rotate.test.js | 78 ++++++++++++++++ 4 files changed, 307 insertions(+), 46 deletions(-) diff --git a/src/ui/handler/drag_pan.js b/src/ui/handler/drag_pan.js index 4e7d95b7df3..90853602060 100644 --- a/src/ui/handler/drag_pan.js +++ b/src/ui/handler/drag_pan.js @@ -5,6 +5,7 @@ const util = require('../../util/util'); const window = require('../../util/window'); const browser = require('../../util/browser'); const {Event} = require('../../util/evented'); +const assert = require('assert'); import type Map from '../map'; import type Point from '@mapbox/point-geometry'; @@ -22,8 +23,7 @@ const inertiaLinearity = 0.3, class DragPanHandler { _map: Map; _el: HTMLElement; - _enabled: boolean; - _active: boolean; + _state: 'disabled' | 'enabled' | 'pending' | 'active'; _pos: Point; _previousPos: Point; _inertia: Array<[number, Point]>; @@ -35,6 +35,7 @@ class DragPanHandler { constructor(map: Map) { this._map = map; this._el = map.getCanvasContainer(); + this._state = 'disabled'; util.bindAll([ '_onMove', @@ -51,7 +52,7 @@ class DragPanHandler { * @returns {boolean} `true` if the "drag to pan" interaction is enabled. */ isEnabled() { - return !!this._enabled; + return this._state !== 'disabled'; } /** @@ -60,7 +61,7 @@ class DragPanHandler { * @returns {boolean} `true` if the "drag to pan" interaction is active. */ isActive() { - return !!this._active; + return this._state === 'active'; } /** @@ -72,7 +73,7 @@ class DragPanHandler { enable() { if (this.isEnabled()) return; this._el.classList.add('mapboxgl-touch-drag-pan'); - this._enabled = true; + this._state = 'enabled'; } /** @@ -84,11 +85,26 @@ class DragPanHandler { disable() { if (!this.isEnabled()) return; this._el.classList.remove('mapboxgl-touch-drag-pan'); - this._enabled = false; + switch (this._state) { + case 'active': + this._state = 'disabled'; + this._unbind(); + this._deactivate(); + this._fireEvent('dragend'); + this._fireEvent('moveend'); + break; + case 'pending': + this._state = 'disabled'; + this._unbind(); + break; + default: + this._state = 'disabled'; + break; + } } onMouseDown(e: MouseEvent) { - if (!this.isEnabled() || this.isActive()) return; + if (this._state !== 'enabled') return; if (e.ctrlKey || DOM.mouseButton(e) !== 0) return; // Bind window-level event listeners for mousemove/up events. In the absence of @@ -103,7 +119,7 @@ class DragPanHandler { } onTouchStart(e: TouchEvent) { - if (!this.isEnabled() || this.isActive()) return; + if (this._state !== 'enabled') return; if (e.touches.length > 1) return; // Bind window-level event listeners for touchmove/end events. In the absence of @@ -122,7 +138,7 @@ class DragPanHandler { // isn't in focus, dragging will continue even though the mouse is no longer pressed. window.addEventListener('blur', this._onBlur); - this._active = false; + this._state = 'pending'; this._previousPos = DOM.mousePos(this._el, e); this._inertia = [[browser.now(), this._previousPos]]; } @@ -135,10 +151,10 @@ class DragPanHandler { this._drainInertiaBuffer(); this._inertia.push([browser.now(), this._pos]); - if (!this.isActive()) { + if (this._state === 'pending') { // we treat the first move event (rather than the mousedown event) // as the start of the drag - this._active = true; + this._state = 'active'; this._fireEvent('dragstart', e); this._fireEvent('movestart', e); } @@ -164,43 +180,82 @@ class DragPanHandler { _onMouseUp(e: MouseEvent) { if (DOM.mouseButton(e) !== 0) return; - this._finish(e); + switch (this._state) { + case 'active': + this._state = 'enabled'; + DOM.suppressClick(); + this._unbind(); + this._deactivate(); + this._inertialPan(e); + break; + case 'pending': + this._state = 'enabled'; + this._unbind(); + break; + default: + assert(false); + break; + } } _onTouchEnd(e: TouchEvent) { - this._finish(e); + switch (this._state) { + case 'active': + this._state = 'enabled'; + this._unbind(); + this._deactivate(); + this._inertialPan(e); + break; + case 'pending': + this._state = 'enabled'; + this._unbind(); + break; + default: + assert(false); + break; + } } _onBlur(e: FocusEvent) { - this._finish(e); + switch (this._state) { + case 'active': + this._state = 'enabled'; + this._unbind(); + this._deactivate(); + this._fireEvent('dragend', e); + this._fireEvent('moveend', e); + break; + case 'pending': + this._state = 'enabled'; + this._unbind(); + break; + default: + assert(false); + break; + } } - _finish(e: MouseEvent | TouchEvent | FocusEvent) { + _unbind() { window.document.removeEventListener('touchmove', this._onMove, {capture: true}); window.document.removeEventListener('touchend', this._onTouchEnd); window.document.removeEventListener('mousemove', this._onMove, {capture: true}); window.document.removeEventListener('mouseup', this._onMouseUp); window.removeEventListener('blur', this._onBlur); + } - if (!this.isActive()) return; - - this._active = false; + _deactivate() { delete this._lastMoveEvent; delete this._previousPos; delete this._pos; + } - DOM.suppressClick(); - + _inertialPan(e: MouseEvent | TouchEvent) { this._fireEvent('dragend', e); - this._drainInertiaBuffer(); - - const finish = () => { - this._fireEvent('moveend', e); - }; + this._drainInertiaBuffer(); const inertia = this._inertia; if (inertia.length < 2) { - finish(); + this._fireEvent('moveend', e); return; } @@ -210,7 +265,7 @@ class DragPanHandler { flingDuration = (last[0] - first[0]) / 1000; if (flingDuration === 0 || last[1].equals(first[1])) { - finish(); + this._fireEvent('moveend', e); return; } diff --git a/src/ui/handler/drag_rotate.js b/src/ui/handler/drag_rotate.js index 78cb69c76e3..b8035791900 100644 --- a/src/ui/handler/drag_rotate.js +++ b/src/ui/handler/drag_rotate.js @@ -5,6 +5,7 @@ const util = require('../../util/util'); const window = require('../../util/window'); const browser = require('../../util/browser'); const {Event} = require('../../util/evented'); +const assert = require('assert'); import type Map from '../map'; import type Point from '@mapbox/point-geometry'; @@ -22,8 +23,7 @@ const inertiaLinearity = 0.25, class DragRotateHandler { _map: Map; _el: HTMLElement; - _enabled: boolean; - _active: boolean; + _state: 'disabled' | 'enabled' | 'pending' | 'active'; _button: 'right' | 'left'; _eventButton: number; _bearingSnap: number; @@ -51,6 +51,7 @@ class DragRotateHandler { }) { this._map = map; this._el = options.element || map.getCanvasContainer(); + this._state = 'disabled'; this._button = options.button || 'right'; this._bearingSnap = options.bearingSnap || 0; this._pitchWithRotate = options.pitchWithRotate !== false; @@ -69,7 +70,7 @@ class DragRotateHandler { * @returns {boolean} `true` if the "drag to rotate" interaction is enabled. */ isEnabled() { - return !!this._enabled; + return this._state !== 'disabled'; } /** @@ -78,7 +79,7 @@ class DragRotateHandler { * @returns {boolean} `true` if the "drag to rotate" interaction is active. */ isActive() { - return !!this._active; + return this._state === 'active'; } /** @@ -89,7 +90,7 @@ class DragRotateHandler { */ enable() { if (this.isEnabled()) return; - this._enabled = true; + this._state = 'enabled'; } /** @@ -100,11 +101,29 @@ class DragRotateHandler { */ disable() { if (!this.isEnabled()) return; - this._enabled = false; + switch (this._state) { + case 'active': + this._state = 'disabled'; + this._unbind(); + this._deactivate(); + this._fireEvent('rotateend'); + if (this._pitchWithRotate) { + this._fireEvent('pitchend'); + } + this._fireEvent('moveend'); + break; + case 'pending': + this._state = 'disabled'; + this._unbind(); + break; + default: + this._state = 'disabled'; + break; + } } onMouseDown(e: MouseEvent) { - if (!this.isEnabled() || this.isActive()) return; + if (this._state !== 'enabled') return; if (this._button === 'right') { this._eventButton = DOM.mouseButton(e); @@ -128,7 +147,7 @@ class DragRotateHandler { // isn't in focus, dragging will continue even though the mouse is no longer pressed. window.addEventListener('blur', this._onBlur); - this._active = false; + this._state = 'pending'; this._inertia = [[browser.now(), this._map.getBearing()]]; this._previousPos = DOM.mousePos(this._el, e); this._center = this._map.transform.centerPoint; // Center of rotation @@ -140,8 +159,8 @@ class DragRotateHandler { this._lastMoveEvent = e; this._pos = DOM.mousePos(this._el, e); - if (!this.isActive()) { - this._active = true; + if (this._state === 'pending') { + this._state = 'active'; this._fireEvent('rotatestart', e); this._fireEvent('movestart', e); if (this._pitchWithRotate) { @@ -183,28 +202,59 @@ class DragRotateHandler { _onMouseUp(e: MouseEvent) { if (DOM.mouseButton(e) !== this._eventButton) return; - this._finish(e); + switch (this._state) { + case 'active': + this._state = 'enabled'; + DOM.suppressClick(); + this._unbind(); + this._deactivate(); + this._inertialRotate(e); + break; + case 'pending': + this._state = 'enabled'; + this._unbind(); + break; + default: + assert(false); + break; + } } _onBlur(e: FocusEvent) { - this._finish(e); + switch (this._state) { + case 'active': + this._state = 'enabled'; + this._unbind(); + this._deactivate(); + this._fireEvent('rotateend', e); + if (this._pitchWithRotate) { + this._fireEvent('pitchend', e); + } + this._fireEvent('moveend', e); + break; + case 'pending': + this._state = 'enabled'; + this._unbind(); + break; + default: + assert(false); + break; + } } - _finish(e: MouseEvent | FocusEvent) { + _unbind() { window.document.removeEventListener('mousemove', this._onMouseMove, {capture: true}); window.document.removeEventListener('mouseup', this._onMouseUp); window.removeEventListener('blur', this._onBlur); - DOM.enableDrag(); + } - if (!this.isActive()) return; - - this._active = false; + _deactivate() { delete this._lastMoveEvent; delete this._previousPos; + } - DOM.suppressClick(); - + _inertialRotate(e: MouseEvent) { this._fireEvent('rotateend', e); this._drainInertiaBuffer(); diff --git a/test/unit/ui/handler/drag_pan.test.js b/test/unit/ui/handler/drag_pan.test.js index d11dfc7d864..0ba2b4c3332 100644 --- a/test/unit/ui/handler/drag_pan.test.js +++ b/test/unit/ui/handler/drag_pan.test.js @@ -516,3 +516,81 @@ test('DragPanHandler does not begin a drag if preventDefault is called on the to map.remove(); t.end(); }); + +['dragstart', 'drag'].forEach(event => { + test(`DragPanHandler can be disabled on ${event} (#2419)`, (t) => { + const map = createMap(); + + map.on(event, () => map.dragPan.disable()); + + const dragstart = t.spy(); + const drag = t.spy(); + const dragend = t.spy(); + + map.on('dragstart', dragstart); + map.on('drag', drag); + map.on('dragend', dragend); + + simulate.mousedown(map.getCanvas()); + map._updateCamera(); + + simulate.mousemove(map.getCanvas()); + map._updateCamera(); + + t.equal(dragstart.callCount, 1); + t.equal(drag.callCount, event === 'dragstart' ? 0 : 1); + t.equal(dragend.callCount, 1); + t.equal(map.isMoving(), false); + t.equal(map.dragPan.isEnabled(), false); + + simulate.mouseup(map.getCanvas()); + map._updateCamera(); + + t.equal(dragstart.callCount, 1); + t.equal(drag.callCount, event === 'dragstart' ? 0 : 1); + t.equal(dragend.callCount, 1); + t.equal(map.isMoving(), false); + t.equal(map.dragPan.isEnabled(), false); + + map.remove(); + t.end(); + }); +}); + +test(`DragPanHandler can be disabled after mousedown (#2419)`, (t) => { + const map = createMap(); + + const dragstart = t.spy(); + const drag = t.spy(); + const dragend = t.spy(); + + map.on('dragstart', dragstart); + map.on('drag', drag); + map.on('dragend', dragend); + + simulate.mousedown(map.getCanvas()); + map._updateCamera(); + + map.dragPan.disable(); + + simulate.mousemove(map.getCanvas()); + map._updateCamera(); + + t.equal(dragstart.callCount, 0); + t.equal(drag.callCount, 0); + t.equal(dragend.callCount, 0); + t.equal(map.isMoving(), false); + t.equal(map.dragPan.isEnabled(), false); + + simulate.mouseup(map.getCanvas()); + map._updateCamera(); + + t.equal(dragstart.callCount, 0); + t.equal(drag.callCount, 0); + t.equal(dragend.callCount, 0); + t.equal(map.isMoving(), false); + t.equal(map.dragPan.isEnabled(), false); + + map.remove(); + t.end(); +}); diff --git a/test/unit/ui/handler/drag_rotate.test.js b/test/unit/ui/handler/drag_rotate.test.js index 23a2238a185..3d08f52e0a0 100644 --- a/test/unit/ui/handler/drag_rotate.test.js +++ b/test/unit/ui/handler/drag_rotate.test.js @@ -603,3 +603,81 @@ test('DragRotateHandler does not begin a drag if preventDefault is called on the map.remove(); t.end(); }); + +['rotatestart', 'rotate'].forEach(event => { + test(`DragRotateHandler can be disabled on ${event} (#2419)`, (t) => { + const map = createMap(); + + map.on(event, () => map.dragRotate.disable()); + + const rotatestart = t.spy(); + const rotate = t.spy(); + const rotateend = t.spy(); + + map.on('rotatestart', rotatestart); + map.on('rotate', rotate); + map.on('rotateend', rotateend); + + simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2}); + map._updateCamera(); + + simulate.mousemove(map.getCanvas(), {buttons: 2}); + map._updateCamera(); + + t.equal(rotatestart.callCount, 1); + t.equal(rotate.callCount, event === 'rotatestart' ? 0 : 1); + t.equal(rotateend.callCount, 1); + t.equal(map.isMoving(), false); + t.equal(map.dragRotate.isEnabled(), false); + + simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2}); + map._updateCamera(); + + t.equal(rotatestart.callCount, 1); + t.equal(rotate.callCount, event === 'rotatestart' ? 0 : 1); + t.equal(rotateend.callCount, 1); + t.equal(map.isMoving(), false); + t.equal(map.dragRotate.isEnabled(), false); + + map.remove(); + t.end(); + }); +}); + +test(`DragRotateHandler can be disabled after mousedown (#2419)`, (t) => { + const map = createMap(); + + const rotatestart = t.spy(); + const rotate = t.spy(); + const rotateend = t.spy(); + + map.on('rotatestart', rotatestart); + map.on('rotate', rotate); + map.on('rotateend', rotateend); + + simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2}); + map._updateCamera(); + + map.dragRotate.disable(); + + simulate.mousemove(map.getCanvas(), {buttons: 2}); + map._updateCamera(); + + t.equal(rotatestart.callCount, 0); + t.equal(rotate.callCount, 0); + t.equal(rotateend.callCount, 0); + t.equal(map.isMoving(), false); + t.equal(map.dragRotate.isEnabled(), false); + + simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2}); + map._updateCamera(); + + t.equal(rotatestart.callCount, 0); + t.equal(rotate.callCount, 0); + t.equal(rotateend.callCount, 0); + t.equal(map.isMoving(), false); + t.equal(map.dragRotate.isEnabled(), false); + + map.remove(); + t.end(); +});