feat(features/touch): add touch gesture support

Allow to navigate i.e. scroll/zoom on the diagram via touch gestures.

Closes #46
This commit is contained in:
jdotzki 2014-06-23 17:16:13 +02:00 committed by Nico Rehwaldt
parent fcb35c366d
commit 3c7033f92e
8 changed files with 233 additions and 15 deletions

View File

@ -10,11 +10,12 @@ var Importer = require('./import/Importer'),
function getSvgContents(diagram) {
var paper = diagram.get('canvas').getPaper();
var outerNode = paper.node.parentNode;
var outerNode = diagram.get('canvas').getContainer();
var svg = outerNode.innerHTML;
return svg.replace(/^<svg[^>]*>|<\/svg>$/g, '');
return svg.replace(/^<svg[^>]*>|<\/svg>$/g, '')
.replace('<desc>Created with Snap</desc>', '')
.replace(/<g class="viewport"( transform="[^"]*")?/, '<g');
}
function initListeners(diagram, listeners) {

View File

@ -1,7 +1,7 @@
'use strict';
var $ = require('jquery');
var _ = require('lodash');
function MoveCanvas(events, canvas) {
@ -9,7 +9,7 @@ function MoveCanvas(events, canvas) {
function init(element) {
var context;
var context = {};
function getPosition(e) {
return { x: e.clientX, y: e.clientY };
@ -40,30 +40,33 @@ function MoveCanvas(events, canvas) {
var position = getPosition(event);
var delta = getDelta(context.start, position);
if (!context.dragging && isThresholdReached(delta)) {
if (!context.dragging && isThresholdReached(getDelta(context.start, position))) {
context.dragging = true;
context.cursor = cursor('move');
}
if (context.dragging) {
var box = context.viewbox;
var lastPos = context.last || context.start;
canvas.viewbox({
x: box.x + delta.x / box.scale,
y: box.y + delta.y / box.scale,
width: box.width,
height: box.height
var delta = getDelta(position, lastPos);
canvas.scroll({
dx: delta.x,
dy: delta.y
});
context.last = position;
}
// prevent select
event.preventDefault();
}
function handleEnd(event) {
$(element).off('mousemove', handleMove);
$(document).off('mouseup', handleEnd);
@ -81,7 +84,6 @@ function MoveCanvas(events, canvas) {
var position = getPosition(event);
context = {
viewbox: canvas.viewbox(),
start: position
};

View File

@ -0,0 +1,172 @@
'use strict';
var $ = require('jquery');
var _ = require('lodash');
function TouchInteraction(eventBus, canvas) {
var RANGE = { min: 0.2, max: 4 };
function cap(scale) {
return Math.max(RANGE.min, Math.min(RANGE.max, scale));
}
function zoom(pinchRatio, position) {
canvas.zoom(cap(pinchRatio), position);
}
function euclideanDistance(touch1, touch2) {
var dist =
Math.sqrt(
(touch1.clientX - touch2.clientX) *
(touch1.clientX - touch2.clientX) +
(touch1.clientY - touch2.clientY) *
(touch1.clientY - touch2.clientY)
);
return dist;
}
function init(element) {
function contextInit() {
context = {
start: {}
};
}
var context;
contextInit();
function handleTouchMove(event) {
var orgEvent = event.originalEvent;
var cX = 0;
var cY = 0;
var mX = 0;
var mY = 0;
if (context.move && !context.scale) {
_.forEach(orgEvent.changedTouches, function (touch) {
if (touch.identifier === context.moveId) {
cX = touch.clientX;
cY = touch.clientY;
}
});
if (!context.lasttouch.cX) {
context.lasttouch.cX = cX;
}
if (!context.lasttouch.cY) {
context.lasttouch.cY = cY;
}
mX = cX - context.lasttouch.cX;
mY = cY - context.lasttouch.cY;
canvas.scroll({
dx: mX,
dy: mY
});
context.lasttouch.cX = cX;
context.lasttouch.cY = cY;
}
if (context.scale) {
var dist = euclideanDistance(orgEvent.touches[0], orgEvent.touches[1]);
var distRatio = dist / context.start.dist;
zoom(distRatio * context.start.zoom, context.start.mid);
context.lastDistance = dist;
}
// prevent select
event.preventDefault();
}
function handleTouchEnd(event) {
var orgEvent = event.originalEvent;
if (orgEvent.touches.length < 1) {
contextInit();
}
if (orgEvent.touches.length === 1) {
initMove(orgEvent);
}
if (orgEvent.touches.length === 2) {
initScale(orgEvent);
}
_.forEach(orgEvent.changedTouches, function (touch) {
delete context[touch.identifier];
});
// prevent select
event.preventDefault();
}
function initMove(orgEvent) {
context.move = true;
context.moveId = orgEvent.touches[0].identifier;
context.scale = false;
if (!context.lasttouch) {
context.lasttouch = {
cX: undefined,
cY: undefined,
mX: 0,
mY: 0
};
}
}
function initScale(orgEvent) {
context.scale = true;
context.start.dist = euclideanDistance(orgEvent.touches[0], orgEvent.touches[1]);
context.lastDistance = context.start.dist;
context.start.zoom = canvas.zoom();
context.start.mid = {
x: (orgEvent.touches[0].clientX + orgEvent.touches[1].clientX) / 2,
y: (orgEvent.touches[0].clientY + orgEvent.touches[1].clientY) / 2
};
}
function handleTouchStart(event) {
var orgEvent = event.originalEvent;
_.forEach(orgEvent.changedTouches, function (touch) {
context.start[touch.identifier] = touch;
});
if (orgEvent.touches.length === 1) {
initMove(orgEvent);
}
if (orgEvent.touches.length === 2) {
initScale(orgEvent);
}
//Stop all interaction for more than 2 touch sources
if (orgEvent.touches.length > 2) {
contextInit();
}
event.preventDefault();
}
$(element).on('touchstart', handleTouchStart);
$(element).on('touchmove', handleTouchMove);
$(document).on('touchend', handleTouchEnd);
}
eventBus.on('canvas.init', function(e) {
init(e.paper.node);
});
}
TouchInteraction.$inject = ['eventBus', 'canvas'];
module.exports = TouchInteraction;

View File

@ -0,0 +1,4 @@
module.exports = {
__init__: [ 'touchInteraction' ],
touchInteraction: [ 'type', require('./TouchInteraction') ]
};

View File

@ -51,6 +51,7 @@ function ZoomScroll(events, canvas) {
canvas.scroll(delta);
} else {
// zoom in relative to diagram {x,y} coordinates
zoom(y, { x: event.offsetX, y: event.offsetY });
}

View File

@ -127,6 +127,7 @@ describe('viewer', function() {
// expect header to be written only once
expect(svg.indexOf('<svg width="100%" height="100%">')).toBe(-1);
expect(svg.indexOf('<g class="viewport"')).toBe(-1);
done();
});

View File

@ -0,0 +1,37 @@
'use strict';
var Matchers = require('../../../Matchers'),
TestHelper = require('../../../TestHelper');
/* global bootstrapBpmnJS, inject */
var fs = require('fs');
var $ = require('jquery');
var touchModule = require('../../../../../lib/features/touch'),
bpmnModule = require('../../../../../lib/core');
describe('features - touch', function() {
beforeEach(Matchers.add);
var diagramXML = fs.readFileSync('test/fixtures/bpmn/complex.bpmn', 'utf-8');
var testModules = [ touchModule, bpmnModule ];
beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules }));
describe('bootstrap', function() {
it('should bootstrap', inject(function(touchInteraction) {
expect(touchInteraction).not.toBe(null);
}));
});
});

View File

@ -29,7 +29,7 @@ describe('features - zoomscroll', function() {
describe('bootstrap', function() {
iit('should bootstrap', inject(function(zoomScroll) {
it('should bootstrap', inject(function(zoomScroll) {
expect(zoomScroll).not.toBe(null);
}));