bpmn-js/lib/features/modeling/behavior/SubProcessPlaneBehavior.js

558 lines
13 KiB
JavaScript

import inherits from 'inherits';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import { find } from 'min-dash';
import { isExpanded } from '../../../util/DiUtil';
import { getBusinessObject, getDi, is } from '../../../util/ModelUtil';
import { getMid } from 'diagram-js/lib/layout/LayoutUtil';
import { getBBox } from 'diagram-js/lib/util/Elements';
import {
getPlaneIdFromShape,
getShapeIdFromPlane,
isPlane,
toPlaneId
} from '../../../util/DrilldownUtil';
var LOW_PRIORITY = 400;
var HIGH_PRIORITY = 600;
var DEFAULT_POSITION = {
x: 180,
y: 160
};
/**
* Creates bpmndi:BPMNPlane elements and canvas planes when collapsed subprocesses are created.
*
*
* @param {Canvas} canvas
* @param {EventBus} eventBus
* @param {Modeling} modeling
* @param {ElementFactory} elementFactory
* @param {BpmnFactory} bpmnFactory
* @param {Bpmnjs} bpmnjs
* @param {ElementRegistry} elementRegistry
*/
export default function SubProcessPlaneBehavior(
canvas, eventBus, modeling,
elementFactory, bpmnFactory, bpmnjs, elementRegistry) {
CommandInterceptor.call(this, eventBus);
this._canvas = canvas;
this._eventBus = eventBus;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._bpmnFactory = bpmnFactory;
this._bpmnjs = bpmnjs;
this._elementRegistry = elementRegistry;
var self = this;
function isCollapsedSubProcess(element) {
return is(element, 'bpmn:SubProcess') && !isExpanded(element);
}
function createRoot(context) {
var shape = context.shape,
rootElement = context.newRootElement;
var businessObject = getBusinessObject(shape);
rootElement = self._addDiagram(rootElement || businessObject);
context.newRootElement = canvas.addRootElement(rootElement);
}
function removeRoot(context) {
var shape = context.shape;
var businessObject = getBusinessObject(shape);
self._removeDiagram(businessObject);
var rootElement = context.newRootElement = elementRegistry.get(getPlaneIdFromShape(businessObject));
canvas.removeRootElement(rootElement);
}
// add plane elements for newly created sub-processes
// this ensures we can actually drill down into the element
this.executed('shape.create', function(context) {
var shape = context.shape;
if (!isCollapsedSubProcess(shape)) {
return;
}
createRoot(context);
}, true);
this.postExecuted('shape.create', function(context) {
var shape = context.shape,
rootElement = context.newRootElement;
if (!rootElement || !shape.children) {
return;
}
self._showRecursively(shape.children);
self._moveChildrenToShape(shape, rootElement);
}, true);
this.reverted('shape.create', function(context) {
var shape = context.shape;
if (!isCollapsedSubProcess(shape)) {
return;
}
removeRoot(context);
}, true);
this.preExecuted('shape.delete', function(context) {
var shape = context.shape;
if (!isCollapsedSubProcess(shape)) {
return;
}
var attachedRoot = elementRegistry.get(getPlaneIdFromShape(shape));
if (!attachedRoot) {
return;
}
modeling.removeElements(attachedRoot.children.slice());
}, true);
this.executed('shape.delete', function(context) {
var shape = context.shape;
if (!isCollapsedSubProcess(shape)) {
return;
}
removeRoot(context);
}, true);
this.reverted('shape.delete', function(context) {
var shape = context.shape;
if (!isCollapsedSubProcess(shape)) {
return;
}
createRoot(context);
}, true);
this.preExecuted('shape.replace', function(context) {
var oldShape = context.oldShape;
var newShape = context.newShape;
if (!isCollapsedSubProcess(oldShape) || !isCollapsedSubProcess(newShape)) {
return;
}
// old plane could have content,
// we remove it so it is not recursively deleted from 'shape.delete'
context.oldRoot = canvas.removeRootElement(getPlaneIdFromShape(oldShape));
}, true);
this.postExecuted('shape.replace', function(context) {
var newShape = context.newShape,
source = context.oldRoot,
target = canvas.findRoot(getPlaneIdFromShape(newShape));
if (!source || !target) {
return;
}
var elements = source.children;
modeling.moveElements(elements, { x: 0, y: 0 }, target);
}, true);
// rename primary elements when the secondary element changes
// this ensures rootElement.id = element.id + '_plane'
this.executed('element.updateProperties', function(context) {
var shape = context.element;
if (!is(shape, 'bpmn:SubProcess')) {
return;
}
var properties = context.properties;
var oldProperties = context.oldProperties;
var oldId = oldProperties.id,
newId = properties.id;
if (oldId === newId) {
return;
}
if (isPlane(shape)) {
elementRegistry.updateId(shape, toPlaneId(newId));
elementRegistry.updateId(oldId, newId);
return;
}
var planeElement = elementRegistry.get(toPlaneId(oldId));
if (!planeElement) {
return;
}
elementRegistry.updateId(toPlaneId(oldId), toPlaneId(newId));
}, true);
this.reverted('element.updateProperties', function(context) {
var shape = context.element;
if (!is(shape, 'bpmn:SubProcess')) {
return;
}
var properties = context.properties;
var oldProperties = context.oldProperties;
var oldId = oldProperties.id,
newId = properties.id;
if (oldId === newId) {
return;
}
if (isPlane(shape)) {
elementRegistry.updateId(shape, toPlaneId(oldId));
elementRegistry.updateId(newId, oldId);
return;
}
var planeElement = elementRegistry.get(toPlaneId(newId));
if (!planeElement) {
return;
}
elementRegistry.updateId(planeElement, toPlaneId(oldId));
}, true);
// re-throw element.changed to re-render primary shape if associated plane has
// changed (e.g. bpmn:name property has changed)
eventBus.on('element.changed', function(context) {
var element = context.element;
if (!isPlane(element)) {
return;
}
var plane = element;
var primaryShape = elementRegistry.get(getShapeIdFromPlane(plane));
// do not re-throw if no associated primary shape (e.g. bpmn:Process)
if (!primaryShape || primaryShape === plane) {
return;
}
eventBus.fire('element.changed', { element: primaryShape });
});
// create/remove plane for the subprocess
this.executed('shape.toggleCollapse', LOW_PRIORITY, function(context) {
var shape = context.shape;
if (!is(shape, 'bpmn:SubProcess')) {
return;
}
if (!isExpanded(shape)) {
createRoot(context);
self._showRecursively(shape.children);
} else {
removeRoot(context);
}
}, true);
// create/remove plane for the subprocess
this.reverted('shape.toggleCollapse', LOW_PRIORITY, function(context) {
var shape = context.shape;
if (!is(shape, 'bpmn:SubProcess')) {
return;
}
if (!isExpanded(shape)) {
createRoot(context);
self._showRecursively(shape.children);
} else {
removeRoot(context);
}
}, true);
// move elements between planes
this.postExecuted('shape.toggleCollapse', HIGH_PRIORITY, function(context) {
var shape = context.shape;
if (!is(shape, 'bpmn:SubProcess')) {
return;
}
var rootElement = context.newRootElement;
if (!rootElement) {
return;
}
if (!isExpanded(shape)) {
// collapsed
self._moveChildrenToShape(shape, rootElement);
} else {
self._moveChildrenToShape(rootElement, shape);
}
}, true);
// copy-paste ///////////
// add elements in plane to tree
eventBus.on('copyPaste.createTree', function(context) {
var element = context.element,
children = context.children;
if (!isCollapsedSubProcess(element)) {
return;
}
var id = getPlaneIdFromShape(element);
var parent = elementRegistry.get(id);
if (parent) {
// do not copy invisible root element
children.push.apply(children, parent.children);
}
});
// set plane children as direct children of collapsed shape
eventBus.on('copyPaste.copyElement', function(context) {
var descriptor = context.descriptor,
element = context.element,
elements = context.elements;
var parent = element.parent;
var isPlane = is(getDi(parent), 'bpmndi:BPMNPlane');
if (!isPlane) {
return;
}
var parentId = getShapeIdFromPlane(parent);
var referencedShape = find(elements, function(element) {
return element.id === parentId;
});
if (!referencedShape) {
return;
}
descriptor.parent = referencedShape.id;
});
// hide children during pasting
eventBus.on('copyPaste.pasteElement', function(context) {
var descriptor = context.descriptor;
if (!descriptor.parent) {
return;
}
if (isCollapsedSubProcess(descriptor.parent) || descriptor.parent.hidden) {
descriptor.hidden = true;
}
});
}
inherits(SubProcessPlaneBehavior, CommandInterceptor);
/**
* Moves the child elements from source to target.
*
* If the target is a plane, the children are moved to the top left corner.
* Otherwise, the center of the target is used.
*
* @param {Object|djs.model.Base} source
* @param {Object|djs.model.Base} target
*/
SubProcessPlaneBehavior.prototype._moveChildrenToShape = function(source, target) {
var modeling = this._modeling;
var children = source.children;
var offset;
if (!children) {
return;
}
// only change plane if there are no visible children, but don't move them
var visibleChildren = children.filter(function(child) {
return !child.hidden;
});
if (!visibleChildren.length) {
modeling.moveElements(children, { x: 0, y: 0 }, target, { autoResize: false });
return;
}
var childrenBounds = getBBox(visibleChildren);
// target is a plane
if (!target.x) {
offset = {
x: DEFAULT_POSITION.x - childrenBounds.x,
y: DEFAULT_POSITION.y - childrenBounds.y
};
}
// source is a plane
else {
// move relative to the center of the shape
var targetMid = getMid(target);
var childrenMid = getMid(childrenBounds);
offset = {
x: targetMid.x - childrenMid.x,
y: targetMid.y - childrenMid.y
};
}
modeling.moveElements(children, offset, target, { autoResize: false });
};
/**
* Sets `hidden` property on all children of the given shape.
*
* @param {Array} elements
* @param {Boolean} [hidden]
* @returns {Array} all child elements
*/
SubProcessPlaneBehavior.prototype._showRecursively = function(elements, hidden) {
var self = this;
var result = [];
elements.forEach(function(element) {
element.hidden = !!hidden;
result = result.concat(element);
if (element.children) {
result = result.concat(
self._showRecursively(element.children, element.collapsed || hidden)
);
}
});
return result;
};
/**
* Adds a given rootElement to the bpmnDi diagrams.
*
* @param {Object} rootElement
* @returns {Object} planeElement
*/
SubProcessPlaneBehavior.prototype._addDiagram = function(planeElement) {
var bpmnjs = this._bpmnjs;
var diagrams = bpmnjs.getDefinitions().diagrams;
if (!planeElement.businessObject) {
planeElement = this._createNewDiagram(planeElement);
}
diagrams.push(planeElement.di.$parent);
return planeElement;
};
/**
* Creates a new plane element for the given sub process.
*
* @param {Object} bpmnElement
*
* @return {Object} new diagram element
*/
SubProcessPlaneBehavior.prototype._createNewDiagram = function(bpmnElement) {
var bpmnFactory = this._bpmnFactory;
var elementFactory = this._elementFactory;
var diPlane = bpmnFactory.create('bpmndi:BPMNPlane', {
bpmnElement: bpmnElement
});
var diDiagram = bpmnFactory.create('bpmndi:BPMNDiagram', {
plane: diPlane
});
diPlane.$parent = diDiagram;
// add a virtual element (not being drawn),
// a copy cat of our BpmnImporter code
var planeElement = elementFactory.createRoot({
id: getPlaneIdFromShape(bpmnElement),
type: bpmnElement.$type,
di: diPlane,
businessObject: bpmnElement,
collapsed: true
});
return planeElement;
};
/**
* Removes the diagram for a given root element
*
* @param {Object} rootElement
* @returns {Object} removed bpmndi:BPMNDiagram
*/
SubProcessPlaneBehavior.prototype._removeDiagram = function(rootElement) {
var bpmnjs = this._bpmnjs;
var diagrams = bpmnjs.getDefinitions().diagrams;
var removedDiagram = find(diagrams, function(diagram) {
return diagram.plane.bpmnElement.id === rootElement.id;
});
diagrams.splice(diagrams.indexOf(removedDiagram), 1);
return removedDiagram;
};
SubProcessPlaneBehavior.$inject = [
'canvas',
'eventBus',
'modeling',
'elementFactory',
'bpmnFactory',
'bpmnjs',
'elementRegistry'
];