feat(features/label-editing): add touch based editing

Closes #84
This commit is contained in:
jdotzki 2014-07-07 17:17:41 +02:00 committed by Nico Rehwaldt
parent 1aa431ca36
commit 7e73e9d7c9
5 changed files with 243 additions and 137 deletions

View File

@ -4,9 +4,8 @@ var _ = require('lodash');
var UpdateTextHandler = require('./cmd/UpdateTextHandler'); var UpdateTextHandler = require('./cmd/UpdateTextHandler');
var LabelUtil = require('./LabelUtil'); var LabelUtil = require('./LabelUtil'),
DiUtil = require('../../util/Di');
var DiUtil = require('../../util/Di');
var minBounds = { var minBounds = {
@ -20,7 +19,6 @@ function LabelEditingProvider(eventBus, canvas, directEditing, commandStack, bpm
directEditing.registerProvider(this); directEditing.registerProvider(this);
commandStack.registerHandler('bpmnElement.updateText', UpdateTextHandler); commandStack.registerHandler('bpmnElement.updateText', UpdateTextHandler);
// per default, listen to double click events // per default, listen to double click events
eventBus.on('shape.dblclick', function(event) { eventBus.on('shape.dblclick', function(event) {
directEditing.activate(event.element); directEditing.activate(event.element);
@ -31,8 +29,7 @@ function LabelEditingProvider(eventBus, canvas, directEditing, commandStack, bpm
directEditing.activate(event.element); directEditing.activate(event.element);
}); });
// intercept direct canvas clicks to deselect all // intercept direct canvas clicks to deselect all selected shapes
// selected shapes
eventBus.on('canvas.click', function(event) { eventBus.on('canvas.click', function(event) {
directEditing.complete(); directEditing.complete();
}); });

View File

@ -5,7 +5,136 @@ var _ = require('lodash');
function TouchInteraction(eventBus, canvas) { function TouchInteraction(eventBus, canvas) {
var context;
var RANGE = { min: 0.2, max: 4 }; var RANGE = { min: 0.2, max: 4 };
var DOUBLE_TAP_THRESHOLD = 300; // ms
function handleTouchStart(event) {
var orgEvent = event.originalEvent ? event.originalEvent : event;
_.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();
}
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 init(element) {
contextInit();
//--- general handlers
$(element).on('touchstart', handleTouchStart);
$(element).on('touchmove', handleTouchMove);
$(document).on('touchend', handleTouchEnd);
//--- snap handler
eventBus.on('shape.touchstart', function(event) {
// ---
// TODO must forwarded to touchstart???#
handleTouchStart(event);
});
eventBus.on('shape.touchend', function(event) {
if(event.touches.length !== 0) {
return;
}
if((Date.now() - context.lastTap.time) < DOUBLE_TAP_THRESHOLD) {
eventBus.fire('shape.dbltap', event);
}
context.lastTap.time = Date.now();
});
}
function cap(scale) { function cap(scale) {
return Math.max(RANGE.min, Math.min(RANGE.max, scale)); return Math.max(RANGE.min, Math.min(RANGE.max, scale));
@ -27,139 +156,36 @@ function TouchInteraction(eventBus, canvas) {
return dist; return dist;
} }
function init(element) { function initMove(orgEvent) {
context.move = true;
function contextInit() { context.moveId = orgEvent.touches[0].identifier;
context = { context.scale = false;
start: {} if (!context.lasttouch) {
context.lasttouch = {
cX: undefined,
cY: undefined,
mX: 0,
mY: 0
}; };
} }
}
var context; function initScale(orgEvent) {
contextInit(); 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 handleTouchMove(event) { function contextInit() {
var orgEvent = event.originalEvent; context = {
var cX = 0; start: {},
var cY = 0; lastTap: {}
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) { eventBus.on('canvas.init', function(e) {
@ -167,6 +193,6 @@ function TouchInteraction(eventBus, canvas) {
}); });
} }
TouchInteraction.$inject = ['eventBus', 'canvas']; TouchInteraction.$inject = [ 'eventBus', 'canvas', 'touchFix' ];
module.exports = TouchInteraction; module.exports = TouchInteraction;

View File

@ -1,4 +1,7 @@
module.exports = { module.exports = {
__depends__: [
require('diagram-js/lib/features/touch')
],
__init__: [ 'touchInteraction' ], __init__: [ 'touchInteraction' ],
touchInteraction: [ 'type', require('./TouchInteraction') ] touchInteraction: [ 'type', require('./TouchInteraction') ]
}; };

View File

@ -44,7 +44,7 @@ describe('features - label-editing', function() {
})); }));
it('should cancel on ESC', inject(function(elementRegistry, bpmnRegistry, directEditing, eventBus) { it('should cancel on <ESC>', inject(function(elementRegistry, bpmnRegistry, directEditing, eventBus) {
// given // given
var shape = elementRegistry.getById('task-nested-embedded'); var shape = elementRegistry.getById('task-nested-embedded');
@ -68,6 +68,31 @@ describe('features - label-editing', function() {
expect(task.name).toBe(oldName); expect(task.name).toBe(oldName);
})); }));
it('should submit on <canvas.click>', inject(function(elementRegistry, bpmnRegistry, directEditing, eventBus) {
// given
var shape = elementRegistry.getById('task-nested-embedded');
var task = bpmnRegistry.getSemantic('task-nested-embedded');
// activate
eventBus.fire('shape.dblclick', { element: shape });
var newName = 'new value';
// a jQuery <textarea /> element
var textarea = directEditing._textbox.textarea;
// when
// change + <canvas.click>
textarea.val(newName);
eventBus.fire('canvas.click', {});
// then
expect(directEditing.isActive()).toBe(false);
expect(task.name).toBe(newName);
}));
}); });

View File

@ -0,0 +1,55 @@
'use strict';
var Matchers = require('../../../Matchers'),
TestHelper = require('../../../TestHelper');
/* global bootstrapBpmnJS, inject */
var fs = require('fs');
var Modeler = require('../../../../../lib/Modeler');
var labelEditingModule = require('../../../../../lib/features/label-editing'),
touchModule = require('diagram-js/lib/features/touch');
describe('direct editing - touch integration', function() {
beforeEach(Matchers.add);
var container;
beforeEach(function() {
container = jasmine.getEnv().getTestContainer();
});
function createModeler(xml, done) {
var modeler = new Modeler({ container: container });
modeler.importXML(xml, function(err) {
done(err, modeler);
});
}
it('should work on modeler (manual test)', function(done) {
var xml = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf8');
createModeler(xml, done);
});
describe('event integration', function() {
var diagramXML = fs.readFileSync('test/fixtures/bpmn/features/label-editing/labels.bpmn', 'utf-8');
var testModules = [ labelEditingModule, touchModule ];
beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules }));
it('should work via dbltap (manual test)', function() { });
});
});