From bbc3b572200f1e4d832aa8c60d4a598210f80800 Mon Sep 17 00:00:00 2001 From: Daniel Freedman Date: Tue, 28 Jul 2015 18:07:08 -0700 Subject: [PATCH] Automatically filter mouseevents without the left mouse button Keep test compatibility by treating undefined button values as left mouse button Fixes #2166 --- src/standard/gestures.html | 113 ++++++++++++++++++++++++++++--- test/unit/gestures-elements.html | 20 ++++++ test/unit/gestures.html | 96 +++++++++++++++++++++++++- 3 files changed, 219 insertions(+), 10 deletions(-) diff --git a/src/standard/gestures.html b/src/standard/gestures.html index dfea157a15..ed721f334f 100644 --- a/src/standard/gestures.html +++ b/src/standard/gestures.html @@ -26,6 +26,15 @@ // Disabling "mouse" handlers for 2500ms is enough var MOUSE_TIMEOUT = 2500; var MOUSE_EVENTS = ['mousedown', 'mousemove', 'mouseup', 'click']; + // an array of bitmask values for mapping MouseEvent.which to MouseEvent.buttons + var MOUSE_WHICH_TO_BUTTONS = [0, 1, 4, 2]; + var MOUSE_HAS_BUTTONS = (function() { + try { + return new MouseEvent('test', {buttons: 1}).buttons === 1; + } catch (e) { + return false; + } + })(); // Check for touch-only devices var IS_TOUCH_ONLY = navigator.userAgent.match(/iP(?:[oa]d|hone)|Android/); @@ -78,6 +87,30 @@ Polymer.Debounce(POINTERSTATE.mouse.mouseIgnoreJob, unset, MOUSE_TIMEOUT); } + function hasLeftMouseButton(ev) { + var type = ev.type; + // exit early if the event is not a mouse event + if (MOUSE_EVENTS.indexOf(type) === -1) { + return false; + } + // ev.button is not reliable for mousemove (0 is overloaded as both left button and no buttons) + // instead we use ev.buttons (bitmask of buttons) or fall back to ev.which (deprecated, 0 for no buttons, 1 for left button) + if (type === 'mousemove') { + // allow undefined for testing events + var buttons = ev.buttons === undefined ? 1 : ev.buttons; + if ((ev instanceof window.MouseEvent) && !MOUSE_HAS_BUTTONS) { + buttons = MOUSE_WHICH_TO_BUTTONS[ev.which] || 0; + } + // buttons is a bitmask, check that the left button bit is set (1) + return Boolean(buttons & 1); + } else { + // allow undefined for testing events + var button = ev.button === undefined ? 0 : ev.button; + // ev.button is 0 in mousedown/mouseup/click for left button activation + return button === 0; + } + } + var POINTERSTATE = { mouse: { target: null, @@ -336,16 +369,52 @@ Gestures.register({ name: 'downup', deps: ['mousedown', 'touchstart', 'touchend'], + flow: { + start: ['mousedown', 'touchstart'], + end: ['mouseup', 'touchend'] + }, emits: ['down', 'up'], + info: { + movefn: function(){}, + upfn: function(){} + }, + + reset: function() { + this.untrackDocument(); + }, + + trackDocument: function(movefn, upfn) { + this.info.movefn = movefn; + this.info.upfn = upfn; + document.addEventListener('mousemove', movefn); + document.addEventListener('mouseup', upfn); + }, + + untrackDocument: function() { + document.removeEventListener('mousemove', this.info.movefn); + document.removeEventListener('mouseup', this.info.upfn); + }, + mousedown: function(e) { + if (!hasLeftMouseButton(e)) { + return; + } var t = Gestures.findOriginalTarget(e); var self = this; + var movefn = function movefn(e) { + if (!hasLeftMouseButton(e)) { + self.fire('up', t, e); + self.untrackDocument(); + } + }; var upfn = function upfn(e) { - self.fire('up', t, e); - document.removeEventListener('mouseup', upfn); + if (hasLeftMouseButton(e)) { + self.fire('up', t, e); + } + self.untrackDocument(); }; - document.addEventListener('mouseup', upfn); + this.trackDocument(movefn, upfn); this.fire('down', t, e); }, touchstart: function(e) { @@ -387,9 +456,23 @@ } this.moves.push(move); }, + movefn: function(){}, + upfn: function(){}, prevent: false }, + trackDocument: function(movefn, upfn) { + this.info.movefn = movefn; + this.info.upfn = upfn; + document.addEventListener('mousemove', movefn); + document.addEventListener('mouseup', upfn); + }, + + untrackDocument: function() { + document.removeEventListener('mousemove', this.info.movefn); + document.removeEventListener('mouseup', this.info.upfn); + }, + reset: function() { this.info.state = 'start'; this.info.started = false; @@ -397,6 +480,7 @@ this.info.x = 0; this.info.y = 0; this.info.prevent = false; + this.untrackDocument(); }, hasMovedEnough: function(x, y) { @@ -412,6 +496,9 @@ }, mousedown: function(e) { + if (!hasLeftMouseButton(e)) { + return; + } var t = Gestures.findOriginalTarget(e); var self = this; var movefn = function movefn(e) { @@ -420,6 +507,11 @@ // first move is 'start', subsequent moves are 'move', mouseup is 'end' self.info.state = self.info.started ? (e.type === 'mouseup' ? 'end' : 'track') : 'start'; self.info.addMove({x: x, y: y}); + if (!hasLeftMouseButton(e)) { + // always fire "end" + self.info.state = 'end'; + self.untrackDocument(); + } self.fire(t, e); self.info.started = true; } @@ -429,13 +521,12 @@ Gestures.prevent('tap'); movefn(e); } + // remove the temporary listeners - document.removeEventListener('mousemove', movefn); - document.removeEventListener('mouseup', upfn); + self.untrackDocument(); }; // add temporary document listeners as mouse retargets - document.addEventListener('mousemove', movefn); - document.addEventListener('mouseup', upfn); + this.trackDocument(movefn, upfn); this.info.x = e.clientX; this.info.y = e.clientY; }, @@ -523,10 +614,14 @@ }, mousedown: function(e) { - this.save(e); + if (hasLeftMouseButton(e)) { + this.save(e); + } }, click: function(e) { - this.forward(e); + if (hasLeftMouseButton(e)) { + this.forward(e); + } }, touchstart: function(e) { diff --git a/test/unit/gestures-elements.html b/test/unit/gestures-elements.html index 6108b65c53..17c55f2b59 100644 --- a/test/unit/gestures-elements.html +++ b/test/unit/gestures-elements.html @@ -119,3 +119,23 @@ }); + + + + diff --git a/test/unit/gestures.html b/test/unit/gestures.html index eef5fcd09a..bcef38e9ea 100644 --- a/test/unit/gestures.html +++ b/test/unit/gestures.html @@ -38,7 +38,7 @@ test('tap on x-foo and check localTarget and rootTarget', function() { var foo = app.$.foo; - foo.dispatchEvent(new CustomEvent('click', {bubble: true})); + foo.dispatchEvent(new CustomEvent('click', {bubbles: true})); assert.equal(app._testLocalTarget, app, 'local target'); assert.equal(app._testRootTarget, foo, 'root target'); }); @@ -280,6 +280,100 @@ assert.equal(el.stream[1].type, 'up', 'up was found'); }); }); + + suite('Buttons', function() { + var el; + + setup(function() { + el = document.createElement('x-buttons'); + document.body.appendChild(el); + }); + + teardown(function() { + el.parentNode.removeChild(el); + }); + + suite('Down and Up', function() { + test('Left Mouse Button Only', function() { + var options = {bubbles: true}; + var evLeftDown = new CustomEvent('mousedown', options); + // left button + evLeftDown.button = 0; + evLeftDown.clientX = 1; + var evLeftUp = new CustomEvent('mouseup', options); + var evRightDown = new CustomEvent('mousedown', options); + // right button + evRightDown.button = 2; + evRightDown.clientX = 2; + var evRightUp = new CustomEvent('mouseup', options); + + el.dispatchEvent(evLeftDown); + el.dispatchEvent(evLeftUp); + el.dispatchEvent(evRightDown); + el.dispatchEvent(evRightUp); + + assert.equal(el.stream.length, 2, 'only saw one up and down pair'); + assert.equal(el.stream[0].type, 'down'); + assert.equal(el.stream[1].type, 'up'); + assert.equal(el.stream[0].detail.x, 1, 'only from the left button'); + }); + + test('Recover from right click', function() { + var options = {bubbles: true}; + var evDown = new CustomEvent('mousedown', options); + var evMove = new CustomEvent('mousemove', options); + evMove.buttons = 0; + var evUp = new CustomEvent('mouseup', options); + + el.dispatchEvent(evDown); + el.dispatchEvent(evMove); + el.dispatchEvent(evUp); + + assert.equal(el.stream.length, 2, 'always get an up'); + }); + }); + + suite('Tap', function() { + test('Left Mouse Button Only', function() { + var evMid = new CustomEvent('click', {bubbles: true}); + evMid.button = 1; + var evLeft = new CustomEvent('click', {bubbles: true}); + evLeft.button = 0; + + el.dispatchEvent(evMid); + el.dispatchEvent(evLeft); + + assert.equal(el.stream.length, 1, 'only one tap'); + }); + }); + + suite('Track', function() { + test('Left Mouse Button Only', function() { + var options = {bubbles: true}; + var ev = new CustomEvent('mousedown', options); + ev.clientX = ev.clientY = 0; + el.dispatchEvent(ev); + for (var i = 0; i < 5; i++) { + ev = new CustomEvent('mousemove', options); + ev.clientX = 10 * i; + ev.clientY = 10 * i; + // left button until move 4 + ev.buttons = (i > 3) ? 2 : 1; + el.dispatchEvent(ev); + } + el.dispatchEvent(new CustomEvent('mouseup', options)); + + // down, , track:start, track:track, track:track, track:end, up + assert.equal(el.stream.length, 6); + assert.equal(el.stream[0].type, 'down'); + assert.equal(el.stream[1].detail.state, 'start'); + assert.equal(el.stream[2].detail.state, 'track'); + assert.equal(el.stream[3].detail.state, 'track'); + assert.equal(el.stream[4].type, 'up'); + assert.equal(el.stream[5].detail.state, 'end'); + }); + }); + });