jstd-web/node_modules/diagram-js/lib/features/distribute-elements/DistributeElements.js

222 lines
4.8 KiB
JavaScript

import {
sortBy,
forEach,
isArray
} from 'min-dash';
/**
* @typedef {Object} Range
* @property {number} min
* @property {number} max
*/
/**
* @typedef {import('../../util/Types').Axis} Axis
* @typedef {import('../../util/Types').Dimension} Dimension
* @typedef {import('../../util/Types').Rect} Rect
*
* @typedef {import('../modeling/Modeling').default} Modeling
* @typedef {import('../rules/Rules').default} Rules
*/
var AXIS_DIMENSIONS = {
horizontal: [ 'x', 'width' ],
vertical: [ 'y', 'height' ]
};
var THRESHOLD = 5;
/**
* Groups and filters elements and then trigger even distribution.
*
* @param {Modeling} modeling
* @param {Rules} rules
*/
export default function DistributeElements(modeling, rules) {
this._modeling = modeling;
this._filters = [];
this.registerFilter(function(elements) {
var allowed = rules.allowed('elements.distribute', { elements: elements });
if (isArray(allowed)) {
return allowed;
}
return allowed ? elements : [];
});
}
DistributeElements.$inject = [ 'modeling', 'rules' ];
/**
* Registers filter functions that allow external parties to filter
* out certain elements.
*
* @param {Function} filterFn
*/
DistributeElements.prototype.registerFilter = function(filterFn) {
if (typeof filterFn !== 'function') {
throw new Error('the filter has to be a function');
}
this._filters.push(filterFn);
};
/**
* Distributes the elements with a given orientation
*
* @param {Array} elements
* @param {string} orientation
*/
DistributeElements.prototype.trigger = function(elements, orientation) {
var modeling = this._modeling;
var groups,
distributableElements;
if (elements.length < 3) {
return;
}
this._setOrientation(orientation);
distributableElements = this._filterElements(elements);
groups = this._createGroups(distributableElements);
// nothing to distribute
if (groups.length <= 2) {
return;
}
modeling.distributeElements(groups, this._axis, this._dimension);
return groups;
};
/**
* Filters the elements with provided filters by external parties
*
* @param {Array[Elements]} elements
*
* @return {Array[Elements]}
*/
DistributeElements.prototype._filterElements = function(elements) {
var filters = this._filters,
axis = this._axis,
dimension = this._dimension,
distributableElements = [].concat(elements);
if (!filters.length) {
return elements;
}
forEach(filters, function(filterFn) {
distributableElements = filterFn(distributableElements, axis, dimension);
});
return distributableElements;
};
/**
* Create range (min, max) groups. Also tries to group elements
* together that share the same range.
*
* @example
* var distributableElements = [
* {
* range: {
* min: 100,
* max: 200
* },
* elements: [ { id: 'shape1', .. }]
* }
* ]
*
* @param {Array} elements
*
* @return {Array[Objects]}
*/
DistributeElements.prototype._createGroups = function(elements) {
var rangeGroups = [],
self = this,
axis = this._axis,
dimension = this._dimension;
if (!axis) {
throw new Error('must have a defined "axis" and "dimension"');
}
// sort by 'left->right' or 'top->bottom'
var sortedElements = sortBy(elements, axis);
forEach(sortedElements, function(element, idx) {
var elementRange = self._findRange(element, axis, dimension),
range;
var previous = rangeGroups[rangeGroups.length - 1];
if (previous && self._hasIntersection(previous.range, elementRange)) {
rangeGroups[rangeGroups.length - 1].elements.push(element);
} else {
range = { range: elementRange, elements: [ element ] };
rangeGroups.push(range);
}
});
return rangeGroups;
};
/**
* Maps a direction to the according axis and dimension
*
* @param {string} direction 'horizontal' or 'vertical'
*/
DistributeElements.prototype._setOrientation = function(direction) {
var orientation = AXIS_DIMENSIONS[direction];
this._axis = orientation[0];
this._dimension = orientation[1];
};
/**
* Checks if the two ranges intercept each other
*
* @param {Object} rangeA {min, max}
* @param {Object} rangeB {min, max}
*
* @return {boolean}
*/
DistributeElements.prototype._hasIntersection = function(rangeA, rangeB) {
return Math.max(rangeA.min, rangeA.max) >= Math.min(rangeB.min, rangeB.max) &&
Math.min(rangeA.min, rangeA.max) <= Math.max(rangeB.min, rangeB.max);
};
/**
* Returns the min and max values for an element
*
* @param {Rect} element
* @param {Axis} axis
* @param {Dimension} dimension
*
* @return {Range}
*/
DistributeElements.prototype._findRange = function(element) {
var axis = element[this._axis],
dimension = element[this._dimension];
return {
min: axis + THRESHOLD,
max: axis + dimension - THRESHOLD
};
};