Skip to content

Commit bbc3b57

Browse files
committed
Automatically filter mouseevents without the left mouse button
Keep test compatibility by treating undefined button values as left mouse button Fixes #2166
1 parent 720f2c0 commit bbc3b57

File tree

3 files changed

+219
-10
lines changed

3 files changed

+219
-10
lines changed

src/standard/gestures.html

+104-9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@
2626
// Disabling "mouse" handlers for 2500ms is enough
2727
var MOUSE_TIMEOUT = 2500;
2828
var MOUSE_EVENTS = ['mousedown', 'mousemove', 'mouseup', 'click'];
29+
// an array of bitmask values for mapping MouseEvent.which to MouseEvent.buttons
30+
var MOUSE_WHICH_TO_BUTTONS = [0, 1, 4, 2];
31+
var MOUSE_HAS_BUTTONS = (function() {
32+
try {
33+
return new MouseEvent('test', {buttons: 1}).buttons === 1;
34+
} catch (e) {
35+
return false;
36+
}
37+
})();
2938

3039
// Check for touch-only devices
3140
var IS_TOUCH_ONLY = navigator.userAgent.match(/iP(?:[oa]d|hone)|Android/);
@@ -78,6 +87,30 @@
7887
Polymer.Debounce(POINTERSTATE.mouse.mouseIgnoreJob, unset, MOUSE_TIMEOUT);
7988
}
8089

90+
function hasLeftMouseButton(ev) {
91+
var type = ev.type;
92+
// exit early if the event is not a mouse event
93+
if (MOUSE_EVENTS.indexOf(type) === -1) {
94+
return false;
95+
}
96+
// ev.button is not reliable for mousemove (0 is overloaded as both left button and no buttons)
97+
// instead we use ev.buttons (bitmask of buttons) or fall back to ev.which (deprecated, 0 for no buttons, 1 for left button)
98+
if (type === 'mousemove') {
99+
// allow undefined for testing events
100+
var buttons = ev.buttons === undefined ? 1 : ev.buttons;
101+
if ((ev instanceof window.MouseEvent) && !MOUSE_HAS_BUTTONS) {
102+
buttons = MOUSE_WHICH_TO_BUTTONS[ev.which] || 0;
103+
}
104+
// buttons is a bitmask, check that the left button bit is set (1)
105+
return Boolean(buttons & 1);
106+
} else {
107+
// allow undefined for testing events
108+
var button = ev.button === undefined ? 0 : ev.button;
109+
// ev.button is 0 in mousedown/mouseup/click for left button activation
110+
return button === 0;
111+
}
112+
}
113+
81114
var POINTERSTATE = {
82115
mouse: {
83116
target: null,
@@ -336,16 +369,52 @@
336369
Gestures.register({
337370
name: 'downup',
338371
deps: ['mousedown', 'touchstart', 'touchend'],
372+
flow: {
373+
start: ['mousedown', 'touchstart'],
374+
end: ['mouseup', 'touchend']
375+
},
339376
emits: ['down', 'up'],
340377

378+
info: {
379+
movefn: function(){},
380+
upfn: function(){}
381+
},
382+
383+
reset: function() {
384+
this.untrackDocument();
385+
},
386+
387+
trackDocument: function(movefn, upfn) {
388+
this.info.movefn = movefn;
389+
this.info.upfn = upfn;
390+
document.addEventListener('mousemove', movefn);
391+
document.addEventListener('mouseup', upfn);
392+
},
393+
394+
untrackDocument: function() {
395+
document.removeEventListener('mousemove', this.info.movefn);
396+
document.removeEventListener('mouseup', this.info.upfn);
397+
},
398+
341399
mousedown: function(e) {
400+
if (!hasLeftMouseButton(e)) {
401+
return;
402+
}
342403
var t = Gestures.findOriginalTarget(e);
343404
var self = this;
405+
var movefn = function movefn(e) {
406+
if (!hasLeftMouseButton(e)) {
407+
self.fire('up', t, e);
408+
self.untrackDocument();
409+
}
410+
};
344411
var upfn = function upfn(e) {
345-
self.fire('up', t, e);
346-
document.removeEventListener('mouseup', upfn);
412+
if (hasLeftMouseButton(e)) {
413+
self.fire('up', t, e);
414+
}
415+
self.untrackDocument();
347416
};
348-
document.addEventListener('mouseup', upfn);
417+
this.trackDocument(movefn, upfn);
349418
this.fire('down', t, e);
350419
},
351420
touchstart: function(e) {
@@ -387,16 +456,31 @@
387456
}
388457
this.moves.push(move);
389458
},
459+
movefn: function(){},
460+
upfn: function(){},
390461
prevent: false
391462
},
392463

464+
trackDocument: function(movefn, upfn) {
465+
this.info.movefn = movefn;
466+
this.info.upfn = upfn;
467+
document.addEventListener('mousemove', movefn);
468+
document.addEventListener('mouseup', upfn);
469+
},
470+
471+
untrackDocument: function() {
472+
document.removeEventListener('mousemove', this.info.movefn);
473+
document.removeEventListener('mouseup', this.info.upfn);
474+
},
475+
393476
reset: function() {
394477
this.info.state = 'start';
395478
this.info.started = false;
396479
this.info.moves = [];
397480
this.info.x = 0;
398481
this.info.y = 0;
399482
this.info.prevent = false;
483+
this.untrackDocument();
400484
},
401485

402486
hasMovedEnough: function(x, y) {
@@ -412,6 +496,9 @@
412496
},
413497

414498
mousedown: function(e) {
499+
if (!hasLeftMouseButton(e)) {
500+
return;
501+
}
415502
var t = Gestures.findOriginalTarget(e);
416503
var self = this;
417504
var movefn = function movefn(e) {
@@ -420,6 +507,11 @@
420507
// first move is 'start', subsequent moves are 'move', mouseup is 'end'
421508
self.info.state = self.info.started ? (e.type === 'mouseup' ? 'end' : 'track') : 'start';
422509
self.info.addMove({x: x, y: y});
510+
if (!hasLeftMouseButton(e)) {
511+
// always fire "end"
512+
self.info.state = 'end';
513+
self.untrackDocument();
514+
}
423515
self.fire(t, e);
424516
self.info.started = true;
425517
}
@@ -429,13 +521,12 @@
429521
Gestures.prevent('tap');
430522
movefn(e);
431523
}
524+
432525
// remove the temporary listeners
433-
document.removeEventListener('mousemove', movefn);
434-
document.removeEventListener('mouseup', upfn);
526+
self.untrackDocument();
435527
};
436528
// add temporary document listeners as mouse retargets
437-
document.addEventListener('mousemove', movefn);
438-
document.addEventListener('mouseup', upfn);
529+
this.trackDocument(movefn, upfn);
439530
this.info.x = e.clientX;
440531
this.info.y = e.clientY;
441532
},
@@ -523,10 +614,14 @@
523614
},
524615

525616
mousedown: function(e) {
526-
this.save(e);
617+
if (hasLeftMouseButton(e)) {
618+
this.save(e);
619+
}
527620
},
528621
click: function(e) {
529-
this.forward(e);
622+
if (hasLeftMouseButton(e)) {
623+
this.forward(e);
624+
}
530625
},
531626

532627
touchstart: function(e) {

test/unit/gestures-elements.html

+20
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,23 @@
119119
});
120120
</script>
121121
</dom-module>
122+
123+
<dom-module id="x-buttons">
124+
<script>
125+
Polymer({
126+
is: 'x-buttons',
127+
listeners: {
128+
'down': 'handle',
129+
'up': 'handle',
130+
'tap': 'handle',
131+
'track': 'handle'
132+
},
133+
created: function() {
134+
this.stream = [];
135+
},
136+
handle: function(e) {
137+
this.stream.push(e);
138+
}
139+
});
140+
</script>
141+
</dom-module>

test/unit/gestures.html

+95-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
test('tap on x-foo and check localTarget and rootTarget', function() {
4040
var foo = app.$.foo;
41-
foo.dispatchEvent(new CustomEvent('click', {bubble: true}));
41+
foo.dispatchEvent(new CustomEvent('click', {bubbles: true}));
4242
assert.equal(app._testLocalTarget, app, 'local target');
4343
assert.equal(app._testRootTarget, foo, 'root target');
4444
});
@@ -280,6 +280,100 @@
280280
assert.equal(el.stream[1].type, 'up', 'up was found');
281281
});
282282
});
283+
284+
suite('Buttons', function() {
285+
var el;
286+
287+
setup(function() {
288+
el = document.createElement('x-buttons');
289+
document.body.appendChild(el);
290+
});
291+
292+
teardown(function() {
293+
el.parentNode.removeChild(el);
294+
});
295+
296+
suite('Down and Up', function() {
297+
test('Left Mouse Button Only', function() {
298+
var options = {bubbles: true};
299+
var evLeftDown = new CustomEvent('mousedown', options);
300+
// left button
301+
evLeftDown.button = 0;
302+
evLeftDown.clientX = 1;
303+
var evLeftUp = new CustomEvent('mouseup', options);
304+
var evRightDown = new CustomEvent('mousedown', options);
305+
// right button
306+
evRightDown.button = 2;
307+
evRightDown.clientX = 2;
308+
var evRightUp = new CustomEvent('mouseup', options);
309+
310+
el.dispatchEvent(evLeftDown);
311+
el.dispatchEvent(evLeftUp);
312+
el.dispatchEvent(evRightDown);
313+
el.dispatchEvent(evRightUp);
314+
315+
assert.equal(el.stream.length, 2, 'only saw one up and down pair');
316+
assert.equal(el.stream[0].type, 'down');
317+
assert.equal(el.stream[1].type, 'up');
318+
assert.equal(el.stream[0].detail.x, 1, 'only from the left button');
319+
});
320+
321+
test('Recover from right click', function() {
322+
var options = {bubbles: true};
323+
var evDown = new CustomEvent('mousedown', options);
324+
var evMove = new CustomEvent('mousemove', options);
325+
evMove.buttons = 0;
326+
var evUp = new CustomEvent('mouseup', options);
327+
328+
el.dispatchEvent(evDown);
329+
el.dispatchEvent(evMove);
330+
el.dispatchEvent(evUp);
331+
332+
assert.equal(el.stream.length, 2, 'always get an up');
333+
});
334+
});
335+
336+
suite('Tap', function() {
337+
test('Left Mouse Button Only', function() {
338+
var evMid = new CustomEvent('click', {bubbles: true});
339+
evMid.button = 1;
340+
var evLeft = new CustomEvent('click', {bubbles: true});
341+
evLeft.button = 0;
342+
343+
el.dispatchEvent(evMid);
344+
el.dispatchEvent(evLeft);
345+
346+
assert.equal(el.stream.length, 1, 'only one tap');
347+
});
348+
});
349+
350+
suite('Track', function() {
351+
test('Left Mouse Button Only', function() {
352+
var options = {bubbles: true};
353+
var ev = new CustomEvent('mousedown', options);
354+
ev.clientX = ev.clientY = 0;
355+
el.dispatchEvent(ev);
356+
for (var i = 0; i < 5; i++) {
357+
ev = new CustomEvent('mousemove', options);
358+
ev.clientX = 10 * i;
359+
ev.clientY = 10 * i;
360+
// left button until move 4
361+
ev.buttons = (i > 3) ? 2 : 1;
362+
el.dispatchEvent(ev);
363+
}
364+
el.dispatchEvent(new CustomEvent('mouseup', options));
365+
366+
// down, <skipped>, track:start, track:track, track:track, track:end, up
367+
assert.equal(el.stream.length, 6);
368+
assert.equal(el.stream[0].type, 'down');
369+
assert.equal(el.stream[1].detail.state, 'start');
370+
assert.equal(el.stream[2].detail.state, 'track');
371+
assert.equal(el.stream[3].detail.state, 'track');
372+
assert.equal(el.stream[4].type, 'up');
373+
assert.equal(el.stream[5].detail.state, 'end');
374+
});
375+
});
376+
});
283377
</script>
284378

285379
</body>

0 commit comments

Comments
 (0)