LabelBehavior.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import {
  2. assign
  3. } from 'min-dash';
  4. import inherits from 'inherits-browser';
  5. import {
  6. is,
  7. getBusinessObject
  8. } from '../../../util/ModelUtil';
  9. import {
  10. isLabelExternal,
  11. getLabel,
  12. hasExternalLabel,
  13. isLabel
  14. } from '../../../util/LabelUtil';
  15. import {
  16. getLabelAdjustment
  17. } from './util/LabelLayoutUtil';
  18. import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
  19. import {
  20. getNewAttachPoint
  21. } from 'diagram-js/lib/util/AttachUtil';
  22. import {
  23. getMid,
  24. roundPoint
  25. } from 'diagram-js/lib/layout/LayoutUtil';
  26. import {
  27. delta
  28. } from 'diagram-js/lib/util/PositionUtil';
  29. import {
  30. sortBy
  31. } from 'min-dash';
  32. import {
  33. getDistancePointLine,
  34. perpendicularFoot
  35. } from './util/GeometricUtil';
  36. var NAME_PROPERTY = 'name';
  37. var TEXT_PROPERTY = 'text';
  38. /**
  39. * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
  40. * @typedef {import('../Modeling').default} Modeling
  41. * @typedef {import('../BpmnFactory').default} BpmnFactory
  42. * @typedef {import('../../../draw/TextRenderer').default} TextRenderer
  43. *
  44. * @typedef {import('diagram-js/lib/util/Types').Point} Point
  45. * @typedef {import('diagram-js/lib/util/Types').Rect} Rect
  46. *
  47. * @typedef {Point[]} Line
  48. */
  49. /**
  50. * A component that makes sure that external labels are added
  51. * together with respective elements and properly updated (DI wise)
  52. * during move.
  53. *
  54. * @param {EventBus} eventBus
  55. * @param {Modeling} modeling
  56. * @param {BpmnFactory} bpmnFactory
  57. * @param {TextRenderer} textRenderer
  58. */
  59. export default function LabelBehavior(
  60. eventBus, modeling, bpmnFactory,
  61. textRenderer) {
  62. CommandInterceptor.call(this, eventBus);
  63. // update label if name property was updated
  64. this.postExecute('element.updateProperties', onPropertyUpdate);
  65. this.postExecute('element.updateModdleProperties', e => {
  66. const elementBo = getBusinessObject(e.context.element);
  67. if (elementBo === e.context.moddleElement) {
  68. onPropertyUpdate(e);
  69. }
  70. });
  71. function onPropertyUpdate(e) {
  72. var context = e.context,
  73. element = context.element,
  74. properties = context.properties;
  75. if (NAME_PROPERTY in properties) {
  76. modeling.updateLabel(element, properties[NAME_PROPERTY]);
  77. }
  78. if (TEXT_PROPERTY in properties
  79. && is(element, 'bpmn:TextAnnotation')) {
  80. var newBounds = textRenderer.getTextAnnotationBounds(
  81. {
  82. x: element.x,
  83. y: element.y,
  84. width: element.width,
  85. height: element.height
  86. },
  87. properties[TEXT_PROPERTY] || ''
  88. );
  89. modeling.updateLabel(element, properties.text, newBounds);
  90. }
  91. }
  92. // create label shape after shape/connection was created
  93. this.postExecute([ 'shape.create', 'connection.create' ], function(e) {
  94. var context = e.context,
  95. hints = context.hints || {};
  96. if (hints.createElementsBehavior === false) {
  97. return;
  98. }
  99. var element = context.shape || context.connection;
  100. if (isLabel(element) || !isLabelExternal(element)) {
  101. return;
  102. }
  103. // only create label if attribute available
  104. if (!getLabel(element)) {
  105. return;
  106. }
  107. modeling.updateLabel(element, getLabel(element));
  108. });
  109. // update label after label shape was deleted
  110. this.postExecute('shape.delete', function(event) {
  111. var context = event.context,
  112. labelTarget = context.labelTarget,
  113. hints = context.hints || {};
  114. // check if label
  115. if (labelTarget && hints.unsetLabel !== false) {
  116. modeling.updateLabel(labelTarget, null, null, { removeShape: false });
  117. }
  118. });
  119. function getVisibleLabelAdjustment(event) {
  120. var context = event.context,
  121. connection = context.connection,
  122. label = connection.label,
  123. hints = assign({}, context.hints),
  124. newWaypoints = context.newWaypoints || connection.waypoints,
  125. oldWaypoints = context.oldWaypoints;
  126. if (typeof hints.startChanged === 'undefined') {
  127. hints.startChanged = !!hints.connectionStart;
  128. }
  129. if (typeof hints.endChanged === 'undefined') {
  130. hints.endChanged = !!hints.connectionEnd;
  131. }
  132. return getLabelAdjustment(label, newWaypoints, oldWaypoints, hints);
  133. }
  134. this.postExecute([
  135. 'connection.layout',
  136. 'connection.updateWaypoints'
  137. ], function(event) {
  138. var context = event.context,
  139. hints = context.hints || {};
  140. if (hints.labelBehavior === false) {
  141. return;
  142. }
  143. var connection = context.connection,
  144. label = connection.label,
  145. labelAdjustment;
  146. // handle missing label as well as the case
  147. // that the label parent does not exist (yet),
  148. // because it is being pasted / created via multi element create
  149. //
  150. // Cf. https://github.com/bpmn-io/bpmn-js/pull/1227
  151. if (!label || !label.parent) {
  152. return;
  153. }
  154. labelAdjustment = getVisibleLabelAdjustment(event);
  155. modeling.moveShape(label, labelAdjustment);
  156. });
  157. // keep label position on shape replace
  158. this.postExecute([ 'shape.replace' ], function(event) {
  159. var context = event.context,
  160. newShape = context.newShape,
  161. oldShape = context.oldShape;
  162. var businessObject = getBusinessObject(newShape);
  163. if (businessObject
  164. && isLabelExternal(businessObject)
  165. && oldShape.label
  166. && newShape.label) {
  167. newShape.label.x = oldShape.label.x;
  168. newShape.label.y = oldShape.label.y;
  169. }
  170. });
  171. // move external label after resizing
  172. this.postExecute('shape.resize', function(event) {
  173. var context = event.context,
  174. shape = context.shape,
  175. newBounds = context.newBounds,
  176. oldBounds = context.oldBounds;
  177. if (hasExternalLabel(shape)) {
  178. var label = shape.label,
  179. labelMid = getMid(label),
  180. edges = asEdges(oldBounds);
  181. // get nearest border point to label as reference point
  182. var referencePoint = getReferencePoint(labelMid, edges);
  183. var delta = getReferencePointDelta(referencePoint, oldBounds, newBounds);
  184. modeling.moveShape(label, delta);
  185. }
  186. });
  187. }
  188. inherits(LabelBehavior, CommandInterceptor);
  189. LabelBehavior.$inject = [
  190. 'eventBus',
  191. 'modeling',
  192. 'bpmnFactory',
  193. 'textRenderer'
  194. ];
  195. // helpers //////////////////////
  196. /**
  197. * Calculates a reference point delta relative to a new position
  198. * of a certain element's bounds
  199. *
  200. * @param {Point} referencePoint
  201. * @param {Rect} oldBounds
  202. * @param {Rect} newBounds
  203. *
  204. * @return {Point}
  205. */
  206. export function getReferencePointDelta(referencePoint, oldBounds, newBounds) {
  207. var newReferencePoint = getNewAttachPoint(referencePoint, oldBounds, newBounds);
  208. return roundPoint(delta(newReferencePoint, referencePoint));
  209. }
  210. /**
  211. * Generates the nearest point (reference point) for a given point
  212. * onto given set of lines
  213. *
  214. * @param {Point} point
  215. * @param {Line[]} lines
  216. *
  217. * @return {Point}
  218. */
  219. export function getReferencePoint(point, lines) {
  220. if (!lines.length) {
  221. return;
  222. }
  223. var nearestLine = getNearestLine(point, lines);
  224. return perpendicularFoot(point, nearestLine);
  225. }
  226. /**
  227. * Convert the given bounds to a lines array containing all edges
  228. *
  229. * @param {Rect|Point} bounds
  230. *
  231. * @return {Line[]}
  232. */
  233. export function asEdges(bounds) {
  234. return [
  235. [ // top
  236. {
  237. x: bounds.x,
  238. y: bounds.y
  239. },
  240. {
  241. x: bounds.x + (bounds.width || 0),
  242. y: bounds.y
  243. }
  244. ],
  245. [ // right
  246. {
  247. x: bounds.x + (bounds.width || 0),
  248. y: bounds.y
  249. },
  250. {
  251. x: bounds.x + (bounds.width || 0),
  252. y: bounds.y + (bounds.height || 0)
  253. }
  254. ],
  255. [ // bottom
  256. {
  257. x: bounds.x,
  258. y: bounds.y + (bounds.height || 0)
  259. },
  260. {
  261. x: bounds.x + (bounds.width || 0),
  262. y: bounds.y + (bounds.height || 0)
  263. }
  264. ],
  265. [ // left
  266. {
  267. x: bounds.x,
  268. y: bounds.y
  269. },
  270. {
  271. x: bounds.x,
  272. y: bounds.y + (bounds.height || 0)
  273. }
  274. ]
  275. ];
  276. }
  277. /**
  278. * Returns the nearest line for a given point by distance
  279. * @param {Point} point
  280. * @param {Line[]} lines
  281. *
  282. * @return {Line}
  283. */
  284. function getNearestLine(point, lines) {
  285. var distances = lines.map(function(l) {
  286. return {
  287. line: l,
  288. distance: getDistancePointLine(point, l)
  289. };
  290. });
  291. var sorted = sortBy(distances, 'distance');
  292. return sorted[0].line;
  293. }