UNPKG

diagram-js

Version:

A toolbox for displaying and modifying diagrams on the web

303 lines (238 loc) 7.04 kB
import inherits from 'inherits-browser'; import { getBBox as getBoundingBox } from '../../util/Elements'; import { asTRBL, asBounds } from '../../layout/LayoutUtil'; import { assign, flatten, find, forEach, groupBy, isArray, matchPattern, pick, values } from 'min-dash'; import CommandInterceptor from '../../command/CommandInterceptor'; /** * @typedef {import('../../model/Types').Element} Element * @typedef {import('../../model/Types').Shape} Shape * * @typedef {import('../../util/Types').Direction} Direction * @typedef {import('../../util/Types').Rect} Rect * @typedef {import('../../util/Types').RectTRBL} RectTRBL * * @typedef {import('../../core/ElementRegistry').default} ElementRegistry * @typedef {import('../../core/EventBus').default} EventBus * @typedef {import('../modeling/Modeling').default} Modeling * @typedef {import('../rules/Rules').default} Rules */ /** * An auto resize component that takes care of expanding a parent element * if child elements are created or moved close the parents edge. * * @param {EventBus} eventBus * @param {ElementRegistry} elementRegistry * @param {Modeling} modeling * @param {Rules} rules */ export default function AutoResize(eventBus, elementRegistry, modeling, rules) { CommandInterceptor.call(this, eventBus); this._elementRegistry = elementRegistry; this._modeling = modeling; this._rules = rules; var self = this; this.postExecuted([ 'shape.create' ], function(event) { var context = event.context, hints = context.hints || {}, shape = context.shape, parent = context.parent || context.newParent; if (hints.autoResize === false) { return; } self._expand([ shape ], parent); }); this.postExecuted([ 'elements.move' ], function(event) { var context = event.context, elements = flatten(values(context.closure.topLevel)), hints = context.hints; var autoResize = hints ? hints.autoResize : true; if (autoResize === false) { return; } var expandings = groupBy(elements, function(element) { return element.parent.id; }); forEach(expandings, function(elements, parentId) { // optionally filter elements to be considered when resizing if (isArray(autoResize)) { elements = elements.filter(function(element) { return find(autoResize, matchPattern({ id: element.id })); }); } self._expand(elements, parentId); }); }); this.postExecuted([ 'shape.toggleCollapse' ], function(event) { var context = event.context, hints = context.hints, shape = context.shape; if (hints && hints.autoResize === false) { return; } if (shape.collapsed) { return; } self._expand(shape.children || [], shape); }); this.postExecuted([ 'shape.resize' ], function(event) { var context = event.context, hints = context.hints, shape = context.shape, parent = shape.parent; if (hints && hints.autoResize === false) { return; } if (parent) { self._expand([ shape ], parent); } }); } AutoResize.$inject = [ 'eventBus', 'elementRegistry', 'modeling', 'rules' ]; inherits(AutoResize, CommandInterceptor); /** * Calculate the new bounds of the target shape, given * a number of elements have been moved or added into the parent. * * This method considers the current size, the added elements as well as * the provided padding for the new bounds. * * @param {Shape[]} elements * @param {Shape} target */ AutoResize.prototype._getOptimalBounds = function(elements, target) { var offset = this.getOffset(target), padding = this.getPadding(target); var elementsTrbl = asTRBL(getBoundingBox(elements)), targetTrbl = asTRBL(target); var newTrbl = {}; if (elementsTrbl.top - targetTrbl.top < padding.top) { newTrbl.top = elementsTrbl.top - offset.top; } if (elementsTrbl.left - targetTrbl.left < padding.left) { newTrbl.left = elementsTrbl.left - offset.left; } if (targetTrbl.right - elementsTrbl.right < padding.right) { newTrbl.right = elementsTrbl.right + offset.right; } if (targetTrbl.bottom - elementsTrbl.bottom < padding.bottom) { newTrbl.bottom = elementsTrbl.bottom + offset.bottom; } return asBounds(assign({}, targetTrbl, newTrbl)); }; /** * Expand the target shape respecting rules, offset and padding * * @param {Shape[]} elements * @param {Shape|string} target The target or its ID. */ AutoResize.prototype._expand = function(elements, target) { if (typeof target === 'string') { target = this._elementRegistry.get(target); } var allowed = this._rules.allowed('element.autoResize', { elements: elements, target: target }); if (!allowed) { return; } // calculate the new bounds var newBounds = this._getOptimalBounds(elements, target); if (!boundsChanged(newBounds, target)) { return; } var resizeDirections = getResizeDirections(pick(target, [ 'x', 'y', 'width', 'height' ]), newBounds); // resize the parent shape this.resize(target, newBounds, { autoResize: resizeDirections }); var parent = target.parent; // recursively expand parent elements if (parent) { this._expand([ target ], parent); } }; /** * Get the amount to expand the given shape in each direction. * * @param {Shape} shape * * @return {RectTRBL} */ AutoResize.prototype.getOffset = function(shape) { return { top: 60, bottom: 60, left: 100, right: 100 }; }; /** * Get the activation threshold for each side for which * resize triggers. * * @param {Shape} shape * * @return {RectTRBL} */ AutoResize.prototype.getPadding = function(shape) { return { top: 2, bottom: 2, left: 15, right: 15 }; }; /** * Perform the actual resize operation. * * @param {Shape} shape * @param {Rect} newBounds * @param {Object} [hints] * @param {string} [hints.autoResize] */ AutoResize.prototype.resize = function(shape, newBounds, hints) { this._modeling.resizeShape(shape, newBounds, null, hints); }; function boundsChanged(newBounds, oldBounds) { return ( newBounds.x !== oldBounds.x || newBounds.y !== oldBounds.y || newBounds.width !== oldBounds.width || newBounds.height !== oldBounds.height ); } /** * Get directions of resize as {n|w|s|e} e.g. "nw". * * @param {Rect} oldBounds * @param {Rect} newBounds * * @return {Direction} Resize directions as {n|w|s|e}. */ function getResizeDirections(oldBounds, newBounds) { var directions = ''; oldBounds = asTRBL(oldBounds); newBounds = asTRBL(newBounds); if (oldBounds.top > newBounds.top) { directions = directions.concat('n'); } if (oldBounds.right < newBounds.right) { directions = directions.concat('w'); } if (oldBounds.bottom < newBounds.bottom) { directions = directions.concat('s'); } if (oldBounds.left > newBounds.left) { directions = directions.concat('e'); } return directions; }