feat(snapping): add bpmn-specific move snapping

This commit adds message flow + collaboration specific snapping by
subclassing the diagram-js provided default Snapping implementation.

* Add collaboration snapping
* Rename lib/util/{Name}.js -> lib/util/{Name}Util.js

Closes #255
This commit is contained in:
Nico Rehwaldt 2015-04-27 16:50:09 +02:00 committed by Ricardo Matias
parent d8ef4772cd
commit b233ab957c
21 changed files with 235 additions and 114 deletions

View File

@ -64,7 +64,6 @@ Modeler.prototype._interactionModules = [
Modeler.prototype._modelingModules = [
// modeling components
require('diagram-js/lib/features/keyboard'),
require('diagram-js/lib/features/snapping'),
require('diagram-js/lib/features/move'),
require('diagram-js/lib/features/bendpoints'),
require('diagram-js/lib/features/resize'),

View File

@ -11,7 +11,7 @@ var inherits = require('inherits'),
var DefaultRenderer = require('diagram-js/lib/draw/Renderer'),
TextUtil = require('diagram-js/lib/util/Text'),
DiUtil = require('../util/Di');
DiUtil = require('../util/DiUtil');
var createLine = DefaultRenderer.createLine;
@ -869,7 +869,7 @@ function BpmnRenderer(events, styles, pathMap) {
var lane = renderer('bpmn:Lane')(p, element);
var expandedPool = DiUtil.isExpandedPool(getSemantic(element));
var expandedPool = DiUtil.isExpanded(element);
if (expandedPool) {
drawLine(p, [

View File

@ -2,8 +2,10 @@
var UpdateLabelHandler = require('./cmd/UpdateLabelHandler');
var LabelUtil = require('./LabelUtil'),
DiUtil = require('../../util/Di');
var LabelUtil = require('./LabelUtil');
var is = require('../../util/ModelUtil').is,
isExpanded = require('../../util/DiUtil').isExpanded;
var MIN_BOUNDS = {
@ -46,11 +48,14 @@ function LabelEditingProvider(eventBus, canvas, directEditing, commandStack, inj
eventBus.on('create.end', 500, function(e) {
var element = e.shape,
businessObject = element.businessObject;
canExecute = e.context.canExecute;
if (businessObject.$instanceOf('bpmn:Task') ||
businessObject.$instanceOf('bpmn:TextAnnotation') ||
(businessObject.$instanceOf('bpmn:SubProcess') && !businessObject.di.isExpanded)) {
if (!canExecute) {
return;
}
if (is(element, 'bpmn:Task') || is(element, 'bpmn:TextAnnotation') ||
(is(element, 'bpmn:SubProcess') && !isExpanded(element))) {
directEditing.activate(element);
}
@ -68,9 +73,6 @@ module.exports = LabelEditingProvider;
LabelEditingProvider.prototype.activate = function(element) {
var semantic = element.businessObject,
di = semantic.di;
var text = LabelUtil.getLabel(element);
if (text === undefined) {
@ -79,9 +81,8 @@ LabelEditingProvider.prototype.activate = function(element) {
var bbox = this.getEditingBBox(element);
// adjust for expanded pools / lanes
if ((semantic.$instanceOf('bpmn:Participant') && DiUtil.isExpandedPool(semantic)) ||
semantic.$instanceOf('bpmn:Lane')) {
// adjust for expanded pools AND lanes
if ((is(element, 'bpmn:Participant') && isExpanded(element)) || is(element, 'bpmn:Lane')) {
bbox.width = MIN_BOUNDS.width;
bbox.height = MIN_BOUNDS.height;
@ -90,8 +91,8 @@ LabelEditingProvider.prototype.activate = function(element) {
bbox.y = bbox.mid.y - bbox.height / 2;
}
// adjust for sub processes
if (semantic.$instanceOf('bpmn:SubProcess') && DiUtil.isExpanded(semantic, di)) {
// adjust for expanded sub processes
if (is(element, 'bpmn:SubProcess') && isExpanded(element)) {
bbox.height = MIN_BOUNDS.height;

View File

@ -8,7 +8,7 @@ var BaseLayouter = require('diagram-js/lib/layout/BaseLayouter'),
LayoutUtil = require('diagram-js/lib/layout/LayoutUtil'),
ManhattanLayout = require('diagram-js/lib/layout/ManhattanLayout');
var is = require('./ModelingUtil').is;
var is = require('../../util/ModelUtil').is;
function BpmnLayouter() {}

View File

@ -1,12 +1,13 @@
'use strict';
var isArray = require('lodash/lang/isArray'),
assign = require('lodash/object/assign'),
forEach = require('lodash/collection/forEach');
var assign = require('lodash/object/assign'),
inherits = require('inherits');
var Collections = require('diagram-js/lib/util/Collections'),
Model = require('diagram-js/lib/model');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
/**
* A handler responsible for updating the underlying BPMN 2.0 XML + DI
@ -14,7 +15,8 @@ var Collections = require('diagram-js/lib/util/Collections'),
*/
function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
this._eventBus = eventBus;
CommandInterceptor.call(this, eventBus);
this._bpmnFactory = bpmnFactory;
var self = this;
@ -122,6 +124,8 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
], updateConnectionWaypoints);
}
inherits(BpmnUpdater, CommandInterceptor);
module.exports = BpmnUpdater;
BpmnUpdater.$inject = [ 'eventBus', 'bpmnFactory', 'connectionDocking'];
@ -331,23 +335,3 @@ BpmnUpdater.prototype._getLabel = function(di) {
return di.label;
};
BpmnUpdater.prototype.pre = function(commands, callback) {
this.on(commands, 'preExecute', callback);
};
BpmnUpdater.prototype.executed = function(commands, callback) {
this.on(commands, 'executed', callback);
};
BpmnUpdater.prototype.reverted = function(commands, callback) {
this.on(commands, 'reverted', callback);
};
BpmnUpdater.prototype.on = function(commands, suffix, callback) {
commands = isArray(commands) ? commands : [ commands ];
forEach(commands, function(c) {
this._eventBus.on('commandStack.' + c + '.' + suffix, callback);
}, this);
};

View File

@ -4,7 +4,7 @@ var assign = require('lodash/object/assign'),
inherits = require('inherits');
var BaseElementFactory = require('diagram-js/lib/core/ElementFactory'),
LabelUtil = require('../../util/Label');
LabelUtil = require('../../util/LabelUtil');
/**

View File

@ -3,7 +3,7 @@
var assign = require('lodash/object/assign'),
forEach = require('lodash/collection/forEach');
var LabelUtil = require('../../util/Label');
var LabelUtil = require('../../util/LabelUtil');
var hasExternalLabel = LabelUtil.hasExternalLabel,
getExternalLabelMid = LabelUtil.getExternalLabelMid;

View File

@ -32,25 +32,3 @@ function getSharedParent(a, b) {
}
module.exports.getSharedParent = getSharedParent;
/**
* Is an element of the given BPMN type?
*
* @param {djs.model.Base|ModdleElement} element
* @param {String} type
* @return {Boolean}
*/
function is(element, type) {
var bo = getBusinessObject(element);
return bo && bo.$instanceOf(type);
}
module.exports.is = is;
function getBusinessObject(element) {
return (element && element.businessObject) || element;
}
module.exports.getBusinessObject = getBusinessObject;

View File

@ -4,6 +4,7 @@ var inherits = require('inherits');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var is = require('../../../util/ModelUtil').is;
/**
* BPMN specific create behavior
@ -24,8 +25,7 @@ function CreateBehavior(eventBus, modeling) {
shape = context.shape,
position = context.position;
if (parent.businessObject.$instanceOf('bpmn:Process') &&
shape.businessObject.$instanceOf('bpmn:Participant')) {
if (is(parent, 'bpmn:Process') && is(shape, 'bpmn:Participant')) {
// this is going to detach the process root
// and set the returned collaboration element

View File

@ -5,7 +5,7 @@ var forEach = require('lodash/collection/forEach'),
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var is = require('../ModelingUtil').is,
var is = require('../../../util/ModelUtil').is,
getSharedParent = require('../ModelingUtil').getSharedParent;

View File

@ -4,6 +4,9 @@ var inherits = require('inherits');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var is = require('../../../util/ModelUtil').is;
/**
* BPMN specific remove behavior
*/
@ -24,7 +27,7 @@ function RemoveBehavior(eventBus, modeling) {
// activate the behavior if the shape to be removed
// is a participant
if (shape.businessObject.$instanceOf('bpmn:Participant')) {
if (is(shape, 'bpmn:Participant')) {
context.collaborationRoot = parent;
}
}, true);

View File

@ -6,8 +6,9 @@ var groupBy = require('lodash/collection/groupBy'),
inherits = require('inherits');
var getParents = require('../ModelingUtil').getParents,
is = require('../ModelingUtil').is,
getBusinessObject = require('../ModelingUtil').getBusinessObject;
is = require('../../../util/ModelUtil').is,
getBusinessObject = require('../../../util/ModelUtil').getBusinessObject,
isExpanded = require('../../../util/DiUtil').isExpanded;
var RuleProvider = require('diagram-js/lib/features/rules/RuleProvider');
@ -164,11 +165,6 @@ function getScopeParent(element) {
return bo;
}
function isExpanded(element) {
var bo = getBusinessObject(element);
return bo.di.isExpanded;
}
function isSameScope(a, b) {
var scopeParentA = getScopeParent(a),
scopeParentB = getScopeParent(b);

View File

@ -1,7 +1,23 @@
'use strict';
var inherits = require('inherits');
var forEach = require('lodash/collection/forEach');
var getBoundingBox = require('diagram-js/lib/util/Elements').getBBox;
var Snapping = require('diagram-js/lib/features/snapping/Snapping'),
SnapUtil = require('diagram-js/lib/features/snapping/SnapUtil');
var is = require('../../util/ModelUtil').is;
var mid = SnapUtil.mid,
topLeft = SnapUtil.topLeft,
bottomRight = SnapUtil.bottomRight;
var round = Math.round;
/**
* BPMN specific snapping functionality
*
@ -13,8 +29,12 @@ var getBoundingBox = require('diagram-js/lib/util/Elements').getBBox;
*/
function BpmnSnapping(eventBus, canvas) {
// instantiate super
Snapping.call(this, eventBus, canvas);
/**
* Drop articipant on process <> process elements snapping
* Drop participant on process <> process elements snapping
*/
function initParticipantSnapping(context, shape, elements) {
@ -55,27 +75,24 @@ function BpmnSnapping(eventBus, canvas) {
};
var snapTopLeft = snapBox,
snapBottomRight = {
x: snapBox.x + snapBox.width,
y: snapBox.y + snapBox.height
};
snapBottomRight = bottomRight(snapBox);
if (currentTopLeft.x >= snapTopLeft.x) {
event.x = snapTopLeft.x + 30 + shapeHalfWidth;
event.snapping = true;
event.snapped = true;
} else
if (currentBottomRight.x <= snapBottomRight.x) {
event.x = snapBottomRight.x - 30 - shapeHalfWidth;
event.snapping = true;
event.snapped = true;
}
if (currentTopLeft.y >= snapTopLeft.y) {
event.y = snapTopLeft.y + shapeHalfHeight;
event.snapping = true;
event.snapped = true;
} else
if (currentBottomRight.y <= snapBottomRight.y) {
event.y = snapBottomRight.y - shapeHalfHeight;
event.snapping = true;
event.snapped = true;
}
}
@ -86,8 +103,7 @@ function BpmnSnapping(eventBus, canvas) {
rootElement = canvas.getRootElement();
// snap participant around existing elements (if any)
if (shape.businessObject.$instanceOf('bpmn:Participant') &&
rootElement.businessObject.$instanceOf('bpmn:Process')) {
if (is(shape, 'bpmn:Participant') && is(rootElement, 'bpmn:Process')) {
initParticipantSnapping(context, shape, rootElement.children);
}
@ -99,12 +115,120 @@ function BpmnSnapping(eventBus, canvas) {
shape = context.shape,
participantSnapBox = context.participantSnapBox;
if (!event.snapping && participantSnapBox) {
if (!event.snapped && participantSnapBox) {
snapParticipant(participantSnapBox, shape, event);
}
});
}
inherits(BpmnSnapping, Snapping);
BpmnSnapping.$inject = [ 'eventBus', 'canvas' ];
module.exports = BpmnSnapping;
BpmnSnapping.prototype.initSnap = function(event) {
var context = event.context,
shape = context.shape,
shapeMid,
shapeBounds,
shapeTopLeft,
shapeBottomRight,
snapContext;
snapContext = Snapping.prototype.initSnap.call(this, event);
if (is(shape, 'bpmn:Participant')) {
// assign higher priority for outer snaps on participants
snapContext.setSnapLocations([ 'top-left', 'bottom-right', 'mid' ]);
}
if (shape) {
shapeMid = mid(shape, event);
shapeBounds = {
width: shape.width,
height: shape.height,
x: isNaN(shape.x) ? round(shapeMid.x - shape.width / 2) : shape.x,
y: isNaN(shape.y) ? round(shapeMid.y - shape.height / 2) : shape.y,
};
shapeTopLeft = topLeft(shapeBounds);
shapeBottomRight = bottomRight(shapeBounds);
snapContext.setSnapOrigin('top-left', {
x: shapeTopLeft.x - event.x,
y: shapeTopLeft.y - event.y
});
snapContext.setSnapOrigin('bottom-right', {
x: shapeBottomRight.x - event.x,
y: shapeBottomRight.y - event.y
});
forEach(shape.outgoing, function(c) {
var docking = c.waypoints[0];
docking = docking.original || docking;
snapContext.setSnapOrigin(c.id + '-docking', {
x: docking.x - event.x,
y: docking.y - event.y
});
});
forEach(shape.incoming, function(c) {
var docking = c.waypoints[c.waypoints.length - 1];
docking = docking.original || docking;
snapContext.setSnapOrigin(c.id + '-docking', {
x: docking.x - event.x,
y: docking.y - event.y
});
});
}
};
BpmnSnapping.prototype.addTargetSnaps = function(snapPoints, shape, target) {
var siblings = this.getSiblings(shape, target);
forEach(siblings, function(s) {
snapPoints.add('mid', mid(s));
if (is(s, 'bpmn:Participant')) {
snapPoints.add('top-left', topLeft(s));
snapPoints.add('bottom-right', bottomRight(s));
}
});
forEach(shape.incoming, function(c) {
if (siblings.indexOf(c.source) === -1) {
snapPoints.add('mid', mid(c.source));
var docking = c.waypoints[0];
snapPoints.add(c.id + '-docking', docking.original || docking);
}
});
forEach(shape.outgoing, function(c) {
if (siblings.indexOf(c.target) === -1) {
snapPoints.add('mid', mid(c.target));
var docking = c.waypoints[c.waypoints.length - 1];
snapPoints.add(c.id + '-docking', docking.original || docking);
}
});
};

View File

@ -1,4 +1,4 @@
module.exports = {
__init__: [ 'bpmnSnapping' ],
bpmnSnapping: [ 'type', require('./BpmnSnapping') ]
__init__: [ 'snapping' ],
snapping: [ 'type', require('./BpmnSnapping') ]
};

View File

@ -3,11 +3,11 @@
var assign = require('lodash/object/assign'),
map = require('lodash/collection/map');
var LabelUtil = require('../util/Label');
var LabelUtil = require('../util/LabelUtil');
var hasExternalLabel = LabelUtil.hasExternalLabel,
getExternalLabelBounds = LabelUtil.getExternalLabelBounds,
isExpanded = require('../util/Di').isExpanded,
isExpanded = require('../util/DiUtil').isExpanded,
elementToString = require('./Util').elementToString;

View File

@ -1,16 +0,0 @@
'use strict';
module.exports.isExpandedPool = function(semantic) {
return !!semantic.processRef;
};
module.exports.isExpanded = function(semantic) {
// Is type expanded by default?
var isDefaultExpanded = !(semantic.$instanceOf('bpmn:SubProcess') || semantic.$instanceOf('bpmn:CallActivity'));
// For non default expanded types -> evaluate the expanded flag
var isExpanded = isDefaultExpanded || semantic.di.isExpanded;
return isExpanded;
};

21
lib/util/DiUtil.js Normal file
View File

@ -0,0 +1,21 @@
'use strict';
var is = require('./ModelUtil').is,
getBusinessObject = require('./ModelUtil').getBusinessObject;
module.exports.isExpanded = function(element) {
if (is(element, 'bpmn:CallActivity')) {
return false;
}
if (is(element, 'bpmn:SubProcess')) {
return getBusinessObject(element).di.isExpanded;
}
if (is(element, 'bpmn:Participant')) {
return !!getBusinessObject(element).processRef;
}
return true;
};

31
lib/util/ModelUtil.js Normal file
View File

@ -0,0 +1,31 @@
'use strict';
/**
* Is an element of the given BPMN type?
*
* @param {djs.model.Base|ModdleElement} element
* @param {String} type
*
* @return {Boolean}
*/
function is(element, type) {
var bo = getBusinessObject(element);
return bo && bo.$instanceOf(type);
}
module.exports.is = is;
/**
* Return the business object for a given element.
*
* @param {djs.model.Base|ModdleElement} element
*
* @return {ModdleElement}
*/
function getBusinessObject(element) {
return (element && element.businessObject) || element;
}
module.exports.getBusinessObject = getBusinessObject;

View File

@ -10,7 +10,7 @@ var modelingModule = require('../../../../lib/features/modeling'),
coreModule = require('../../../../lib/core');
var LabelUtil = require('../../../../lib/util/Label');
var LabelUtil = require('../../../../lib/util/LabelUtil');
describe('features/modeling - append shape', function() {

View File

@ -4,7 +4,7 @@ var TestHelper = require('../../../../TestHelper');
/* global bootstrapModeler, inject */
var is = require('../../../../../lib/features/modeling/ModelingUtil').is,
var is = require('../../../../../lib/util/ModelUtil').is,
find = require('lodash/collection/find');
var modelingModule = require('../../../../../lib/features/modeling'),