AutoResize.js 5.6 KB

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