feat(features/modeling): implement bpmn update as command listeners

This commit adds

* handling of bpmn update as command listeners
* label support

Related to bpmn-io/diagram-js#45

BREAKING CHANGE:

* rename bpmnModeling -> modeling to achive parity with diagram-js
This commit is contained in:
Nico Rehwaldt 2014-07-23 18:53:33 +02:00
parent e1ed479314
commit d729818b94
21 changed files with 597 additions and 471 deletions

View File

@ -2,23 +2,47 @@
var _ = require('lodash'); var _ = require('lodash');
var hasExternalLabel = require('../util/Label').hasExternalLabel, var LabelUtil = require('../util/Label');
var hasExternalLabel = LabelUtil.hasExternalLabel,
getExternalLabelBounds = LabelUtil.getExternalLabelBounds,
isExpanded = require('../util/Di').isExpanded; isExpanded = require('../util/Di').isExpanded;
function elementData(semantic, attrs) {
return _.extend({
id: semantic.id,
type: semantic.$type,
businessObject: semantic
}, attrs);
}
function collectWaypoints(waypoints) {
return _.collect(waypoints, function(p) {
return { x: p.x, y: p.y };
});
}
/** /**
* An importer that adds bpmn elements to the canvas * An importer that adds bpmn elements to the canvas
* *
* @param {EventBus} eventBus * @param {EventBus} eventBus
* @param {Canvas} canvas * @param {Canvas} canvas
* @param {ElementFactory} elementFactory
* @param {ElementRegistry} elementRegistry
*/ */
function BpmnImporter(eventBus, canvas, elementFactory) { function BpmnImporter(eventBus, canvas, elementFactory, elementRegistry) {
this._eventBus = eventBus; this._eventBus = eventBus;
this._canvas = canvas; this._canvas = canvas;
this._elementFactory = elementFactory; this._elementFactory = elementFactory;
this._elementRegistry = elementRegistry;
} }
BpmnImporter.$inject = [ 'eventBus', 'canvas', 'elementFactory' ]; BpmnImporter.$inject = [ 'eventBus', 'canvas', 'elementFactory', 'elementRegistry' ];
module.exports = BpmnImporter;
/** /**
@ -30,12 +54,13 @@ BpmnImporter.prototype.add = function(semantic, parentElement) {
var di = semantic.di, var di = semantic.di,
element; element;
// ROOT ELEMENT
// handle the special case that we deal with a // handle the special case that we deal with a
// invisible root element (process or collaboration) // invisible root element (process or collaboration)
if (di.$instanceOf('bpmndi:BPMNPlane')) { if (di.$instanceOf('bpmndi:BPMNPlane')) {
// add a virtual element (not being drawn) // add a virtual element (not being drawn)
element = this._elementFactory.createRoot(semantic); element = this._elementFactory.createRoot(elementData(semantic));
} }
// SHAPE // SHAPE
@ -44,18 +69,39 @@ BpmnImporter.prototype.add = function(semantic, parentElement) {
var collapsed = !isExpanded(semantic); var collapsed = !isExpanded(semantic);
var hidden = parentElement && (parentElement.hidden || parentElement.collapsed); var hidden = parentElement && (parentElement.hidden || parentElement.collapsed);
element = this._elementFactory.createShape(semantic, { var bounds = semantic.di.bounds;
element = this._elementFactory.createShape(elementData(semantic, {
collapsed: collapsed, collapsed: collapsed,
hidden: hidden hidden: hidden,
}); x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height
}));
this._canvas.addShape(element, parentElement); this._canvas.addShape(element, parentElement);
} }
// CONNECTION // CONNECTION
else { else if (di.$instanceOf('bpmndi:BPMNEdge')) {
element = this._elementFactory.createConnection(semantic, parentElement);
var source = this._getSource(semantic),
target = this._getTarget(semantic);
if (!source || !target) {
throw new Error('source or target not rendered for element <' + semantic.id + '>');
}
element = this._elementFactory.createConnection(elementData(semantic, {
source: source,
target: target,
waypoints: collectWaypoints(semantic.di.waypoint)
}));
this._canvas.addConnection(element, parentElement); this._canvas.addConnection(element, parentElement);
} else {
throw new Error('unknown di <' + di.$type + '> for element <' + semantic.id + '>');
} }
// (optional) LABEL // (optional) LABEL
@ -73,9 +119,66 @@ BpmnImporter.prototype.add = function(semantic, parentElement) {
* add label for an element * add label for an element
*/ */
BpmnImporter.prototype.addLabel = function (semantic, element) { BpmnImporter.prototype.addLabel = function (semantic, element) {
var label = this._elementFactory.createLabel(semantic, element); var bounds = getExternalLabelBounds(semantic, element);
var label = this._elementFactory.createLabel(elementData(semantic, {
id: semantic.id + '_label',
labelTarget: element,
type: 'label',
hidden: element.hidden,
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height
}));
return this._canvas.addShape(label, element.parent); return this._canvas.addShape(label, element.parent);
}; };
module.exports = BpmnImporter; BpmnImporter.prototype._getSource = function(semantic) {
var element,
elementSemantic = semantic.sourceRef;
// handle mysterious isMany DataAssociation#sourceRef
if (_.isArray(elementSemantic)) {
elementSemantic = elementSemantic[0];
}
if (elementSemantic && elementSemantic.$instanceOf('bpmn:DataOutput')) {
elementSemantic = elementSemantic.$parent.$parent;
}
element = elementSemantic && this._getElement(elementSemantic);
if (element) {
return element;
}
throw new Error('element <' + elementSemantic.id + '> referenced by <' + semantic.id + '> not yet drawn');
};
BpmnImporter.prototype._getTarget = function(semantic) {
var element,
elementSemantic = semantic.targetRef;
if (elementSemantic && elementSemantic.$instanceOf('bpmn:DataInput')) {
elementSemantic = elementSemantic.$parent.$parent;
}
element = elementSemantic && this._getElement(elementSemantic);
if (element) {
return element;
}
throw new Error('element <' + elementSemantic.id + '> referenced by <' + semantic.id + '> not yet drawn');
};
BpmnImporter.prototype._getElement = function(semantic) {
return this._elementRegistry.getById(semantic.id);
};

View File

@ -1,78 +0,0 @@
'use strict';
var _ = require('lodash');
var LabelUtil = require('../util/Label');
var getExternalLabelBounds = LabelUtil.getExternalLabelBounds;
/**
* A factory for diagram-js shapes
*
* @param {ElementFactory} canvas
*/
function ElementFactory(canvas) {
this._canvas = canvas;
}
ElementFactory.$inject = [ 'canvas' ];
module.exports = ElementFactory;
ElementFactory.prototype.createRoot = function(semantic) {
return this._canvas.create('root', _.extend({
id: semantic.id,
type: semantic.$type,
businessObject: semantic
}));
};
ElementFactory.prototype.createLabel = function(semantic, element) {
var labelBounds = getExternalLabelBounds(semantic, element);
var labelData = _.extend({
id: semantic.id + '_label',
labelTarget: element,
type: 'label',
hidden: element.hidden,
businessObject: semantic
}, labelBounds);
return this._canvas.create('label', labelData);
};
ElementFactory.prototype.createShape = function(semantic, attrs) {
var bounds = semantic.di.bounds;
var shapeData = _.extend({
id: semantic.id,
type: semantic.$type,
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
businessObject: semantic
}, attrs);
return this._canvas.create('shape', shapeData);
};
ElementFactory.prototype.createConnection = function(semantic) {
var waypoints = _.collect(semantic.di.waypoint, function(p) {
return { x: p.x, y: p.y };
});
return this._canvas.create('connection', {
id: semantic.id,
type: semantic.$type,
waypoints: waypoints,
businessObject: semantic
});
};

View File

@ -1,4 +1,3 @@
module.exports = { module.exports = {
bpmnImporter: [ 'type', require('./BpmnImporter') ], bpmnImporter: [ 'type', require('./BpmnImporter') ]
elementFactory: [ 'type', require('./ElementFactory') ]
}; };

View File

@ -1020,7 +1020,7 @@ function BpmnRenderer(events, styles, pathMap) {
}); });
var sequenceFlow = getSemantic(element); var sequenceFlow = getSemantic(element);
var source = sequenceFlow.sourceRef; var source = element.source.businessObject;
// conditional flow marker // conditional flow marker
if (sequenceFlow.conditionExpression && source.$instanceOf('bpmn:Task')) { if (sequenceFlow.conditionExpression && source.$instanceOf('bpmn:Task')) {

View File

@ -1,37 +0,0 @@
'use strict';
var AppendFlowNodeHandler = require('./cmd/AppendFlowNodeHandler');
/**
* BPMN 2.0 modeling features activator
*
* @param {CommandStack} commandStack
*/
function BpmnModeling(commandStack) {
commandStack.registerHandler('shape.appendNode', AppendFlowNodeHandler);
this._commandStack = commandStack;
}
BpmnModeling.$inject = [ 'commandStack' ];
/**
* Append a flow node to the element with the given source
* at the specified position.
*/
BpmnModeling.prototype.appendFlowNode = function(source, parent, type, position) {
var context = {
source: source,
type: type,
parent: parent,
position: position
};
this._commandStack.execute('shape.appendNode', context);
return context.target;
};
module.exports = BpmnModeling;

View File

@ -1,22 +0,0 @@
'use strict';
var getMidPoint = module.exports.getMidPoint = function(bounds) {
return {
x: bounds.x + bounds.width / 2,
y: bounds.y + bounds.height / 2
};
};
module.exports.getDirectConnectionPoints = function(boundsA, boundsB) {
return [
// workaround until we can compute the extend of an element
{
x: boundsA.x + boundsA.width,
y: boundsA.y + boundsA.height / 2
},
{
x: boundsB.x,
y: boundsB.y + boundsB.height / 2
}
];
};

View File

@ -1,215 +0,0 @@
'use strict';
var LayoutUtil = require('../LayoutUtil'),
Collections = require('diagram-js/lib/util/Collections');
var hasExternalLabel = require('../../../util/Label').hasExternalLabel;
var AppendShapeHandler = require('diagram-js/lib/features/modeling/cmd/AppendShapeHandler');
var Refs = require('object-refs');
var diRefs = new Refs({ name: 'bpmnElement', enumerable: true }, { name: 'di' });
function AppendFlowNodeHandler(canvas, bpmnFactory, bpmnImporter, elementFactory) {
AppendShapeHandler.call(this);
this._canvas = canvas;
this._bpmnFactory = bpmnFactory;
this._bpmnImporter = bpmnImporter;
this._elementFactory = elementFactory;
}
AppendFlowNodeHandler.prototype = Object.create(AppendShapeHandler.prototype);
AppendFlowNodeHandler.$inject = [ 'canvas', 'bpmnFactory', 'bpmnImporter', 'elementFactory' ];
AppendFlowNodeHandler.prototype.createShape = function(source, position, parent, context) {
var bpmnFactory = this._bpmnFactory,
elementFactory = this._elementFactory,
canvas = this._canvas;
var sourceSemantic = source.businessObject,
parentSemantic = parent.businessObject;
var target = context.target,
targetSemantic,
targetDi;
// we need to create shape + bpmn elements
if (!target) {
// create semantic
targetSemantic = bpmnFactory.create(context.type);
// create di
targetDi = bpmnFactory.createDiShape(targetSemantic, position, {
id: targetSemantic.id + '_di'
});
// bind semantic -> di -> semantic
diRefs.bind(targetSemantic, 'di');
targetSemantic.di = targetDi;
// create node
target = elementFactory.createShape(targetSemantic);
// add label
if (hasExternalLabel(targetSemantic)) {
target.label = elementFactory.createLabel(targetSemantic, target);
}
}
// load di + semantic from target
else {
targetSemantic = target.businessObject;
targetDi = targetSemantic.di;
}
// reconnect everything
// wire semantic
parentSemantic.get('flowElements').push(targetSemantic);
targetSemantic.$parent = parentSemantic;
// wire di
sourceSemantic.di.$parent.get('planeElement').push(targetDi);
targetDi.$parent = sourceSemantic.di.$parent;
canvas.addShape(target, parent);
if (target.label) {
canvas.addShape(target.label, parent);
}
return target;
};
AppendFlowNodeHandler.prototype.createConnection = function(source, target, parent, context) {
var bpmnFactory = this._bpmnFactory,
elementFactory = this._elementFactory,
canvas = this._canvas;
var sourceSemantic = source.businessObject,
targetSemantic = target.businessObject,
parentSemantic = parent.businessObject;
var connection = context.connection,
connectionSemantic,
connectionDi;
// we need to create connection + bpmn elements
if (!connection) {
// create semantic
connectionSemantic = bpmnFactory.create('bpmn:SequenceFlow');
// create di
var waypoints = LayoutUtil.getDirectConnectionPoints(sourceSemantic.di.bounds, targetSemantic.di.bounds);
connectionDi = bpmnFactory.createDiEdge(connectionSemantic, waypoints, {
id: connectionSemantic.id + '_di'
});
// bind semantic -> di -> semantic
diRefs.bind(connectionSemantic, 'di');
connectionSemantic.di = connectionDi;
// create connection
connection = elementFactory.createConnection(connectionSemantic);
// add label
if (hasExternalLabel(connectionSemantic)) {
connection.label = elementFactory.createLabel(connectionSemantic, connection);
}
}
// load di + semantic from target
else {
connectionSemantic = connection.businessObject;
connectionDi = connectionSemantic.di;
}
// connect
bpmnFactory.connectSequenceFlow(connectionSemantic, sourceSemantic, targetSemantic);
// TODO(nre): connect connection
// wire semantic
parentSemantic.get('flowElements').push(connectionSemantic);
connectionSemantic.$parent = parentSemantic;
// wire di
sourceSemantic.di.$parent.get('planeElement').push(connectionDi);
connectionDi.$parent = sourceSemantic.di.$parent;
canvas.addConnection(connection, parent);
if (connection.label) {
canvas.addShape(connection.label, parent);
}
return connection;
};
AppendFlowNodeHandler.prototype.removeShape = function(shape) {
var semantic = shape.businessObject,
di = semantic.di,
parentSemantic = semantic.$parent;
// undo wire semantic
parentSemantic.get('flowElements').splice(parentSemantic.get('flowElements').indexOf(semantic), 1);
semantic.$parent = null;
// undo wire di
di.$parent.get('planeElement').splice(di.$parent.get('planeElement').indexOf(di), 1);
di.$parent = null;
// remove label
if (shape.label) {
this._canvas.removeShape(shape.label);
}
// actual remove shape
return this._canvas.removeShape(shape);
};
AppendFlowNodeHandler.prototype.removeConnection = function(connection) {
var semantic = connection.businessObject,
di = semantic.di,
parentSemantic = semantic.$parent;
// undo wire semantic
Collections.remove(parentSemantic.get('flowElements'), semantic);
semantic.$parent = null;
// undo wire di
Collections.remove(di.$parent.get('planeElement'), di);
di.$parent = null;
// undo wire refs
this._bpmnFactory.disconnectSequenceFlow(semantic);
// TODO(nre): disconnect connection
// remove label
if (connection.label) {
this._canvas.removeShape(connection.label);
}
// actual remove connection
return this._canvas.removeConnection(connection);
};
module.exports = AppendFlowNodeHandler;

View File

@ -1,9 +0,0 @@
module.exports = {
__init__: [ 'bpmnModeling' ],
__depends__: [
require('../../core'),
require('diagram-js/lib/cmd')
],
bpmnFactory: [ 'type', require('./BpmnFactory') ],
bpmnModeling: [ 'type', require('./BpmnModeling') ]
};

View File

@ -20,22 +20,22 @@ var images = {
* *
* @param {ContextPad} contextPad * @param {ContextPad} contextPad
*/ */
function ContextPadProvider(contextPad, directEditing, bpmnModeling, selection) { function ContextPadProvider(contextPad, directEditing, modeling, selection) {
contextPad.registerProvider(this); contextPad.registerProvider(this);
this._selection = selection; this._selection = selection;
this._directEditing = directEditing; this._directEditing = directEditing;
this._bpmnModeling = bpmnModeling; this._modeling = modeling;
} }
ContextPadProvider.$inject = [ 'contextPad', 'directEditing', 'bpmnModeling', 'selection' ]; ContextPadProvider.$inject = [ 'contextPad', 'directEditing', 'modeling', 'selection' ];
ContextPadProvider.prototype.getContextPadEntries = function(element) { ContextPadProvider.prototype.getContextPadEntries = function(element) {
var directEditing = this._directEditing, var directEditing = this._directEditing,
bpmnModeling = this._bpmnModeling, modeling = this._modeling,
selection = this._selection; selection = this._selection;
var actions = {}; var actions = {};
@ -47,7 +47,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) {
var bpmnElement = element.businessObject; var bpmnElement = element.businessObject;
function append(element, type) { function append(element, type) {
var target = bpmnModeling.appendFlowNode(element, null, type); var target = modeling.appendFlowNode(element, type);
selection.select(target); selection.select(target);
directEditing.activate(target); directEditing.activate(target);

View File

@ -1,9 +1,9 @@
module.exports = { module.exports = {
__depends__: [ __depends__: [
require('diagram-js-direct-editing'),
require('diagram-js/lib/features/context-pad'), require('diagram-js/lib/features/context-pad'),
require('diagram-js/lib/features/selection'), require('diagram-js/lib/features/selection'),
require('diagram-js-direct-editing'), require('../modeling')
require('../bpmn-modeling')
], ],
__init__: [ 'contextPadProvider' ], __init__: [ 'contextPadProvider' ],
contextPadProvider: [ 'type', require('./ContextPadProvider') ] contextPadProvider: [ 'type', require('./ContextPadProvider') ]

View File

@ -2,6 +2,7 @@ module.exports = {
__depends__: [ __depends__: [
require('../../core'), require('../../core'),
require('diagram-js/lib/cmd'), require('diagram-js/lib/cmd'),
require('diagram-js/lib/features/change-support'),
require('diagram-js-direct-editing') require('diagram-js-direct-editing')
], ],
__init__: [ 'labelEditingProvider' ], __init__: [ 'labelEditingProvider' ],

View File

@ -41,22 +41,7 @@ BpmnFactory.prototype.create = function(type, attrs) {
}; };
BpmnFactory.prototype.createDiShape = function(semantic, position, attrs) { BpmnFactory.prototype.createDiShape = function(semantic, bounds, attrs) {
position = position || { x: 0, y: 0 };
var bounds;
if (semantic.$instanceOf('bpmn:Task')) {
bounds = { width: 100, height: 80 };
} else {
bounds = { width: 36, height: 36 };
}
_.extend(bounds, {
x: (position.x || 0) - bounds.width / 2,
y: (position.y || 0) - bounds.height / 2
});
return this.create('bpmndi:BPMNShape', _.extend({ return this.create('bpmndi:BPMNShape', _.extend({
bpmnElement: semantic, bpmnElement: semantic,
@ -70,45 +55,22 @@ BpmnFactory.prototype.createDiBounds = function(bounds) {
}; };
BpmnFactory.prototype.createDiWaypoints = function(waypoints) {
return _.map(waypoints, function(pos) {
return this.createDiWaypoint(pos);
}, this);
};
BpmnFactory.prototype.createDiWaypoint = function(point) { BpmnFactory.prototype.createDiWaypoint = function(point) {
return this.create('dc:Point', point); return this.create('dc:Point', point);
}; };
BpmnFactory.prototype.createDiEdge = function(sequenceFlow, points, attrs) { BpmnFactory.prototype.createDiEdge = function(semantic, waypoints, attrs) {
var waypoints = _.map(points, function(pos) {
return this.createDiWaypoint(pos);
}, this);
return this.create('bpmndi:BPMNEdge', _.extend({ return this.create('bpmndi:BPMNEdge', _.extend({
bpmnElement: sequenceFlow, bpmnElement: semantic
waypoint: waypoints
}, attrs)); }, attrs));
}; };
BpmnFactory.prototype.disconnectSequenceFlow = function(sequenceFlow) {
Collections.remove(sequenceFlow.sourceRef && sequenceFlow.sourceRef.get('outgoing'), sequenceFlow);
Collections.remove(sequenceFlow.targetRef && sequenceFlow.targetRef.get('incoming'), sequenceFlow);
_.extend(sequenceFlow, {
sourceRef: null,
targetRef: null
});
};
BpmnFactory.prototype.connectSequenceFlow = function(sequenceFlow, source, target) {
_.extend(sequenceFlow, {
sourceRef: source,
targetRef: target
});
source.get('outgoing').push(sequenceFlow);
target.get('incoming').push(sequenceFlow);
};
module.exports = BpmnFactory; module.exports = BpmnFactory;

View File

@ -0,0 +1,181 @@
'use strict';
var _ = require('lodash');
var Collections = require('diagram-js/lib/util/Collections');
/**
* A handler responsible for updating the underlying BPMN 2.0 XML + DI
* once changes on the diagram happen
*/
function BpmnUpdater(eventBus, bpmnFactory) {
this._eventBus = eventBus;
this._bpmnFactory = bpmnFactory;
var self = this;
this.executed('shape.create', function(e) {
self.createShapeDi(e.context.shape);
});
this.executed('connection.create', function(e) {
self.createConnectionDi(e.context.connection);
});
function updateShapeParent(e) {
self.updateShapeParent(e.context.shape || e.context.connection);
}
this.executed([ 'shape.move', 'shape.create', 'connection.create' ], updateShapeParent);
this.reverted([ 'shape.move', 'shape.create', 'connection.create' ], updateShapeParent);
function updateConnection(e) {
self.updateConnection(e.context.connection);
}
this.executed([ 'connection.create' ], updateConnection);
this.reverted([ 'connection.create' ], updateConnection);
}
module.exports = BpmnUpdater;
BpmnUpdater.$inject = [ 'eventBus', 'bpmnFactory' ];
/////// implementation //////////////////////////////////
BpmnUpdater.prototype.createShapeDi = function(shape) {
var businessObject = shape.businessObject;
if (!businessObject.di) {
businessObject.di = this._bpmnFactory.createDiShape(businessObject, shape, {
id: businessObject.id + '_di'
});
}
};
BpmnUpdater.prototype.createConnectionDi = function(connection) {
var businessObject = connection.businessObject;
if (!businessObject.di) {
businessObject.di = this._bpmnFactory.createDiEdge(businessObject, connection.waypoints, {
id: businessObject.id + '_di'
});
}
};
BpmnUpdater.prototype.updateShapeParent = function(shape) {
var parentShape = shape.parent;
var businessObject = shape.businessObject,
parentBusinessObject = parentShape && parentShape.businessObject,
parentDi = parentBusinessObject && parentBusinessObject.di;
this.updateSemanticParent(businessObject, parentBusinessObject);
this.updateDiParent(businessObject.di, parentDi);
};
BpmnUpdater.prototype.updateDiParent = function(di, parentDi) {
if (parentDi && !parentDi.$instanceOf('bpmndi:BPMNPlane')) {
parentDi = parentDi.$parent;
}
if (di.$parent === parentDi) {
return;
}
var planeElements = (parentDi || di.$parent).get('planeElement');
if (parentDi) {
planeElements.push(di);
di.$parent = parentDi;
} else {
Collections.remove(planeElements, di);
di.$parent = null;
}
};
BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent) {
if (businessObject.$parent === newParent) {
return;
}
var children;
if (businessObject.$parent) {
// remove from old parent
children = businessObject.$parent.get('flowElements');
Collections.remove(children, businessObject);
}
if (!newParent) {
businessObject.$parent = null;
} else {
// add to new parent
children = newParent.get('flowElements');
children.push(businessObject);
businessObject.$parent = newParent;
}
};
BpmnUpdater.prototype.updateConnection = function(connection) {
var businessObject = connection.businessObject,
newSource = connection.source && connection.source.businessObject,
newTarget = connection.target && connection.target.businessObject;
if (businessObject.sourceRef !== newSource) {
Collections.remove(businessObject.sourceRef && businessObject.sourceRef.get('outgoing'), businessObject);
if (newSource) {
newSource.get('outgoing').push(businessObject);
}
businessObject.sourceRef = newSource;
}
if (businessObject.targetRef !== newTarget) {
Collections.remove(businessObject.targetRef && businessObject.targetRef.get('incoming'), businessObject);
if (newTarget) {
newTarget.get('incoming').push(businessObject);
}
businessObject.targetRef = newTarget;
}
businessObject.di.set('waypoint', this._bpmnFactory.createDiWaypoints(connection.waypoints));
};
/////// helpers /////////////////////////////////////////
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

@ -0,0 +1,58 @@
'use strict';
var _ = require('lodash');
var BaseElementFactory = require('diagram-js/lib/core/ElementFactory');
/**
* A bpmn-aware factory for diagram-js shapes
*/
function ElementFactory(bpmnFactory) {
BaseElementFactory.call(this);
this._bpmnFactory = bpmnFactory;
}
ElementFactory.prototype = Object.create(BaseElementFactory.prototype);
module.exports = ElementFactory;
ElementFactory.prototype.createWithBpmn = function(elementType, attrs) {
attrs = attrs || {};
var businessObject = attrs.businessObject;
if (!businessObject) {
if (!attrs.type) {
throw new Error('no shape type specified');
}
businessObject = this._bpmnFactory.create(attrs.type);
_.extend(attrs, {
businessObject: businessObject,
id: businessObject.id
});
}
return this.create(elementType, attrs);
};
ElementFactory.prototype.createRoot = function(attrs) {
return this.createWithBpmn('root', attrs);
};
ElementFactory.prototype.createLabel = function(attrs) {
return this.create('label', _.extend({ type: 'label' }, attrs));
};
ElementFactory.prototype.createShape = function(attrs) {
return this.createWithBpmn('shape', attrs);
};
ElementFactory.prototype.createConnection = function(attrs) {
return this.createWithBpmn('connection', attrs);
};

View File

@ -0,0 +1,59 @@
'use strict';
var _ = require('lodash');
var LabelUtil = require('../../util/Label');
var hasExternalLabel = LabelUtil.hasExternalLabel,
getExternalLabelMid = LabelUtil.getExternalLabelMid;
function LabelSupport(eventBus, modeling, bpmnFactory) {
eventBus.on([
'commandStack.shape.create.postExecute',
'commandStack.connection.create.postExecute'
], function(e) {
var context = e.context;
var element = context.shape || context.connection,
businessObject = element.businessObject;
var position;
if (hasExternalLabel(businessObject)) {
position = getExternalLabelMid(element);
modeling.createLabel(element, position, {
id: businessObject.id + '_label',
businessObject: businessObject
});
}
});
eventBus.on([
'commandStack.label.create.executed',
'commandStack.label.moved.executed'
], function(e) {
var element = e.context.shape,
businessObject = element.businessObject,
di = businessObject.di;
if (!di.label) {
di.label = bpmnFactory.create('bpmndi:BPMNLabel', {
bounds: bpmnFactory.create('dc:Bounds')
});
}
_.extend(di.label.bounds, {
x: element.x,
y: element.y,
width: element.width,
height: element.height
});
});
}
LabelSupport.$inject = [ 'eventBus', 'modeling', 'bpmnFactory' ];
module.exports = LabelSupport;

View File

@ -0,0 +1,50 @@
'use strict';
var _ = require('lodash');
var BaseModeling = require('diagram-js/lib/features/modeling/Modeling');
var AppendShapeHandler = require('./cmd/AppendShapeHandler'),
CreateShapeHandler = require('diagram-js/lib/features/modeling/cmd/CreateShapeHandler'),
CreateConnectionHandler = require('diagram-js/lib/features/modeling/cmd/CreateConnectionHandler'),
CreateLabelHandler = require('diagram-js/lib/features/modeling/cmd/CreateLabelHandler');
/**
* BPMN 2.0 modeling features activator
*
* @param {EventBus} eventBus
* @param {CommandStack} commandStack
*/
function Modeling(eventBus, commandStack) {
BaseModeling.call(this, eventBus, commandStack);
}
Modeling.prototype = Object.create(BaseModeling.prototype);
Modeling.$inject = [ 'eventBus', 'commandStack' ];
module.exports = Modeling;
Modeling.prototype.registerHandlers = function(commandStack) {
commandStack.registerHandler('shape.appendShape', AppendShapeHandler);
commandStack.registerHandler('shape.create', CreateShapeHandler);
commandStack.registerHandler('connection.create', CreateConnectionHandler);
commandStack.registerHandler('label.create', CreateLabelHandler);
};
/**
* Append a flow node to the element with the given source
* at the specified position.
*/
Modeling.prototype.appendFlowNode = function(source, type, position) {
position = position || {
x: source.x + source.width + 100,
y: source.y + source.height / 2
};
return this.appendShape(source, { type: type }, position);
};

View File

@ -0,0 +1,28 @@
'use strict';
var BaseAppendShapeHandler = require('diagram-js/lib/features/modeling/cmd/AppendShapeHandler');
/**
* A bpmn-aware append shape handler
*
* @param {canvas} Canvas
* @param {elementFactory} ElementFactory
* @param {modeling} Modeling
*/
function AppendShapeHandler(modeling) {
this._modeling = modeling;
}
AppendShapeHandler.prototype = Object.create(BaseAppendShapeHandler.prototype);
module.exports = AppendShapeHandler;
AppendShapeHandler.$inject = [ 'modeling' ];
AppendShapeHandler.prototype.postExecute = function(context) {
this._modeling.createConnection(context.source, context.shape, {
type: 'bpmn:SequenceFlow',
}, context.source.parent);
};

View File

@ -0,0 +1,14 @@
module.exports = {
__init__: [ 'modeling', 'bpmnUpdater', 'labelSupport' ],
__depends__: [
require('../../core'),
require('diagram-js/lib/cmd'),
require('diagram-js/lib/features/change-support')
],
bpmnFactory: [ 'type', require('./BpmnFactory') ],
bpmnUpdater: [ 'type', require('./BpmnUpdater') ],
elementFactory: [ 'type', require('./ElementFactory') ],
modeling: [ 'type', require('./Modeling') ],
labelSupport: [ 'type', require('./LabelSupport') ],
layouter: [ 'type', require('diagram-js/lib/features/modeling/Layouter') ]
};

View File

@ -26,7 +26,7 @@ module.exports.hasExternalLabel = function(semantic) {
* @param {Array<Point>} waypoints * @param {Array<Point>} waypoints
* @return {Point} the mid point * @return {Point} the mid point
*/ */
module.exports.getWaypointsMid = function(waypoints) { var getWaypointsMid = module.exports.getWaypointsMid = function(waypoints) {
var mid = waypoints.length / 2 - 1; var mid = waypoints.length / 2 - 1;
@ -40,6 +40,18 @@ module.exports.getWaypointsMid = function(waypoints) {
}; };
var getExternalLabelMid = module.exports.getExternalLabelMid = function(element) {
if (element.waypoints) {
return getWaypointsMid(element.waypoints);
} else {
return {
x: element.x + element.width / 2,
y: element.y + element.height - 5
};
}
};
/** /**
* Returns the bounds of an elements label, parsed from the elements DI or * Returns the bounds of an elements label, parsed from the elements DI or
* generated from its bounds. * generated from its bounds.
@ -69,14 +81,7 @@ module.exports.getExternalLabelBounds = function(semantic, element) {
}; };
} else { } else {
if (element.waypoints) { mid = getExternalLabelMid(element);
mid = module.exports.getWaypointsMid(element.waypoints);
} else {
mid = {
x: element.x + element.width / 2,
y: element.y + element.height - 5
};
}
size = { size = {
width: 90, width: 90,

View File

@ -8,7 +8,7 @@ var Matchers = require('../../../Matchers'),
var fs = require('fs'); var fs = require('fs');
var bpmnFactoryModule = require('../../../../../lib/features/bpmn-modeling'); var modelingModule = require('../../../../../lib/features/modeling');
xdescribe('features - bpmn-factory', function() { xdescribe('features - bpmn-factory', function() {
@ -18,7 +18,7 @@ xdescribe('features - bpmn-factory', function() {
var diagramXML = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf-8'); var diagramXML = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf-8');
var testModules = [ bpmnFactoryModule ]; var testModules = [ modelingModule ];
beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules })); beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules }));

View File

@ -9,8 +9,8 @@ var _ = require('lodash');
var fs = require('fs'); var fs = require('fs');
var bpmnFactoryModule = require('../../../../../lib/features/bpmn-modeling'), var modelingModule = require('../../../../../lib/features/modeling'),
bpmnDrawModule = require('../../../../../lib/draw'); drawModule = require('../../../../../lib/draw');
describe('features - bpmn-modeling', function() { describe('features - bpmn-modeling', function() {
@ -20,7 +20,7 @@ describe('features - bpmn-modeling', function() {
var diagramXML = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf-8'); var diagramXML = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf-8');
var testModules = [ bpmnFactoryModule, bpmnDrawModule ]; var testModules = [ modelingModule, drawModule ];
beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules })); beforeEach(bootstrapBpmnJS(diagramXML, { modules: testModules }));
@ -30,13 +30,13 @@ describe('features - bpmn-modeling', function() {
describe('shape.appendNode', function() { describe('shape.appendNode', function() {
it('should execute', inject(function(elementRegistry, bpmnModeling) { it('should execute', inject(function(elementRegistry, modeling) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
// when // when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
// then // then
@ -44,13 +44,13 @@ describe('features - bpmn-modeling', function() {
expect(target.$instanceOf('bpmn:Task')).toBe(true); expect(target.$instanceOf('bpmn:Task')).toBe(true);
})); }));
it('should execute', inject(function(elementRegistry, bpmnModeling) { it('should execute', inject(function(elementRegistry, modeling) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
// when // when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:ExclusiveGateway'), var targetShape = modeling.appendFlowNode(startEventShape, null, 'bpmn:ExclusiveGateway'),
target = targetShape.businessObject; target = targetShape.businessObject;
// then // then
@ -58,7 +58,7 @@ describe('features - bpmn-modeling', function() {
expect(target.$instanceOf('bpmn:ExclusiveGateway')).toBe(true); expect(target.$instanceOf('bpmn:ExclusiveGateway')).toBe(true);
})); }));
it('should create DI', inject(function(elementRegistry, bpmnModeling) { it('should create DI', inject(function(elementRegistry, modeling) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -68,7 +68,7 @@ describe('features - bpmn-modeling', function() {
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
// when // when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
// then // then
@ -77,7 +77,7 @@ describe('features - bpmn-modeling', function() {
})); }));
it('should add to parent (sub process)', inject(function(elementRegistry, bpmnModeling) { it('should add to parent (sub process)', inject(function(elementRegistry, modeling) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -87,14 +87,14 @@ describe('features - bpmn-modeling', function() {
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
// when // when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
// then // then
expect(subProcess.get('flowElements')).toContain(target); expect(subProcess.get('flowElements')).toContain(target);
})); }));
it('should add to parent (sub process)', inject(function(elementRegistry, bpmnModeling) { it('should add to parent (sub process)', inject(function(elementRegistry, modeling) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -104,7 +104,7 @@ describe('features - bpmn-modeling', function() {
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
// when // when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:ExclusiveGateway'), var targetShape = modeling.appendFlowNode(startEventShape, null, 'bpmn:ExclusiveGateway'),
target = targetShape.businessObject; target = targetShape.businessObject;
// then // then
@ -112,7 +112,7 @@ describe('features - bpmn-modeling', function() {
})); }));
it('should add connection', inject(function(elementRegistry, bpmnModeling) { it('should add shape label', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -122,7 +122,33 @@ describe('features - bpmn-modeling', function() {
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
// when // when
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:EndEvent'),
target = targetShape.businessObject;
// then
expect(targetShape.label).toBeDefined();
expect(elementRegistry.getById(targetShape.label.id)).toBeDefined();
expect(target.di.label).toBeDefined();
expect(target.di.label.bounds.x).toBe(targetShape.label.x);
expect(target.di.label.bounds.y).toBe(targetShape.label.y);
expect(target.di.label.bounds.width).toBe(targetShape.label.width);
expect(target.di.label.bounds.height).toBe(targetShape.label.height);
}));
it('should add connection', inject(function(elementRegistry, modeling) {
// given
var startEventShape = elementRegistry.getById('StartEvent_1');
var subProcessShape = elementRegistry.getById('SubProcess_1');
var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject;
// when
var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
var connection = _.find(subProcess.get('flowElements'), function(e) { var connection = _.find(subProcess.get('flowElements'), function(e) {
@ -137,7 +163,7 @@ describe('features - bpmn-modeling', function() {
describe('undo support', function() { describe('undo support', function() {
it('should undo add to parent', inject(function(elementRegistry, bpmnModeling, commandStack) { it('should undo add to parent', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -146,7 +172,7 @@ describe('features - bpmn-modeling', function() {
var startEvent = startEventShape.businessObject, var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
// when // when
@ -158,7 +184,7 @@ describe('features - bpmn-modeling', function() {
})); }));
it('should undo add shape label', inject(function(elementRegistry, bpmnModeling, commandStack) { it('should undo add shape label', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -167,7 +193,7 @@ describe('features - bpmn-modeling', function() {
var startEvent = startEventShape.businessObject, var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:EndEvent'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:EndEvent'),
target = targetShape.businessObject; target = targetShape.businessObject;
var connection = _.find(subProcess.get('flowElements'), function(e) { var connection = _.find(subProcess.get('flowElements'), function(e) {
@ -183,11 +209,12 @@ describe('features - bpmn-modeling', function() {
expect(connection.$parent).toBe(null); expect(connection.$parent).toBe(null);
expect(subProcess.di.$parent.get('planeElement')).not.toContain(connection.di); expect(subProcess.di.$parent.get('planeElement')).not.toContain(connection.di);
expect(elementRegistry.getById(targetShape.label.id)).not.toBeDefined(); expect(targetShape.label).not.toBeDefined();
expect(elementRegistry.getById(target.id + '_label')).not.toBeDefined();
})); }));
it('should undo add connection', inject(function(elementRegistry, bpmnModeling, commandStack) { it('should undo add connection', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -196,7 +223,7 @@ describe('features - bpmn-modeling', function() {
var startEvent = startEventShape.businessObject, var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
var connection = _.find(subProcess.get('flowElements'), function(e) { var connection = _.find(subProcess.get('flowElements'), function(e) {
@ -220,7 +247,7 @@ describe('features - bpmn-modeling', function() {
})); }));
it('should undo add connection label', inject(function(elementRegistry, bpmnModeling, commandStack) { it('should undo add connection label', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -229,7 +256,7 @@ describe('features - bpmn-modeling', function() {
var startEvent = startEventShape.businessObject, var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
var connection = _.find(subProcess.get('flowElements'), function(e) { var connection = _.find(subProcess.get('flowElements'), function(e) {
@ -248,7 +275,7 @@ describe('features - bpmn-modeling', function() {
expect(elementRegistry.getById(connection.id + '_label')).not.toBeDefined(); expect(elementRegistry.getById(connection.id + '_label')).not.toBeDefined();
})); }));
it('should undo add to parent', inject(function(elementRegistry, bpmnModeling, commandStack) { it('should undo add to parent', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -257,7 +284,7 @@ describe('features - bpmn-modeling', function() {
var startEvent = startEventShape.businessObject, var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:ExclusiveGateway'), var targetShape = modeling.appendFlowNode(startEventShape, null, 'bpmn:ExclusiveGateway'),
target = targetShape.businessObject; target = targetShape.businessObject;
// when // when
@ -269,7 +296,7 @@ describe('features - bpmn-modeling', function() {
})); }));
it('should undo/redo appending multiple shapes', inject(function(elementRegistry, bpmnModeling, commandStack) { it('should redo appending multiple shapes', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -278,10 +305,10 @@ describe('features - bpmn-modeling', function() {
var startEvent = startEventShape.businessObject, var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
var targetShape2 = bpmnModeling.appendFlowNode(targetShape, null, 'bpmn:UserTask'); var targetShape2 = modeling.appendFlowNode(targetShape, 'bpmn:UserTask');
// when // when
commandStack.undo(); commandStack.undo();
@ -303,7 +330,7 @@ describe('features - bpmn-modeling', function() {
})); }));
it('should undo/redo add connection', inject(function(elementRegistry, bpmnModeling, commandStack) { it('should redo add connection', inject(function(elementRegistry, modeling, commandStack) {
// given // given
var startEventShape = elementRegistry.getById('StartEvent_1'); var startEventShape = elementRegistry.getById('StartEvent_1');
@ -312,7 +339,7 @@ describe('features - bpmn-modeling', function() {
var startEvent = startEventShape.businessObject, var startEvent = startEventShape.businessObject,
subProcess = subProcessShape.businessObject; subProcess = subProcessShape.businessObject;
var targetShape = bpmnModeling.appendFlowNode(startEventShape, null, 'bpmn:Task'), var targetShape = modeling.appendFlowNode(startEventShape, 'bpmn:Task'),
target = targetShape.businessObject; target = targetShape.businessObject;
var connection = _.find(subProcess.get('flowElements'), function(e) { var connection = _.find(subProcess.get('flowElements'), function(e) {