bpmn-js/lib/features/modeling/rules/ModelingRules.js

260 lines
6.7 KiB
JavaScript

'use strict';
var groupBy = require('lodash/collection/groupBy'),
size = require('lodash/collection/size'),
forEach = require('lodash/collection/forEach');
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() {
function nonExistantOrLabel(element) {
return !element || element.labelTarget;
}
function isSame(a, b) {
return a === b;
}
function isEventConnectionInvalid(source, target) {
var sourceBo = source.businessObject,
targetBo = target.businessObject;
// handle start and end event cases
var startEventCheck = targetBo.$instanceOf('bpmn:StartEvent') ||
sourceBo.$instanceOf('bpmn:EndEvent');
// handle event based gateway cases
var eventBasedGatewayCheck = false;
// Ensure target of event based gateway is one of:
// receive task, receiving message, timer, signal, condition event
if (sourceBo.$instanceOf('bpmn:EventBasedGateway')) {
eventBasedGatewayCheck = true;
if (targetBo.$instanceOf('bpmn:ReceiveTask')) {
eventBasedGatewayCheck = false;
} else if (isEventType(targetBo, 'bpmn:IntermediateCatchEvent', 'bpmn:MessageEventDefinition') ||
isEventType(targetBo, 'bpmn:IntermediateCatchEvent', 'bpmn:TimerEventDefinition') ||
isEventType(targetBo, 'bpmn:IntermediateCatchEvent', 'bpmn:ConditionalEventDefinition') ||
isEventType(targetBo, 'bpmn:IntermediateCatchEvent', 'bpmn:SignalEventDefinition')) {
eventBasedGatewayCheck = false;
}
}
// handle link event
var linkEventCheck = false;
if (isEventType(targetBo, 'bpmn:IntermediateCatchEvent', 'bpmn:LinkEventDefinition') ||
isEventType(sourceBo, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition')) {
linkEventCheck = true;
}
return startEventCheck || eventBasedGatewayCheck || linkEventCheck;
}
function isEventType(eventBo, type, definition) {
var isType = eventBo.$instanceOf(type);
var isDefinition = false;
var definitions = eventBo.eventDefinitions || [];
forEach(definitions, function(def) {
if (def.$type === definition) {
isDefinition = true;
}
});
return isType && isDefinition;
}
// rules
function canConnect(source, target, connection) {
if (nonExistantOrLabel(source) || nonExistantOrLabel(target)) {
return null;
}
// See https://github.com/bpmn-io/bpmn-js/issues/178
// as a workround we disallow connections with same
// target and source element.
// This rule must be removed if a auto layout for this
// connections is implemented.
if (isSame(source, target)) {
return false;
}
// only move between the same parent
if (!isSame(source.parent, target.parent)) {
return false;
}
var sourceBo = source.businessObject,
targetBo = target.businessObject,
connectionBo = connection && connection.businessObject;
if (connectionBo && connectionBo.$instanceOf('bpmn:SequenceFlow')) {
if (!sourceBo.$instanceOf('bpmn:FlowNode') ||
!targetBo.$instanceOf('bpmn:FlowNode') ||
sourceBo.$instanceOf('bpmn:EndEvent') ||
targetBo.$instanceOf('bpmn:StartEvent')) {
return false;
}
}
// Do not allow incoming connections on StartEvents
// and outgoing connections on EndEvents
if (isEventConnectionInvalid(source, target)) {
return false;
}
return (sourceBo.$instanceOf('bpmn:FlowNode') ||
sourceBo.$instanceOf('bpmn:TextAnnotation')) &&
(targetBo.$instanceOf('bpmn:FlowNode') ||
targetBo.$instanceOf('bpmn:TextAnnotation'));
}
this.addRule('connection.create', function(context) {
var source = context.source,
target = context.target;
return canConnect(source, target);
});
this.addRule('connection.reconnectStart', function(context) {
var connection = context.connection,
source = context.hover,
target = connection.target;
return canConnect(source, target, connection);
});
this.addRule('connection.reconnectEnd', function(context) {
var connection = context.connection,
source = connection.source,
target = context.hover;
return canConnect(source, target, connection);
});
this.addRule('connection.updateWaypoints', function(context) {
// OK! but visually ignore
return null;
});
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;
}
}
});
/**
* Can an element be dropped into the target element
*
* @return {Boolean}
*/
function canDrop(businessObject, targetBusinessObject, targetDi) {
if (businessObject.$instanceOf('bpmn:FlowElement') &&
targetBusinessObject.$instanceOf('bpmn:FlowElementsContainer')) {
// may not drop into collapsed sub processes
if (targetDi.isExpanded === false) {
return false;
}
return true;
}
if (businessObject.$instanceOf('bpmn:TextAnnotation') &&
targetBusinessObject.$instanceOf('bpmn:FlowElementsContainer')) {
return true;
}
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; })) === 1;
if (!sameParent) {
return false;
}
if (!target) {
return true;
}
var targetBusinessObject = target.businessObject,
targetDi = targetBusinessObject.di;
return shapes.every(function(s) {
return canDrop(s.businessObject, targetBusinessObject, targetDi);
});
});
this.addRule([ 'shape.create', 'shape.append' ], function(context) {
var target = context.parent,
shape = context.shape,
source = context.source;
// ensure we do not drop the element
// into source
var t = target;
while (t) {
if (isSame(t, source)) {
return false;
}
t = t.parent;
}
if (!target) {
return false;
}
if (target.labelTarget) {
return null;
}
return canDrop(shape.businessObject, target.businessObject, target.businessObject.di);
});
};