BpmnUpdater.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. import {
  2. assign,
  3. forEach
  4. } from 'min-dash';
  5. import inherits from 'inherits';
  6. import {
  7. remove as collectionRemove,
  8. add as collectionAdd
  9. } from 'diagram-js/lib/util/Collections';
  10. import {
  11. Label
  12. } from 'diagram-js/lib/model';
  13. import {
  14. getBusinessObject,
  15. is
  16. } from '../../util/ModelUtil';
  17. import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
  18. /**
  19. * A handler responsible for updating the underlying BPMN 2.0 XML + DI
  20. * once changes on the diagram happen
  21. */
  22. export default function BpmnUpdater(
  23. eventBus, bpmnFactory, connectionDocking,
  24. translate) {
  25. CommandInterceptor.call(this, eventBus);
  26. this._bpmnFactory = bpmnFactory;
  27. this._translate = translate;
  28. var self = this;
  29. // connection cropping //////////////////////
  30. // crop connection ends during create/update
  31. function cropConnection(e) {
  32. var context = e.context,
  33. connection;
  34. if (!context.cropped) {
  35. connection = context.connection;
  36. connection.waypoints = connectionDocking.getCroppedWaypoints(connection);
  37. context.cropped = true;
  38. }
  39. }
  40. this.executed([
  41. 'connection.layout',
  42. 'connection.create'
  43. ], cropConnection);
  44. this.reverted([ 'connection.layout' ], function(e) {
  45. delete e.context.cropped;
  46. });
  47. // BPMN + DI update //////////////////////
  48. // update parent
  49. function updateParent(e) {
  50. var context = e.context;
  51. self.updateParent(context.shape || context.connection, context.oldParent);
  52. }
  53. function reverseUpdateParent(e) {
  54. var context = e.context;
  55. var element = context.shape || context.connection,
  56. // oldParent is the (old) new parent, because we are undoing
  57. oldParent = context.parent || context.newParent;
  58. self.updateParent(element, oldParent);
  59. }
  60. this.executed([
  61. 'shape.move',
  62. 'shape.create',
  63. 'shape.delete',
  64. 'connection.create',
  65. 'connection.move',
  66. 'connection.delete'
  67. ], ifBpmn(updateParent));
  68. this.reverted([
  69. 'shape.move',
  70. 'shape.create',
  71. 'shape.delete',
  72. 'connection.create',
  73. 'connection.move',
  74. 'connection.delete'
  75. ], ifBpmn(reverseUpdateParent));
  76. /*
  77. * ## Updating Parent
  78. *
  79. * When morphing a Process into a Collaboration or vice-versa,
  80. * make sure that both the *semantic* and *di* parent of each element
  81. * is updated.
  82. *
  83. */
  84. function updateRoot(event) {
  85. var context = event.context,
  86. oldRoot = context.oldRoot,
  87. children = oldRoot.children;
  88. forEach(children, function(child) {
  89. if (is(child, 'bpmn:BaseElement')) {
  90. self.updateParent(child);
  91. }
  92. });
  93. }
  94. this.executed([ 'canvas.updateRoot' ], updateRoot);
  95. this.reverted([ 'canvas.updateRoot' ], updateRoot);
  96. // update bounds
  97. function updateBounds(e) {
  98. var shape = e.context.shape;
  99. if (!is(shape, 'bpmn:BaseElement')) {
  100. return;
  101. }
  102. self.updateBounds(shape);
  103. }
  104. this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) {
  105. // exclude labels because they're handled separately during shape.changed
  106. if (event.context.shape.type === 'label') {
  107. return;
  108. }
  109. updateBounds(event);
  110. }));
  111. this.reverted([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) {
  112. // exclude labels because they're handled separately during shape.changed
  113. if (event.context.shape.type === 'label') {
  114. return;
  115. }
  116. updateBounds(event);
  117. }));
  118. // Handle labels separately. This is necessary, because the label bounds have to be updated
  119. // every time its shape changes, not only on move, create and resize.
  120. eventBus.on('shape.changed', function(event) {
  121. if (event.element.type === 'label') {
  122. updateBounds({ context: { shape: event.element } });
  123. }
  124. });
  125. // attach / detach connection
  126. function updateConnection(e) {
  127. self.updateConnection(e.context);
  128. }
  129. this.executed([
  130. 'connection.create',
  131. 'connection.move',
  132. 'connection.delete',
  133. 'connection.reconnectEnd',
  134. 'connection.reconnectStart'
  135. ], ifBpmn(updateConnection));
  136. this.reverted([
  137. 'connection.create',
  138. 'connection.move',
  139. 'connection.delete',
  140. 'connection.reconnectEnd',
  141. 'connection.reconnectStart'
  142. ], ifBpmn(updateConnection));
  143. // update waypoints
  144. function updateConnectionWaypoints(e) {
  145. self.updateConnectionWaypoints(e.context.connection);
  146. }
  147. this.executed([
  148. 'connection.layout',
  149. 'connection.move',
  150. 'connection.updateWaypoints',
  151. ], ifBpmn(updateConnectionWaypoints));
  152. this.reverted([
  153. 'connection.layout',
  154. 'connection.move',
  155. 'connection.updateWaypoints',
  156. ], ifBpmn(updateConnectionWaypoints));
  157. // update Default & Conditional flows
  158. this.executed([
  159. 'connection.reconnectEnd',
  160. 'connection.reconnectStart'
  161. ], ifBpmn(function(e) {
  162. var context = e.context,
  163. connection = context.connection,
  164. businessObject = getBusinessObject(connection),
  165. oldSource = getBusinessObject(context.oldSource),
  166. oldTarget = getBusinessObject(context.oldTarget),
  167. newSource = getBusinessObject(connection.source),
  168. newTarget = getBusinessObject(connection.target);
  169. if (oldSource === newSource || oldTarget === newTarget) {
  170. return;
  171. }
  172. // on reconnectStart -> default flow
  173. if (oldSource && oldSource.default === businessObject) {
  174. context.default = oldSource.default;
  175. oldSource.default = undefined;
  176. }
  177. // on reconnectEnd -> default flow
  178. if ((businessObject.sourceRef && businessObject.sourceRef.default) &&
  179. !(is(newTarget, 'bpmn:Activity') ||
  180. is(newTarget, 'bpmn:EndEvent') ||
  181. is(newTarget, 'bpmn:Gateway') ||
  182. is(newTarget, 'bpmn:IntermediateThrowEvent'))) {
  183. context.default = businessObject.sourceRef.default;
  184. businessObject.sourceRef.default = undefined;
  185. }
  186. // on reconnectStart -> conditional flow
  187. if (oldSource && (businessObject.conditionExpression) &&
  188. !(is(newSource, 'bpmn:Activity') ||
  189. is(newSource, 'bpmn:Gateway'))) {
  190. context.conditionExpression = businessObject.conditionExpression;
  191. businessObject.conditionExpression = undefined;
  192. }
  193. // on reconnectEnd -> conditional flow
  194. if (oldTarget && (businessObject.conditionExpression) &&
  195. !(is(newTarget, 'bpmn:Activity') ||
  196. is(newTarget, 'bpmn:EndEvent') ||
  197. is(newTarget, 'bpmn:Gateway') ||
  198. is(newTarget, 'bpmn:IntermediateThrowEvent'))) {
  199. context.conditionExpression = businessObject.conditionExpression;
  200. businessObject.conditionExpression = undefined;
  201. }
  202. }));
  203. this.reverted([
  204. 'connection.reconnectEnd',
  205. 'connection.reconnectStart'
  206. ], ifBpmn(function(e) {
  207. var context = e.context,
  208. connection = context.connection,
  209. businessObject = getBusinessObject(connection),
  210. newSource = getBusinessObject(connection.source);
  211. // default flow
  212. if (context.default) {
  213. if (is(newSource, 'bpmn:ExclusiveGateway') || is(newSource, 'bpmn:InclusiveGateway') ||
  214. is(newSource, 'bpmn:Activity')) {
  215. newSource.default = context.default;
  216. }
  217. }
  218. // conditional flow
  219. if (context.conditionExpression && is(newSource, 'bpmn:Activity')) {
  220. businessObject.conditionExpression = context.conditionExpression;
  221. }
  222. }));
  223. // update attachments
  224. function updateAttachment(e) {
  225. self.updateAttachment(e.context);
  226. }
  227. this.executed([ 'element.updateAttachment' ], ifBpmn(updateAttachment));
  228. this.reverted([ 'element.updateAttachment' ], ifBpmn(updateAttachment));
  229. }
  230. inherits(BpmnUpdater, CommandInterceptor);
  231. BpmnUpdater.$inject = [
  232. 'eventBus',
  233. 'bpmnFactory',
  234. 'connectionDocking',
  235. 'translate'
  236. ];
  237. // implementation //////////////////////
  238. BpmnUpdater.prototype.updateAttachment = function(context) {
  239. var shape = context.shape,
  240. businessObject = shape.businessObject,
  241. host = shape.host;
  242. businessObject.attachedToRef = host && host.businessObject;
  243. };
  244. BpmnUpdater.prototype.updateParent = function(element, oldParent) {
  245. // do not update BPMN 2.0 label parent
  246. if (element instanceof Label) {
  247. return;
  248. }
  249. // data stores in collaborations are handled seperately by DataStoreBehavior
  250. if (is(element, 'bpmn:DataStoreReference') &&
  251. element.parent &&
  252. is(element.parent, 'bpmn:Collaboration')) {
  253. return;
  254. }
  255. var parentShape = element.parent;
  256. var businessObject = element.businessObject,
  257. parentBusinessObject = parentShape && parentShape.businessObject,
  258. parentDi = parentBusinessObject && parentBusinessObject.di;
  259. if (is(element, 'bpmn:FlowNode')) {
  260. this.updateFlowNodeRefs(businessObject, parentBusinessObject, oldParent && oldParent.businessObject);
  261. }
  262. if (is(element, 'bpmn:DataOutputAssociation')) {
  263. if (element.source) {
  264. parentBusinessObject = element.source.businessObject;
  265. } else {
  266. parentBusinessObject = null;
  267. }
  268. }
  269. if (is(element, 'bpmn:DataInputAssociation')) {
  270. if (element.target) {
  271. parentBusinessObject = element.target.businessObject;
  272. } else {
  273. parentBusinessObject = null;
  274. }
  275. }
  276. this.updateSemanticParent(businessObject, parentBusinessObject);
  277. if (is(element, 'bpmn:DataObjectReference') && businessObject.dataObjectRef) {
  278. this.updateSemanticParent(businessObject.dataObjectRef, parentBusinessObject);
  279. }
  280. this.updateDiParent(businessObject.di, parentDi);
  281. };
  282. BpmnUpdater.prototype.updateBounds = function(shape) {
  283. var di = shape.businessObject.di;
  284. var target = (shape instanceof Label) ? this._getLabel(di) : di;
  285. var bounds = target.bounds;
  286. if (!bounds) {
  287. bounds = this._bpmnFactory.createDiBounds();
  288. target.set('bounds', bounds);
  289. }
  290. assign(bounds, {
  291. x: shape.x,
  292. y: shape.y,
  293. width: shape.width,
  294. height: shape.height
  295. });
  296. };
  297. BpmnUpdater.prototype.updateFlowNodeRefs = function(businessObject, newContainment, oldContainment) {
  298. if (oldContainment === newContainment) {
  299. return;
  300. }
  301. var oldRefs, newRefs;
  302. if (is (oldContainment, 'bpmn:Lane')) {
  303. oldRefs = oldContainment.get('flowNodeRef');
  304. collectionRemove(oldRefs, businessObject);
  305. }
  306. if (is(newContainment, 'bpmn:Lane')) {
  307. newRefs = newContainment.get('flowNodeRef');
  308. collectionAdd(newRefs, businessObject);
  309. }
  310. };
  311. // update existing sourceElement and targetElement di information
  312. BpmnUpdater.prototype.updateDiConnection = function(di, newSource, newTarget) {
  313. if (di.sourceElement && di.sourceElement.bpmnElement !== newSource) {
  314. di.sourceElement = newSource && newSource.di;
  315. }
  316. if (di.targetElement && di.targetElement.bpmnElement !== newTarget) {
  317. di.targetElement = newTarget && newTarget.di;
  318. }
  319. };
  320. BpmnUpdater.prototype.updateDiParent = function(di, parentDi) {
  321. if (parentDi && !is(parentDi, 'bpmndi:BPMNPlane')) {
  322. parentDi = parentDi.$parent;
  323. }
  324. if (di.$parent === parentDi) {
  325. return;
  326. }
  327. var planeElements = (parentDi || di.$parent).get('planeElement');
  328. if (parentDi) {
  329. planeElements.push(di);
  330. di.$parent = parentDi;
  331. } else {
  332. collectionRemove(planeElements, di);
  333. di.$parent = null;
  334. }
  335. };
  336. function getDefinitions(element) {
  337. while (element && !is(element, 'bpmn:Definitions')) {
  338. element = element.$parent;
  339. }
  340. return element;
  341. }
  342. BpmnUpdater.prototype.getLaneSet = function(container) {
  343. var laneSet, laneSets;
  344. // bpmn:Lane
  345. if (is(container, 'bpmn:Lane')) {
  346. laneSet = container.childLaneSet;
  347. if (!laneSet) {
  348. laneSet = this._bpmnFactory.create('bpmn:LaneSet');
  349. container.childLaneSet = laneSet;
  350. laneSet.$parent = container;
  351. }
  352. return laneSet;
  353. }
  354. // bpmn:Participant
  355. if (is(container, 'bpmn:Participant')) {
  356. container = container.processRef;
  357. }
  358. // bpmn:FlowElementsContainer
  359. laneSets = container.get('laneSets');
  360. laneSet = laneSets[0];
  361. if (!laneSet) {
  362. laneSet = this._bpmnFactory.create('bpmn:LaneSet');
  363. laneSet.$parent = container;
  364. laneSets.push(laneSet);
  365. }
  366. return laneSet;
  367. };
  368. BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, visualParent) {
  369. var containment,
  370. translate = this._translate;
  371. if (businessObject.$parent === newParent) {
  372. return;
  373. }
  374. if (is(businessObject, 'bpmn:DataInput') || is(businessObject, 'bpmn:DataOutput')) {
  375. if (is(newParent, 'bpmn:Participant') && 'processRef' in newParent) {
  376. newParent = newParent.processRef;
  377. }
  378. // already in correct ioSpecification
  379. if ('ioSpecification' in newParent && newParent.ioSpecification === businessObject.$parent) {
  380. return;
  381. }
  382. }
  383. if (is(businessObject, 'bpmn:Lane')) {
  384. if (newParent) {
  385. newParent = this.getLaneSet(newParent);
  386. }
  387. containment = 'lanes';
  388. } else
  389. if (is(businessObject, 'bpmn:FlowElement')) {
  390. if (newParent) {
  391. if (is(newParent, 'bpmn:Participant')) {
  392. newParent = newParent.processRef;
  393. } else
  394. if (is(newParent, 'bpmn:Lane')) {
  395. do {
  396. // unwrap Lane -> LaneSet -> (Lane | FlowElementsContainer)
  397. newParent = newParent.$parent.$parent;
  398. } while (is(newParent, 'bpmn:Lane'));
  399. }
  400. }
  401. containment = 'flowElements';
  402. } else
  403. if (is(businessObject, 'bpmn:Artifact')) {
  404. while (newParent &&
  405. !is(newParent, 'bpmn:Process') &&
  406. !is(newParent, 'bpmn:SubProcess') &&
  407. !is(newParent, 'bpmn:Collaboration')) {
  408. if (is(newParent, 'bpmn:Participant')) {
  409. newParent = newParent.processRef;
  410. break;
  411. } else {
  412. newParent = newParent.$parent;
  413. }
  414. }
  415. containment = 'artifacts';
  416. } else
  417. if (is(businessObject, 'bpmn:MessageFlow')) {
  418. containment = 'messageFlows';
  419. } else
  420. if (is(businessObject, 'bpmn:Participant')) {
  421. containment = 'participants';
  422. // make sure the participants process is properly attached / detached
  423. // from the XML document
  424. var process = businessObject.processRef,
  425. definitions;
  426. if (process) {
  427. definitions = getDefinitions(businessObject.$parent || newParent);
  428. if (businessObject.$parent) {
  429. collectionRemove(definitions.get('rootElements'), process);
  430. process.$parent = null;
  431. }
  432. if (newParent) {
  433. collectionAdd(definitions.get('rootElements'), process);
  434. process.$parent = definitions;
  435. }
  436. }
  437. } else
  438. if (is(businessObject, 'bpmn:DataOutputAssociation')) {
  439. containment = 'dataOutputAssociations';
  440. } else
  441. if (is(businessObject, 'bpmn:DataInputAssociation')) {
  442. containment = 'dataInputAssociations';
  443. }
  444. if (!containment) {
  445. throw new Error(translate(
  446. 'no parent for {element} in {parent}',
  447. {
  448. element: businessObject.id,
  449. parent: newParent.id
  450. }
  451. ));
  452. }
  453. var children;
  454. if (businessObject.$parent) {
  455. // remove from old parent
  456. children = businessObject.$parent.get(containment);
  457. collectionRemove(children, businessObject);
  458. }
  459. if (!newParent) {
  460. businessObject.$parent = null;
  461. } else {
  462. // add to new parent
  463. children = newParent.get(containment);
  464. children.push(businessObject);
  465. businessObject.$parent = newParent;
  466. }
  467. if (visualParent) {
  468. var diChildren = visualParent.get(containment);
  469. collectionRemove(children, businessObject);
  470. if (newParent) {
  471. if (!diChildren) {
  472. diChildren = [];
  473. newParent.set(containment, diChildren);
  474. }
  475. diChildren.push(businessObject);
  476. }
  477. }
  478. };
  479. BpmnUpdater.prototype.updateConnectionWaypoints = function(connection) {
  480. connection.businessObject.di.set('waypoint', this._bpmnFactory.createDiWaypoints(connection.waypoints));
  481. };
  482. BpmnUpdater.prototype.updateConnection = function(context) {
  483. var connection = context.connection,
  484. businessObject = getBusinessObject(connection),
  485. newSource = getBusinessObject(connection.source),
  486. newTarget = getBusinessObject(connection.target),
  487. visualParent;
  488. if (!is(businessObject, 'bpmn:DataAssociation')) {
  489. var inverseSet = is(businessObject, 'bpmn:SequenceFlow');
  490. if (businessObject.sourceRef !== newSource) {
  491. if (inverseSet) {
  492. collectionRemove(businessObject.sourceRef && businessObject.sourceRef.get('outgoing'), businessObject);
  493. if (newSource && newSource.get('outgoing')) {
  494. newSource.get('outgoing').push(businessObject);
  495. }
  496. }
  497. businessObject.sourceRef = newSource;
  498. }
  499. if (businessObject.targetRef !== newTarget) {
  500. if (inverseSet) {
  501. collectionRemove(businessObject.targetRef && businessObject.targetRef.get('incoming'), businessObject);
  502. if (newTarget && newTarget.get('incoming')) {
  503. newTarget.get('incoming').push(businessObject);
  504. }
  505. }
  506. businessObject.targetRef = newTarget;
  507. }
  508. } else
  509. if (is(businessObject, 'bpmn:DataInputAssociation')) {
  510. // handle obnoxious isMsome sourceRef
  511. businessObject.get('sourceRef')[0] = newSource;
  512. visualParent = context.parent || context.newParent || newTarget;
  513. this.updateSemanticParent(businessObject, newTarget, parent.businessObject);
  514. } else
  515. if (is(businessObject, 'bpmn:DataOutputAssociation')) {
  516. visualParent = context.parent || context.newParent || newSource;
  517. this.updateSemanticParent(businessObject, newSource, visualParent);
  518. // targetRef = new target
  519. businessObject.targetRef = newTarget;
  520. }
  521. this.updateConnectionWaypoints(connection);
  522. this.updateDiConnection(businessObject.di, newSource, newTarget);
  523. };
  524. // helpers //////////////////////
  525. BpmnUpdater.prototype._getLabel = function(di) {
  526. if (!di.label) {
  527. di.label = this._bpmnFactory.createDiLabel();
  528. }
  529. return di.label;
  530. };
  531. /**
  532. * Make sure the event listener is only called
  533. * if the touched element is a BPMN element.
  534. *
  535. * @param {Function} fn
  536. * @return {Function} guarded function
  537. */
  538. function ifBpmn(fn) {
  539. return function(event) {
  540. var context = event.context,
  541. element = context.shape || context.connection;
  542. if (is(element, 'bpmn:BaseElement')) {
  543. fn(event);
  544. }
  545. };
  546. }