Skip to content

Commit

Permalink
Automatically filter mouseevents without the left mouse button
Browse files Browse the repository at this point in the history
Keep test compatibility by treating undefined button values as left mouse button

Fixes #2166
  • Loading branch information
dfreedm committed Jul 29, 2015
1 parent 720f2c0 commit bbc3b57
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 10 deletions.
113 changes: 104 additions & 9 deletions src/standard/gestures.html
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -387,16 +456,31 @@
}
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;
this.info.moves = [];
this.info.x = 0;
this.info.y = 0;
this.info.prevent = false;
this.untrackDocument();
},

hasMovedEnough: function(x, y) {
Expand All @@ -412,6 +496,9 @@
},

mousedown: function(e) {
if (!hasLeftMouseButton(e)) {
return;
}
var t = Gestures.findOriginalTarget(e);
var self = this;
var movefn = function movefn(e) {
Expand All @@ -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;
}
Expand All @@ -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;
},
Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 20 additions & 0 deletions test/unit/gestures-elements.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,23 @@
});
</script>
</dom-module>

<dom-module id="x-buttons">
<script>
Polymer({
is: 'x-buttons',
listeners: {
'down': 'handle',
'up': 'handle',
'tap': 'handle',
'track': 'handle'
},
created: function() {
this.stream = [];
},
handle: function(e) {
this.stream.push(e);
}
});
</script>
</dom-module>
96 changes: 95 additions & 1 deletion test/unit/gestures.html
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down Expand Up @@ -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, <skipped>, 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');
});
});
});
</script>

</body>
Expand Down

0 comments on commit bbc3b57

Please sign in to comment.