Skip to content

Commit 17b0d38

Browse files
authored
Merge pull request #1804 from dfcreative/select-touch
Enable touch interactions for select/lasso/others
2 parents d0ad698 + 80db1af commit 17b0d38

File tree

8 files changed

+196
-21
lines changed

8 files changed

+196
-21
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"gl-shader": "4.2.0",
8484
"gl-spikes2d": "^1.0.1",
8585
"gl-surface3d": "^1.3.0",
86+
"has-hover": "^1.0.0",
8687
"mapbox-gl": "^0.22.0",
8788
"matrix-camera-controller": "^2.1.3",
8889
"mouse-change": "^1.4.0",

src/components/dragelement/index.js

+52-18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
'use strict';
1111

12+
var mouseOffset = require('mouse-event-offset');
13+
var hasHover = require('has-hover');
14+
1215
var Plotly = require('../../plotly');
1316
var Lib = require('../../lib');
1417

@@ -61,18 +64,25 @@ dragElement.init = function init(options) {
6164
startX,
6265
startY,
6366
newMouseDownTime,
67+
cursor,
6468
dragCover,
6569
initialTarget;
6670

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

73+
options.element.style.pointerEvents = 'all';
74+
75+
options.element.onmousedown = onStart;
76+
options.element.ontouchstart = onStart;
77+
6978
function onStart(e) {
7079
// make dragging and dragged into properties of gd
7180
// so that others can look at and modify them
7281
gd._dragged = false;
7382
gd._dragging = true;
74-
startX = e.clientX;
75-
startY = e.clientY;
83+
var offset = pointerOffset(e);
84+
startX = offset[0];
85+
startY = offset[1];
7686
initialTarget = e.target;
7787

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

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

91-
dragCover = coverSlip();
92-
93-
dragCover.onmousemove = onMove;
94-
dragCover.onmouseup = onDone;
95-
dragCover.onmouseout = onDone;
101+
if(hasHover) {
102+
dragCover = coverSlip();
103+
dragCover.style.cursor = window.getComputedStyle(options.element).cursor;
104+
}
105+
else {
106+
// document acts as a dragcover for mobile, bc we can't create dragcover dynamically
107+
dragCover = document;
108+
cursor = window.getComputedStyle(document.documentElement).cursor;
109+
document.documentElement.style.cursor = window.getComputedStyle(options.element).cursor;
110+
}
96111

97-
dragCover.style.cursor = window.getComputedStyle(options.element).cursor;
112+
dragCover.addEventListener('mousemove', onMove);
113+
dragCover.addEventListener('mouseup', onDone);
114+
dragCover.addEventListener('mouseout', onDone);
115+
dragCover.addEventListener('touchmove', onMove);
116+
dragCover.addEventListener('touchend', onDone);
98117

99118
return Lib.pauseEvent(e);
100119
}
101120

102121
function onMove(e) {
103-
var dx = e.clientX - startX,
104-
dy = e.clientY - startY,
122+
var offset = pointerOffset(e),
123+
dx = offset[0] - startX,
124+
dy = offset[1] - startY,
105125
minDrag = options.minDrag || constants.MINDRAG;
106126

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

119139
function onDone(e) {
120-
dragCover.onmousemove = null;
121-
dragCover.onmouseup = null;
122-
dragCover.onmouseout = null;
123-
Lib.removeElement(dragCover);
140+
dragCover.removeEventListener('mousemove', onMove);
141+
dragCover.removeEventListener('mouseup', onDone);
142+
dragCover.removeEventListener('mouseout', onDone);
143+
dragCover.removeEventListener('touchmove', onMove);
144+
dragCover.removeEventListener('touchend', onDone);
145+
146+
if(hasHover) {
147+
Lib.removeElement(dragCover);
148+
}
149+
else if(cursor) {
150+
dragCover.documentElement.style.cursor = cursor;
151+
cursor = null;
152+
}
124153

125154
if(!gd._dragging) {
126155
gd._dragged = false;
@@ -143,12 +172,13 @@ dragElement.init = function init(options) {
143172
e2 = new MouseEvent('click', e);
144173
}
145174
catch(err) {
175+
var offset = pointerOffset(e);
146176
e2 = document.createEvent('MouseEvents');
147177
e2.initMouseEvent('click',
148178
e.bubbles, e.cancelable,
149179
e.view, e.detail,
150180
e.screenX, e.screenY,
151-
e.clientX, e.clientY,
181+
offset[0], offset[1],
152182
e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
153183
e.button, e.relatedTarget);
154184
}
@@ -162,9 +192,6 @@ dragElement.init = function init(options) {
162192

163193
return Lib.pauseEvent(e);
164194
}
165-
166-
options.element.onmousedown = onStart;
167-
options.element.style.pointerEvents = 'all';
168195
};
169196

170197
function coverSlip() {
@@ -191,3 +218,10 @@ function finishDrag(gd) {
191218
gd._dragging = false;
192219
if(gd._replotPending) Plotly.plot(gd);
193220
}
221+
222+
function pointerOffset(e) {
223+
return mouseOffset(
224+
e.changedTouches ? e.changedTouches[0] : e,
225+
document.body
226+
);
227+
}

src/plot_api/plot_api.js

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
var d3 = require('d3');
1414
var isNumeric = require('fast-isnumeric');
15+
var hasHover = require('has-hover');
1516

1617
var Plotly = require('../plotly');
1718
var Lib = require('../lib');
@@ -425,6 +426,11 @@ function setPlotContext(gd, config) {
425426
context.showLink = false;
426427
context.displayModeBar = false;
427428
}
429+
430+
// make sure hover-only devices have mode bar visible
431+
if(context.displayModeBar === 'hover' && !hasHover) {
432+
context.displayModeBar = true;
433+
}
428434
}
429435

430436
function plotPolar(gd, data, layout) {

src/plots/gl2d/camera.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
var mouseChange = require('mouse-change');
1313
var mouseWheel = require('mouse-wheel');
14+
var mouseOffset = require('mouse-event-offset');
1415
var cartesianConstants = require('../cartesian/constants');
1516

1617
module.exports = createCamera;
@@ -55,7 +56,24 @@ function createCamera(scene) {
5556
return false;
5657
}
5758

58-
result.mouseListener = mouseChange(element, function(buttons, x, y) {
59+
result.mouseListener = mouseChange(element, handleInteraction);
60+
61+
// enable simple touch interactions
62+
element.addEventListener('touchstart', function(ev) {
63+
var xy = mouseOffset(ev.changedTouches[0], element);
64+
handleInteraction(0, xy[0], xy[1]);
65+
handleInteraction(1, xy[0], xy[1]);
66+
});
67+
element.addEventListener('touchmove', function(ev) {
68+
ev.preventDefault();
69+
var xy = mouseOffset(ev.changedTouches[0], element);
70+
handleInteraction(1, xy[0], xy[1]);
71+
});
72+
element.addEventListener('touchend', function() {
73+
handleInteraction(0, result.lastPos[0], result.lastPos[1]);
74+
});
75+
76+
function handleInteraction(buttons, x, y) {
5977
var dataBox = scene.calcDataBox(),
6078
viewBox = plot.viewBox;
6179

@@ -235,7 +253,7 @@ function createCamera(scene) {
235253

236254
result.lastPos[0] = x;
237255
result.lastPos[1] = y;
238-
});
256+
}
239257

240258
result.wheelListener = mouseWheel(element, function(dx, dy) {
241259
var dataBox = scene.calcDataBox(),

src/plots/gl2d/scene2d.js

+12
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,23 @@ proto.updateTraces = function(fullData, calcData) {
528528
};
529529

530530
proto.updateFx = function(dragmode) {
531+
// switch to svg interactions in lasso/select mode
531532
if(dragmode === 'lasso' || dragmode === 'select') {
532533
this.mouseContainer.style['pointer-events'] = 'none';
533534
} else {
534535
this.mouseContainer.style['pointer-events'] = 'auto';
535536
}
537+
538+
// set proper cursor
539+
if(dragmode === 'pan') {
540+
this.mouseContainer.style.cursor = 'move';
541+
}
542+
else if(dragmode === 'zoom') {
543+
this.mouseContainer.style.cursor = 'crosshair';
544+
}
545+
else {
546+
this.mouseContainer.style.cursor = null;
547+
}
536548
};
537549

538550
proto.emitPointAction = function(nextSelection, eventType) {

test/jasmine/assets/touch_event.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
var Lib = require('../../../src/lib');
2+
3+
module.exports = function(type, x, y, opts) {
4+
var el = (opts && opts.element) || document.elementFromPoint(x, y),
5+
ev;
6+
7+
var touchObj = new Touch({
8+
identifier: Date.now(),
9+
target: el,
10+
clientX: x,
11+
clientY: y,
12+
radiusX: 2.5,
13+
radiusY: 2.5,
14+
rotationAngle: 10,
15+
force: 0.5,
16+
});
17+
18+
var fullOpts = {
19+
touches: [touchObj],
20+
targetTouches: [],
21+
changedTouches: [touchObj],
22+
bubbles: true
23+
};
24+
25+
if(opts && opts.altKey) {
26+
fullOpts.altKey = opts.altKey;
27+
}
28+
if(opts && opts.ctrlKey) {
29+
fullOpts.ctrlKey = opts.ctrlKey;
30+
}
31+
if(opts && opts.metaKey) {
32+
fullOpts.metaKey = opts.metaKey;
33+
}
34+
if(opts && opts.shiftKey) {
35+
fullOpts.shiftKey = opts.shiftKey;
36+
}
37+
38+
39+
ev = new window.TouchEvent(type, Lib.extendFlat({}, fullOpts, opts));
40+
41+
el.dispatchEvent(ev);
42+
43+
return el;
44+
};

test/jasmine/karma.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func.defaultConfig = {
182182
_Chrome: {
183183
base: 'Chrome',
184184
flags: [
185+
'--touch-events',
185186
'--window-size=' + argv.width + ',' + argv.height,
186187
isCI ? '--ignore-gpu-blacklist' : ''
187188
]

test/jasmine/tests/select_test.js

+60-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var createGraphDiv = require('../assets/create_graph_div');
88
var destroyGraphDiv = require('../assets/destroy_graph_div');
99
var fail = require('../assets/fail_test');
1010
var mouseEvent = require('../assets/mouse_event');
11+
var touchEvent = require('../assets/touch_event');
1112
var customMatchers = require('../assets/custom_matchers');
1213

1314

@@ -23,9 +24,23 @@ describe('select box and lasso', function() {
2324

2425
afterEach(destroyGraphDiv);
2526

26-
function drag(path) {
27+
function drag(path, options) {
2728
var len = path.length;
2829

30+
if(!options) options = {type: 'mouse'};
31+
32+
if(options.type === 'touch') {
33+
touchEvent('touchstart', path[0][0], path[0][1]);
34+
35+
path.slice(1, len).forEach(function(pt) {
36+
touchEvent('touchmove', pt[0], pt[1]);
37+
});
38+
39+
touchEvent('touchend', path[len - 1][0], path[len - 1][1]);
40+
41+
return;
42+
}
43+
2944
mouseEvent('mousemove', path[0][0], path[0][1]);
3045
mouseEvent('mousedown', path[0][0], path[0][1]);
3146

@@ -289,6 +304,50 @@ describe('select box and lasso', function() {
289304
done();
290305
});
291306
});
307+
308+
it('should trigger selecting/selected/deselect events for touches', function(done) {
309+
var selectingCnt = 0,
310+
selectingData;
311+
gd.on('plotly_selecting', function(data) {
312+
selectingCnt++;
313+
selectingData = data;
314+
});
315+
316+
var selectedCnt = 0,
317+
selectedData;
318+
gd.on('plotly_selected', function(data) {
319+
selectedCnt++;
320+
selectedData = data;
321+
});
322+
323+
var doubleClickData;
324+
gd.on('plotly_deselect', function(data) {
325+
doubleClickData = data;
326+
});
327+
328+
drag(lassoPath, {type: 'touch'});
329+
330+
expect(selectingCnt).toEqual(3, 'with the correct selecting count');
331+
assertEventData(selectingData.points, [{
332+
curveNumber: 0,
333+
pointNumber: 10,
334+
x: 0.099,
335+
y: 2.75
336+
}], 'with the correct selecting points (1)');
337+
338+
expect(selectedCnt).toEqual(1, 'with the correct selected count');
339+
assertEventData(selectedData.points, [{
340+
curveNumber: 0,
341+
pointNumber: 10,
342+
x: 0.099,
343+
y: 2.75,
344+
}], 'with the correct selected points (2)');
345+
346+
doubleClick(250, 200).then(function() {
347+
expect(doubleClickData).toBe(null, 'with the correct deselect data');
348+
done();
349+
});
350+
});
292351
});
293352

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

0 commit comments

Comments
 (0)