bpmn-js/lib/features/modeling/BpmnUpdater.js

733 lines
18 KiB
JavaScript

import {
assign,
forEach
} from 'min-dash';
import inherits from 'inherits';
import {
remove as collectionRemove,
add as collectionAdd
} from 'diagram-js/lib/util/Collections';
import {
Label
} from 'diagram-js/lib/model';
import {
getBusinessObject,
is
} from '../../util/ModelUtil';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
/**
* A handler responsible for updating the underlying BPMN 2.0 XML + DI
* once changes on the diagram happen
*/
export default function BpmnUpdater(
eventBus, bpmnFactory, connectionDocking,
translate) {
CommandInterceptor.call(this, eventBus);
this._bpmnFactory = bpmnFactory;
this._translate = translate;
var self = this;
// connection cropping //////////////////////
// crop connection ends during create/update
function cropConnection(e) {
var context = e.context,
connection;
if (!context.cropped) {
connection = context.connection;
connection.waypoints = connectionDocking.getCroppedWaypoints(connection);
context.cropped = true;
}
}
this.executed([
'connection.layout',
'connection.create'
], cropConnection);
this.reverted([ 'connection.layout' ], function(e) {
delete e.context.cropped;
});
// BPMN + DI update //////////////////////
// update parent
function updateParent(e) {
var context = e.context;
self.updateParent(context.shape || context.connection, context.oldParent);
}
function reverseUpdateParent(e) {
var context = e.context;
var element = context.shape || context.connection,
// oldParent is the (old) new parent, because we are undoing
oldParent = context.parent || context.newParent;
self.updateParent(element, oldParent);
}
this.executed([
'shape.move',
'shape.create',
'shape.delete',
'connection.create',
'connection.move',
'connection.delete'
], ifBpmn(updateParent));
this.reverted([
'shape.move',
'shape.create',
'shape.delete',
'connection.create',
'connection.move',
'connection.delete'
], ifBpmn(reverseUpdateParent));
/*
* ## Updating Parent
*
* When morphing a Process into a Collaboration or vice-versa,
* make sure that both the *semantic* and *di* parent of each element
* is updated.
*
*/
function updateRoot(event) {
var context = event.context,
oldRoot = context.oldRoot,
children = oldRoot.children;
forEach(children, function(child) {
if (is(child, 'bpmn:BaseElement')) {
self.updateParent(child);
}
});
}
this.executed([ 'canvas.updateRoot' ], updateRoot);
this.reverted([ 'canvas.updateRoot' ], updateRoot);
// update bounds
function updateBounds(e) {
var shape = e.context.shape;
if (!is(shape, 'bpmn:BaseElement')) {
return;
}
self.updateBounds(shape);
}
this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) {
// exclude labels because they're handled separately during shape.changed
if (event.context.shape.type === 'label') {
return;
}
updateBounds(event);
}));
this.reverted([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) {
// exclude labels because they're handled separately during shape.changed
if (event.context.shape.type === 'label') {
return;
}
updateBounds(event);
}));
// Handle labels separately. This is necessary, because the label bounds have to be updated
// every time its shape changes, not only on move, create and resize.
eventBus.on('shape.changed', function(event) {
if (event.element.type === 'label') {
updateBounds({ context: { shape: event.element } });
}
});
// attach / detach connection
function updateConnection(e) {
self.updateConnection(e.context);
}
this.executed([
'connection.create',
'connection.move',
'connection.delete',
'connection.reconnectEnd',
'connection.reconnectStart'
], ifBpmn(updateConnection));
this.reverted([
'connection.create',
'connection.move',
'connection.delete',
'connection.reconnectEnd',
'connection.reconnectStart'
], ifBpmn(updateConnection));
// update waypoints
function updateConnectionWaypoints(e) {
self.updateConnectionWaypoints(e.context.connection);
}
this.executed([
'connection.layout',
'connection.move',
'connection.updateWaypoints',
], ifBpmn(updateConnectionWaypoints));
this.reverted([
'connection.layout',
'connection.move',
'connection.updateWaypoints',
], ifBpmn(updateConnectionWaypoints));
// update Default & Conditional flows
this.executed([
'connection.reconnectEnd',
'connection.reconnectStart'
], ifBpmn(function(e) {
var context = e.context,
connection = context.connection,
businessObject = getBusinessObject(connection),
oldSource = getBusinessObject(context.oldSource),
oldTarget = getBusinessObject(context.oldTarget),
newSource = getBusinessObject(connection.source),
newTarget = getBusinessObject(connection.target);
if (oldSource === newSource || oldTarget === newTarget) {
return;
}
// on reconnectStart -> default flow
if (oldSource && oldSource.default === businessObject) {
context.default = oldSource.default;
oldSource.default = undefined;
}
// on reconnectEnd -> default flow
if ((businessObject.sourceRef && businessObject.sourceRef.default) &&
!(is(newTarget, 'bpmn:Activity') ||
is(newTarget, 'bpmn:EndEvent') ||
is(newTarget, 'bpmn:Gateway') ||
is(newTarget, 'bpmn:IntermediateThrowEvent'))) {
context.default = businessObject.sourceRef.default;
businessObject.sourceRef.default = undefined;
}
// on reconnectStart -> conditional flow
if (oldSource && (businessObject.conditionExpression) &&
!(is(newSource, 'bpmn:Activity') ||
is(newSource, 'bpmn:Gateway'))) {
context.conditionExpression = businessObject.conditionExpression;
businessObject.conditionExpression = undefined;
}
// on reconnectEnd -> conditional flow
if (oldTarget && (businessObject.conditionExpression) &&
!(is(newTarget, 'bpmn:Activity') ||
is(newTarget, 'bpmn:EndEvent') ||
is(newTarget, 'bpmn:Gateway') ||
is(newTarget, 'bpmn:IntermediateThrowEvent'))) {
context.conditionExpression = businessObject.conditionExpression;
businessObject.conditionExpression = undefined;
}
}));
this.reverted([
'connection.reconnectEnd',
'connection.reconnectStart'
], ifBpmn(function(e) {
var context = e.context,
connection = context.connection,
businessObject = getBusinessObject(connection),
newSource = getBusinessObject(connection.source);
// default flow
if (context.default) {
if (is(newSource, 'bpmn:ExclusiveGateway') || is(newSource, 'bpmn:InclusiveGateway') ||
is(newSource, 'bpmn:Activity')) {
newSource.default = context.default;
}
}
// conditional flow
if (context.conditionExpression && is(newSource, 'bpmn:Activity')) {
businessObject.conditionExpression = context.conditionExpression;
}
}));
// update attachments
function updateAttachment(e) {
self.updateAttachment(e.context);
}
this.executed([ 'element.updateAttachment' ], ifBpmn(updateAttachment));
this.reverted([ 'element.updateAttachment' ], ifBpmn(updateAttachment));
}
inherits(BpmnUpdater, CommandInterceptor);
BpmnUpdater.$inject = [
'eventBus',
'bpmnFactory',
'connectionDocking',
'translate'
];
// implementation //////////////////////
BpmnUpdater.prototype.updateAttachment = function(context) {
var shape = context.shape,
businessObject = shape.businessObject,
host = shape.host;
businessObject.attachedToRef = host && host.businessObject;
};
BpmnUpdater.prototype.updateParent = function(element, oldParent) {
// do not update BPMN 2.0 label parent
if (element instanceof Label) {
return;
}
// data stores in collaborations are handled seperately by DataStoreBehavior
if (is(element, 'bpmn:DataStoreReference') &&
element.parent &&
is(element.parent, 'bpmn:Collaboration')) {
return;
}
var parentShape = element.parent;
var businessObject = element.businessObject,
parentBusinessObject = parentShape && parentShape.businessObject,
parentDi = parentBusinessObject && parentBusinessObject.di;
if (is(element, 'bpmn:FlowNode')) {
this.updateFlowNodeRefs(businessObject, parentBusinessObject, oldParent && oldParent.businessObject);
}
if (is(element, 'bpmn:DataOutputAssociation')) {
if (element.source) {
parentBusinessObject = element.source.businessObject;
} else {
parentBusinessObject = null;
}
}
if (is(element, 'bpmn:DataInputAssociation')) {
if (element.target) {
parentBusinessObject = element.target.businessObject;
} else {
parentBusinessObject = null;
}
}
this.updateSemanticParent(businessObject, parentBusinessObject);
if (is(element, 'bpmn:DataObjectReference') && businessObject.dataObjectRef) {
this.updateSemanticParent(businessObject.dataObjectRef, parentBusinessObject);
}
this.updateDiParent(businessObject.di, parentDi);
};
BpmnUpdater.prototype.updateBounds = function(shape) {
var di = shape.businessObject.di;
var target = (shape instanceof Label) ? this._getLabel(di) : di;
var bounds = target.bounds;
if (!bounds) {
bounds = this._bpmnFactory.createDiBounds();
target.set('bounds', bounds);
}
assign(bounds, {
x: shape.x,
y: shape.y,
width: shape.width,
height: shape.height
});
};
BpmnUpdater.prototype.updateFlowNodeRefs = function(businessObject, newContainment, oldContainment) {
if (oldContainment === newContainment) {
return;
}
var oldRefs, newRefs;
if (is (oldContainment, 'bpmn:Lane')) {
oldRefs = oldContainment.get('flowNodeRef');
collectionRemove(oldRefs, businessObject);
}
if (is(newContainment, 'bpmn:Lane')) {
newRefs = newContainment.get('flowNodeRef');
collectionAdd(newRefs, businessObject);
}
};
// update existing sourceElement and targetElement di information
BpmnUpdater.prototype.updateDiConnection = function(di, newSource, newTarget) {
if (di.sourceElement && di.sourceElement.bpmnElement !== newSource) {
di.sourceElement = newSource && newSource.di;
}
if (di.targetElement && di.targetElement.bpmnElement !== newTarget) {
di.targetElement = newTarget && newTarget.di;
}
};
BpmnUpdater.prototype.updateDiParent = function(di, parentDi) {
if (parentDi && !is(parentDi, '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 {
collectionRemove(planeElements, di);
di.$parent = null;
}
};
function getDefinitions(element) {
while (element && !is(element, 'bpmn:Definitions')) {
element = element.$parent;
}
return element;
}
BpmnUpdater.prototype.getLaneSet = function(container) {
var laneSet, laneSets;
// bpmn:Lane
if (is(container, 'bpmn:Lane')) {
laneSet = container.childLaneSet;
if (!laneSet) {
laneSet = this._bpmnFactory.create('bpmn:LaneSet');
container.childLaneSet = laneSet;
laneSet.$parent = container;
}
return laneSet;
}
// bpmn:Participant
if (is(container, 'bpmn:Participant')) {
container = container.processRef;
}
// bpmn:FlowElementsContainer
laneSets = container.get('laneSets');
laneSet = laneSets[0];
if (!laneSet) {
laneSet = this._bpmnFactory.create('bpmn:LaneSet');
laneSet.$parent = container;
laneSets.push(laneSet);
}
return laneSet;
};
BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, visualParent) {
var containment,
translate = this._translate;
if (businessObject.$parent === newParent) {
return;
}
if (is(businessObject, 'bpmn:DataInput') || is(businessObject, 'bpmn:DataOutput')) {
if (is(newParent, 'bpmn:Participant') && 'processRef' in newParent) {
newParent = newParent.processRef;
}
// already in correct ioSpecification
if ('ioSpecification' in newParent && newParent.ioSpecification === businessObject.$parent) {
return;
}
}
if (is(businessObject, 'bpmn:Lane')) {
if (newParent) {
newParent = this.getLaneSet(newParent);
}
containment = 'lanes';
} else
if (is(businessObject, 'bpmn:FlowElement')) {
if (newParent) {
if (is(newParent, 'bpmn:Participant')) {
newParent = newParent.processRef;
} else
if (is(newParent, 'bpmn:Lane')) {
do {
// unwrap Lane -> LaneSet -> (Lane | FlowElementsContainer)
newParent = newParent.$parent.$parent;
} while (is(newParent, 'bpmn:Lane'));
}
}
containment = 'flowElements';
} else
if (is(businessObject, 'bpmn:Artifact')) {
while (newParent &&
!is(newParent, 'bpmn:Process') &&
!is(newParent, 'bpmn:SubProcess') &&
!is(newParent, 'bpmn:Collaboration')) {
if (is(newParent, 'bpmn:Participant')) {
newParent = newParent.processRef;
break;
} else {
newParent = newParent.$parent;
}
}
containment = 'artifacts';
} else
if (is(businessObject, 'bpmn:MessageFlow')) {
containment = 'messageFlows';
} else
if (is(businessObject, 'bpmn:Participant')) {
containment = 'participants';
// make sure the participants process is properly attached / detached
// from the XML document
var process = businessObject.processRef,
definitions;
if (process) {
definitions = getDefinitions(businessObject.$parent || newParent);
if (businessObject.$parent) {
collectionRemove(definitions.get('rootElements'), process);
process.$parent = null;
}
if (newParent) {
collectionAdd(definitions.get('rootElements'), process);
process.$parent = definitions;
}
}
} else
if (is(businessObject, 'bpmn:DataOutputAssociation')) {
containment = 'dataOutputAssociations';
} else
if (is(businessObject, 'bpmn:DataInputAssociation')) {
containment = 'dataInputAssociations';
}
if (!containment) {
throw new Error(translate(
'no parent for {element} in {parent}',
{
element: businessObject.id,
parent: newParent.id
}
));
}
var children;
if (businessObject.$parent) {
// remove from old parent
children = businessObject.$parent.get(containment);
collectionRemove(children, businessObject);
}
if (!newParent) {
businessObject.$parent = null;
} else {
// add to new parent
children = newParent.get(containment);
children.push(businessObject);
businessObject.$parent = newParent;
}
if (visualParent) {
var diChildren = visualParent.get(containment);
collectionRemove(children, businessObject);
if (newParent) {
if (!diChildren) {
diChildren = [];
newParent.set(containment, diChildren);
}
diChildren.push(businessObject);
}
}
};
BpmnUpdater.prototype.updateConnectionWaypoints = function(connection) {
connection.businessObject.di.set('waypoint', this._bpmnFactory.createDiWaypoints(connection.waypoints));
};
BpmnUpdater.prototype.updateConnection = function(context) {
var connection = context.connection,
businessObject = getBusinessObject(connection),
newSource = getBusinessObject(connection.source),
newTarget = getBusinessObject(connection.target),
visualParent;
if (!is(businessObject, 'bpmn:DataAssociation')) {
var inverseSet = is(businessObject, 'bpmn:SequenceFlow');
if (businessObject.sourceRef !== newSource) {
if (inverseSet) {
collectionRemove(businessObject.sourceRef && businessObject.sourceRef.get('outgoing'), businessObject);
if (newSource && newSource.get('outgoing')) {
newSource.get('outgoing').push(businessObject);
}
}
businessObject.sourceRef = newSource;
}
if (businessObject.targetRef !== newTarget) {
if (inverseSet) {
collectionRemove(businessObject.targetRef && businessObject.targetRef.get('incoming'), businessObject);
if (newTarget && newTarget.get('incoming')) {
newTarget.get('incoming').push(businessObject);
}
}
businessObject.targetRef = newTarget;
}
} else
if (is(businessObject, 'bpmn:DataInputAssociation')) {
// handle obnoxious isMsome sourceRef
businessObject.get('sourceRef')[0] = newSource;
visualParent = context.parent || context.newParent || newTarget;
this.updateSemanticParent(businessObject, newTarget, visualParent);
} else
if (is(businessObject, 'bpmn:DataOutputAssociation')) {
visualParent = context.parent || context.newParent || newSource;
this.updateSemanticParent(businessObject, newSource, visualParent);
// targetRef = new target
businessObject.targetRef = newTarget;
}
this.updateConnectionWaypoints(connection);
this.updateDiConnection(businessObject.di, newSource, newTarget);
};
// helpers //////////////////////
BpmnUpdater.prototype._getLabel = function(di) {
if (!di.label) {
di.label = this._bpmnFactory.createDiLabel();
}
return di.label;
};
/**
* Make sure the event listener is only called
* if the touched element is a BPMN element.
*
* @param {Function} fn
* @return {Function} guarded function
*/
function ifBpmn(fn) {
return function(event) {
var context = event.context,
element = context.shape || context.connection;
if (is(element, 'bpmn:BaseElement')) {
fn(event);
}
};
}