diff --git a/src/ui/bind_handlers.js b/src/ui/bind_handlers.js index 8c655f25f8b..af3192b5f39 100644 --- a/src/ui/bind_handlers.js +++ b/src/ui/bind_handlers.js @@ -54,8 +54,14 @@ module.exports = function bindHandlers(map: Map, options: {}) { } map.boxZoom.onMouseDown(e); - map.dragRotate.onDown(e); - map.dragPan.onDown(e); + + if (!map.boxZoom.isActive() && !map.dragPan.isActive()) { + map.dragRotate.onMouseDown(e); + } + + if (!map.boxZoom.isActive() && !map.dragRotate.isActive()) { + map.dragPan.onMouseDown(e); + } } function onMouseUp(e: MouseEvent) { @@ -105,7 +111,10 @@ module.exports = function bindHandlers(map: Map, options: {}) { map.stop(); - map.dragPan.onDown(e); + if (!map.boxZoom.isActive() && !map.dragRotate.isActive()) { + map.dragPan.onTouchStart(e); + } + map.touchZoomRotate.onStart(e); map.doubleClickZoom.onTouchStart(mapEvent); } diff --git a/src/ui/handler/drag_pan.js b/src/ui/handler/drag_pan.js index 24dc9828f46..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,10 +35,13 @@ class DragPanHandler { constructor(map: Map) { this._map = map; this._el = map.getCanvasContainer(); + this._state = 'disabled'; util.bindAll([ '_onMove', - '_onUp', + '_onMouseUp', + '_onTouchEnd', + '_onBlur', '_onDragFrame' ], this); } @@ -49,7 +52,7 @@ class DragPanHandler { * @returns {boolean} `true` if the "drag to pan" interaction is enabled. */ isEnabled() { - return !!this._enabled; + return this._state !== 'disabled'; } /** @@ -58,7 +61,7 @@ class DragPanHandler { * @returns {boolean} `true` if the "drag to pan" interaction is active. */ isActive() { - return !!this._active; + return this._state === 'active'; } /** @@ -70,7 +73,7 @@ class DragPanHandler { enable() { if (this.isEnabled()) return; this._el.classList.add('mapboxgl-touch-drag-pan'); - this._enabled = true; + this._state = 'enabled'; } /** @@ -82,35 +85,60 @@ 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; + } } - onDown(e: MouseEvent | TouchEvent) { - if (!this.isEnabled()) return; - if (this._map.boxZoom.isActive()) return; - if (this._map.dragRotate.isActive()) return; - if (this.isActive()) return; + onMouseDown(e: MouseEvent) { + if (this._state !== 'enabled') return; + if (e.ctrlKey || DOM.mouseButton(e) !== 0) return; - // Bind window-level event listeners for move and up/end events. In the absence of + // Bind window-level event listeners for mousemove/up events. In the absence of // the pointer capture API, which is not supported by all necessary platforms, // window-level event listeners give us the best shot at capturing events that // fall outside the map canvas element. Use `{capture: true}` for the move event // to prevent map move events from being fired during a drag. - if (e.touches) { - if ((e.touches: any).length > 1) return; - window.document.addEventListener('touchmove', this._onMove, {capture: true}); - window.document.addEventListener('touchend', this._onUp); - } else { - if (e.ctrlKey || DOM.mouseButton((e: any)) !== 0) return; - window.document.addEventListener('mousemove', this._onMove, {capture: true}); - window.document.addEventListener('mouseup', this._onUp); - } + window.document.addEventListener('mousemove', this._onMove, {capture: true}); + window.document.addEventListener('mouseup', this._onMouseUp); + + this._start(e); + } + + onTouchStart(e: TouchEvent) { + if (this._state !== 'enabled') return; + if (e.touches.length > 1) return; + + // Bind window-level event listeners for touchmove/end events. In the absence of + // the pointer capture API, which is not supported by all necessary platforms, + // window-level event listeners give us the best shot at capturing events that + // fall outside the map canvas element. Use `{capture: true}` for the move event + // to prevent map move events from being fired during a drag. + window.document.addEventListener('touchmove', this._onMove, {capture: true}); + window.document.addEventListener('touchend', this._onTouchEnd); + this._start(e); + } + + _start(e: MouseEvent | TouchEvent) { // Deactivate when the window loses focus. Otherwise if a mouseup occurs when the window // isn't in focus, dragging will continue even though the mouse is no longer pressed. - window.addEventListener('blur', this._onUp); + window.addEventListener('blur', this._onBlur); - this._active = false; + this._state = 'pending'; this._previousPos = DOM.mousePos(this._el, e); this._inertia = [[browser.now(), this._previousPos]]; } @@ -123,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); } @@ -150,38 +178,84 @@ class DragPanHandler { delete this._lastMoveEvent; } - /** - * Called when dragging stops. - * @private - */ - _onUp(e: MouseEvent | TouchEvent | FocusEvent) { - if (e.type === 'mouseup' && DOM.mouseButton((e: any)) !== 0) return; + _onMouseUp(e: MouseEvent) { + if (DOM.mouseButton(e) !== 0) return; + 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) { + 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) { + 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; + } + } + + _unbind() { window.document.removeEventListener('touchmove', this._onMove, {capture: true}); - window.document.removeEventListener('touchend', this._onUp); + window.document.removeEventListener('touchend', this._onTouchEnd); window.document.removeEventListener('mousemove', this._onMove, {capture: true}); - window.document.removeEventListener('mouseup', this._onUp); - window.removeEventListener('blur', this._onUp); - - if (!this.isActive()) return; + window.document.removeEventListener('mouseup', this._onMouseUp); + window.removeEventListener('blur', this._onBlur); + } - 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; } @@ -191,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 c9b1530b8e0..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,13 +51,15 @@ 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; util.bindAll([ - '_onMove', - '_onUp', + '_onMouseMove', + '_onMouseUp', + '_onBlur', '_onDragFrame' ], this); } @@ -68,7 +70,7 @@ class DragRotateHandler { * @returns {boolean} `true` if the "drag to rotate" interaction is enabled. */ isEnabled() { - return !!this._enabled; + return this._state !== 'disabled'; } /** @@ -77,7 +79,7 @@ class DragRotateHandler { * @returns {boolean} `true` if the "drag to rotate" interaction is active. */ isActive() { - return !!this._active; + return this._state === 'active'; } /** @@ -88,7 +90,7 @@ class DragRotateHandler { */ enable() { if (this.isEnabled()) return; - this._enabled = true; + this._state = 'enabled'; } /** @@ -99,14 +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; + } } - onDown(e: MouseEvent) { - if (!this.isEnabled()) return; - if (this._map.boxZoom.isActive()) return; - if (this._map.dragPan.isActive()) return; - if (this.isActive()) return; + onMouseDown(e: MouseEvent) { + if (this._state !== 'enabled') return; if (this._button === 'right') { this._eventButton = DOM.mouseButton(e); @@ -123,14 +140,14 @@ class DragRotateHandler { // window-level event listeners give us the best shot at capturing events that // fall outside the map canvas element. Use `{capture: true}` for the move event // to prevent map move events from being fired during a drag. - window.document.addEventListener('mousemove', this._onMove, {capture: true}); - window.document.addEventListener('mouseup', this._onUp); + window.document.addEventListener('mousemove', this._onMouseMove, {capture: true}); + window.document.addEventListener('mouseup', this._onMouseUp); // Deactivate when the window loses focus. Otherwise if a mouseup occurs when the window // isn't in focus, dragging will continue even though the mouse is no longer pressed. - window.addEventListener('blur', this._onUp); + 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 @@ -138,12 +155,12 @@ class DragRotateHandler { e.preventDefault(); } - _onMove(e: MouseEvent) { + _onMouseMove(e: MouseEvent) { 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,23 +200,61 @@ class DragRotateHandler { this._previousPos = this._pos; } - _onUp(e: MouseEvent | FocusEvent) { - if (e.type === 'mouseup' && DOM.mouseButton((e: any)) !== this._eventButton) return; + _onMouseUp(e: MouseEvent) { + if (DOM.mouseButton(e) !== this._eventButton) return; + 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; + } + } - window.document.removeEventListener('mousemove', this._onMove, {capture: true}); - window.document.removeEventListener('mouseup', this._onUp); - window.removeEventListener('blur', this._onUp); + _onBlur(e: FocusEvent) { + 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; + } + } + _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(); +});