From e1f60bd08d328756f327d93c47c2d08a4a4a6d92 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 16 Jun 2017 14:30:15 -0400 Subject: [PATCH 01/17] Show modebar if no hover available --- package.json | 1 + src/plot_api/plot_api.js | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/package.json b/package.json index 4a70535470e..88bd4002fe4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index c067d798f58..19d1b1bb567 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -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'); @@ -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) { From 4b4ae27b34c758712ca3cb0db93f2682dc1ada4d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 16 Jun 2017 15:23:31 -0400 Subject: [PATCH 02/17] Fix touch implementation --- src/components/dragelement/index.js | 31 ++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 5106cfaa2c9..ee592f1b06d 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -9,6 +9,8 @@ 'use strict'; +var mouseOffset = require('mouse-event-offset'); + var Plotly = require('../../plotly'); var Lib = require('../../lib'); @@ -71,8 +73,9 @@ dragElement.init = function init(options) { // 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(); @@ -90,9 +93,15 @@ dragElement.init = function init(options) { dragCover = coverSlip(); + dragCover.onmousemove = onMove; + dragCover.ontouchmove = onMove; dragCover.onmouseup = onDone; dragCover.onmouseout = onDone; + dragCover.ontouchend = onDone; + + // var moveEvent = new TouchEvent('touchstart', e) + // dragCover.dispatchEvent(moveEvent) dragCover.style.cursor = window.getComputedStyle(options.element).cursor; @@ -100,8 +109,9 @@ dragElement.init = function init(options) { } function onMove(e) { - var dx = e.clientX - startX, - dy = e.clientY - startY, + var offset = pointerOffset(e) + var dx = offset[0] - startX, + dy = offset[1] - startY, minDrag = options.minDrag || constants.MINDRAG; if(Math.abs(dx) < minDrag) dx = 0; @@ -118,8 +128,10 @@ dragElement.init = function init(options) { function onDone(e) { dragCover.onmousemove = null; + dragCover.ontouchmove = null; dragCover.onmouseup = null; dragCover.onmouseout = null; + dragCover.ontouchend = null; Lib.removeElement(dragCover); if(!gd._dragging) { @@ -143,12 +155,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); } @@ -164,6 +177,7 @@ dragElement.init = function init(options) { } options.element.onmousedown = onStart; + options.element.ontouchstart = onStart; options.element.style.pointerEvents = 'all'; }; @@ -191,3 +205,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 + ) +} From a1d3261ae2e4540ce1c58105168e57e13f72c04c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 20 Jun 2017 14:38:44 -0400 Subject: [PATCH 03/17] Remove dragCover --- src/components/dragelement/index.js | 48 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index ee592f1b06d..98b965c03ba 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -10,6 +10,7 @@ 'use strict'; var mouseOffset = require('mouse-event-offset'); +var hasHover = require('has-hover'); var Plotly = require('../../plotly'); var Lib = require('../../lib'); @@ -63,11 +64,16 @@ dragElement.init = function init(options) { startX, startY, newMouseDownTime, - dragCover, + cursor, + root = document.documentElement, 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 @@ -91,19 +97,16 @@ dragElement.init = function init(options) { if(options.prepFn) options.prepFn(e, startX, startY); - dragCover = coverSlip(); - - - dragCover.onmousemove = onMove; - dragCover.ontouchmove = onMove; - dragCover.onmouseup = onDone; - dragCover.onmouseout = onDone; - dragCover.ontouchend = onDone; + document.addEventListener('mousemove', onMove) + document.addEventListener('mouseup', onDone) + document.addEventListener('mouseout', onDone) - // var moveEvent = new TouchEvent('touchstart', e) - // dragCover.dispatchEvent(moveEvent) + document.addEventListener('touchmove', onMove) + document.addEventListener('touchend', onDone) - dragCover.style.cursor = window.getComputedStyle(options.element).cursor; + // disable cursor + cursor = window.getComputedStyle(root).cursor + root.style.cursor = window.getComputedStyle(options.element).cursor; return Lib.pauseEvent(e); } @@ -127,12 +130,17 @@ dragElement.init = function init(options) { } function onDone(e) { - dragCover.onmousemove = null; - dragCover.ontouchmove = null; - dragCover.onmouseup = null; - dragCover.onmouseout = null; - dragCover.ontouchend = null; - Lib.removeElement(dragCover); + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onDone); + document.removeEventListener('mouseout', onDone); + document.removeEventListener('touchmove', onMove); + document.removeEventListener('touchend', onDone); + + // enable cursor + if (cursor) { + root.style.cursor = cursor + cursor = null + } if(!gd._dragging) { gd._dragged = false; @@ -175,10 +183,6 @@ dragElement.init = function init(options) { return Lib.pauseEvent(e); } - - options.element.onmousedown = onStart; - options.element.ontouchstart = onStart; - options.element.style.pointerEvents = 'all'; }; function coverSlip() { From dc997808ac195986cd3969496a63d6bbdede5c24 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 20 Jun 2017 14:49:16 -0400 Subject: [PATCH 04/17] Make document a mobile-specific dragCover --- src/components/dragelement/index.js | 63 +++++++++++++++++++---------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 98b965c03ba..180b159990f 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -65,14 +65,19 @@ dragElement.init = function init(options) { startY, newMouseDownTime, cursor, - root = document.documentElement, + dragCover, initialTarget; if(!gd._mouseDownTime) gd._mouseDownTime = 0; options.element.style.pointerEvents = 'all'; - options.element.onmousedown = onStart; - options.element.ontouchstart = onStart; + + if(hasHover) { + options.element.onmousedown = onStart; + } + else { + options.element.ontouchstart = onStart; + } function onStart(e) { // make dragging and dragged into properties of gd @@ -97,16 +102,25 @@ dragElement.init = function init(options) { if(options.prepFn) options.prepFn(e, startX, startY); - document.addEventListener('mousemove', onMove) - document.addEventListener('mouseup', onDone) - document.addEventListener('mouseout', onDone) + if(hasHover) { + dragCover = coverSlip(); + dragCover.addEventListener('mousemove', onMove); + dragCover.addEventListener('mouseup', onDone); + dragCover.addEventListener('mouseout', onDone); + + dragCover.style.cursor = window.getComputedStyle(options.element).cursor; + } + + // document acts as a dragcover for mobile, bc we can't create dragcover dynamically + else { + dragCover = document + document.addEventListener('touchmove', onMove); + document.addEventListener('touchend', onDone); - document.addEventListener('touchmove', onMove) - document.addEventListener('touchend', onDone) + cursor = window.getComputedStyle(document.documentElement).cursor; + document.documentElement.style.cursor = window.getComputedStyle(options.element).cursor; + } - // disable cursor - cursor = window.getComputedStyle(root).cursor - root.style.cursor = window.getComputedStyle(options.element).cursor; return Lib.pauseEvent(e); } @@ -130,18 +144,25 @@ dragElement.init = function init(options) { } function onDone(e) { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onDone); - document.removeEventListener('mouseout', onDone); - document.removeEventListener('touchmove', onMove); - document.removeEventListener('touchend', onDone); - - // enable cursor - if (cursor) { - root.style.cursor = cursor - cursor = null + if(hasHover) { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onDone); + document.removeEventListener('mouseout', onDone); + + Lib.removeElement(dragCover); } + else { + document.removeEventListener('touchmove', onMove); + document.removeEventListener('touchend', onDone); + + if(cursor) { + document.documentElement.style.cursor = cursor; + cursor = null; + } + } + + if(!gd._dragging) { gd._dragged = false; return; From bc83d44885e0ced6773d26cf8aebfd739893d88d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 20 Jun 2017 15:01:51 -0400 Subject: [PATCH 05/17] Fix typos --- src/components/dragelement/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 180b159990f..d8b5e6ce5e9 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -145,19 +145,19 @@ dragElement.init = function init(options) { function onDone(e) { if(hasHover) { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onDone); - document.removeEventListener('mouseout', onDone); + dragCover.removeEventListener('mousemove', onMove); + dragCover.removeEventListener('mouseup', onDone); + dragCover.removeEventListener('mouseout', onDone); Lib.removeElement(dragCover); } else { - document.removeEventListener('touchmove', onMove); - document.removeEventListener('touchend', onDone); + dragCover.removeEventListener('touchmove', onMove); + dragCover.removeEventListener('touchend', onDone); if(cursor) { - document.documentElement.style.cursor = cursor; + dragCover.documentElement.style.cursor = cursor; cursor = null; } } From ab8b03eb4ef6c97b1968e92a6d005820ddf8e306 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 20 Jun 2017 15:48:47 -0400 Subject: [PATCH 06/17] Lintify --- src/components/dragelement/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index d8b5e6ce5e9..387058d9b69 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -84,7 +84,7 @@ dragElement.init = function init(options) { // so that others can look at and modify them gd._dragged = false; gd._dragging = true; - var offset = pointerOffset(e) + var offset = pointerOffset(e); startX = offset[0]; startY = offset[1]; initialTarget = e.target; @@ -113,7 +113,7 @@ dragElement.init = function init(options) { // document acts as a dragcover for mobile, bc we can't create dragcover dynamically else { - dragCover = document + dragCover = document; document.addEventListener('touchmove', onMove); document.addEventListener('touchend', onDone); @@ -126,8 +126,8 @@ dragElement.init = function init(options) { } function onMove(e) { - var offset = pointerOffset(e) - var dx = offset[0] - startX, + var offset = pointerOffset(e), + dx = offset[0] - startX, dy = offset[1] - startY, minDrag = options.minDrag || constants.MINDRAG; @@ -184,7 +184,7 @@ dragElement.init = function init(options) { e2 = new MouseEvent('click', e); } catch(err) { - var offset = pointerOffset(e) + var offset = pointerOffset(e); e2 = document.createEvent('MouseEvents'); e2.initMouseEvent('click', e.bubbles, e.cancelable, @@ -235,5 +235,5 @@ function pointerOffset(e) { return mouseOffset( e.changedTouches && e.changedTouches[0] || e, document.body - ) + ); } From d1b812bb4907b971b5211d4ef6c7fe793f1c2a64 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 20 Jun 2017 16:10:38 -0400 Subject: [PATCH 07/17] Enable pan/zoom touch interactions --- src/plots/gl2d/camera.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js index a043dead109..c5d0ab7a92a 100644 --- a/src/plots/gl2d/camera.js +++ b/src/plots/gl2d/camera.js @@ -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; @@ -55,7 +56,23 @@ 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) { + 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; @@ -235,7 +252,7 @@ function createCamera(scene) { result.lastPos[0] = x; result.lastPos[1] = y; - }); + } result.wheelListener = mouseWheel(element, function(dx, dy) { var dataBox = scene.calcDataBox(), From 3b5fab84a7ec4adf630b8634ff4ddbed25efa663 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 20 Jun 2017 16:54:40 -0400 Subject: [PATCH 08/17] Set proper cursor for scattergl mode --- src/plots/gl2d/scene2d.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 6c4da94cbd8..4e43607dd7f 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -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) { From 38fef3725c0b5929d42ca4ed53cf820f99bb93bb Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 22 Jun 2017 15:10:43 -0400 Subject: [PATCH 09/17] Fix panning touchmove --- src/plots/gl2d/camera.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js index c5d0ab7a92a..14e5fa217ab 100644 --- a/src/plots/gl2d/camera.js +++ b/src/plots/gl2d/camera.js @@ -65,6 +65,7 @@ function createCamera(scene) { 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]); }); From 52ae0c5dd5e1af1c81a967229e55f164d43e0d80 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 22 Jun 2017 16:22:11 -0400 Subject: [PATCH 10/17] Add semicolon --- src/plots/gl2d/camera.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js index 14e5fa217ab..fdf9d37fbdc 100644 --- a/src/plots/gl2d/camera.js +++ b/src/plots/gl2d/camera.js @@ -65,7 +65,7 @@ function createCamera(scene) { handleInteraction(1, xy[0], xy[1]); }); element.addEventListener('touchmove', function(ev) { - ev.preventDefault() + ev.preventDefault(); var xy = mouseOffset(ev.changedTouches[0], element); handleInteraction(1, xy[0], xy[1]); }); From fcb83513572e44784aee58196c98e2abe867ad32 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 22 Jun 2017 16:36:25 -0400 Subject: [PATCH 11/17] Reset selection by doubleclick --- src/traces/scattergl/convert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index 74a2a1d9ea1..cb37467368c 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -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++) { From 8579d78a97283a238bffa81dd3d66d76173cf85d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 6 Jul 2017 12:31:46 -0400 Subject: [PATCH 12/17] Enhance logic --- src/components/dragelement/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 387058d9b69..9227599c059 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -233,7 +233,7 @@ function finishDrag(gd) { function pointerOffset(e) { return mouseOffset( - e.changedTouches && e.changedTouches[0] || e, + e.changedTouches ? e.changedTouches[0] : e, document.body ); } From 473f72afe69e8bc1cdc5752b8fe37b7b58d8eacd Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 6 Jul 2017 16:17:46 -0400 Subject: [PATCH 13/17] Add test, refactor cover slip --- src/components/dragelement/index.js | 65 ++++++++--------------------- test/jasmine/assets/touch_event.js | 44 +++++++++++++++++++ test/jasmine/tests/select_test.js | 61 ++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 48 deletions(-) create mode 100644 test/jasmine/assets/touch_event.js diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 9227599c059..4bfb6b99f74 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -10,7 +10,6 @@ 'use strict'; var mouseOffset = require('mouse-event-offset'); -var hasHover = require('has-hover'); var Plotly = require('../../plotly'); var Lib = require('../../lib'); @@ -30,13 +29,6 @@ dragElement.unhoverRaw = unhover.raw; /** * Abstracts click & drag interactions * - * During the interaction, a "coverSlip" element - a transparent - * div covering the whole page - is created, which has two key effects: - * - Lets you drag beyond the boundaries of the plot itself without - * dropping (but if you drag all the way out of the browser window the - * interaction will end) - * - Freezes the cursor: whatever mouse cursor the drag element had when the - * interaction started gets copied to the coverSlip for use until mouseup * * @param {object} options with keys: * element (required) the DOM element to drag @@ -65,19 +57,14 @@ dragElement.init = function init(options) { startY, newMouseDownTime, cursor, - dragCover, initialTarget; if(!gd._mouseDownTime) gd._mouseDownTime = 0; options.element.style.pointerEvents = 'all'; - if(hasHover) { - options.element.onmousedown = onStart; - } - else { - options.element.ontouchstart = onStart; - } + options.element.onmousedown = onStart; + options.element.ontouchstart = onStart; function onStart(e) { // make dragging and dragged into properties of gd @@ -102,24 +89,16 @@ dragElement.init = function init(options) { if(options.prepFn) options.prepFn(e, startX, startY); - if(hasHover) { - dragCover = coverSlip(); - dragCover.addEventListener('mousemove', onMove); - dragCover.addEventListener('mouseup', onDone); - dragCover.addEventListener('mouseout', onDone); - - dragCover.style.cursor = window.getComputedStyle(options.element).cursor; - } // document acts as a dragcover for mobile, bc we can't create dragcover dynamically - else { - dragCover = document; - document.addEventListener('touchmove', onMove); - document.addEventListener('touchend', onDone); + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onDone); + document.addEventListener('mouseout', onDone); + document.addEventListener('touchmove', onMove); + document.addEventListener('touchend', onDone); - cursor = window.getComputedStyle(document.documentElement).cursor; - document.documentElement.style.cursor = window.getComputedStyle(options.element).cursor; - } + cursor = window.getComputedStyle(document.documentElement).cursor; + document.documentElement.style.cursor = window.getComputedStyle(options.element).cursor; return Lib.pauseEvent(e); @@ -144,25 +123,17 @@ dragElement.init = function init(options) { } function onDone(e) { - if(hasHover) { - dragCover.removeEventListener('mousemove', onMove); - dragCover.removeEventListener('mouseup', onDone); - dragCover.removeEventListener('mouseout', onDone); - - Lib.removeElement(dragCover); + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onDone); + document.removeEventListener('mouseout', onDone); + document.removeEventListener('touchmove', onMove); + document.removeEventListener('touchend', onDone); + + if(cursor) { + document.documentElement.style.cursor = cursor; + cursor = null; } - else { - dragCover.removeEventListener('touchmove', onMove); - dragCover.removeEventListener('touchend', onDone); - - if(cursor) { - dragCover.documentElement.style.cursor = cursor; - cursor = null; - } - } - - if(!gd._dragging) { gd._dragged = false; return; diff --git a/test/jasmine/assets/touch_event.js b/test/jasmine/assets/touch_event.js new file mode 100644 index 00000000000..ea16b1db664 --- /dev/null +++ b/test/jasmine/assets/touch_event.js @@ -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; +}; diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 37cf778b940..3fb53bda8f8 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -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'); @@ -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]); @@ -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) { From e215ffc43f7821574268a8180951f496db16e096 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 6 Jul 2017 17:05:02 -0400 Subject: [PATCH 14/17] Make tests not fail --- src/components/dragelement/index.js | 53 +++++++++++++++++++---------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 4bfb6b99f74..6b0d47b3b42 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -10,6 +10,7 @@ 'use strict'; var mouseOffset = require('mouse-event-offset'); +var hasHover = require('has-hover'); var Plotly = require('../../plotly'); var Lib = require('../../lib'); @@ -29,6 +30,13 @@ dragElement.unhoverRaw = unhover.raw; /** * Abstracts click & drag interactions * + * During the interaction, a "coverSlip" element - a transparent + * div covering the whole page - is created, which has two key effects: + * - Lets you drag beyond the boundaries of the plot itself without + * dropping (but if you drag all the way out of the browser window the + * interaction will end) + * - Freezes the cursor: whatever mouse cursor the drag element had when the + * interaction started gets copied to the coverSlip for use until mouseup * * @param {object} options with keys: * element (required) the DOM element to drag @@ -57,6 +65,7 @@ dragElement.init = function init(options) { startY, newMouseDownTime, cursor, + dragCover, initialTarget; if(!gd._mouseDownTime) gd._mouseDownTime = 0; @@ -89,17 +98,22 @@ dragElement.init = function init(options) { if(options.prepFn) options.prepFn(e, startX, startY); + 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; + } - // document acts as a dragcover for mobile, bc we can't create dragcover dynamically - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onDone); - document.addEventListener('mouseout', onDone); - document.addEventListener('touchmove', onMove); - document.addEventListener('touchend', onDone); - - cursor = window.getComputedStyle(document.documentElement).cursor; - document.documentElement.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); } @@ -123,14 +137,17 @@ dragElement.init = function init(options) { } function onDone(e) { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onDone); - document.removeEventListener('mouseout', onDone); - document.removeEventListener('touchmove', onMove); - document.removeEventListener('touchend', onDone); - - if(cursor) { - document.documentElement.style.cursor = cursor; + 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; } From 5dffeab5f8cee8684083689e1b2548e2ce9c4819 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 6 Jul 2017 17:26:12 -0400 Subject: [PATCH 15/17] Gate touch interactions for CI --- test/jasmine/tests/select_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 3fb53bda8f8..fd40c8da6b8 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -315,7 +315,7 @@ describe('select box and lasso', function() { }); }); - it('should trigger selecting/selected/deselect events for touches', function(done) { + it('@noCI should trigger selecting/selected/deselect events for touches', function(done) { var selectingCnt = 0, selectingData; gd.on('plotly_selecting', function(data) { From fed96fdfd34f259b7ad398cb90d35d9139cef52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 10 Jul 2017 12:00:52 -0400 Subject: [PATCH 16/17] Revert "Gate touch interactions for CI" This reverts commit 5dffeab5f8cee8684083689e1b2548e2ce9c4819. --- test/jasmine/tests/select_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index fd40c8da6b8..3fb53bda8f8 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -315,7 +315,7 @@ describe('select box and lasso', function() { }); }); - it('@noCI should trigger selecting/selected/deselect events for touches', function(done) { + it('should trigger selecting/selected/deselect events for touches', function(done) { var selectingCnt = 0, selectingData; gd.on('plotly_selecting', function(data) { From 80db1af9a5d4a5a9ee320113a1c92243b83ce278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 10 Jul 2017 11:54:45 -0400 Subject: [PATCH 17/17] add --touch-events chrome CLI flag - so that window.TouchEvent API works in Ubuntu --- test/jasmine/karma.conf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index 8f8af91fbf7..74866dffcda 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -182,6 +182,7 @@ func.defaultConfig = { _Chrome: { base: 'Chrome', flags: [ + '--touch-events', '--window-size=' + argv.width + ',' + argv.height, isCI ? '--ignore-gpu-blacklist' : '' ]