LabelEditingProvider.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. import {
  2. assign
  3. } from 'min-dash';
  4. import {
  5. getLabel
  6. } from '../../util/LabelUtil';
  7. import {
  8. is
  9. } from '../../util/ModelUtil';
  10. import { isAny } from '../modeling/util/ModelingUtil';
  11. import {
  12. isExpanded,
  13. isHorizontal
  14. } from '../../util/DiUtil';
  15. import {
  16. getExternalLabelMid,
  17. isLabelExternal,
  18. hasExternalLabel,
  19. isLabel
  20. } from '../../util/LabelUtil';
  21. /**
  22. * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
  23. * @typedef {import('../modeling/BpmnFactory').default} BpmnFactory
  24. * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas
  25. * @typedef {import('diagram-js-direct-editing/lib/DirectEditing').default} DirectEditing
  26. * @typedef {import('../modeling/Modeling').default} Modeling
  27. * @typedef {import('diagram-js/lib/features/resize/ResizeHandles').default} ResizeHandles
  28. * @typedef {import('../../draw/TextRenderer').default} TextRenderer
  29. *
  30. * @typedef {import('../../model/Types').Element} Element
  31. *
  32. * @typedef { {
  33. * bounds: {
  34. * x: number;
  35. * y: number;
  36. * width: number;
  37. * height: number;
  38. * minWidth?: number;
  39. * minHeight?: number;
  40. * };
  41. * style: Object;
  42. * } } DirectEditingContext
  43. */
  44. var HIGH_PRIORITY = 2000;
  45. /**
  46. * @param {EventBus} eventBus
  47. * @param {BpmnFactory} bpmnFactory
  48. * @param {Canvas} canvas
  49. * @param {DirectEditing} directEditing
  50. * @param {Modeling} modeling
  51. * @param {ResizeHandles} resizeHandles
  52. * @param {TextRenderer} textRenderer
  53. */
  54. export default function LabelEditingProvider(
  55. eventBus, bpmnFactory, canvas, directEditing,
  56. modeling, resizeHandles, textRenderer) {
  57. this._bpmnFactory = bpmnFactory;
  58. this._canvas = canvas;
  59. this._modeling = modeling;
  60. this._textRenderer = textRenderer;
  61. directEditing.registerProvider(this);
  62. // listen to dblclick on non-root elements
  63. eventBus.on('element.dblclick', function(event) {
  64. activateDirectEdit(event.element, true);
  65. });
  66. // complete on followup canvas operation
  67. eventBus.on([
  68. 'autoPlace.start',
  69. 'canvas.viewbox.changing',
  70. 'drag.init',
  71. 'element.mousedown',
  72. 'popupMenu.open',
  73. 'root.set',
  74. 'selection.changed'
  75. ], function() {
  76. if (directEditing.isActive()) {
  77. directEditing.complete();
  78. }
  79. });
  80. eventBus.on([
  81. 'shape.remove',
  82. 'connection.remove'
  83. ], HIGH_PRIORITY, function(event) {
  84. if (directEditing.isActive(event.element)) {
  85. directEditing.cancel();
  86. }
  87. });
  88. // cancel on command stack changes
  89. eventBus.on([ 'commandStack.changed' ], function(e) {
  90. if (directEditing.isActive()) {
  91. directEditing.cancel();
  92. }
  93. });
  94. eventBus.on('directEditing.activate', function(event) {
  95. resizeHandles.removeResizers();
  96. });
  97. eventBus.on('create.end', 500, function(event) {
  98. var context = event.context,
  99. element = context.shape,
  100. canExecute = event.context.canExecute,
  101. isTouch = event.isTouch;
  102. // TODO(nikku): we need to find a way to support the
  103. // direct editing on mobile devices; right now this will
  104. // break for desworkflowediting on mobile devices
  105. // as it breaks the user interaction workflow
  106. // TODO(nikku): we should temporarily focus the edited element
  107. // here and release the focused viewport after the direct edit
  108. // operation is finished
  109. if (isTouch) {
  110. return;
  111. }
  112. if (!canExecute) {
  113. return;
  114. }
  115. if (context.hints && context.hints.createElementsBehavior === false) {
  116. return;
  117. }
  118. activateDirectEdit(element);
  119. });
  120. eventBus.on('autoPlace.end', 500, function(event) {
  121. activateDirectEdit(event.shape);
  122. });
  123. function activateDirectEdit(element, force) {
  124. if (force ||
  125. isAny(element, [ 'bpmn:Task', 'bpmn:TextAnnotation', 'bpmn:Participant' ]) ||
  126. isCollapsedSubProcess(element)) {
  127. directEditing.activate(element);
  128. }
  129. }
  130. }
  131. LabelEditingProvider.$inject = [
  132. 'eventBus',
  133. 'bpmnFactory',
  134. 'canvas',
  135. 'directEditing',
  136. 'modeling',
  137. 'resizeHandles',
  138. 'textRenderer'
  139. ];
  140. /**
  141. * Activate direct editing for activities and text annotations.
  142. *
  143. * @param {Element} element
  144. *
  145. * @return { {
  146. * text: string;
  147. * options?: {
  148. * autoResize?: boolean;
  149. * centerVertically?: boolean;
  150. * resizable?: boolean;
  151. * }
  152. * } & DirectEditingContext }
  153. */
  154. LabelEditingProvider.prototype.activate = function(element) {
  155. // text
  156. var text = getLabel(element);
  157. if (text === undefined) {
  158. return;
  159. }
  160. var context = {
  161. text: text
  162. };
  163. // bounds
  164. var bounds = this.getEditingBBox(element);
  165. assign(context, bounds);
  166. var options = {};
  167. // tasks
  168. if (
  169. isAny(element, [
  170. 'bpmn:Task',
  171. 'bpmn:Participant',
  172. 'bpmn:Lane',
  173. 'bpmn:CallActivity'
  174. ]) ||
  175. isCollapsedSubProcess(element)
  176. ) {
  177. assign(options, {
  178. centerVertically: true
  179. });
  180. }
  181. // external labels
  182. if (isLabelExternal(element)) {
  183. assign(options, {
  184. autoResize: true
  185. });
  186. }
  187. // text annotations
  188. if (is(element, 'bpmn:TextAnnotation')) {
  189. assign(options, {
  190. resizable: true,
  191. autoResize: true
  192. });
  193. }
  194. assign(context, {
  195. options: options
  196. });
  197. return context;
  198. };
  199. /**
  200. * Get the editing bounding box based on the element's size and position.
  201. *
  202. * @param {Element} element
  203. *
  204. * @return {DirectEditingContext}
  205. */
  206. LabelEditingProvider.prototype.getEditingBBox = function(element) {
  207. var canvas = this._canvas;
  208. var target = element.label || element;
  209. var bbox = canvas.getAbsoluteBBox(target);
  210. var mid = {
  211. x: bbox.x + bbox.width / 2,
  212. y: bbox.y + bbox.height / 2
  213. };
  214. // default position
  215. var bounds = { x: bbox.x, y: bbox.y };
  216. var zoom = canvas.zoom();
  217. var defaultStyle = this._textRenderer.getDefaultStyle(),
  218. externalStyle = this._textRenderer.getExternalStyle();
  219. // take zoom into account
  220. var externalFontSize = externalStyle.fontSize * zoom,
  221. externalLineHeight = externalStyle.lineHeight,
  222. defaultFontSize = defaultStyle.fontSize * zoom,
  223. defaultLineHeight = defaultStyle.lineHeight;
  224. var style = {
  225. fontFamily: this._textRenderer.getDefaultStyle().fontFamily,
  226. fontWeight: this._textRenderer.getDefaultStyle().fontWeight
  227. };
  228. // adjust for expanded pools AND lanes
  229. if (is(element, 'bpmn:Lane') || isExpandedPool(element)) {
  230. var isHorizontalLane = isHorizontal(element);
  231. var laneBounds = isHorizontalLane ? {
  232. width: bbox.height,
  233. height: 30 * zoom,
  234. x: bbox.x - bbox.height / 2 + (15 * zoom),
  235. y: mid.y - (30 * zoom) / 2
  236. } : {
  237. width: bbox.width,
  238. height: 30 * zoom
  239. };
  240. assign(bounds, laneBounds);
  241. assign(style, {
  242. fontSize: defaultFontSize + 'px',
  243. lineHeight: defaultLineHeight,
  244. paddingTop: (7 * zoom) + 'px',
  245. paddingBottom: (7 * zoom) + 'px',
  246. paddingLeft: (5 * zoom) + 'px',
  247. paddingRight: (5 * zoom) + 'px',
  248. transform: isHorizontalLane ? 'rotate(-90deg)' : null
  249. });
  250. }
  251. // internal labels for collapsed participants
  252. if (isCollapsedPool(element)) {
  253. var isHorizontalPool = isHorizontal(element);
  254. var poolBounds = isHorizontalPool ? {
  255. width: bbox.width,
  256. height: bbox.height
  257. } : {
  258. width: bbox.height,
  259. height: bbox.width,
  260. x: mid.x - bbox.height / 2,
  261. y: mid.y - bbox.width / 2
  262. };
  263. assign(bounds, poolBounds);
  264. assign(style, {
  265. fontSize: defaultFontSize + 'px',
  266. lineHeight: defaultLineHeight,
  267. paddingTop: (7 * zoom) + 'px',
  268. paddingBottom: (7 * zoom) + 'px',
  269. paddingLeft: (5 * zoom) + 'px',
  270. paddingRight: (5 * zoom) + 'px',
  271. transform: isHorizontalPool ? null : 'rotate(-90deg)'
  272. });
  273. }
  274. // internal labels for tasks and collapsed call activities
  275. // and sub processes
  276. if (isAny(element, [ 'bpmn:Task', 'bpmn:CallActivity' ]) ||
  277. isCollapsedSubProcess(element)) {
  278. assign(bounds, {
  279. width: bbox.width,
  280. height: bbox.height
  281. });
  282. assign(style, {
  283. fontSize: defaultFontSize + 'px',
  284. lineHeight: defaultLineHeight,
  285. paddingTop: (7 * zoom) + 'px',
  286. paddingBottom: (7 * zoom) + 'px',
  287. paddingLeft: (5 * zoom) + 'px',
  288. paddingRight: (5 * zoom) + 'px'
  289. });
  290. }
  291. // internal labels for expanded sub processes
  292. if (isExpandedSubProcess(element)) {
  293. assign(bounds, {
  294. width: bbox.width,
  295. x: bbox.x
  296. });
  297. assign(style, {
  298. fontSize: defaultFontSize + 'px',
  299. lineHeight: defaultLineHeight,
  300. paddingTop: (7 * zoom) + 'px',
  301. paddingBottom: (7 * zoom) + 'px',
  302. paddingLeft: (5 * zoom) + 'px',
  303. paddingRight: (5 * zoom) + 'px'
  304. });
  305. }
  306. var width = 90 * zoom,
  307. paddingTop = 7 * zoom,
  308. paddingBottom = 4 * zoom;
  309. // external labels for events, data elements, gateways, groups and connections
  310. if (target.labelTarget) {
  311. assign(bounds, {
  312. width: width,
  313. height: bbox.height + paddingTop + paddingBottom,
  314. x: mid.x - width / 2,
  315. y: bbox.y - paddingTop
  316. });
  317. assign(style, {
  318. fontSize: externalFontSize + 'px',
  319. lineHeight: externalLineHeight,
  320. paddingTop: paddingTop + 'px',
  321. paddingBottom: paddingBottom + 'px'
  322. });
  323. }
  324. // external label not yet created
  325. if (isLabelExternal(target)
  326. && !hasExternalLabel(target)
  327. && !isLabel(target)) {
  328. var externalLabelMid = getExternalLabelMid(element);
  329. var absoluteBBox = canvas.getAbsoluteBBox({
  330. x: externalLabelMid.x,
  331. y: externalLabelMid.y,
  332. width: 0,
  333. height: 0
  334. });
  335. var height = externalFontSize + paddingTop + paddingBottom;
  336. assign(bounds, {
  337. width: width,
  338. height: height,
  339. x: absoluteBBox.x - width / 2,
  340. y: absoluteBBox.y - height / 2
  341. });
  342. assign(style, {
  343. fontSize: externalFontSize + 'px',
  344. lineHeight: externalLineHeight,
  345. paddingTop: paddingTop + 'px',
  346. paddingBottom: paddingBottom + 'px'
  347. });
  348. }
  349. // text annotations
  350. if (is(element, 'bpmn:TextAnnotation')) {
  351. assign(bounds, {
  352. width: bbox.width,
  353. height: bbox.height,
  354. minWidth: 30 * zoom,
  355. minHeight: 10 * zoom
  356. });
  357. assign(style, {
  358. textAlign: 'left',
  359. paddingTop: (5 * zoom) + 'px',
  360. paddingBottom: (7 * zoom) + 'px',
  361. paddingLeft: (7 * zoom) + 'px',
  362. paddingRight: (5 * zoom) + 'px',
  363. fontSize: defaultFontSize + 'px',
  364. lineHeight: defaultLineHeight
  365. });
  366. }
  367. return { bounds: bounds, style: style };
  368. };
  369. LabelEditingProvider.prototype.update = function(
  370. element, newLabel,
  371. activeContextText, bounds) {
  372. var newBounds,
  373. bbox;
  374. if (is(element, 'bpmn:TextAnnotation')) {
  375. bbox = this._canvas.getAbsoluteBBox(element);
  376. newBounds = {
  377. x: element.x,
  378. y: element.y,
  379. width: element.width / bbox.width * bounds.width,
  380. height: element.height / bbox.height * bounds.height
  381. };
  382. }
  383. if (isEmptyText(newLabel)) {
  384. newLabel = null;
  385. }
  386. this._modeling.updateLabel(element, newLabel, newBounds);
  387. };
  388. // helpers //////////
  389. function isCollapsedSubProcess(element) {
  390. return is(element, 'bpmn:SubProcess') && !isExpanded(element);
  391. }
  392. function isExpandedSubProcess(element) {
  393. return is(element, 'bpmn:SubProcess') && isExpanded(element);
  394. }
  395. function isCollapsedPool(element) {
  396. return is(element, 'bpmn:Participant') && !isExpanded(element);
  397. }
  398. function isExpandedPool(element) {
  399. return is(element, 'bpmn:Participant') && isExpanded(element);
  400. }
  401. function isEmptyText(label) {
  402. return !label || !label.trim();
  403. }