From 5355fc6e4c4a2c9e046613a877c368b0ce5ffb1e Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 2 Mar 2018 08:07:31 -0800 Subject: [PATCH] Force touchstart, touchmove, and wheel listeners to be non-passive Yes, Chrome, yes WebKit: we really do intend to prevent the default scroll behavior for these events. This is the only way to silence the console warnings from Chrome and support iOS Safari 11.3+. --- src/ui/bind_handlers.js | 27 +++++++++++++------------ src/ui/handler/drag_pan.js | 18 ++++++++--------- src/ui/handler/touch_zoom_rotate.js | 8 ++++---- src/util/dom.js | 31 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/ui/bind_handlers.js b/src/ui/bind_handlers.js index c020e35efec..04d35260c95 100644 --- a/src/ui/bind_handlers.js +++ b/src/ui/bind_handlers.js @@ -5,6 +5,7 @@ const { MapTouchEvent, MapWheelEvent } = require('../ui/events'); +const DOM = require('../util/dom'); import type Map from './map'; @@ -30,19 +31,19 @@ module.exports = function bindHandlers(map: Map, options: {}) { } } - el.addEventListener('mouseout', onMouseOut, false); - el.addEventListener('mousedown', onMouseDown, false); - el.addEventListener('mouseup', onMouseUp, false); - el.addEventListener('mousemove', onMouseMove, false); - el.addEventListener('mouseover', onMouseOver, false); - el.addEventListener('touchstart', onTouchStart, false); - el.addEventListener('touchend', onTouchEnd, false); - el.addEventListener('touchmove', onTouchMove, false); - el.addEventListener('touchcancel', onTouchCancel, false); - el.addEventListener('click', onClick, false); - el.addEventListener('dblclick', onDblClick, false); - el.addEventListener('contextmenu', onContextMenu, false); - el.addEventListener('wheel', onWheel, false); + DOM.addEventListener(el, 'mouseout', onMouseOut); + DOM.addEventListener(el, 'mousedown', onMouseDown); + DOM.addEventListener(el, 'mouseup', onMouseUp); + DOM.addEventListener(el, 'mousemove', onMouseMove); + DOM.addEventListener(el, 'mouseover', onMouseOver); + DOM.addEventListener(el, 'touchstart', onTouchStart, {passive: false}); + DOM.addEventListener(el, 'touchmove', onTouchMove, {passive: true}); // `passive: true` because onTouchMove only sends a map event. + DOM.addEventListener(el, 'touchend', onTouchEnd); // The real action is in DragPanHandler and TouchZoomRotateHandler. + DOM.addEventListener(el, 'touchcancel', onTouchCancel); + DOM.addEventListener(el, 'click', onClick); + DOM.addEventListener(el, 'dblclick', onDblClick); + DOM.addEventListener(el, 'contextmenu', onContextMenu); + DOM.addEventListener(el, 'wheel', onWheel, {passive: false}); function onMouseDown(e: MouseEvent) { mouseDown = true; diff --git a/src/ui/handler/drag_pan.js b/src/ui/handler/drag_pan.js index 90853602060..33f92aac707 100644 --- a/src/ui/handler/drag_pan.js +++ b/src/ui/handler/drag_pan.js @@ -112,8 +112,8 @@ class DragPanHandler { // 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._onMouseUp); + DOM.addEventListener(window.document, 'mousemove', this._onMove, {capture: true}); + DOM.addEventListener(window.document, 'mouseup', this._onMouseUp); this._start(e); } @@ -127,8 +127,8 @@ class DragPanHandler { // 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); + DOM.addEventListener(window.document, 'touchmove', this._onMove, {capture: true, passive: false}); + DOM.addEventListener(window.document, 'touchend', this._onTouchEnd); this._start(e); } @@ -236,11 +236,11 @@ class DragPanHandler { } _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); + DOM.removeEventListener(window.document, 'touchmove', this._onMove, {capture: true, passive: false}); + DOM.removeEventListener(window.document, 'touchend', this._onTouchEnd); + DOM.removeEventListener(window.document, 'mousemove', this._onMove, {capture: true}); + DOM.removeEventListener(window.document, 'mouseup', this._onMouseUp); + DOM.removeEventListener(window, 'blur', this._onBlur); } _deactivate() { diff --git a/src/ui/handler/touch_zoom_rotate.js b/src/ui/handler/touch_zoom_rotate.js index cf115fc0251..65ebf40df46 100644 --- a/src/ui/handler/touch_zoom_rotate.js +++ b/src/ui/handler/touch_zoom_rotate.js @@ -120,8 +120,8 @@ class TouchZoomRotateHandler { this._gestureIntent = undefined; this._inertia = []; - window.document.addEventListener('touchmove', this._onMove, false); - window.document.addEventListener('touchend', this._onEnd, false); + DOM.addEventListener(window.document, 'touchmove', this._onMove, {passive: false}); + DOM.addEventListener(window.document, 'touchend', this._onEnd); } _getTouchEventData(e: TouchEvent) { @@ -196,8 +196,8 @@ class TouchZoomRotateHandler { } _onEnd(e: TouchEvent) { - window.document.removeEventListener('touchmove', this._onMove); - window.document.removeEventListener('touchend', this._onEnd); + DOM.removeEventListener(window.document, 'touchmove', this._onMove, {passive: false}); + DOM.removeEventListener(window.document, 'touchend', this._onEnd); const gestureIntent = this._gestureIntent; const startScale = this._startScale; diff --git a/src/util/dom.js b/src/util/dom.js index 489323c5324..affa74c6010 100644 --- a/src/util/dom.js +++ b/src/util/dom.js @@ -46,6 +46,37 @@ exports.setTransform = function(el: HTMLElement, value: string) { (el.style: any)[transformProp] = value; }; +// Feature detection for {passive: false} support in add/removeEventListener. +let passiveSupported = false; + +try { + const options = (Object.defineProperty: any)({}, "passive", { + get: function() { + passiveSupported = true; + } + }); + (window.addEventListener: any)("test", options, options); + (window.removeEventListener: any)("test", options, options); +} catch (err) { + passiveSupported = false; +} + +exports.addEventListener = function(target: *, type: *, callback: *, options: {passive?: boolean, capture?: boolean} = {}) { + if ('passive' in options && passiveSupported) { + target.addEventListener(type, callback, (options: any)); + } else { + target.addEventListener(type, callback, options.capture); + } +}; + +exports.removeEventListener = function(target: *, type: *, callback: *, options: {passive?: boolean, capture?: boolean} = {}) { + if ('passive' in options && passiveSupported) { + target.removeEventListener(type, callback, (options: any)); + } else { + target.removeEventListener(type, callback, options.capture); + } +}; + // Suppress the next click, but only if it's immediate. const suppressClick: MouseEventListener = function (e) { e.preventDefault();