LabelEditingProvider.js 9.7 KB

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