AutoResize.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import inherits from 'inherits-browser';
  2. import { getBBox as getBoundingBox } from '../../util/Elements';
  3. import {
  4. asTRBL,
  5. asBounds
  6. } from '../../layout/LayoutUtil';
  7. import {
  8. assign,
  9. flatten,
  10. find,
  11. forEach,
  12. groupBy,
  13. isArray,
  14. matchPattern,
  15. pick,
  16. values
  17. } from 'min-dash';
  18. import CommandInterceptor from '../../command/CommandInterceptor';
  19. /**
  20. * An auto resize component that takes care of expanding a parent element
  21. * if child elements are created or moved close the parents edge.
  22. *
  23. * @param {EventBus} eventBus
  24. * @param {ElementRegistry} elementRegistry
  25. * @param {Modeling} modeling
  26. * @param {Rules} rules
  27. */
  28. export default function AutoResize(eventBus, elementRegistry, modeling, rules) {
  29. CommandInterceptor.call(this, eventBus);
  30. this._elementRegistry = elementRegistry;
  31. this._modeling = modeling;
  32. this._rules = rules;
  33. var self = this;
  34. this.postExecuted([ 'shape.create' ], function(event) {
  35. var context = event.context,
  36. hints = context.hints || {},
  37. shape = context.shape,
  38. parent = context.parent || context.newParent;
  39. if (hints.autoResize === false) {
  40. return;
  41. }
  42. self._expand([ shape ], parent);
  43. });
  44. this.postExecuted([ 'elements.move' ], function(event) {
  45. var context = event.context,
  46. elements = flatten(values(context.closure.topLevel)),
  47. hints = context.hints;
  48. var autoResize = hints ? hints.autoResize : true;
  49. if (autoResize === false) {
  50. return;
  51. }
  52. var expandings = groupBy(elements, function(element) {
  53. return element.parent.id;
  54. });
  55. forEach(expandings, function(elements, parentId) {
  56. // optionally filter elements to be considered when resizing
  57. if (isArray(autoResize)) {
  58. elements = elements.filter(function(element) {
  59. return find(autoResize, matchPattern({ id: element.id }));
  60. });
  61. }
  62. self._expand(elements, parentId);
  63. });
  64. });
  65. this.postExecuted([ 'shape.toggleCollapse' ], function(event) {
  66. var context = event.context,
  67. hints = context.hints,
  68. shape = context.shape;
  69. if (hints && hints.autoResize === false) {
  70. return;
  71. }
  72. if (shape.collapsed) {
  73. return;
  74. }
  75. self._expand(shape.children || [], shape);
  76. });
  77. this.postExecuted([ 'shape.resize' ], function(event) {
  78. var context = event.context,
  79. hints = context.hints,
  80. shape = context.shape,
  81. parent = shape.parent;
  82. if (hints && hints.autoResize === false) {
  83. return;
  84. }
  85. if (parent) {
  86. self._expand([ shape ], parent);
  87. }
  88. });
  89. }
  90. AutoResize.$inject = [
  91. 'eventBus',
  92. 'elementRegistry',
  93. 'modeling',
  94. 'rules'
  95. ];
  96. inherits(AutoResize, CommandInterceptor);
  97. /**
  98. * Calculate the new bounds of the target shape, given
  99. * a number of elements have been moved or added into the parent.
  100. *
  101. * This method considers the current size, the added elements as well as
  102. * the provided padding for the new bounds.
  103. *
  104. * @param {Array<djs.model.Shape>} elements
  105. * @param {djs.model.Shape} target
  106. */
  107. AutoResize.prototype._getOptimalBounds = function(elements, target) {
  108. var offset = this.getOffset(target),
  109. padding = this.getPadding(target);
  110. var elementsTrbl = asTRBL(getBoundingBox(elements)),
  111. targetTrbl = asTRBL(target);
  112. var newTrbl = {};
  113. if (elementsTrbl.top - targetTrbl.top < padding.top) {
  114. newTrbl.top = elementsTrbl.top - offset.top;
  115. }
  116. if (elementsTrbl.left - targetTrbl.left < padding.left) {
  117. newTrbl.left = elementsTrbl.left - offset.left;
  118. }
  119. if (targetTrbl.right - elementsTrbl.right < padding.right) {
  120. newTrbl.right = elementsTrbl.right + offset.right;
  121. }
  122. if (targetTrbl.bottom - elementsTrbl.bottom < padding.bottom) {
  123. newTrbl.bottom = elementsTrbl.bottom + offset.bottom;
  124. }
  125. return asBounds(assign({}, targetTrbl, newTrbl));
  126. };
  127. /**
  128. * Expand the target shape respecting rules, offset and padding
  129. *
  130. * @param {Array<djs.model.Shape>} elements
  131. * @param {djs.model.Shape|string} target|targetId
  132. */
  133. AutoResize.prototype._expand = function(elements, target) {
  134. if (typeof target === 'string') {
  135. target = this._elementRegistry.get(target);
  136. }
  137. var allowed = this._rules.allowed('element.autoResize', {
  138. elements: elements,
  139. target: target
  140. });
  141. if (!allowed) {
  142. return;
  143. }
  144. // calculate the new bounds
  145. var newBounds = this._getOptimalBounds(elements, target);
  146. if (!boundsChanged(newBounds, target)) {
  147. return;
  148. }
  149. var resizeDirections = getResizeDirections(pick(target, [ 'x', 'y', 'width', 'height' ]), newBounds);
  150. // resize the parent shape
  151. this.resize(target, newBounds, {
  152. autoResize: resizeDirections
  153. });
  154. var parent = target.parent;
  155. // recursively expand parent elements
  156. if (parent) {
  157. this._expand([ target ], parent);
  158. }
  159. };
  160. /**
  161. * Get the amount to expand the given shape in each direction.
  162. *
  163. * @param {djs.model.Shape} shape
  164. *
  165. * @return {TRBL}
  166. */
  167. AutoResize.prototype.getOffset = function(shape) {
  168. return { top: 60, bottom: 60, left: 100, right: 100 };
  169. };
  170. /**
  171. * Get the activation threshold for each side for which
  172. * resize triggers.
  173. *
  174. * @param {djs.model.Shape} shape
  175. *
  176. * @return {TRBL}
  177. */
  178. AutoResize.prototype.getPadding = function(shape) {
  179. return { top: 2, bottom: 2, left: 15, right: 15 };
  180. };
  181. /**
  182. * Perform the actual resize operation.
  183. *
  184. * @param {djs.model.Shape} shape
  185. * @param {Bounds} newBounds
  186. * @param {Object} [hints]
  187. * @param {string} [hints.autoResize]
  188. */
  189. AutoResize.prototype.resize = function(shape, newBounds, hints) {
  190. this._modeling.resizeShape(shape, newBounds, null, hints);
  191. };
  192. function boundsChanged(newBounds, oldBounds) {
  193. return (
  194. newBounds.x !== oldBounds.x ||
  195. newBounds.y !== oldBounds.y ||
  196. newBounds.width !== oldBounds.width ||
  197. newBounds.height !== oldBounds.height
  198. );
  199. }
  200. /**
  201. * Get directions of resize as {n|w|s|e} e.g. "nw".
  202. *
  203. * @param {Bounds} oldBounds
  204. * @param {Bounds} newBounds
  205. *
  206. * @returns {string} Resize directions as {n|w|s|e}.
  207. */
  208. function getResizeDirections(oldBounds, newBounds) {
  209. var directions = '';
  210. oldBounds = asTRBL(oldBounds);
  211. newBounds = asTRBL(newBounds);
  212. if (oldBounds.top > newBounds.top) {
  213. directions = directions.concat('n');
  214. }
  215. if (oldBounds.right < newBounds.right) {
  216. directions = directions.concat('w');
  217. }
  218. if (oldBounds.bottom < newBounds.bottom) {
  219. directions = directions.concat('s');
  220. }
  221. if (oldBounds.left > newBounds.left) {
  222. directions = directions.concat('e');
  223. }
  224. return directions;
  225. }