369 lines
8.0 KiB
JavaScript
369 lines
8.0 KiB
JavaScript
|
|
import {
|
||
|
|
assign,
|
||
|
|
filter,
|
||
|
|
find,
|
||
|
|
isArray,
|
||
|
|
isNumber,
|
||
|
|
isObject,
|
||
|
|
isUndefined,
|
||
|
|
groupBy,
|
||
|
|
forEach
|
||
|
|
} from 'min-dash';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {import('../../model').Base} Base
|
||
|
|
* @typedef {import('../../model').Shape} Shape
|
||
|
|
*
|
||
|
|
* @typedef {import('../../type/Types').Rect} Rect
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get parent elements.
|
||
|
|
*
|
||
|
|
* @param {Base[]} elements
|
||
|
|
*
|
||
|
|
* @returns {Base[]}
|
||
|
|
*/
|
||
|
|
export function getParents(elements) {
|
||
|
|
|
||
|
|
// find elements that are not children of any other elements
|
||
|
|
return filter(elements, function(element) {
|
||
|
|
return !find(elements, function(e) {
|
||
|
|
return e !== element && getParent(element, e);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function getParent(element, parent) {
|
||
|
|
if (!parent) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (element === parent) {
|
||
|
|
return parent;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!element.parent) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
return getParent(element.parent, parent);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Adds an element to a collection and returns true if the
|
||
|
|
* element was added.
|
||
|
|
*
|
||
|
|
* @param {Object[]} elements
|
||
|
|
* @param {Object} e
|
||
|
|
* @param {boolean} unique
|
||
|
|
*/
|
||
|
|
export function add(elements, e, unique) {
|
||
|
|
var canAdd = !unique || elements.indexOf(e) === -1;
|
||
|
|
|
||
|
|
if (canAdd) {
|
||
|
|
elements.push(e);
|
||
|
|
}
|
||
|
|
|
||
|
|
return canAdd;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Iterate over each element in a collection, calling the iterator function `fn`
|
||
|
|
* with (element, index, recursionDepth).
|
||
|
|
*
|
||
|
|
* Recurse into all elements that are returned by `fn`.
|
||
|
|
*
|
||
|
|
* @param {Object|Object[]} elements
|
||
|
|
* @param {Function} fn iterator function called with (element, index, recursionDepth)
|
||
|
|
* @param {number} [depth] maximum recursion depth
|
||
|
|
*/
|
||
|
|
export function eachElement(elements, fn, depth) {
|
||
|
|
|
||
|
|
depth = depth || 0;
|
||
|
|
|
||
|
|
if (!isArray(elements)) {
|
||
|
|
elements = [ elements ];
|
||
|
|
}
|
||
|
|
|
||
|
|
forEach(elements, function(s, i) {
|
||
|
|
var filter = fn(s, i, depth);
|
||
|
|
|
||
|
|
if (isArray(filter) && filter.length) {
|
||
|
|
eachElement(filter, fn, depth + 1);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Collects self + child elements up to a given depth from a list of elements.
|
||
|
|
*
|
||
|
|
* @param {Base|Base[]} elements the elements to select the children from
|
||
|
|
* @param {boolean} unique whether to return a unique result set (no duplicates)
|
||
|
|
* @param {number} maxDepth the depth to search through or -1 for infinite
|
||
|
|
*
|
||
|
|
* @return {Base[]} found elements
|
||
|
|
*/
|
||
|
|
export function selfAndChildren(elements, unique, maxDepth) {
|
||
|
|
var result = [],
|
||
|
|
processedChildren = [];
|
||
|
|
|
||
|
|
eachElement(elements, function(element, i, depth) {
|
||
|
|
add(result, element, unique);
|
||
|
|
|
||
|
|
var children = element.children;
|
||
|
|
|
||
|
|
// max traversal depth not reached yet
|
||
|
|
if (maxDepth === -1 || depth < maxDepth) {
|
||
|
|
|
||
|
|
// children exist && children not yet processed
|
||
|
|
if (children && add(processedChildren, children, unique)) {
|
||
|
|
return children;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return self + direct children for a number of elements
|
||
|
|
*
|
||
|
|
* @param {Base[]} elements to query
|
||
|
|
* @param {boolean} allowDuplicates to allow duplicates in the result set
|
||
|
|
*
|
||
|
|
* @return {Base[]} the collected elements
|
||
|
|
*/
|
||
|
|
export function selfAndDirectChildren(elements, allowDuplicates) {
|
||
|
|
return selfAndChildren(elements, !allowDuplicates, 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return self + ALL children for a number of elements
|
||
|
|
*
|
||
|
|
* @param {Base[]} elements to query
|
||
|
|
* @param {boolean} allowDuplicates to allow duplicates in the result set
|
||
|
|
*
|
||
|
|
* @return {Base[]} the collected elements
|
||
|
|
*/
|
||
|
|
export function selfAndAllChildren(elements, allowDuplicates) {
|
||
|
|
return selfAndChildren(elements, !allowDuplicates, -1);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets the the closure for all selected elements,
|
||
|
|
* their enclosed children and connections.
|
||
|
|
*
|
||
|
|
* @param {Base[]} elements
|
||
|
|
* @param {boolean} [isTopLevel=true]
|
||
|
|
* @param {Object} [existingClosure]
|
||
|
|
*
|
||
|
|
* @return {Object} newClosure
|
||
|
|
*/
|
||
|
|
export function getClosure(elements, isTopLevel, closure) {
|
||
|
|
|
||
|
|
if (isUndefined(isTopLevel)) {
|
||
|
|
isTopLevel = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isObject(isTopLevel)) {
|
||
|
|
closure = isTopLevel;
|
||
|
|
isTopLevel = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
closure = closure || {};
|
||
|
|
|
||
|
|
var allShapes = copyObject(closure.allShapes),
|
||
|
|
allConnections = copyObject(closure.allConnections),
|
||
|
|
enclosedElements = copyObject(closure.enclosedElements),
|
||
|
|
enclosedConnections = copyObject(closure.enclosedConnections);
|
||
|
|
|
||
|
|
var topLevel = copyObject(
|
||
|
|
closure.topLevel,
|
||
|
|
isTopLevel && groupBy(elements, function(e) { return e.id; })
|
||
|
|
);
|
||
|
|
|
||
|
|
|
||
|
|
function handleConnection(c) {
|
||
|
|
if (topLevel[c.source.id] && topLevel[c.target.id]) {
|
||
|
|
topLevel[c.id] = [ c ];
|
||
|
|
}
|
||
|
|
|
||
|
|
// not enclosed as a child, but maybe logically
|
||
|
|
// (connecting two moved elements?)
|
||
|
|
if (allShapes[c.source.id] && allShapes[c.target.id]) {
|
||
|
|
enclosedConnections[c.id] = enclosedElements[c.id] = c;
|
||
|
|
}
|
||
|
|
|
||
|
|
allConnections[c.id] = c;
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleElement(element) {
|
||
|
|
|
||
|
|
enclosedElements[element.id] = element;
|
||
|
|
|
||
|
|
if (element.waypoints) {
|
||
|
|
|
||
|
|
// remember connection
|
||
|
|
enclosedConnections[element.id] = allConnections[element.id] = element;
|
||
|
|
} else {
|
||
|
|
|
||
|
|
// remember shape
|
||
|
|
allShapes[element.id] = element;
|
||
|
|
|
||
|
|
// remember all connections
|
||
|
|
forEach(element.incoming, handleConnection);
|
||
|
|
|
||
|
|
forEach(element.outgoing, handleConnection);
|
||
|
|
|
||
|
|
// recurse into children
|
||
|
|
return element.children;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
eachElement(elements, handleElement);
|
||
|
|
|
||
|
|
return {
|
||
|
|
allShapes: allShapes,
|
||
|
|
allConnections: allConnections,
|
||
|
|
topLevel: topLevel,
|
||
|
|
enclosedConnections: enclosedConnections,
|
||
|
|
enclosedElements: enclosedElements
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the surrounding bbox for all elements in
|
||
|
|
* the array or the element primitive.
|
||
|
|
*
|
||
|
|
* @param {Base|Base[]} elements
|
||
|
|
* @param {boolean} [stopRecursion=false]
|
||
|
|
*
|
||
|
|
* @return {Rect}
|
||
|
|
*/
|
||
|
|
export function getBBox(elements, stopRecursion) {
|
||
|
|
|
||
|
|
stopRecursion = !!stopRecursion;
|
||
|
|
if (!isArray(elements)) {
|
||
|
|
elements = [ elements ];
|
||
|
|
}
|
||
|
|
|
||
|
|
var minX,
|
||
|
|
minY,
|
||
|
|
maxX,
|
||
|
|
maxY;
|
||
|
|
|
||
|
|
forEach(elements, function(element) {
|
||
|
|
|
||
|
|
// If element is a connection the bbox must be computed first
|
||
|
|
var bbox = element;
|
||
|
|
if (element.waypoints && !stopRecursion) {
|
||
|
|
bbox = getBBox(element.waypoints, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
var x = bbox.x,
|
||
|
|
y = bbox.y,
|
||
|
|
height = bbox.height || 0,
|
||
|
|
width = bbox.width || 0;
|
||
|
|
|
||
|
|
if (x < minX || minX === undefined) {
|
||
|
|
minX = x;
|
||
|
|
}
|
||
|
|
if (y < minY || minY === undefined) {
|
||
|
|
minY = y;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ((x + width) > maxX || maxX === undefined) {
|
||
|
|
maxX = x + width;
|
||
|
|
}
|
||
|
|
if ((y + height) > maxY || maxY === undefined) {
|
||
|
|
maxY = y + height;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
x: minX,
|
||
|
|
y: minY,
|
||
|
|
height: maxY - minY,
|
||
|
|
width: maxX - minX
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns all elements that are enclosed from the bounding box.
|
||
|
|
*
|
||
|
|
* * If bbox.(width|height) is not specified the method returns
|
||
|
|
* all elements with element.x/y > bbox.x/y
|
||
|
|
* * If only bbox.x or bbox.y is specified, method return all elements with
|
||
|
|
* e.x > bbox.x or e.y > bbox.y
|
||
|
|
*
|
||
|
|
* @param {Base[]} elements List of Elements to search through
|
||
|
|
* @param {Rect} bbox the enclosing bbox.
|
||
|
|
*
|
||
|
|
* @return {Base[]} enclosed elements
|
||
|
|
*/
|
||
|
|
export function getEnclosedElements(elements, bbox) {
|
||
|
|
|
||
|
|
var filteredElements = {};
|
||
|
|
|
||
|
|
forEach(elements, function(element) {
|
||
|
|
|
||
|
|
var e = element;
|
||
|
|
|
||
|
|
if (e.waypoints) {
|
||
|
|
e = getBBox(e);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!isNumber(bbox.y) && (e.x > bbox.x)) {
|
||
|
|
filteredElements[element.id] = element;
|
||
|
|
}
|
||
|
|
if (!isNumber(bbox.x) && (e.y > bbox.y)) {
|
||
|
|
filteredElements[element.id] = element;
|
||
|
|
}
|
||
|
|
if (e.x > bbox.x && e.y > bbox.y) {
|
||
|
|
if (isNumber(bbox.width) && isNumber(bbox.height) &&
|
||
|
|
e.width + e.x < bbox.width + bbox.x &&
|
||
|
|
e.height + e.y < bbox.height + bbox.y) {
|
||
|
|
|
||
|
|
filteredElements[element.id] = element;
|
||
|
|
} else if (!isNumber(bbox.width) || !isNumber(bbox.height)) {
|
||
|
|
filteredElements[element.id] = element;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return filteredElements;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
export function getType(element) {
|
||
|
|
|
||
|
|
if ('waypoints' in element) {
|
||
|
|
return 'connection';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ('x' in element) {
|
||
|
|
return 'shape';
|
||
|
|
}
|
||
|
|
|
||
|
|
return 'root';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isFrameElement(element) {
|
||
|
|
|
||
|
|
return !!(element && element.isFrame);
|
||
|
|
}
|
||
|
|
|
||
|
|
// helpers ///////////////////////////////
|
||
|
|
|
||
|
|
function copyObject(src1, src2) {
|
||
|
|
return assign({}, src1 || {}, src2 || {});
|
||
|
|
}
|