feat(modeling): update Lane#flowNodeRefs while modeling

Closes #387
This commit is contained in:
Nico Rehwaldt 2015-10-15 23:50:15 +02:00 committed by pedesen
parent 4be7324856
commit d789342b10
7 changed files with 424 additions and 262 deletions

View File

@ -8,7 +8,8 @@ var UpdatePropertiesHandler = require('./cmd/UpdatePropertiesHandler'),
UpdateCanvasRootHandler = require('./cmd/UpdateCanvasRootHandler'),
AddLaneHandler = require('./cmd/AddLaneHandler'),
SplitLaneHandler = require('./cmd/SplitLaneHandler'),
ResizeLaneHandler = require('./cmd/ResizeLaneHandler');
ResizeLaneHandler = require('./cmd/ResizeLaneHandler'),
UpdateFlowNodeRefsHandler = require('./cmd/UpdateFlowNodeRefsHandler');
/**
@ -40,6 +41,7 @@ Modeling.prototype.getHandlers = function() {
handlers['lane.add'] = AddLaneHandler;
handlers['lane.resize'] = ResizeLaneHandler;
handlers['lane.split'] = SplitLaneHandler;
handlers['lane.updateRefs'] = UpdateFlowNodeRefsHandler;
return handlers;
};
@ -118,6 +120,14 @@ Modeling.prototype.makeCollaboration = function() {
return collaborationElement;
};
Modeling.prototype.updateLaneRefs = function(flowNodeShapes, laneShapes) {
this._commandStack.execute('lane.updateRefs', {
flowNodeShapes: flowNodeShapes,
laneShapes: laneShapes
});
};
/**
* Transform the current diagram into a process.
*

View File

@ -11,6 +11,9 @@ var getChildLanes = require('../util/LaneUtil').getChildLanes;
var eachElement = require('diagram-js/lib/util/Elements').eachElement;
var LOW_PRIORITY = 500;
/**
* BPMN specific delete lane behavior
*/
@ -18,62 +21,84 @@ function DeleteLaneBehavior(eventBus, modeling, spaceTool) {
CommandInterceptor.call(this, eventBus);
/**
* adjust sizes of other lanes after lane deletion
*/
this.postExecute('shape.delete', function(context) {
var shape = context.shape;
if (is(shape, 'bpmn:Lane')) {
function compensateLaneDelete(shape, oldParent) {
var siblings = getChildLanes(context.oldParent);
var siblings = getChildLanes(oldParent);
var topAffected = [];
var bottomAffected = [];
var topAffected = [];
var bottomAffected = [];
eachElement(siblings, function(element) {
eachElement(siblings, function(element) {
if (element.y > shape.y) {
bottomAffected.push(element);
} else {
topAffected.push(element);
}
return element.children;
});
if (!siblings.length) {
return;
}
var offset;
if (bottomAffected.length && topAffected.length) {
offset = shape.height / 2;
if (element.y > shape.y) {
bottomAffected.push(element);
} else {
offset = shape.height;
topAffected.push(element);
}
var topAdjustments,
bottomAdjustments;
return element.children;
});
if (topAffected.length) {
topAdjustments = spaceTool.calculateAdjustments(topAffected, 'y', offset, shape.y - 10);
spaceTool.makeSpace(topAdjustments.movingShapes, topAdjustments.resizingShapes, { x: 0, y: offset }, 's');
}
if (bottomAffected.length) {
bottomAdjustments = spaceTool.calculateAdjustments(bottomAffected, 'y', -offset, shape.y + shape.height + 10);
spaceTool.makeSpace(
bottomAdjustments.movingShapes,
bottomAdjustments.resizingShapes,
{ x: 0, y: -offset },
'n');
}
if (!siblings.length) {
return;
}
}, true);
var offset;
if (bottomAffected.length && topAffected.length) {
offset = shape.height / 2;
} else {
offset = shape.height;
}
var topAdjustments,
bottomAdjustments;
if (topAffected.length) {
topAdjustments = spaceTool.calculateAdjustments(
topAffected, 'y', offset, shape.y - 10);
spaceTool.makeSpace(
topAdjustments.movingShapes,
topAdjustments.resizingShapes,
{ x: 0, y: offset }, 's');
}
if (bottomAffected.length) {
bottomAdjustments = spaceTool.calculateAdjustments(
bottomAffected, 'y', -offset, shape.y + shape.height + 10);
spaceTool.makeSpace(
bottomAdjustments.movingShapes,
bottomAdjustments.resizingShapes,
{ x: 0, y: -offset }, 'n');
}
}
/**
* Adjust sizes of other lanes after lane deletion
*/
this.postExecuted('shape.delete', LOW_PRIORITY, function(event) {
var context = event.context,
hints = context.hints,
shape = context.shape,
oldParent = context.oldParent;
// only compensate lane deletes
if (!is(shape, 'bpmn:Lane')) {
return;
}
// compensate root deletes only
if (hints && hints.nested) {
return;
}
compensateLaneDelete(shape, oldParent);
});
}
DeleteLaneBehavior.$inject = [ 'eventBus', 'modeling', 'spaceTool' ];

View File

@ -0,0 +1,157 @@
'use strict';
var inherits = require('inherits');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var is = require('../../../util/ModelUtil').is;
var LOW_PRIORITY = 500,
HIGH_PRIORITY = 5000;
/**
* BPMN specific delete lane behavior
*/
function UpdateFlowNodeRefsBehavior(eventBus, modeling) {
CommandInterceptor.call(this, eventBus);
/**
* Ok, this is it:
*
* We have to update the Lane#flowNodeRefs _and_
* FlowNode#lanes with every FlowNode move/resize and
* Lane move/resize.
*
* We want to group that stuff to recompute containments
* as efficient as possible.
*
* Yea!
*/
// the update context
var context;
function initContext() {
context = context || new UpdateContext();
context.enter();
return context;
}
function getContext() {
if (!context) {
throw new Error('out of bounds release');
}
return context;
}
function releaseContext() {
if (!context) {
throw new Error('out of bounds release');
}
var triggerUpdate = context.leave();
if (triggerUpdate) {
modeling.updateLaneRefs(context.flowNodes, context.lanes);
context = null;
}
return triggerUpdate;
}
var laneRefUpdateEvents = [
'spaceTool',
'lane.add',
'lane.resize',
'lane.split',
'elements.move',
'elements.delete',
'shape.create',
'shape.delete',
'shape.move',
'shape.resize'
];
// listen to a lot of stuff to group lane updates
this.preExecute(laneRefUpdateEvents, HIGH_PRIORITY, function(event) {
initContext();
});
this.postExecuted(laneRefUpdateEvents, LOW_PRIORITY, function(event) {
releaseContext();
});
// Mark flow nodes + lanes that need an update
this.preExecute([
'shape.create',
'shape.move',
'shape.delete',
'shape.resize'
], function(event) {
var context = event.context,
shape = context.shape;
var updateContext = getContext();
// no need to update labels
if (shape.labelTarget) {
return;
}
if (is(shape, 'bpmn:Lane')) {
updateContext.addLane(shape);
}
if (is(shape, 'bpmn:FlowNode')) {
updateContext.addFlowNode(shape);
}
});
}
UpdateFlowNodeRefsBehavior.$inject = [ 'eventBus', 'modeling' ];
inherits(UpdateFlowNodeRefsBehavior, CommandInterceptor);
module.exports = UpdateFlowNodeRefsBehavior;
function UpdateContext() {
this.flowNodes = [];
this.lanes = [];
this.counter = 0;
this.addLane = function(lane) {
this.lanes.push(lane);
};
this.addFlowNode = function(flowNode) {
this.flowNodes.push(flowNode);
};
this.enter = function() {
this.counter++;
};
this.leave = function() {
this.counter--;
return !this.counter;
};
}

View File

@ -1,212 +0,0 @@
'use strict';
var inherits = require('inherits');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var is = require('../../../util/ModelUtil').is;
var getLanesRoot = require('../util/LaneUtil').getLanesRoot;
var eachElement = require('diagram-js/lib/util/Elements').eachElement;
var LOW_PRIORITY = 500;
/**
* BPMN specific delete lane behavior
*/
function UpdateLaneRefsBehavior(eventBus) {
CommandInterceptor.call(this, eventBus);
/**
* Ok, this is it:
*
* We cannot simply update the lane refs for every flow node move
*/
var context;
function schedule(type, elements) {
context = context || {
flowNodes: {},
participants: {},
skipParticipants: {},
counter: 0
};
if (type === 'updateflowNodes') {
elements.forEach(function(e) {
if (!e.labelTarget) {
context.flowNodes[e.id] = e;
}
});
} else
if (type === 'updateLanes') {
elements.forEach(function(e) {
var root = is(e, 'bpmn:Participant') ? e : getLanesRoot(e);
if (root) {
context.participants[root.id] = root;
}
});
} else
if (type === 'skipParticipants') {
elements.forEach(function(e) {
context.skipParticipants[e.id] = e;
});
}
context.counter++;
}
function release() {
if (!context) {
throw new Error('out of band release; :-(');
}
context.counter--;
if (!context.counter) {
console.log('updating flowNodeRefs');
console.log(context);
console.log();
context = null;
}
}
// use general space tool as grouping
this.preExecute([ 'spaceTool' ], function(event) {
schedule('resizeMaybe');
});
this.postExecuted([ 'spaceTool' ], function(event) {
release();
});
/**
* Mark flow nodes
*/
this.preExecute([
'lane.add',
'lane.resize',
'lane.split',
'shape.delete',
'shape.resize'
], function(event) {
var context = event.context,
shape = context.shape;
if (is(shape, 'bpmn:Lane') || is(shape, 'bpmn:Participant')) {
schedule('updateLanes', [ shape ]);
context.updateLanes = true;
}
});
this.postExecuted([
'lane.add',
'lane.resize',
'lane.split',
'shape.delete',
'shape.resize'
], LOW_PRIORITY, function(event) {
var context = event.context;
if (context.updateLanes) {
release();
}
});
this.preExecute([
'elements.move'
], function(event) {
var context = event.context,
shapes = context.shapes;
var participantShapes = shapes.filter(function(s) {
return is(s, 'bpmn:Participant');
});
if (participantShapes.length) {
schedule('skipParticipants', participantShapes);
context.skipParticipants = true;
}
var flowNodeShapes = shapes.filter(function(s) {
return is(s, 'bpmn:FlowNode') && is(s.parent, 'bpmn:Participant');
});
if (flowNodeShapes.length) {
schedule('updateFlowNodes', flowNodeShapes);
context.updateFlowNodes = true;
}
});
this.postExecuted([
'elements.move'
], LOW_PRIORITY, function(event) {
var context = event.context;
if (context.updateFlowNodes) {
release();
}
if (context.skipParticipants) {
release();
}
});
this.preExecute([
'shape.create',
'shape.delete',
'shape.move'
], function(event) {
var context = event.context,
shape = context.shape;
if (is(shape, 'bpmn:FlowNode')) {
schedule('updateFlowNodes', [ shape ]);
context.updateFlowNodes = true;
}
});
this.postExecuted([
'shape.create',
'shape.delete',
'shape.move'
], LOW_PRIORITY, function(event) {
var context = event.context;
if (context.updateFlowNodes) {
release();
}
});
}
UpdateLaneRefsBehavior.$inject = [ 'eventBus', 'modeling' ];
inherits(UpdateLaneRefsBehavior, CommandInterceptor);
module.exports = UpdateLaneRefsBehavior;

View File

@ -10,7 +10,8 @@ module.exports = {
'removeParticipantBehavior',
'replaceConnectionBehavior',
'replaceElementBehaviour',
'resizeLaneBehavior'
'resizeLaneBehavior',
'updateFlowNodeRefsBehavior'
],
appendBehavior: [ 'type', require('./AppendBehavior') ],
createBoundaryEventBehavior: [ 'type', require('./CreateBoundaryEventBehavior') ],
@ -22,5 +23,6 @@ module.exports = {
removeParticipantBehavior: [ 'type', require('./RemoveParticipantBehavior') ],
replaceConnectionBehavior: [ 'type', require('./ReplaceConnectionBehavior') ],
replaceElementBehaviour: [ 'type', require('./ReplaceElementBehaviour') ],
resizeLaneBehavior: [ 'type', require('./ResizeLaneBehavior') ]
resizeLaneBehavior: [ 'type', require('./ResizeLaneBehavior') ],
updateFlowNodeRefsBehavior: [ 'type', require('./UpdateFlowNodeRefsBehavior') ]
};

View File

@ -0,0 +1,180 @@
'use strict';
var collectLanes = require('../util/LaneUtil').collectLanes;
var getLanesRoot = require('../util/LaneUtil').getLanesRoot;
var is = require('../../../util/ModelUtil').is;
var Collections = require('diagram-js/lib/util/Collections');
var asTRBL = require('diagram-js/lib/layout/LayoutUtil').asTRBL;
var FLOW_NODE_REFS_ATTR = 'flowNodeRef',
LANES_ATTR = 'lanes';
/**
* A handler that updates lane refs on changed elements
*/
function UpdateFlowNodeRefsHandler(elementRegistry) {
this._elementRegistry = elementRegistry;
}
UpdateFlowNodeRefsHandler.$inject = [ 'elementRegistry' ];
module.exports = UpdateFlowNodeRefsHandler;
UpdateFlowNodeRefsHandler.prototype.computeUpdates = function(flowNodeShapes, laneShapes) {
var handledNodes = {};
var updates = [];
var participantCache = {};
var allFlowNodeShapes = [];
function isInLaneShape(element, laneShape) {
var laneTrbl = asTRBL(laneShape);
var elementMid = {
x: element.x + element.width / 2,
y: element.y + element.height / 2
};
return elementMid.x > laneTrbl.left &&
elementMid.x < laneTrbl.right &&
elementMid.y > laneTrbl.top &&
elementMid.y < laneTrbl.bottom;
}
function addFlowNodeShape(flowNodeShape) {
if (!handledNodes[flowNodeShape.id]) {
allFlowNodeShapes.push(flowNodeShape);
handledNodes[flowNodeShape.id] = flowNodeShape;
}
}
function getAllLaneShapes(flowNodeShape) {
var root = getLanesRoot(flowNodeShape);
if (!participantCache[root.id]) {
participantCache[root.id] = collectLanes(root);
}
return participantCache[root.id];
}
function getNewLanes(flowNodeShape) {
if (!flowNodeShape.parent) {
return [];
}
var allLaneShapes = getAllLaneShapes(flowNodeShape);
return allLaneShapes.filter(function(l) {
return isInLaneShape(flowNodeShape, l);
}).map(function(shape) {
return shape.businessObject;
});
}
laneShapes.forEach(function(laneShape) {
var root = getLanesRoot(laneShape);
if (!root || handledNodes[root.id]) {
return;
}
var children = root.children.filter(function(c) {
return is(c, 'bpmn:FlowNode');
});
children.forEach(addFlowNodeShape);
handledNodes[root.id] = root;
});
flowNodeShapes.forEach(addFlowNodeShape);
allFlowNodeShapes.forEach(function(flowNodeShape) {
var flowNode = flowNodeShape.businessObject;
var lanes = flowNode.get(LANES_ATTR),
remove = lanes.slice(),
add = getNewLanes(flowNodeShape);
updates.push({ flowNode: flowNode, remove: remove, add: add });
});
laneShapes.forEach(function(laneShape) {
var lane = laneShape.businessObject;
// lane got removed XX-)
if (!laneShape.parent) {
lane.get(FLOW_NODE_REFS_ATTR).forEach(function(flowNode) {
updates.push({ flowNode: flowNode, remove: [ lane ], add: [] });
});
}
});
return updates;
};
UpdateFlowNodeRefsHandler.prototype.execute = function(context) {
var updates = context.updates;
if (!updates) {
updates = context.updates = this.computeUpdates(context.flowNodeShapes, context.laneShapes);
}
updates.forEach(function(update) {
var flowNode = update.flowNode,
lanes = flowNode.get(LANES_ATTR);
// unwire old
update.remove.forEach(function(oldLane) {
Collections.remove(lanes, oldLane);
Collections.remove(oldLane.get(FLOW_NODE_REFS_ATTR), flowNode);
});
// wire new
update.add.forEach(function(newLane) {
Collections.add(lanes, newLane);
Collections.add(newLane.get(FLOW_NODE_REFS_ATTR), flowNode);
});
});
};
UpdateFlowNodeRefsHandler.prototype.revert = function(context) {
var updates = context.updates;
updates.forEach(function(update) {
var flowNode = update.flowNode,
lanes = flowNode.get(LANES_ATTR);
// unwire new
update.add.forEach(function(newLane) {
Collections.remove(lanes, newLane);
Collections.remove(newLane.get(FLOW_NODE_REFS_ATTR), flowNode);
});
// wire old
update.remove.forEach(function(oldLane) {
Collections.add(lanes, oldLane);
Collections.add(oldLane.get(FLOW_NODE_REFS_ATTR), flowNode);
});
});
};

View File

@ -9,7 +9,7 @@ var modelingModule = require('../../../../../lib/features/modeling'),
coreModule = require('../../../../../lib/core');
describe.skip('features/modeling - lanes - flowNodeRefs', function() {
describe('features/modeling - lanes - flowNodeRefs', function() {
var diagramXML = require('./flowNodeRefs.bpmn');