'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; } // allow all associations between elements if (connection && connection.businessObject.$instanceOf('bpmn:Association')) { return true; } // 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 || context.source, target = connection.target; return canConnect(source, target, connection); }); this.addRule('connection.reconnectEnd', function(context) { var connection = context.connection, source = connection.source, target = context.hover || context.target; 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); }); };