contentPadProvider.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import { assign, forEach, isArray } from 'min-dash';
  2. import { is } from 'bpmn-js/lib/util/ModelUtil';
  3. import { isExpanded, isEventSubProcess } from 'bpmn-js/lib/util/DiUtil';
  4. import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
  5. import { getChildLanes } from 'bpmn-js/lib/features/modeling/util/LaneUtil';
  6. import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse';
  7. /**
  8. * A provider for BPMN 2.0 elements context pad
  9. */
  10. export default function ContextPadProvider(
  11. config,
  12. injector,
  13. eventBus,
  14. contextPad,
  15. modeling,
  16. elementFactory,
  17. connect,
  18. create,
  19. popupMenu,
  20. canvas,
  21. rules,
  22. translate,
  23. elementRegistry
  24. ) {
  25. config = config || {};
  26. contextPad.registerProvider(this);
  27. this._contextPad = contextPad;
  28. this._modeling = modeling;
  29. this._elementFactory = elementFactory;
  30. this._connect = connect;
  31. this._create = create;
  32. this._popupMenu = popupMenu;
  33. this._canvas = canvas;
  34. this._rules = rules;
  35. this._translate = translate;
  36. if (config.autoPlace !== false) {
  37. this._autoPlace = injector.get('autoPlace', false);
  38. }
  39. eventBus.on('create.end', 250, function (event) {
  40. const context = event.context,
  41. shape = context.shape;
  42. if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
  43. return;
  44. }
  45. const entries = contextPad.getEntries(shape);
  46. if (entries.replace) {
  47. entries.replace.action.click(event, shape);
  48. }
  49. });
  50. }
  51. ContextPadProvider.$inject = [
  52. 'config.contextPad',
  53. 'injector',
  54. 'eventBus',
  55. 'contextPad',
  56. 'modeling',
  57. 'elementFactory',
  58. 'connect',
  59. 'create',
  60. 'popupMenu',
  61. 'canvas',
  62. 'rules',
  63. 'translate',
  64. 'elementRegistry'
  65. ];
  66. ContextPadProvider.prototype.getContextPadEntries = function (element) {
  67. const contextPad = this._contextPad,
  68. modeling = this._modeling,
  69. elementFactory = this._elementFactory,
  70. connect = this._connect,
  71. create = this._create,
  72. popupMenu = this._popupMenu,
  73. canvas = this._canvas,
  74. rules = this._rules,
  75. autoPlace = this._autoPlace,
  76. translate = this._translate;
  77. const actions = {};
  78. if (element.type === 'label') {
  79. return actions;
  80. }
  81. const businessObject = element.businessObject;
  82. function startConnect(event, element) {
  83. connect.start(event, element);
  84. }
  85. function removeElement() {
  86. modeling.removeElements([element]);
  87. }
  88. function getReplaceMenuPosition(element) {
  89. const Y_OFFSET = 5;
  90. const diagramContainer = canvas.getContainer(),
  91. pad = contextPad.getPad(element).html;
  92. const diagramRect = diagramContainer.getBoundingClientRect(),
  93. padRect = pad.getBoundingClientRect();
  94. const top = padRect.top - diagramRect.top;
  95. const left = padRect.left - diagramRect.left;
  96. const pos = {
  97. x: left,
  98. y: top + padRect.height + Y_OFFSET
  99. };
  100. return pos;
  101. }
  102. /**
  103. * Create an append action
  104. *
  105. * @param {string} type
  106. * @param {string} className
  107. * @param {string} [title]
  108. * @param {Object} [options]
  109. *
  110. * @return {Object} descriptor
  111. */
  112. function appendAction(type, className, title, options) {
  113. if (typeof title !== 'string') {
  114. options = title;
  115. title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') });
  116. }
  117. function appendStart(event, element) {
  118. const shape = elementFactory.createShape(assign({ type: type }, options));
  119. create.start(event, shape, {
  120. source: element
  121. });
  122. }
  123. const append = autoPlace
  124. ? function (event, element) {
  125. const shape = elementFactory.createShape(
  126. assign({ type: type }, options)
  127. );
  128. autoPlace.append(element, shape);
  129. }
  130. : appendStart;
  131. return {
  132. group: 'model',
  133. className: className,
  134. title: title,
  135. action: {
  136. dragstart: appendStart,
  137. click: append
  138. }
  139. };
  140. }
  141. function splitLaneHandler(count) {
  142. return function (event, element) {
  143. // actual split
  144. modeling.splitLane(element, count);
  145. // refresh context pad after split to
  146. // get rid of split icons
  147. contextPad.open(element, true);
  148. };
  149. }
  150. if (
  151. isAny(businessObject, ['bpmn:Lane', 'bpmn:Participant']) &&
  152. isExpanded(businessObject)
  153. ) {
  154. const childLanes = getChildLanes(element);
  155. assign(actions, {
  156. 'lane-insert-above': {
  157. group: 'lane-insert-above',
  158. className: 'bpmn-icon-lane-insert-above',
  159. title: translate('Add Lane above'),
  160. action: {
  161. click: function (event, element) {
  162. modeling.addLane(element, 'top');
  163. }
  164. }
  165. }
  166. });
  167. if (childLanes.length < 2) {
  168. if (element.height >= 120) {
  169. assign(actions, {
  170. 'lane-divide-two': {
  171. group: 'lane-divide',
  172. className: 'bpmn-icon-lane-divide-two',
  173. title: translate('Divide into two Lanes'),
  174. action: {
  175. click: splitLaneHandler(2)
  176. }
  177. }
  178. });
  179. }
  180. if (element.height >= 180) {
  181. assign(actions, {
  182. 'lane-divide-three': {
  183. group: 'lane-divide',
  184. className: 'bpmn-icon-lane-divide-three',
  185. title: translate('Divide into three Lanes'),
  186. action: {
  187. click: splitLaneHandler(3)
  188. }
  189. }
  190. });
  191. }
  192. }
  193. assign(actions, {
  194. 'lane-insert-below': {
  195. group: 'lane-insert-below',
  196. className: 'bpmn-icon-lane-insert-below',
  197. title: translate('Add Lane below'),
  198. action: {
  199. click: function (event, element) {
  200. modeling.addLane(element, 'bottom');
  201. }
  202. }
  203. }
  204. });
  205. }
  206. if (is(businessObject, 'bpmn:FlowNode')) {
  207. if (is(businessObject, 'bpmn:EventBasedGateway')) {
  208. assign(actions, {
  209. 'append.receive-task': appendAction(
  210. 'bpmn:ReceiveTask',
  211. 'bpmn-icon-receive-task',
  212. translate('Append ReceiveTask')
  213. ),
  214. 'append.message-intermediate-event': appendAction(
  215. 'bpmn:IntermediateCatchEvent',
  216. 'bpmn-icon-intermediate-event-catch-message',
  217. translate('Append MessageIntermediateCatchEvent'),
  218. { eventDefinitionType: 'bpmn:MessageEventDefinition' }
  219. ),
  220. 'append.timer-intermediate-event': appendAction(
  221. 'bpmn:IntermediateCatchEvent',
  222. 'bpmn-icon-intermediate-event-catch-timer',
  223. translate('Append TimerIntermediateCatchEvent'),
  224. { eventDefinitionType: 'bpmn:TimerEventDefinition' }
  225. ),
  226. 'append.condition-intermediate-event': appendAction(
  227. 'bpmn:IntermediateCatchEvent',
  228. 'bpmn-icon-intermediate-event-catch-condition',
  229. translate('Append ConditionIntermediateCatchEvent'),
  230. { eventDefinitionType: 'bpmn:ConditionalEventDefinition' }
  231. ),
  232. 'append.signal-intermediate-event': appendAction(
  233. 'bpmn:IntermediateCatchEvent',
  234. 'bpmn-icon-intermediate-event-catch-signal',
  235. translate('Append SignalIntermediateCatchEvent'),
  236. { eventDefinitionType: 'bpmn:SignalEventDefinition' }
  237. )
  238. });
  239. } else if (
  240. isEventType(
  241. businessObject,
  242. 'bpmn:BoundaryEvent',
  243. 'bpmn:CompensateEventDefinition'
  244. )
  245. ) {
  246. assign(actions, {
  247. 'append.compensation-activity': appendAction(
  248. 'bpmn:Task',
  249. 'bpmn-icon-task',
  250. translate('Append compensation activity'),
  251. {
  252. isForCompensation: true
  253. }
  254. )
  255. });
  256. } else if (
  257. !is(businessObject, 'bpmn:EndEvent') &&
  258. !businessObject.isForCompensation &&
  259. !isEventType(
  260. businessObject,
  261. 'bpmn:IntermediateThrowEvent',
  262. 'bpmn:LinkEventDefinition'
  263. ) &&
  264. !isEventSubProcess(businessObject)
  265. ) {
  266. assign(actions, {
  267. 'append.end-event': appendAction(
  268. 'bpmn:EndEvent',
  269. 'bpmn-icon-end-event-none',
  270. translate('Append EndEvent')
  271. ),
  272. 'append.gateway': appendAction(
  273. 'bpmn:ExclusiveGateway',
  274. 'bpmn-icon-gateway-none',
  275. translate('Append Gateway')
  276. ),
  277. 'append.append-task': appendAction(
  278. 'bpmn:UserTask',
  279. 'bpmn-icon-user-task',
  280. translate('Append Task')
  281. ),
  282. 'append.intermediate-event': appendAction(
  283. 'bpmn:IntermediateThrowEvent',
  284. 'bpmn-icon-intermediate-event-none',
  285. translate('Append Intermediate/Boundary Event')
  286. )
  287. });
  288. }
  289. }
  290. if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
  291. // Replace menu entry
  292. assign(actions, {
  293. replace: {
  294. group: 'edit',
  295. className: 'bpmn-icon-screw-wrench',
  296. title: translate('Change type'),
  297. action: {
  298. click: function (event, element) {
  299. const position = assign(getReplaceMenuPosition(element), {
  300. cursor: { x: event.x, y: event.y }
  301. });
  302. popupMenu.open(element, 'bpmn-replace', position);
  303. }
  304. }
  305. }
  306. });
  307. }
  308. if (
  309. isAny(businessObject, [
  310. 'bpmn:FlowNode',
  311. 'bpmn:InteractionNode',
  312. 'bpmn:DataObjectReference',
  313. 'bpmn:DataStoreReference'
  314. ])
  315. ) {
  316. assign(actions, {
  317. 'append.text-annotation': appendAction(
  318. 'bpmn:TextAnnotation',
  319. 'bpmn-icon-text-annotation'
  320. ),
  321. connect: {
  322. group: 'connect',
  323. className: 'bpmn-icon-connection-multi',
  324. title: translate(
  325. 'Connect using ' +
  326. (businessObject.isForCompensation
  327. ? ''
  328. : 'Sequence/MessageFlow or ') +
  329. 'Association'
  330. ),
  331. action: {
  332. click: startConnect,
  333. dragstart: startConnect
  334. }
  335. }
  336. });
  337. }
  338. if (
  339. isAny(businessObject, [
  340. 'bpmn:DataObjectReference',
  341. 'bpmn:DataStoreReference'
  342. ])
  343. ) {
  344. assign(actions, {
  345. connect: {
  346. group: 'connect',
  347. className: 'bpmn-icon-connection-multi',
  348. title: translate('Connect using DataInputAssociation'),
  349. action: {
  350. click: startConnect,
  351. dragstart: startConnect
  352. }
  353. }
  354. });
  355. }
  356. if (is(businessObject, 'bpmn:Group')) {
  357. assign(actions, {
  358. 'append.text-annotation': appendAction(
  359. 'bpmn:TextAnnotation',
  360. 'bpmn-icon-text-annotation'
  361. )
  362. });
  363. }
  364. // delete element entry, only show if allowed by rules
  365. let deleteAllowed = rules.allowed('elements.delete', { elements: [element] });
  366. if (isArray(deleteAllowed)) {
  367. // was the element returned as a deletion candidate?
  368. deleteAllowed = deleteAllowed[0] === element;
  369. }
  370. if (deleteAllowed) {
  371. assign(actions, {
  372. delete: {
  373. group: 'edit',
  374. className: 'bpmn-icon-trash',
  375. title: translate('Remove'),
  376. action: {
  377. click: removeElement
  378. }
  379. }
  380. });
  381. }
  382. return actions;
  383. };
  384. // helpers /////////
  385. function isEventType(eventBo, type, definition) {
  386. const isType = eventBo.$instanceOf(type);
  387. let isDefinition = false;
  388. const definitions = eventBo.eventDefinitions || [];
  389. forEach(definitions, function (def) {
  390. if (def.$type === definition) {
  391. isDefinition = true;
  392. }
  393. });
  394. return isType && isDefinition;
  395. }