feat(modeling): move to rules infrastructure

Related to bpmn-io/diagram-js#55
This commit is contained in:
Nico Rehwaldt 2014-11-21 09:19:35 +01:00
parent 90513e94b4
commit 2f679a36b9
22 changed files with 174 additions and 335 deletions

View File

@ -67,13 +67,9 @@ Modeler.prototype._modelingModules = [
require('diagram-js/lib/features/snapping'),
require('diagram-js/lib/features/move'),
require('diagram-js/lib/features/resize'),
require('diagram-js/lib/features/drop'),
require('./features/modeling'),
require('./features/context-pad'),
require('./features/palette'),
require('./features/resize'),
require('./features/rules'),
require('./features/drop')
require('./features/palette')
];

View File

@ -1,25 +0,0 @@
'use strict';
var _ = require('lodash');
function BpmnDrop(drop, openSequenceflowHandler, updateSequenceFlowParentHandler) {
var actions = {
'updateSequenceFlowParent': updateSequenceFlowParentHandler,
'removeOpenSequenceflow': openSequenceflowHandler
};
var self = this;
this._drop = drop;
_.forEach(actions, function(action, key) {
self._drop.registerAfterDropAction(key, action.execute);
});
}
BpmnDrop.$inject = [ 'drop', 'openSequenceflowHandler', 'updateSequenceFlowParentHandler' ];
module.exports = BpmnDrop;

View File

@ -1,67 +0,0 @@
'use strict';
var _ = require('lodash');
var self;
function OpenSequenceflowHandler(modeling) {
self = this;
this._modeling = modeling;
}
OpenSequenceflowHandler.$inject = [ 'modeling' ];
module.exports = OpenSequenceflowHandler;
/**
* Removes sequence flows that source or target does not have same parent.
*/
OpenSequenceflowHandler.prototype.execute = function(context) {
var shapes = context.shapes,
connections = context.connections,
target = context.target;
self._removeConnections(shapes, connections, target);
};
OpenSequenceflowHandler.prototype._removeConnections = function(shapes, connections, target) {
var modeling = self._modeling;
var removeableConnections = getRemoveableConnections(shapes, connections);
_.forEach(removeableConnections, function(connection) {
var sourceParent = connection.source.parent,
targetParent = connection.target.parent;
if (sourceParent.id !== targetParent.id) {
delete connections[connection.id];
modeling.removeConnection(connection);
}
});
};
function getRemoveableConnections(shapes, connections) {
var connectionsToRemove = {};
_.forEach(shapes, function(shape) {
var allConnections = _.union(shape.incoming, shape.outgoing);
_.forEach(allConnections, function(connection) {
// if one of the connection endpoints points to a shape that is not part of the map
// delete the connection
if (!(shapes[connection.source.id] && shapes[connection.target.id])) {
connectionsToRemove[connection.id] = connection;
}
});
});
return connectionsToRemove;
}

View File

@ -1,37 +0,0 @@
'use strict';
var _ = require('lodash');
var self;
function UpdateSequenceFlowParentHandler(modeling) {
self = this;
this._modeling = modeling;
}
UpdateSequenceFlowParentHandler.$inject = [ 'modeling' ];
module.exports = UpdateSequenceFlowParentHandler;
UpdateSequenceFlowParentHandler.prototype.execute = function(context) {
var shapes = context.shapes,
target = context.target;
_.forEach(shapes, function(shape) {
handleFlow(shape.incoming);
handleFlow(shape.outgoing);
});
function handleFlow(flows) {
_.forEach(flows, function(flow) {
if (shapes[flow.source.id] && shapes[flow.target.id]) {
flow.parent = target;
}
});
}
};

View File

@ -1,11 +0,0 @@
'use strict';
module.exports = {
__init__: [ 'bpmnDrop', 'openSequenceflowHandler', 'updateSequenceFlowParentHandler' ],
__depends__: [
require('diagram-js/lib/features/drop')
],
bpmnDrop: [ 'type', require('./BpmnDrop') ],
openSequenceflowHandler: [ 'type', require('./OpenSequenceflowHandler') ],
updateSequenceFlowParentHandler: [ 'type', require('./UpdateSequenceFlowParentHandler') ]
};

View File

@ -44,20 +44,22 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
// update parent
function updateShapeParent(e) {
self.updateShapeParent(e.context.shape || e.context.connection);
function updateParent(e) {
self.updateParent(e.context.shape || e.context.connection);
}
this.executed([ 'shape.move',
'shape.create',
'shape.delete',
'connection.create',
'connection.delete' ], updateShapeParent);
'connection.move',
'connection.delete' ], updateParent);
this.reverted([ 'shape.move',
'shape.create',
'shape.delete',
'connection.create',
'connection.delete' ], updateShapeParent);
'connection.move',
'connection.delete' ], updateParent);
// update bounds
@ -95,11 +97,11 @@ BpmnUpdater.$inject = [ 'eventBus', 'bpmnFactory', 'connectionDocking'];
/////// implementation //////////////////////////////////
BpmnUpdater.prototype.updateShapeParent = function(shape) {
BpmnUpdater.prototype.updateParent = function(element) {
var parentShape = shape.parent;
var parentShape = element.parent;
var businessObject = shape.businessObject,
var businessObject = element.businessObject,
parentBusinessObject = parentShape && parentShape.businessObject,
parentDi = parentBusinessObject && parentBusinessObject.di;

View File

@ -38,13 +38,21 @@ function LabelSupport(eventBus, modeling, bpmnFactory) {
eventBus.on('shape.move.start', 50000, function(e) {
var dragContext = e.dragContext,
element = dragContext.element;
shapes = dragContext.shapes;
var label = element.label;
var labels = [];
if (label && !label.hidden && dragContext.shapes.indexOf(label) === -1) {
dragContext.shapes.push(label);
}
_.forEach(shapes, function(element) {
var label = element.label;
if (label && !label.hidden && dragContext.shapes.indexOf(label) === -1) {
labels.push(label);
}
});
_.forEach(labels, function(label) {
shapes.push(label);
});
});

View File

@ -17,7 +17,6 @@ var CreateShapeHandler = require('diagram-js/lib/features/modeling/cmd/CreateSha
CreateConnectionHandler = require('diagram-js/lib/features/modeling/cmd/CreateConnectionHandler'),
DeleteConnectionHandler = require('diagram-js/lib/features/modeling/cmd/DeleteConnectionHandler'),
MoveConnectionHandler = require('diagram-js/lib/features/modeling/cmd/MoveConnectionHandler'),
MoveConnectionsHandler = require('diagram-js/lib/features/modeling/cmd/MoveConnectionsHandler'),
LayoutConnectionHandler = require('diagram-js/lib/features/modeling/cmd/LayoutConnectionHandler');
@ -52,7 +51,6 @@ Modeling.prototype.registerHandlers = function(commandStack) {
commandStack.registerHandler('connection.create', CreateConnectionHandler);
commandStack.registerHandler('connection.delete', DeleteConnectionHandler);
commandStack.registerHandler('connection.move', MoveConnectionHandler);
commandStack.registerHandler('connections.move', MoveConnectionsHandler);
commandStack.registerHandler('connection.layout', LayoutConnectionHandler);
};

View File

@ -0,0 +1,30 @@
var _ = require('lodash');
function DropBehavior(eventBus, modeling) {
// sequence flow handling
eventBus.on([
'commandStack.shapes.move.postExecute'
], function(e) {
var context = e.context,
closure = context.closure,
allConnections = closure.allConnections,
allShapes = closure.allShapes;
_.forEach(allConnections, function(c) {
// remove sequence flows having source / target on different parents
if (c.businessObject.$instanceOf('bpmn:SequenceFlow') && c.source.parent !== c.target.parent) {
modeling.removeConnection(c);
}
});
});
}
DropBehavior.$inject = [ 'eventBus', 'modeling' ];
module.exports = DropBehavior;

View File

@ -0,0 +1,4 @@
module.exports = {
__init__: [ 'dropBehavior' ],
dropBehavior: [ 'type', require('./Drop') ]
};

View File

@ -2,9 +2,10 @@ module.exports = {
__init__: [ 'modeling', 'bpmnUpdater', 'labelSupport' ],
__depends__: [
require('../label-editing'),
require('./rules'),
require('./behavior'),
require('diagram-js/lib/command'),
require('diagram-js/lib/features/change-support'),
require('diagram-js/lib/features/drop')
require('diagram-js/lib/features/change-support')
],
bpmnFactory: [ 'type', require('./BpmnFactory') ],
bpmnUpdater: [ 'type', require('./BpmnUpdater') ],

View File

@ -0,0 +1,90 @@
'use strict';
var _ = require('lodash');
var RuleProvider = require('diagram-js/lib/features/rules/RuleProvider');
function ModelingRules(eventBus) {
RuleProvider.call(this, eventBus);
}
ModelingRules.$inject = [ 'eventBus' ];
module.exports = ModelingRules;
ModelingRules.prototype = Object.create(RuleProvider.prototype);
ModelingRules.prototype.init = function() {
// rules
this.addRule('connection.create', function(context) {
var source = context.source,
target = context.target;
if (!source || source.labelTarget || !target || target.labelTarget) {
return null;
}
return source.businessObject.$parent === target.businessObject.$parent &&
source.businessObject.$instanceOf('bpmn:FlowNode') &&
!source.businessObject.$instanceOf('bpmn:EndEvent') &&
!target.businessObject.$instanceOf('bpmn:StartEvent') &&
target.businessObject.$instanceOf('bpmn:FlowElement');
});
this.addRule('shape.resize', function(context) {
var shape = context.shape,
newBounds = context.newBounds,
bo = shape.businessObject;
if (!bo.$instanceOf('bpmn:SubProcess') || !bo.di.isExpanded) {
return false;
}
if (newBounds) {
if (newBounds.width < 100 || newBounds.height < 80) {
return false;
}
}
});
this.addRule('shapes.move', function(context) {
var target = context.newParent,
shapes = context.shapes;
// only move if they have the same parent
var sameParent = _.size(_.groupBy(shapes, function(s) { return s.parent && s.parent.id; }));
if (!sameParent) {
return false;
}
if (!target) {
return true;
}
var targetBusinessObject = target.businessObject,
targetDi = targetBusinessObject.di;
// allow to drop elements elements into sub processes
// unless they are participants or lanes themselves
if (targetBusinessObject.$instanceOf('bpmn:SubProcess') && targetDi.isExpanded) {
return shapes.every(function(shape) {
var bo = shape.businessObject;
return !(bo.$instanceOf('bpmn:Participant') || bo.$instanceOf('bpmn:Lane'));
});
} else {
return false;
}
});
};

View File

@ -0,0 +1,4 @@
module.exports = {
__init__: [ 'modelingRules' ],
modelingRules: [ 'type', require('./ModelingRules') ]
};

View File

@ -1,24 +0,0 @@
module.exports = {
'bpmn:Task': {
'height': 40,
'width': 40
},
'bpmn:SubProcess': {
'resolver': function(element) {
var isExpanded = element.businessObject.di.isExpanded;
if (isExpanded) {
return {
'height': 100,
'width': 100
};
} else {
return {
'height': 40,
'width': 40
};
}
}
}
};

View File

@ -1,6 +0,0 @@
{
"elements": [
"bpmn:Task",
"bpmn:SubProcess"
]
}

View File

@ -1,47 +0,0 @@
'use strict';
var _ = require('lodash');
var resizableElements = require('./BpmnResizableElements.json').elements,
minimumSize = require('./BpmnMinimumSizes.js');
function BpmnResizeHandler(eventBus, resize) {
var isResizeAllowed = function isResizeAllowed(element) {
var isAllowed = _.contains(resizableElements, element.type);
if (isAllowed) {
return true;
} else {
return false;
}
};
var getMinimumSize = function getMinimumSize(element) {
var size = minimumSize[element.type];
if (!size) {
size = {
height: 30,
width: 30
};
}
if (!!size.resolver) {
return size.resolver(element);
} else {
return size;
}
};
// Register Handler
eventBus.on('canvas.init', function() {
resize.registerResizableHandler(isResizeAllowed);
resize.registerMinimumSizeResolver(getMinimumSize);
});
}
module.exports = BpmnResizeHandler;
BpmnResizeHandler.$inject = [ 'eventBus', 'resize' ];

View File

@ -1,7 +0,0 @@
module.exports = {
__depends__: [
require('diagram-js/lib/features/resize')
],
__init__: [ 'bpmnResizeHandler' ],
bpmnResizeHandler: [ 'type', require('./BpmnResizeHandler') ]
};

View File

@ -1,15 +0,0 @@
'use strict';
var _ = require('lodash');
var DropAction = require('./DropRules'),
ConnectHandler = require('./ConnectRules');
function BpmnRules(rules) {
rules.registerRule('drop', 'validateSubProcess', DropAction.validateSubProcess);
}
BpmnRules.$inject = [ 'rules' ];
module.exports = BpmnRules;

View File

@ -1,22 +0,0 @@
'use strict';
var _ = require('lodash');
function can(context) {
var source = context.source,
target = context.target;
if (source.labelTarget || target.labelTarget) {
return null;
}
return source.businessObject.$parent === target.businessObject.$parent &&
source.businessObject.$instanceOf('bpmn:FlowNode') &&
!source.businessObject.$instanceOf('bpmn:EndEvent') &&
!target.businessObject.$instanceOf('bpmn:StartEvent') &&
target.businessObject.$instanceOf('bpmn:FlowElement');
}
module.exports.can = can;

View File

@ -1,26 +0,0 @@
'use strict';
var _ = require('lodash');
function validateSubProcess(context) {
var target = context.target,
shapes = context.shape,
di = target.businessObject.di;
// can't drop anything to a collapsed subprocess
if (!di.isExpanded) {
return false;
}
// Elements that can't be dropped to a subprocess
_.forEach(shapes, function(shape) {
if (_.contains(['bpmn:Participant', 'bpmn:Lane'], shape.type)) {
return false;
}
});
return true;
}
module.exports.validateSubProcess = validateSubProcess;

View File

@ -1,6 +0,0 @@
'use strict';
module.exports = {
__init__: [ 'bpmnRules' ],
bpmnRules: [ 'type', require('./BpmnRules') ]
};

View File

@ -10,29 +10,27 @@ var _ = require('lodash');
var fs = require('fs');
var modelingModule = require('../../../../lib/features/modeling'),
dropModule = require('../../../../lib/features/drop'),
coreModule = require('../../../../lib/core');
describe('features/drop ', function() {
describe('features/move - drop', function() {
beforeEach(Matchers.addDeepEquals);
var diagramXML = fs.readFileSync('test/fixtures/bpmn/features/drop/drop.bpmn', 'utf-8');
var diagramXML2 = fs.readFileSync('test/fixtures/bpmn/features/drop/recursive-task.bpmn', 'utf-8');
var diagramXML = fs.readFileSync('test/fixtures/bpmn/features/drop/drop.bpmn', 'utf8');
var diagramXML2 = fs.readFileSync('test/fixtures/bpmn/features/drop/recursive-task.bpmn', 'utf8');
var testModules = [ coreModule, modelingModule, dropModule ];
var testModules = [ coreModule, modelingModule ];
describe('elements', function() {
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should update parent', inject(function(elementRegistry, modeling, drop) {
it('should update parent', inject(function(elementRegistry, modeling) {
// given
var task_1 = elementRegistry.get('ID_Task_1'),
parent = elementRegistry.get('ID_SubProcess_1');
@ -44,12 +42,13 @@ describe('features/drop ', function() {
expect(task_1.businessObject.$parent).toBe(parent.businessObject);
}));
it('should update parents', inject(function(elementRegistry, modeling, drop) {
it('should update parents', inject(function(elementRegistry, modeling) {
// given
var task_1 = elementRegistry.getById('ID_Task_1'),
var task_1 = elementRegistry.get('ID_Task_1'),
task_2 = elementRegistry.get('ID_Task_2'),
parent = elementRegistry.getById('ID_SubProcess_1');
parent = elementRegistry.get('ID_SubProcess_1');
// when
modeling.moveShapes([ task_1, task_2 ], { x: 0, y: 200 }, parent);
@ -60,17 +59,17 @@ describe('features/drop ', function() {
expect(task_2.parent).toBe(parent);
expect(task_2.businessObject.$parent).toBe(parent.businessObject);
}));
});
describe('Sequence Flows', function() {
describe('connection handling', function() {
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should remove flow if target and source have different parents',
inject(function(elementRegistry, modeling, drop) {
inject(function(elementRegistry, modeling) {
// given
var task_1 = elementRegistry.get('ID_Task_1'),
@ -78,8 +77,7 @@ describe('features/drop ', function() {
flow = elementRegistry.get('ID_Sequenceflow_1');
// when
modeling.moveShape(task_1, { x: 0, y: 200 }, parent);
modeling.moveShapes([ task_1 ], { x: 0, y: 200 }, parent);
// then
expect(flow.parent).toBe(null);
@ -87,8 +85,7 @@ describe('features/drop ', function() {
}));
it('should update flow parent if target and source have same parents',
inject(function(elementRegistry, modeling, drop) {
it('should update flow parent if target and source have same parents', inject(function(elementRegistry, modeling) {
// given
var task_1 = elementRegistry.get('ID_Task_1'),
@ -97,28 +94,29 @@ describe('features/drop ', function() {
flow = elementRegistry.get('ID_Sequenceflow_1');
// when
modeling.moveShapes([task_1, task_2], { x: 0, y: 250 }, parent);
modeling.moveShapes([ task_1, task_2 ], { x: 0, y: 250 }, parent);
// then
expect(flow.parent).toBe(parent);
expect(flow.businessObject.$parent).toBe(parent.businessObject);
}));
});
describe('recursion', function() {
beforeEach(bootstrapModeler(diagramXML2, { modules: testModules }));
it('should update parent', inject(function(elementRegistry, modeling, drop) {
it('should update parent', inject(function(elementRegistry, modeling) {
// given
var task_1 = elementRegistry.get('ID_task_1'),
parent = elementRegistry.get('ID_subprocess_1'),
sequenceFlow = elementRegistry.get('ID_sequenceflow_1');
// when
modeling.moveShape(task_1, { x: 0, y: 200 }, parent);
modeling.moveShapes([ task_1 ], { x: 0, y: 200 }, parent);
// then
expect(task_1.parent).toBe(parent);
@ -127,6 +125,7 @@ describe('features/drop ', function() {
expect(sequenceFlow.parent).toBe(parent);
expect(sequenceFlow.businessObject.$parent).toBe(parent.businessObject);
}));
});
});