ContextPadProvider.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. import {
  2. assign,
  3. forEach,
  4. isArray,
  5. every
  6. } from 'min-dash';
  7. import {
  8. is
  9. } from '../../util/ModelUtil';
  10. import {
  11. isExpanded,
  12. isEventSubProcess
  13. } from '../../util/DiUtil';
  14. import {
  15. isAny
  16. } from '../modeling/util/ModelingUtil';
  17. import {
  18. getChildLanes
  19. } from '../modeling/util/LaneUtil';
  20. import {
  21. hasPrimaryModifier
  22. } from 'diagram-js/lib/util/Mouse';
  23. /**
  24. * @typedef {import('didi').Injector} Injector
  25. * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
  26. * @typedef {import('diagram-js/lib/features/context-pad/ContextPad').default} ContextPad
  27. * @typedef {import('../modeling/Modeling').default} Modeling
  28. * @typedef {import('../modeling/ElementFactory').default} ElementFactory
  29. * @typedef {import('diagram-js/lib/features/connect/Connect').default} Connect
  30. * @typedef {import('diagram-js/lib/features/create/Create').default} Create
  31. * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').default} PopupMenu
  32. * @typedef {import('diagram-js/lib/features/canvas/Canvas').default} Canvas
  33. * @typedef {import('diagram-js/lib/features/rules/Rules').default} Rules
  34. * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate
  35. *
  36. * @typedef {import('../../model/Types').Element} Element
  37. * @typedef {import('../../model/Types').ModdleElement} ModdleElement
  38. *
  39. * @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').default<Element>} BaseContextPadProvider
  40. * @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').ContextPadEntries} ContextPadEntries
  41. * @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').ContextPadEntry} ContextPadEntry
  42. *
  43. * @typedef { { autoPlace?: boolean; } } ContextPadConfig
  44. */
  45. /**
  46. * BPMN-specific context pad provider.
  47. *
  48. * @implements {BaseContextPadProvider}
  49. *
  50. * @param {ContextPadConfig} config
  51. * @param {Injector} injector
  52. * @param {EventBus} eventBus
  53. * @param {ContextPad} contextPad
  54. * @param {Modeling} modeling
  55. * @param {ElementFactory} elementFactory
  56. * @param {Connect} connect
  57. * @param {Create} create
  58. * @param {PopupMenu} popupMenu
  59. * @param {Canvas} canvas
  60. * @param {Rules} rules
  61. * @param {Translate} translate
  62. */
  63. export default function ContextPadProvider(
  64. config, injector, eventBus,
  65. contextPad, modeling, elementFactory,
  66. connect, create, popupMenu,
  67. canvas, rules, translate, appendPreview) {
  68. config = config || {};
  69. contextPad.registerProvider(this);
  70. this._contextPad = contextPad;
  71. this._modeling = modeling;
  72. this._elementFactory = elementFactory;
  73. this._connect = connect;
  74. this._create = create;
  75. this._popupMenu = popupMenu;
  76. this._canvas = canvas;
  77. this._rules = rules;
  78. this._translate = translate;
  79. this._eventBus = eventBus;
  80. this._appendPreview = appendPreview;
  81. if (config.autoPlace !== false) {
  82. this._autoPlace = injector.get('autoPlace', false);
  83. }
  84. eventBus.on('create.end', 250, function(event) {
  85. var context = event.context,
  86. shape = context.shape;
  87. if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
  88. return;
  89. }
  90. var entries = contextPad.getEntries(shape);
  91. if (entries.replace) {
  92. entries.replace.action.click(event, shape);
  93. }
  94. });
  95. }
  96. ContextPadProvider.$inject = [
  97. 'config.contextPad',
  98. 'injector',
  99. 'eventBus',
  100. 'contextPad',
  101. 'modeling',
  102. 'elementFactory',
  103. 'connect',
  104. 'create',
  105. 'popupMenu',
  106. 'canvas',
  107. 'rules',
  108. 'translate',
  109. 'appendPreview'
  110. ];
  111. /**
  112. * @param {Element[]} elements
  113. *
  114. * @return {ContextPadEntries}
  115. */
  116. ContextPadProvider.prototype.getMultiElementContextPadEntries = function(elements) {
  117. var modeling = this._modeling;
  118. var actions = {};
  119. if (this._isDeleteAllowed(elements)) {
  120. assign(actions, {
  121. 'delete': {
  122. group: 'edit',
  123. className: 'bpmn-icon-trash',
  124. title: this._translate('Remove'),
  125. action: {
  126. click: function(event, elements) {
  127. modeling.removeElements(elements.slice());
  128. }
  129. }
  130. }
  131. });
  132. }
  133. return actions;
  134. };
  135. /**
  136. * @param {Element[]} elements
  137. *
  138. * @return {boolean}
  139. */
  140. ContextPadProvider.prototype._isDeleteAllowed = function(elements) {
  141. var baseAllowed = this._rules.allowed('elements.delete', {
  142. elements: elements
  143. });
  144. if (isArray(baseAllowed)) {
  145. return every(baseAllowed, function(element) {
  146. return includes(baseAllowed, element);
  147. });
  148. }
  149. return baseAllowed;
  150. };
  151. /**
  152. * @param {Element} element
  153. *
  154. * @return {ContextPadEntries}
  155. */
  156. ContextPadProvider.prototype.getContextPadEntries = function(element) {
  157. var contextPad = this._contextPad,
  158. modeling = this._modeling,
  159. elementFactory = this._elementFactory,
  160. connect = this._connect,
  161. create = this._create,
  162. popupMenu = this._popupMenu,
  163. rules = this._rules,
  164. autoPlace = this._autoPlace,
  165. translate = this._translate,
  166. appendPreview = this._appendPreview;
  167. var actions = {};
  168. if (element.type === 'label') {
  169. return actions;
  170. }
  171. var businessObject = element.businessObject;
  172. function startConnect(event, element) {
  173. connect.start(event, element);
  174. }
  175. function removeElement(e, element) {
  176. modeling.removeElements([ element ]);
  177. }
  178. function getReplaceMenuPosition(element) {
  179. var Y_OFFSET = 5;
  180. var pad = contextPad.getPad(element).html;
  181. var padRect = pad.getBoundingClientRect();
  182. var pos = {
  183. x: padRect.left,
  184. y: padRect.bottom + Y_OFFSET
  185. };
  186. return pos;
  187. }
  188. /**
  189. * Create an append action.
  190. *
  191. * @param {string} type
  192. * @param {string} className
  193. * @param {string} title
  194. * @param {Object} [options]
  195. *
  196. * @return {ContextPadEntry}
  197. */
  198. function appendAction(type, className, title, options) {
  199. function appendStart(event, element) {
  200. var shape = elementFactory.createShape(assign({ type: type }, options));
  201. create.start(event, shape, {
  202. source: element
  203. });
  204. appendPreview.cleanUp();
  205. }
  206. var append = autoPlace ? function(_, element) {
  207. var shape = elementFactory.createShape(assign({ type: type }, options));
  208. autoPlace.append(element, shape);
  209. appendPreview.cleanUp();
  210. } : appendStart;
  211. var previewAppend = autoPlace ? function(_, element) {
  212. // mouseover
  213. appendPreview.create(element, type, options);
  214. return () => {
  215. // mouseout
  216. appendPreview.cleanUp();
  217. };
  218. } : null;
  219. return {
  220. group: 'model',
  221. className: className,
  222. title: title,
  223. action: {
  224. dragstart: appendStart,
  225. click: append,
  226. hover: previewAppend
  227. }
  228. };
  229. }
  230. function splitLaneHandler(count) {
  231. return function(_, element) {
  232. // actual split
  233. modeling.splitLane(element, count);
  234. // refresh context pad after split to
  235. // get rid of split icons
  236. contextPad.open(element, true);
  237. };
  238. }
  239. if (isAny(businessObject, [ 'bpmn:Lane', 'bpmn:Participant' ]) && isExpanded(element)) {
  240. var childLanes = getChildLanes(element);
  241. assign(actions, {
  242. 'lane-insert-above': {
  243. group: 'lane-insert-above',
  244. className: 'bpmn-icon-lane-insert-above',
  245. title: translate('Add lane above'),
  246. action: {
  247. click: function(event, element) {
  248. modeling.addLane(element, 'top');
  249. }
  250. }
  251. }
  252. });
  253. if (childLanes.length < 2) {
  254. if (element.height >= 120) {
  255. assign(actions, {
  256. 'lane-divide-two': {
  257. group: 'lane-divide',
  258. className: 'bpmn-icon-lane-divide-two',
  259. title: translate('Divide into two lanes'),
  260. action: {
  261. click: splitLaneHandler(2)
  262. }
  263. }
  264. });
  265. }
  266. if (element.height >= 180) {
  267. assign(actions, {
  268. 'lane-divide-three': {
  269. group: 'lane-divide',
  270. className: 'bpmn-icon-lane-divide-three',
  271. title: translate('Divide into three lanes'),
  272. action: {
  273. click: splitLaneHandler(3)
  274. }
  275. }
  276. });
  277. }
  278. }
  279. assign(actions, {
  280. 'lane-insert-below': {
  281. group: 'lane-insert-below',
  282. className: 'bpmn-icon-lane-insert-below',
  283. title: translate('Add lane below'),
  284. action: {
  285. click: function(event, element) {
  286. modeling.addLane(element, 'bottom');
  287. }
  288. }
  289. }
  290. });
  291. }
  292. if (is(businessObject, 'bpmn:FlowNode')) {
  293. if (is(businessObject, 'bpmn:EventBasedGateway')) {
  294. assign(actions, {
  295. 'append.receive-task': appendAction(
  296. 'bpmn:ReceiveTask',
  297. 'bpmn-icon-receive-task',
  298. translate('Append receive task')
  299. ),
  300. 'append.message-intermediate-event': appendAction(
  301. 'bpmn:IntermediateCatchEvent',
  302. 'bpmn-icon-intermediate-event-catch-message',
  303. translate('Append message intermediate catch event'),
  304. { eventDefinitionType: 'bpmn:MessageEventDefinition' }
  305. ),
  306. 'append.timer-intermediate-event': appendAction(
  307. 'bpmn:IntermediateCatchEvent',
  308. 'bpmn-icon-intermediate-event-catch-timer',
  309. translate('Append timer intermediate catch event'),
  310. { eventDefinitionType: 'bpmn:TimerEventDefinition' }
  311. ),
  312. 'append.condition-intermediate-event': appendAction(
  313. 'bpmn:IntermediateCatchEvent',
  314. 'bpmn-icon-intermediate-event-catch-condition',
  315. translate('Append conditional intermediate catch event'),
  316. { eventDefinitionType: 'bpmn:ConditionalEventDefinition' }
  317. ),
  318. 'append.signal-intermediate-event': appendAction(
  319. 'bpmn:IntermediateCatchEvent',
  320. 'bpmn-icon-intermediate-event-catch-signal',
  321. translate('Append signal intermediate catch event'),
  322. { eventDefinitionType: 'bpmn:SignalEventDefinition' }
  323. )
  324. });
  325. } else
  326. if (isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')) {
  327. assign(actions, {
  328. 'append.compensation-activity':
  329. appendAction(
  330. 'bpmn:Task',
  331. 'bpmn-icon-task',
  332. translate('Append compensation activity'),
  333. {
  334. isForCompensation: true
  335. }
  336. )
  337. });
  338. } else
  339. if (!is(businessObject, 'bpmn:EndEvent') &&
  340. !businessObject.isForCompensation &&
  341. !isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') &&
  342. !isEventSubProcess(businessObject)) {
  343. assign(actions, {
  344. 'append.end-event': appendAction(
  345. 'bpmn:EndEvent',
  346. 'bpmn-icon-end-event-none',
  347. translate('Append end event')
  348. ),
  349. 'append.gateway': appendAction(
  350. 'bpmn:ExclusiveGateway',
  351. 'bpmn-icon-gateway-none',
  352. translate('Append gateway')
  353. ),
  354. 'append.append-task': appendAction(
  355. 'bpmn:Task',
  356. 'bpmn-icon-task',
  357. translate('Append task')
  358. ),
  359. 'append.intermediate-event': appendAction(
  360. 'bpmn:IntermediateThrowEvent',
  361. 'bpmn-icon-intermediate-event-none',
  362. translate('Append intermediate/boundary event')
  363. )
  364. });
  365. }
  366. }
  367. if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
  368. // Replace menu entry
  369. assign(actions, {
  370. 'replace': {
  371. group: 'edit',
  372. className: 'bpmn-icon-screw-wrench',
  373. title: translate('Change type'),
  374. action: {
  375. click: function(event, element) {
  376. var position = assign(getReplaceMenuPosition(element), {
  377. cursor: { x: event.x, y: event.y }
  378. });
  379. popupMenu.open(element, 'bpmn-replace', position, {
  380. title: translate('Change element'),
  381. width: 300,
  382. search: true
  383. });
  384. }
  385. }
  386. }
  387. });
  388. }
  389. if (is(businessObject, 'bpmn:SequenceFlow')) {
  390. assign(actions, {
  391. 'append.text-annotation': appendAction(
  392. 'bpmn:TextAnnotation',
  393. 'bpmn-icon-text-annotation',
  394. translate('Append text annotation')
  395. )
  396. });
  397. }
  398. if (
  399. isAny(businessObject, [
  400. 'bpmn:FlowNode',
  401. 'bpmn:InteractionNode',
  402. 'bpmn:DataObjectReference',
  403. 'bpmn:DataStoreReference',
  404. ])
  405. ) {
  406. assign(actions, {
  407. 'append.text-annotation': appendAction(
  408. 'bpmn:TextAnnotation',
  409. 'bpmn-icon-text-annotation',
  410. translate('Append text annotation')
  411. ),
  412. 'connect': {
  413. group: 'connect',
  414. className: 'bpmn-icon-connection-multi',
  415. title: translate(
  416. 'Connect using ' +
  417. (businessObject.isForCompensation
  418. ? ''
  419. : 'sequence/message flow or ') +
  420. 'association'
  421. ),
  422. action: {
  423. click: startConnect,
  424. dragstart: startConnect,
  425. },
  426. },
  427. });
  428. }
  429. if (is(businessObject, 'bpmn:TextAnnotation')) {
  430. assign(actions, {
  431. 'connect': {
  432. group: 'connect',
  433. className: 'bpmn-icon-connection-multi',
  434. title: translate('Connect using association'),
  435. action: {
  436. click: startConnect,
  437. dragstart: startConnect,
  438. },
  439. },
  440. });
  441. }
  442. if (isAny(businessObject, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) {
  443. assign(actions, {
  444. 'connect': {
  445. group: 'connect',
  446. className: 'bpmn-icon-connection-multi',
  447. title: translate('Connect using data input association'),
  448. action: {
  449. click: startConnect,
  450. dragstart: startConnect
  451. }
  452. }
  453. });
  454. }
  455. if (is(businessObject, 'bpmn:Group')) {
  456. assign(actions, {
  457. 'append.text-annotation': appendAction(
  458. 'bpmn:TextAnnotation',
  459. 'bpmn-icon-text-annotation',
  460. translate('Append text annotation')
  461. )
  462. });
  463. }
  464. // delete element entry, only show if allowed by rules
  465. var deleteAllowed = rules.allowed('elements.delete', { elements: [ element ] });
  466. if (isArray(deleteAllowed)) {
  467. // was the element returned as a deletion candidate?
  468. deleteAllowed = deleteAllowed[0] === element;
  469. }
  470. if (deleteAllowed) {
  471. assign(actions, {
  472. 'delete': {
  473. group: 'edit',
  474. className: 'bpmn-icon-trash',
  475. title: translate('Remove'),
  476. action: {
  477. click: removeElement
  478. }
  479. }
  480. });
  481. }
  482. return actions;
  483. };
  484. // helpers /////////
  485. /**
  486. * @param {ModdleElement} businessObject
  487. * @param {string} type
  488. * @param {string} eventDefinitionType
  489. *
  490. * @return {boolean}
  491. */
  492. function isEventType(businessObject, type, eventDefinitionType) {
  493. var isType = businessObject.$instanceOf(type);
  494. var isDefinition = false;
  495. var definitions = businessObject.eventDefinitions || [];
  496. forEach(definitions, function(def) {
  497. if (def.$type === eventDefinitionType) {
  498. isDefinition = true;
  499. }
  500. });
  501. return isType && isDefinition;
  502. }
  503. function includes(array, item) {
  504. return array.indexOf(item) !== -1;
  505. }