Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable touch interactions for select/lasso/others #1804

Merged
merged 17 commits into from
Jul 10, 2017
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"gl-shader": "4.2.0",
"gl-spikes2d": "^1.0.1",
"gl-surface3d": "^1.3.0",
"has-hover": "^1.0.0",
"mapbox-gl": "^0.22.0",
"matrix-camera-controller": "^2.1.3",
"mouse-change": "^1.4.0",
Expand Down
70 changes: 52 additions & 18 deletions src/components/dragelement/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

'use strict';

var mouseOffset = require('mouse-event-offset');
var hasHover = require('has-hover');

var Plotly = require('../../plotly');
var Lib = require('../../lib');

Expand Down Expand Up @@ -61,18 +64,25 @@ dragElement.init = function init(options) {
startX,
startY,
newMouseDownTime,
cursor,
dragCover,
initialTarget;

if(!gd._mouseDownTime) gd._mouseDownTime = 0;

options.element.style.pointerEvents = 'all';

options.element.onmousedown = onStart;
options.element.ontouchstart = onStart;

function onStart(e) {
// make dragging and dragged into properties of gd
// so that others can look at and modify them
gd._dragged = false;
gd._dragging = true;
startX = e.clientX;
startY = e.clientY;
var offset = pointerOffset(e);
startX = offset[0];
startY = offset[1];
initialTarget = e.target;

newMouseDownTime = (new Date()).getTime();
Expand All @@ -88,20 +98,30 @@ dragElement.init = function init(options) {

if(options.prepFn) options.prepFn(e, startX, startY);

dragCover = coverSlip();

dragCover.onmousemove = onMove;
dragCover.onmouseup = onDone;
dragCover.onmouseout = onDone;
if(hasHover) {
dragCover = coverSlip();
dragCover.style.cursor = window.getComputedStyle(options.element).cursor;
}
else {
// document acts as a dragcover for mobile, bc we can't create dragcover dynamically
dragCover = document;
cursor = window.getComputedStyle(document.documentElement).cursor;
document.documentElement.style.cursor = window.getComputedStyle(options.element).cursor;
}

dragCover.style.cursor = window.getComputedStyle(options.element).cursor;
dragCover.addEventListener('mousemove', onMove);
dragCover.addEventListener('mouseup', onDone);
dragCover.addEventListener('mouseout', onDone);
dragCover.addEventListener('touchmove', onMove);
dragCover.addEventListener('touchend', onDone);

return Lib.pauseEvent(e);
}

function onMove(e) {
var dx = e.clientX - startX,
dy = e.clientY - startY,
var offset = pointerOffset(e),
dx = offset[0] - startX,
dy = offset[1] - startY,
minDrag = options.minDrag || constants.MINDRAG;

if(Math.abs(dx) < minDrag) dx = 0;
Expand All @@ -117,10 +137,19 @@ dragElement.init = function init(options) {
}

function onDone(e) {
dragCover.onmousemove = null;
dragCover.onmouseup = null;
dragCover.onmouseout = null;
Lib.removeElement(dragCover);
dragCover.removeEventListener('mousemove', onMove);
dragCover.removeEventListener('mouseup', onDone);
dragCover.removeEventListener('mouseout', onDone);
dragCover.removeEventListener('touchmove', onMove);
dragCover.removeEventListener('touchend', onDone);

if(hasHover) {
Lib.removeElement(dragCover);
}
else if(cursor) {
dragCover.documentElement.style.cursor = cursor;
cursor = null;
}

if(!gd._dragging) {
gd._dragged = false;
Expand All @@ -143,12 +172,13 @@ dragElement.init = function init(options) {
e2 = new MouseEvent('click', e);
}
catch(err) {
var offset = pointerOffset(e);
e2 = document.createEvent('MouseEvents');
e2.initMouseEvent('click',
e.bubbles, e.cancelable,
e.view, e.detail,
e.screenX, e.screenY,
e.clientX, e.clientY,
offset[0], offset[1],
e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
e.button, e.relatedTarget);
}
Expand All @@ -162,9 +192,6 @@ dragElement.init = function init(options) {

return Lib.pauseEvent(e);
}

options.element.onmousedown = onStart;
options.element.style.pointerEvents = 'all';
};

function coverSlip() {
Expand All @@ -191,3 +218,10 @@ function finishDrag(gd) {
gd._dragging = false;
if(gd._replotPending) Plotly.plot(gd);
}

function pointerOffset(e) {
return mouseOffset(
e.changedTouches ? e.changedTouches[0] : e,
document.body
);
}
6 changes: 6 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

var d3 = require('d3');
var isNumeric = require('fast-isnumeric');
var hasHover = require('has-hover');

var Plotly = require('../plotly');
var Lib = require('../lib');
Expand Down Expand Up @@ -423,6 +424,11 @@ function setPlotContext(gd, config) {
context.showLink = false;
context.displayModeBar = false;
}

// make sure hover-only devices have mode bar visible
if(context.displayModeBar === 'hover' && !hasHover) {
context.displayModeBar = true;
}
}

function plotPolar(gd, data, layout) {
Expand Down
22 changes: 20 additions & 2 deletions src/plots/gl2d/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

var mouseChange = require('mouse-change');
var mouseWheel = require('mouse-wheel');
var mouseOffset = require('mouse-event-offset');
var cartesianConstants = require('../cartesian/constants');

module.exports = createCamera;
Expand Down Expand Up @@ -55,7 +56,24 @@ function createCamera(scene) {
return false;
}

result.mouseListener = mouseChange(element, function(buttons, x, y) {
result.mouseListener = mouseChange(element, handleInteraction);

// enable simple touch interactions
element.addEventListener('touchstart', function(ev) {
var xy = mouseOffset(ev.changedTouches[0], element);
handleInteraction(0, xy[0], xy[1]);
handleInteraction(1, xy[0], xy[1]);
});
element.addEventListener('touchmove', function(ev) {
ev.preventDefault();
var xy = mouseOffset(ev.changedTouches[0], element);
handleInteraction(1, xy[0], xy[1]);
});
element.addEventListener('touchend', function() {
handleInteraction(0, result.lastPos[0], result.lastPos[1]);
});

function handleInteraction(buttons, x, y) {
var dataBox = scene.calcDataBox(),
viewBox = plot.viewBox;

Expand Down Expand Up @@ -235,7 +253,7 @@ function createCamera(scene) {

result.lastPos[0] = x;
result.lastPos[1] = y;
});
}

result.wheelListener = mouseWheel(element, function(dx, dy) {
var dataBox = scene.calcDataBox(),
Expand Down
12 changes: 12 additions & 0 deletions src/plots/gl2d/scene2d.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,11 +525,23 @@ proto.updateTraces = function(fullData, calcData) {
};

proto.updateFx = function(dragmode) {
// switch to svg interactions in lasso/select mode
if(dragmode === 'lasso' || dragmode === 'select') {
this.mouseContainer.style['pointer-events'] = 'none';
} else {
this.mouseContainer.style['pointer-events'] = 'auto';
}

// set proper cursor
if(dragmode === 'pan') {
this.mouseContainer.style.cursor = 'move';
}
else if(dragmode === 'zoom') {
this.mouseContainer.style.cursor = 'crosshair';
}
else {
this.mouseContainer.style.cursor = null;
}
};

proto.emitPointAction = function(nextSelection, eventType) {
Expand Down
2 changes: 1 addition & 1 deletion src/traces/scattergl/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ proto.updateFast = function(options) {
this.idToIndex = idToIndex;

// form selected set
if(selection) {
if(selection && selection.length) {
selPositions = new Float64Array(2 * selection.length);

for(i = 0, l = selection.length; i < l; i++) {
Expand Down
44 changes: 44 additions & 0 deletions test/jasmine/assets/touch_event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
var Lib = require('../../../src/lib');

module.exports = function(type, x, y, opts) {
var el = (opts && opts.element) || document.elementFromPoint(x, y),
ev;

var touchObj = new Touch({
identifier: Date.now(),
target: el,
clientX: x,
clientY: y,
radiusX: 2.5,
radiusY: 2.5,
rotationAngle: 10,
force: 0.5,
});

var fullOpts = {
touches: [touchObj],
targetTouches: [],
changedTouches: [touchObj],
bubbles: true
};

if(opts && opts.altKey) {
fullOpts.altKey = opts.altKey;
}
if(opts && opts.ctrlKey) {
fullOpts.ctrlKey = opts.ctrlKey;
}
if(opts && opts.metaKey) {
fullOpts.metaKey = opts.metaKey;
}
if(opts && opts.shiftKey) {
fullOpts.shiftKey = opts.shiftKey;
}


ev = new window.TouchEvent(type, Lib.extendFlat({}, fullOpts, opts));

el.dispatchEvent(ev);

return el;
};
1 change: 1 addition & 0 deletions test/jasmine/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ func.defaultConfig = {
_Chrome: {
base: 'Chrome',
flags: [
'--touch-events',
'--window-size=' + argv.width + ',' + argv.height,
isCI ? '--ignore-gpu-blacklist' : ''
]
Expand Down
61 changes: 60 additions & 1 deletion test/jasmine/tests/select_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var doubleClick = require('../assets/double_click');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var mouseEvent = require('../assets/mouse_event');
var touchEvent = require('../assets/touch_event');
var customMatchers = require('../assets/custom_matchers');


Expand All @@ -22,9 +23,23 @@ describe('select box and lasso', function() {

afterEach(destroyGraphDiv);

function drag(path) {
function drag(path, options) {
var len = path.length;

if(!options) options = {type: 'mouse'};

if(options.type === 'touch') {
touchEvent('touchstart', path[0][0], path[0][1]);

path.slice(1, len).forEach(function(pt) {
touchEvent('touchmove', pt[0], pt[1]);
});

touchEvent('touchend', path[len - 1][0], path[len - 1][1]);

return;
}

mouseEvent('mousemove', path[0][0], path[0][1]);
mouseEvent('mousedown', path[0][0], path[0][1]);

Expand Down Expand Up @@ -299,6 +314,50 @@ describe('select box and lasso', function() {
done();
});
});

it('should trigger selecting/selected/deselect events for touches', function(done) {
var selectingCnt = 0,
selectingData;
gd.on('plotly_selecting', function(data) {
selectingCnt++;
selectingData = data;
});

var selectedCnt = 0,
selectedData;
gd.on('plotly_selected', function(data) {
selectedCnt++;
selectedData = data;
});

var doubleClickData;
gd.on('plotly_deselect', function(data) {
doubleClickData = data;
});

drag(lassoPath, {type: 'touch'});

expect(selectingCnt).toEqual(3, 'with the correct selecting count');
assertEventData(selectingData.points, [{
curveNumber: 0,
pointNumber: 10,
x: 0.099,
y: 2.75
}], 'with the correct selecting points (1)');

expect(selectedCnt).toEqual(1, 'with the correct selected count');
assertEventData(selectedData.points, [{
curveNumber: 0,
pointNumber: 10,
x: 0.099,
y: 2.75,
}], 'with the correct selected points (2)');

doubleClick(250, 200).then(function() {
expect(doubleClickData).toBe(null, 'with the correct deselect data');
done();
});
});
});

it('should skip over non-visible traces', function(done) {
Expand Down