AttachSupport.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import {
  2. flatten,
  3. filter,
  4. forEach,
  5. groupBy,
  6. map,
  7. unionBy
  8. } from 'min-dash';
  9. import { saveClear } from '../../util/Removal';
  10. import {
  11. remove as collectionRemove
  12. } from '../../util/Collections';
  13. import { getNewAttachShapeDelta } from '../../util/AttachUtil';
  14. import inherits from 'inherits';
  15. var LOW_PRIORITY = 251,
  16. HIGH_PRIORITY = 1401;
  17. import CommandInterceptor from '../../command/CommandInterceptor';
  18. /**
  19. * Adds the notion of attached elements to the modeler.
  20. *
  21. * Optionally depends on `diagram-js/lib/features/move` to render
  22. * the attached elements during move preview.
  23. *
  24. * Optionally depends on `diagram-js/lib/features/label-support`
  25. * to render attached labels during move preview.
  26. *
  27. * @param {didi.Injector} injector
  28. * @param {EventBus} eventBus
  29. * @param {Rules} rules
  30. * @param {Modeling} modeling
  31. */
  32. export default function AttachSupport(injector, eventBus, rules, modeling) {
  33. CommandInterceptor.call(this, eventBus);
  34. var movePreview = injector.get('movePreview', false);
  35. // remove all the attached elements from the shapes to be validated
  36. // add all the attached shapes to the overall list of moved shapes
  37. eventBus.on('shape.move.start', HIGH_PRIORITY, function(e) {
  38. var context = e.context,
  39. shapes = context.shapes,
  40. validatedShapes = context.validatedShapes;
  41. context.shapes = addAttached(shapes);
  42. context.validatedShapes = removeAttached(validatedShapes);
  43. });
  44. // add attachers to the visual's group
  45. movePreview && eventBus.on('shape.move.start', LOW_PRIORITY, function(e) {
  46. var context = e.context,
  47. shapes = context.shapes,
  48. attachers = getAttachers(shapes);
  49. forEach(attachers, function(attacher) {
  50. movePreview.makeDraggable(context, attacher, true);
  51. forEach(attacher.labels, function(label) {
  52. movePreview.makeDraggable(context, label, true);
  53. });
  54. });
  55. });
  56. // add all attachers to move closure
  57. this.preExecuted('elements.move', HIGH_PRIORITY, function(e) {
  58. var context = e.context,
  59. closure = context.closure,
  60. shapes = context.shapes,
  61. attachers = getAttachers(shapes);
  62. forEach(attachers, function(attacher) {
  63. closure.add(attacher, closure.topLevel[attacher.host.id]);
  64. });
  65. });
  66. // perform the attaching after shapes are done moving
  67. this.postExecuted('elements.move', function(e) {
  68. var context = e.context,
  69. shapes = context.shapes,
  70. newHost = context.newHost,
  71. attachers;
  72. // we only support attachment / detachment of one element
  73. if (shapes.length > 1) {
  74. return;
  75. }
  76. if (newHost) {
  77. attachers = shapes;
  78. } else {
  79. attachers = filter(shapes, function(s) {
  80. return !!s.host;
  81. });
  82. }
  83. forEach(attachers, function(attacher) {
  84. modeling.updateAttachment(attacher, newHost);
  85. });
  86. });
  87. // ensure invalid attachment connections are removed
  88. this.postExecuted('elements.move', function(e) {
  89. var shapes = e.context.shapes;
  90. forEach(shapes, function(shape) {
  91. forEach(shape.attachers, function(attacher) {
  92. // remove invalid outgoing connections
  93. forEach(attacher.outgoing.slice(), function(connection) {
  94. var allowed = rules.allowed('connection.reconnectStart', {
  95. connection: connection,
  96. source: connection.source,
  97. target: connection.target
  98. });
  99. if (!allowed) {
  100. modeling.removeConnection(connection);
  101. }
  102. });
  103. // remove invalid incoming connections
  104. forEach(attacher.incoming.slice(), function(connection) {
  105. var allowed = rules.allowed('connection.reconnectEnd', {
  106. connection: connection,
  107. source: connection.source,
  108. target: connection.target
  109. });
  110. if (!allowed) {
  111. modeling.removeConnection(connection);
  112. }
  113. });
  114. });
  115. });
  116. });
  117. this.postExecute('shape.create', function(e) {
  118. var context = e.context,
  119. shape = context.shape,
  120. host = context.host;
  121. if (host) {
  122. modeling.updateAttachment(shape, host);
  123. }
  124. });
  125. // update attachments if the host is replaced
  126. this.postExecute('shape.replace', function(e) {
  127. var context = e.context,
  128. oldShape = context.oldShape,
  129. newShape = context.newShape;
  130. // move the attachers to the new host
  131. saveClear(oldShape.attachers, function(attacher) {
  132. var allowed = rules.allowed('elements.move', {
  133. target: newShape,
  134. shapes: [attacher]
  135. });
  136. if (allowed === 'attach') {
  137. modeling.updateAttachment(attacher, newShape);
  138. } else {
  139. modeling.removeShape(attacher);
  140. }
  141. });
  142. // move attachers if new host has different size
  143. if (newShape.attachers.length) {
  144. forEach(newShape.attachers, function(attacher) {
  145. var delta = getNewAttachShapeDelta(attacher, oldShape, newShape);
  146. modeling.moveShape(attacher, delta, attacher.parent);
  147. });
  148. }
  149. });
  150. // move shape on host resize
  151. this.postExecute('shape.resize', function(event) {
  152. var context = event.context,
  153. shape = context.shape,
  154. oldBounds = context.oldBounds,
  155. newBounds = context.newBounds,
  156. attachers = shape.attachers;
  157. forEach(attachers, function(attacher) {
  158. var delta = getNewAttachShapeDelta(attacher, oldBounds, newBounds);
  159. modeling.moveShape(attacher, delta, attacher.parent);
  160. forEach(attacher.labels, function(label) {
  161. modeling.moveShape(label, delta, label.parent);
  162. });
  163. });
  164. });
  165. // remove attachments
  166. this.preExecute('shape.delete', function(event) {
  167. var shape = event.context.shape;
  168. saveClear(shape.attachers, function(attacher) {
  169. modeling.removeShape(attacher);
  170. });
  171. if (shape.host) {
  172. modeling.updateAttachment(shape, null);
  173. }
  174. });
  175. // Prevent attachers and their labels from moving, when the space tool is performed.
  176. // Otherwise the attachers and their labels would be moved twice.
  177. eventBus.on('spaceTool.move', function(event) {
  178. var context = event.context,
  179. initialized = context.initialized,
  180. attachSupportInitialized = context.attachSupportInitialized;
  181. if (!initialized || attachSupportInitialized) {
  182. return;
  183. }
  184. var movingShapes = context.movingShapes;
  185. // collect attachers whose host is not being moved using the space tool
  186. var staticAttachers = filter(movingShapes, function(shape) {
  187. var host = shape.host;
  188. return host && movingShapes.indexOf(host) === -1;
  189. });
  190. // remove attachers that are not going to be moved from moving shapes
  191. forEach(staticAttachers, function(shape) {
  192. collectionRemove(movingShapes, shape);
  193. forEach(shape.labels, function(label) {
  194. collectionRemove(movingShapes, shape.label);
  195. });
  196. });
  197. context.attachSupportInitialized = true;
  198. });
  199. }
  200. inherits(AttachSupport, CommandInterceptor);
  201. AttachSupport.$inject = [
  202. 'injector',
  203. 'eventBus',
  204. 'rules',
  205. 'modeling'
  206. ];
  207. /**
  208. * Return attachers of the given shapes
  209. *
  210. * @param {Array<djs.model.Base>} shapes
  211. * @return {Array<djs.model.Base>}
  212. */
  213. function getAttachers(shapes) {
  214. return flatten(map(shapes, function(s) {
  215. return s.attachers || [];
  216. }));
  217. }
  218. /**
  219. * Return a combined list of elements and
  220. * attachers.
  221. *
  222. * @param {Array<djs.model.Base>} elements
  223. * @return {Array<djs.model.Base>} filtered
  224. */
  225. function addAttached(elements) {
  226. var attachers = getAttachers(elements);
  227. return unionBy('id', elements, attachers);
  228. }
  229. /**
  230. * Return a filtered list of elements that do not
  231. * contain attached elements with hosts being part
  232. * of the selection.
  233. *
  234. * @param {Array<djs.model.Base>} elements
  235. *
  236. * @return {Array<djs.model.Base>} filtered
  237. */
  238. function removeAttached(elements) {
  239. var ids = groupBy(elements, 'id');
  240. return filter(elements, function(element) {
  241. while (element) {
  242. // host in selection
  243. if (element.host && ids[element.host.id]) {
  244. return false;
  245. }
  246. element = element.parent;
  247. }
  248. return true;
  249. });
  250. }