Elements.js 8.1 KB

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