AdaptiveLabelPositioningBehavior.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import inherits from 'inherits-browser';
  2. import {
  3. getOrientation,
  4. getMid,
  5. asTRBL
  6. } from 'diagram-js/lib/layout/LayoutUtil';
  7. import {
  8. substract
  9. } from 'diagram-js/lib/util/Math';
  10. import {
  11. hasExternalLabel
  12. } from '../../../util/LabelUtil';
  13. import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
  14. /**
  15. * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
  16. * @typedef {import('../Modeling').default} Modeling
  17. *
  18. * @typedef {import('../../../model/Types').Element} Element
  19. * @typedef {import('../../../model/Types').Shape} Shape
  20. *
  21. * @typedef {import('diagram-js/lib/util/Types').DirectionTRBL} DirectionTRBL
  22. */
  23. var ALIGNMENTS = [
  24. 'top',
  25. 'bottom',
  26. 'left',
  27. 'right'
  28. ];
  29. var ELEMENT_LABEL_DISTANCE = 10;
  30. /**
  31. * A component that makes sure that external labels are added
  32. * together with respective elements and properly updated (DI wise)
  33. * during move.
  34. *
  35. * @param {EventBus} eventBus
  36. * @param {Modeling} modeling
  37. */
  38. export default function AdaptiveLabelPositioningBehavior(eventBus, modeling) {
  39. CommandInterceptor.call(this, eventBus);
  40. this.postExecuted([
  41. 'connection.create',
  42. 'connection.layout',
  43. 'connection.updateWaypoints'
  44. ], function(event) {
  45. var context = event.context,
  46. connection = context.connection,
  47. source = connection.source,
  48. target = connection.target,
  49. hints = context.hints || {};
  50. if (hints.createElementsBehavior !== false) {
  51. checkLabelAdjustment(source);
  52. checkLabelAdjustment(target);
  53. }
  54. });
  55. this.postExecuted([
  56. 'label.create'
  57. ], function(event) {
  58. var context = event.context,
  59. shape = context.shape,
  60. hints = context.hints || {};
  61. if (hints.createElementsBehavior !== false) {
  62. checkLabelAdjustment(shape.labelTarget);
  63. }
  64. });
  65. this.postExecuted([
  66. 'elements.create'
  67. ], function(event) {
  68. var context = event.context,
  69. elements = context.elements,
  70. hints = context.hints || {};
  71. if (hints.createElementsBehavior !== false) {
  72. elements.forEach(function(element) {
  73. checkLabelAdjustment(element);
  74. });
  75. }
  76. });
  77. function checkLabelAdjustment(element) {
  78. // skip non-existing labels
  79. if (!hasExternalLabel(element)) {
  80. return;
  81. }
  82. var optimalPosition = getOptimalPosition(element);
  83. // no optimal position found
  84. if (!optimalPosition) {
  85. return;
  86. }
  87. adjustLabelPosition(element, optimalPosition);
  88. }
  89. function adjustLabelPosition(element, orientation) {
  90. var elementMid = getMid(element),
  91. label = element.label,
  92. labelMid = getMid(label);
  93. // ignore labels that are being created
  94. if (!label.parent) {
  95. return;
  96. }
  97. var elementTrbl = asTRBL(element);
  98. var newLabelMid;
  99. switch (orientation) {
  100. case 'top':
  101. newLabelMid = {
  102. x: elementMid.x,
  103. y: elementTrbl.top - ELEMENT_LABEL_DISTANCE - label.height / 2
  104. };
  105. break;
  106. case 'left':
  107. newLabelMid = {
  108. x: elementTrbl.left - ELEMENT_LABEL_DISTANCE - label.width / 2,
  109. y: elementMid.y
  110. };
  111. break;
  112. case 'bottom':
  113. newLabelMid = {
  114. x: elementMid.x,
  115. y: elementTrbl.bottom + ELEMENT_LABEL_DISTANCE + label.height / 2
  116. };
  117. break;
  118. case 'right':
  119. newLabelMid = {
  120. x: elementTrbl.right + ELEMENT_LABEL_DISTANCE + label.width / 2,
  121. y: elementMid.y
  122. };
  123. break;
  124. }
  125. var delta = substract(newLabelMid, labelMid);
  126. modeling.moveShape(label, delta);
  127. }
  128. }
  129. inherits(AdaptiveLabelPositioningBehavior, CommandInterceptor);
  130. AdaptiveLabelPositioningBehavior.$inject = [
  131. 'eventBus',
  132. 'modeling'
  133. ];
  134. // helpers //////////////////////
  135. /**
  136. * Return alignments which are taken by a boundary's host element
  137. *
  138. * @param {Shape} element
  139. *
  140. * @return {DirectionTRBL[]}
  141. */
  142. function getTakenHostAlignments(element) {
  143. var hostElement = element.host,
  144. elementMid = getMid(element),
  145. hostOrientation = getOrientation(elementMid, hostElement);
  146. var freeAlignments;
  147. // check whether there is a multi-orientation, e.g. 'top-left'
  148. if (hostOrientation.indexOf('-') >= 0) {
  149. freeAlignments = hostOrientation.split('-');
  150. } else {
  151. freeAlignments = [ hostOrientation ];
  152. }
  153. var takenAlignments = ALIGNMENTS.filter(function(alignment) {
  154. return freeAlignments.indexOf(alignment) === -1;
  155. });
  156. return takenAlignments;
  157. }
  158. /**
  159. * Return alignments which are taken by related connections
  160. *
  161. * @param {Element} element
  162. *
  163. * @return {DirectionTRBL[]}
  164. */
  165. function getTakenConnectionAlignments(element) {
  166. var elementMid = getMid(element);
  167. var takenAlignments = [].concat(
  168. element.incoming.map(function(c) {
  169. return c.waypoints[c.waypoints.length - 2 ];
  170. }),
  171. element.outgoing.map(function(c) {
  172. return c.waypoints[1];
  173. })
  174. ).map(function(point) {
  175. return getApproximateOrientation(elementMid, point);
  176. });
  177. return takenAlignments;
  178. }
  179. /**
  180. * Return the optimal label position around an element
  181. * or `undefined`, if none was found.
  182. *
  183. * @param {Element} element
  184. *
  185. * @return {DirectionTRBL|undefined}
  186. */
  187. function getOptimalPosition(element) {
  188. var labelMid = getMid(element.label);
  189. var elementMid = getMid(element);
  190. var labelOrientation = getApproximateOrientation(elementMid, labelMid);
  191. if (!isAligned(labelOrientation)) {
  192. return;
  193. }
  194. var takenAlignments = getTakenConnectionAlignments(element);
  195. if (element.host) {
  196. var takenHostAlignments = getTakenHostAlignments(element);
  197. takenAlignments = takenAlignments.concat(takenHostAlignments);
  198. }
  199. var freeAlignments = ALIGNMENTS.filter(function(alignment) {
  200. return takenAlignments.indexOf(alignment) === -1;
  201. });
  202. // NOTHING TO DO; label already aligned a.O.K.
  203. if (freeAlignments.indexOf(labelOrientation) !== -1) {
  204. return;
  205. }
  206. return freeAlignments[0];
  207. }
  208. function getApproximateOrientation(p0, p1) {
  209. return getOrientation(p1, p0, 5);
  210. }
  211. function isAligned(orientation) {
  212. return ALIGNMENTS.indexOf(orientation) !== -1;
  213. }