BpmnUpdater.js 17 KB

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