From ed9772e590ecfe82a1b0ca4d4c760655343f5e48 Mon Sep 17 00:00:00 2001 From: Max Franz Date: Fri, 19 Dec 2014 12:22:17 -0500 Subject: [PATCH] tap threshold for pan; cleanup; ref #636 --- documentation/md/core/init.md | 6 +- src/core.js | 2 +- .../renderer.canvas.define-and-init-etc.js | 1 + .../renderer.canvas.load-and-listeners.js | 180 +++++++++++++----- 4 files changed, 143 insertions(+), 46 deletions(-) diff --git a/documentation/md/core/init.md b/documentation/md/core/init.md index 973139b91c..fcd3ee4ea0 100644 --- a/documentation/md/core/init.md +++ b/documentation/md/core/init.md @@ -119,6 +119,8 @@ var cy = cytoscape({ userPanningEnabled: true, boxSelectionEnabled: true, selectionType: (isTouchDevice ? 'additive' : 'single'), + touchTapThreshold: 8, + desktopTapThreshold: 4, autolock: false, autoungrabify: false, autounselectify: false, @@ -176,9 +178,11 @@ var cy = cytoscape({ **`selectionType`** : A string indicating the selection behaviour from user input. By default, this is set automatically for you based on the type of input device detected. On touch devices, `'additive'` is default — a new selection made by the user adds to the set of currenly selected elements. On mouse-input devices, `'single'` is default — a new selection made by the user becomes the entire set of currently selected elements (i.e. the previous elements are unselected). +**`touchTapThreshold`** & **`desktopTapThreshold`** : A nonnegative integer that indicates the maximum allowable distance that a user may move during a tap gesture, on touch devices and desktop devices respectively. This makes tapping easier for users. These values have sane defaults, so it is not advised to change these options unless you have very good reason for doing so. Larger values will almost certainly have undesirable consequences. + **`autoungrabify`** : Whether nodes should be ungrabified (not grabbable by user) by default (if `true`, overrides individual node state). -**`autolock`** : Whether nodes should be locked (not movable at all) by default (if `true`, overrides individual node state). +**`autolock`** : Whether nodes should be locked (not draggable at all) by default (if `true`, overrides individual node state). **`autounselectify`** : Whether nodes should be unselectified (immutible selection state) by default (if `true`, overrides individual element state). diff --git a/src/core.js b/src/core.js index 1d0d758dc5..81cdca0a0c 100644 --- a/src/core.js +++ b/src/core.js @@ -136,7 +136,7 @@ wheelSensitivity: $$.is.number(options.wheelSensitivity) && options.wheelSensitivity > 0 ? options.wheelSensitivity : 1, motionBlur: options.motionBlur, pixelRatio: $$.is.number(options.pixelRatio) && options.pixelRatio > 0 ? options.pixelRatio : (options.pixelRatio === 'auto' ? undefined : 1), - tapThreshold: $$.is.touch() ? 8 : 4 + tapThreshold: defVal( $$.is.touch() ? 8 : 4, $$.is.touch() ? options.touchTapThreshold : options.desktopTapThreshold ) }, options.renderer) ); // trigger the passed function for the `initrender` event diff --git a/src/extensions/renderer.canvas.define-and-init-etc.js b/src/extensions/renderer.canvas.define-and-init-etc.js index 16d5e4c95a..4038b1144c 100644 --- a/src/extensions/renderer.canvas.define-and-init-etc.js +++ b/src/extensions/renderer.canvas.define-and-init-etc.js @@ -104,6 +104,7 @@ this.motionBlur = true; // for initial kick off this.tapThreshold = options.tapThreshold; this.tapThreshold2 = options.tapThreshold * options.tapThreshold; + this.tapholdDuration = 500; this.load(); } diff --git a/src/extensions/renderer.canvas.load-and-listeners.js b/src/extensions/renderer.canvas.load-and-listeners.js index 1d154c4ceb..c19cddcaaa 100644 --- a/src/extensions/renderer.canvas.load-and-listeners.js +++ b/src/extensions/renderer.canvas.load-and-listeners.js @@ -231,11 +231,41 @@ r.hoverData.capture = true; r.hoverData.which = e.which; - var cy = r.data.cy; var pos = r.projectIntoViewport(e.clientX, e.clientY); + var cy = r.data.cy; + var pos = r.projectIntoViewport(e.clientX, e.clientY); var select = r.data.select; var near = r.findNearestElement(pos[0], pos[1], true); var draggedElements = r.dragData.possibleDragElements; - var grabEvent = new $$.Event('grab'); + + r.hoverData.mdownPos = pos; + + var checkForTaphold = function(){ + r.hoverData.tapholdCancelled = false; + + clearTimeout( r.hoverData.tapholdTimeout ); + + r.hoverData.tapholdTimeout = setTimeout(function(){ + + if( r.hoverData.tapholdCancelled ){ + return; + } else { + var ele = r.hoverData.down; + + if( ele ){ + ele.trigger( new $$.Event(e, { + type: 'taphold', + cyPosition: { x: pos[0], y: pos[1] } + }) ); + } else { + cy.trigger( new $$.Event(e, { + type: 'taphold', + cyPosition: { x: pos[0], y: pos[1] } + }) ); + } + } + + }, r.tapholdDuration); + }; // Right click button if( e.which == 3 ){ @@ -273,6 +303,11 @@ if( r.nodeIsDraggable(near) ){ + var grabEvent = new $$.Event(e, { + type: 'grab', + cyPosition: { x: pos[0], y: pos[1] } + }); + if ( near.isNode() && !near.selected() ){ draggedElements = r.dragData.possibleDragElements = []; @@ -356,6 +391,8 @@ y: pos[1] }; + checkForTaphold(); + r.data.canvasNeedsRedraw[CanvasRenderer.SELECT_BOX] = true; r.redraw(); @@ -366,6 +403,8 @@ y: pos[1] }; + checkForTaphold(); + r.data.canvasNeedsRedraw[CanvasRenderer.SELECT_BOX] = true; r.redraw(); @@ -430,6 +469,27 @@ var disp = [pos[0] - select[2], pos[1] - select[3]]; var draggedElements = r.dragData.possibleDragElements; + + var dx = select[2] - select[0]; + var dx2 = dx * dx; + var dy = select[3] - select[1]; + var dy2 = dy * dy; + var dist2 = dx2 + dy2; + var rdist2 = dist2 * zoom * zoom; + + r.hoverData.tapholdCancelled = true; + + var updateDragDelta = function(){ + var dragDelta = r.hoverData.dragDelta = r.hoverData.dragDelta || []; + + if( dragDelta.length === 0 ){ + dragDelta.push(0); + dragDelta.push(0); + } else { + dragDelta[0] += disp[0]; + dragDelta[1] += disp[1]; + } + }; preventDefault = true; @@ -516,21 +576,44 @@ preventDefault = true; if( cy.panningEnabled() && cy.userPanningEnabled() ){ - var deltaP = {x: disp[0] * cy.zoom(), y: disp[1] * cy.zoom()}; + var deltaP; + + if( r.hoverData.justStartedPan ){ + var mdPos = r.hoverData.mdownPos; + + deltaP = { + x: ( pos[0] - mdPos[0] ) * zoom, + y: ( pos[1] - mdPos[1] ) * zoom + }; + + r.hoverData.justStartedPan = false; + + } else { + deltaP = { + x: disp[0] * zoom, + y: disp[1] * zoom + }; + + } cy.panBy( deltaP ); + } // Needs reproject due to pan changing viewport pos = r.projectIntoViewport(e.clientX, e.clientY); // Checks primary button down & out of time & mouse not moved much - } else if (select[4] == 1 && (down == null || down.isEdge()) + } else if( + select[4] == 1 && (down == null || down.isEdge()) && ( !cy.boxSelectionEnabled() || +new Date() - r.hoverData.downTime >= CanvasRenderer.panOrBoxSelectDelay ) - && (Math.abs(select[3] - select[1]) + Math.abs(select[2] - select[0]) < 4) - && cy.panningEnabled() && cy.userPanningEnabled() ) { + //&& (Math.abs(select[3] - select[1]) + Math.abs(select[2] - select[0]) < 4) + && rdist2 >= r.tapThreshold2 + && cy.panningEnabled() && cy.userPanningEnabled() + ){ r.hoverData.dragging = true; + r.hoverData.justStartedPan = true; select[4] = 0; } else { @@ -573,13 +656,6 @@ r.hoverData.last = near; } - - var dx = select[2] - select[0]; - var dx2 = dx * dx; - var dy = select[3] - select[1]; - var dy2 = dy * dy; - var dist2 = dx2 + dy2; - var rdist2 = dist2 * zoom * zoom; if( down && down.isNode() && r.nodeIsDraggable(down) ){ @@ -637,15 +713,7 @@ r.redraw(); } else { // otherwise save drag delta for when we actually start dragging so the relative grab pos is constant - var dragDelta = r.hoverData.dragDelta = r.hoverData.dragDelta || []; - - if( dragDelta.length === 0 ){ - dragDelta.push(0); - dragDelta.push(0); - } else { - dragDelta[0] += disp[0]; - dragDelta[1] += disp[1]; - } + updateDragDelta(); } } @@ -1033,13 +1101,16 @@ r.data.bgActivePosistion = undefined; var cy = r.data.cy; - var nodes = r.getCachedNodes(); var edges = r.getCachedEdges(); - var now = r.touchData.now; var earlier = r.touchData.earlier; + var nodes = r.getCachedNodes(); + var edges = r.getCachedEdges(); + var now = r.touchData.now; + var earlier = r.touchData.earlier; if (e.touches[0]) { var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY); now[0] = pos[0]; now[1] = pos[1]; } if (e.touches[1]) { var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY); now[2] = pos[0]; now[3] = pos[1]; } if (e.touches[2]) { var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY); now[4] = pos[0]; now[5] = pos[1]; } - + + // record starting points for pinch-to-zoom if( e.touches[1] ){ @@ -1177,7 +1248,10 @@ addNodeToDrag( near, { addToList: draggedEles } ); } - near.trigger('grab'); + near.trigger( new $$.Event(e, { + type: 'grab', + cyPosition: { x: now[0], y: now[1] } + }) ); } near @@ -1231,13 +1305,16 @@ r.touchData.singleTouchMoved = false; r.touchData.singleTouchStartTime = +new Date(); - setTimeout(function() { - if (r.touchData.singleTouchMoved === false + clearTimeout( r.touchData.tapholdTimeout ); + r.touchData.tapholdTimeout = setTimeout(function() { + if( + r.touchData.singleTouchMoved === false && !r.pinching // if pinching, then taphold unselect shouldn't take effect // This time double constraint prevents multiple quick taps // followed by a taphold triggering multiple taphold events - && (+new Date()) - r.touchData.singleTouchStartTime > 250) { + //&& Date.now() - r.touchData.singleTouchStartTime > 250 + ){ if (r.touchData.start) { r.touchData.start.trigger( new $$.Event(e, { type: 'taphold', @@ -1254,7 +1331,7 @@ // console.log('taphold'); } - }, 1000); + }, r.tapholdDuration); } //r.redraw(); @@ -1278,6 +1355,14 @@ if (e.touches[2]) { var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY); now[4] = pos[0]; now[5] = pos[1]; } var disp = []; for (var j=0;j= r.tapThreshold2 ){ // then dragging can happen var draggedEles = r.dragData.touchDragEles; @@ -1648,7 +1723,27 @@ } } - if ( capture && (start == null || start.isEdge()) && cy.panningEnabled() && cy.userPanningEnabled() ) { + if( + capture + && ( start == null || start.isEdge() ) + && cy.panningEnabled() && cy.userPanningEnabled() + ){ + + if( r.swipePanning ){ + cy.panBy({ + x: disp[0] * zoom, + y: disp[1] * zoom + }); + + } else if( rdist2 >= r.tapThreshold2 ){ + r.swipePanning = true; + + cy.panBy({ + x: dx * zoom, + y: dy * zoom + }); + } + if( start ){ start.unactivate(); @@ -1663,9 +1758,6 @@ r.touchData.start = null; } - - cy.panBy({x: disp[0] * cy.zoom(), y: disp[1] * cy.zoom()}); - r.swipePanning = true; // Re-project var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY);