Elements.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import {
  2. assign,
  3. isArray,
  4. isNumber,
  5. isObject,
  6. isUndefined,
  7. groupBy,
  8. forEach
  9. } from 'min-dash';
  10. /**
  11. * Adds an element to a collection and returns true if the
  12. * element was added.
  13. *
  14. * @param {Array<Object>} elements
  15. * @param {Object} e
  16. * @param {Boolean} unique
  17. */
  18. export function add(elements, e, unique) {
  19. var canAdd = !unique || elements.indexOf(e) === -1;
  20. if (canAdd) {
  21. elements.push(e);
  22. }
  23. return canAdd;
  24. }
  25. /**
  26. * Iterate over each element in a collection, calling the iterator function `fn`
  27. * with (element, index, recursionDepth).
  28. *
  29. * Recurse into all elements that are returned by `fn`.
  30. *
  31. * @param {Object|Array<Object>} elements
  32. * @param {Function} fn iterator function called with (element, index, recursionDepth)
  33. * @param {Number} [depth] maximum recursion depth
  34. */
  35. export function eachElement(elements, fn, depth) {
  36. depth = depth || 0;
  37. if (!isArray(elements)) {
  38. elements = [ elements ];
  39. }
  40. forEach(elements, function(s, i) {
  41. var filter = fn(s, i, depth);
  42. if (isArray(filter) && filter.length) {
  43. eachElement(filter, fn, depth + 1);
  44. }
  45. });
  46. }
  47. /**
  48. * Collects self + child elements up to a given depth from a list of elements.
  49. *
  50. * @param {djs.model.Base|Array<djs.model.Base>} elements the elements to select the children from
  51. * @param {Boolean} unique whether to return a unique result set (no duplicates)
  52. * @param {Number} maxDepth the depth to search through or -1 for infinite
  53. *
  54. * @return {Array<djs.model.Base>} found elements
  55. */
  56. export function selfAndChildren(elements, unique, maxDepth) {
  57. var result = [],
  58. processedChildren = [];
  59. eachElement(elements, function(element, i, depth) {
  60. add(result, element, unique);
  61. var children = element.children;
  62. // max traversal depth not reached yet
  63. if (maxDepth === -1 || depth < maxDepth) {
  64. // children exist && children not yet processed
  65. if (children && add(processedChildren, children, unique)) {
  66. return children;
  67. }
  68. }
  69. });
  70. return result;
  71. }
  72. /**
  73. * Return self + direct children for a number of elements
  74. *
  75. * @param {Array<djs.model.Base>} elements to query
  76. * @param {Boolean} allowDuplicates to allow duplicates in the result set
  77. *
  78. * @return {Array<djs.model.Base>} the collected elements
  79. */
  80. export function selfAndDirectChildren(elements, allowDuplicates) {
  81. return selfAndChildren(elements, !allowDuplicates, 1);
  82. }
  83. /**
  84. * Return self + ALL children for a number of elements
  85. *
  86. * @param {Array<djs.model.Base>} elements to query
  87. * @param {Boolean} allowDuplicates to allow duplicates in the result set
  88. *
  89. * @return {Array<djs.model.Base>} the collected elements
  90. */
  91. export function selfAndAllChildren(elements, allowDuplicates) {
  92. return selfAndChildren(elements, !allowDuplicates, -1);
  93. }
  94. /**
  95. * Gets the the closure for all selected elements,
  96. * their enclosed children and connections.
  97. *
  98. * @param {Array<djs.model.Base>} elements
  99. * @param {Boolean} [isTopLevel=true]
  100. * @param {Object} [existingClosure]
  101. *
  102. * @return {Object} newClosure
  103. */
  104. export function getClosure(elements, isTopLevel, closure) {
  105. if (isUndefined(isTopLevel)) {
  106. isTopLevel = true;
  107. }
  108. if (isObject(isTopLevel)) {
  109. closure = isTopLevel;
  110. isTopLevel = true;
  111. }
  112. closure = closure || {};
  113. var allShapes = copyObject(closure.allShapes),
  114. allConnections = copyObject(closure.allConnections),
  115. enclosedElements = copyObject(closure.enclosedElements),
  116. enclosedConnections = copyObject(closure.enclosedConnections);
  117. var topLevel = copyObject(
  118. closure.topLevel,
  119. isTopLevel && groupBy(elements, function(e) { return e.id; })
  120. );
  121. function handleConnection(c) {
  122. if (topLevel[c.source.id] && topLevel[c.target.id]) {
  123. topLevel[c.id] = [ c ];
  124. }
  125. // not enclosed as a child, but maybe logically
  126. // (connecting two moved elements?)
  127. if (allShapes[c.source.id] && allShapes[c.target.id]) {
  128. enclosedConnections[c.id] = enclosedElements[c.id] = c;
  129. }
  130. allConnections[c.id] = c;
  131. }
  132. function handleElement(element) {
  133. enclosedElements[element.id] = element;
  134. if (element.waypoints) {
  135. // remember connection
  136. enclosedConnections[element.id] = allConnections[element.id] = element;
  137. } else {
  138. // remember shape
  139. allShapes[element.id] = element;
  140. // remember all connections
  141. forEach(element.incoming, handleConnection);
  142. forEach(element.outgoing, handleConnection);
  143. // recurse into children
  144. return element.children;
  145. }
  146. }
  147. eachElement(elements, handleElement);
  148. return {
  149. allShapes: allShapes,
  150. allConnections: allConnections,
  151. topLevel: topLevel,
  152. enclosedConnections: enclosedConnections,
  153. enclosedElements: enclosedElements
  154. };
  155. }
  156. /**
  157. * Returns the surrounding bbox for all elements in
  158. * the array or the element primitive.
  159. *
  160. * @param {Array<djs.model.Shape>|djs.model.Shape} elements
  161. * @param {Boolean} stopRecursion
  162. */
  163. export function getBBox(elements, stopRecursion) {
  164. stopRecursion = !!stopRecursion;
  165. if (!isArray(elements)) {
  166. elements = [elements];
  167. }
  168. var minX,
  169. minY,
  170. maxX,
  171. maxY;
  172. forEach(elements, function(element) {
  173. // If element is a connection the bbox must be computed first
  174. var bbox = element;
  175. if (element.waypoints && !stopRecursion) {
  176. bbox = getBBox(element.waypoints, true);
  177. }
  178. var x = bbox.x,
  179. y = bbox.y,
  180. height = bbox.height || 0,
  181. width = bbox.width || 0;
  182. if (x < minX || minX === undefined) {
  183. minX = x;
  184. }
  185. if (y < minY || minY === undefined) {
  186. minY = y;
  187. }
  188. if ((x + width) > maxX || maxX === undefined) {
  189. maxX = x + width;
  190. }
  191. if ((y + height) > maxY || maxY === undefined) {
  192. maxY = y + height;
  193. }
  194. });
  195. return {
  196. x: minX,
  197. y: minY,
  198. height: maxY - minY,
  199. width: maxX - minX
  200. };
  201. }
  202. /**
  203. * Returns all elements that are enclosed from the bounding box.
  204. *
  205. * * If bbox.(width|height) is not specified the method returns
  206. * all elements with element.x/y > bbox.x/y
  207. * * If only bbox.x or bbox.y is specified, method return all elements with
  208. * e.x > bbox.x or e.y > bbox.y
  209. *
  210. * @param {Array<djs.model.Shape>} elements List of Elements to search through
  211. * @param {djs.model.Shape} bbox the enclosing bbox.
  212. *
  213. * @return {Array<djs.model.Shape>} enclosed elements
  214. */
  215. export function getEnclosedElements(elements, bbox) {
  216. var filteredElements = {};
  217. forEach(elements, function(element) {
  218. var e = element;
  219. if (e.waypoints) {
  220. e = getBBox(e);
  221. }
  222. if (!isNumber(bbox.y) && (e.x > bbox.x)) {
  223. filteredElements[element.id] = element;
  224. }
  225. if (!isNumber(bbox.x) && (e.y > bbox.y)) {
  226. filteredElements[element.id] = element;
  227. }
  228. if (e.x > bbox.x && e.y > bbox.y) {
  229. if (isNumber(bbox.width) && isNumber(bbox.height) &&
  230. e.width + e.x < bbox.width + bbox.x &&
  231. e.height + e.y < bbox.height + bbox.y) {
  232. filteredElements[element.id] = element;
  233. } else if (!isNumber(bbox.width) || !isNumber(bbox.height)) {
  234. filteredElements[element.id] = element;
  235. }
  236. }
  237. });
  238. return filteredElements;
  239. }
  240. export function getType(element) {
  241. if ('waypoints' in element) {
  242. return 'connection';
  243. }
  244. if ('x' in element) {
  245. return 'shape';
  246. }
  247. return 'root';
  248. }
  249. // helpers ///////////////////////////////
  250. function copyObject(src1, src2) {
  251. return assign({}, src1 || {}, src2 || {});
  252. }