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 (!isCollapsedSubProcess(shape)) { return; } var properties = context.properties; var oldProperties = context.oldProperties; var oldId = oldProperties.id, newId = properties.id; if (oldId === newId) { 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' ];