Viewer.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. /**
  2. * The code in the <project-logo></project-logo> area
  3. * must not be changed.
  4. *
  5. * @see http://bpmn.io/license for more information.
  6. */
  7. import {
  8. assign,
  9. find,
  10. isFunction,
  11. isNumber,
  12. omit
  13. } from 'min-dash';
  14. import {
  15. domify,
  16. query as domQuery,
  17. remove as domRemove
  18. } from 'min-dom';
  19. import {
  20. innerSVG
  21. } from 'tiny-svg';
  22. import Diagram from 'diagram-js';
  23. import BpmnModdle from 'bpmn-moddle';
  24. import inherits from 'inherits';
  25. import {
  26. importBpmnDiagram
  27. } from './import/Importer';
  28. import CoreModule from './core';
  29. import TranslateModule from 'diagram-js/lib/i18n/translate';
  30. import SelectionModule from 'diagram-js/lib/features/selection';
  31. import OverlaysModule from 'diagram-js/lib/features/overlays';
  32. function checkValidationError(err) {
  33. // check if we can help the user by indicating wrong BPMN 2.0 xml
  34. // (in case he or the exporting tool did not get that right)
  35. var pattern = /unparsable content <([^>]+)> detected([\s\S]*)$/;
  36. var match = pattern.exec(err.message);
  37. if (match) {
  38. err.message =
  39. 'unparsable content <' + match[1] + '> detected; ' +
  40. 'this may indicate an invalid BPMN 2.0 diagram file' + match[2];
  41. }
  42. return err;
  43. }
  44. var DEFAULT_OPTIONS = {
  45. width: '100%',
  46. height: '100%',
  47. position: 'relative'
  48. };
  49. /**
  50. * Ensure the passed argument is a proper unit (defaulting to px)
  51. */
  52. function ensureUnit(val) {
  53. return val + (isNumber(val) ? 'px' : '');
  54. }
  55. /**
  56. * Find BPMNDiagram in definitions by ID
  57. *
  58. * @param {ModdleElement<Definitions>} definitions
  59. * @param {String} diagramId
  60. *
  61. * @return {ModdleElement<BPMNDiagram>|null}
  62. */
  63. function findBPMNDiagram(definitions, diagramId) {
  64. if (!diagramId) {
  65. return null;
  66. }
  67. return find(definitions.diagrams, function(element) {
  68. return element.id === diagramId;
  69. }) || null;
  70. }
  71. /**
  72. * A viewer for BPMN 2.0 diagrams.
  73. *
  74. * Have a look at {@link NavigatedViewer} or {@link Modeler} for bundles that include
  75. * additional features.
  76. *
  77. *
  78. * ## Extending the Viewer
  79. *
  80. * In order to extend the viewer pass extension modules to bootstrap via the
  81. * `additionalModules` option. An extension module is an object that exposes
  82. * named services.
  83. *
  84. * The following example depicts the integration of a simple
  85. * logging component that integrates with interaction events:
  86. *
  87. *
  88. * ```javascript
  89. *
  90. * // logging component
  91. * function InteractionLogger(eventBus) {
  92. * eventBus.on('element.hover', function(event) {
  93. * console.log()
  94. * })
  95. * }
  96. *
  97. * InteractionLogger.$inject = [ 'eventBus' ]; // minification save
  98. *
  99. * // extension module
  100. * var extensionModule = {
  101. * __init__: [ 'interactionLogger' ],
  102. * interactionLogger: [ 'type', InteractionLogger ]
  103. * };
  104. *
  105. * // extend the viewer
  106. * var bpmnViewer = new Viewer({ additionalModules: [ extensionModule ] });
  107. * bpmnViewer.importXML(...);
  108. * ```
  109. *
  110. * @param {Object} [options] configuration options to pass to the viewer
  111. * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body.
  112. * @param {String|Number} [options.width] the width of the viewer
  113. * @param {String|Number} [options.height] the height of the viewer
  114. * @param {Object} [options.moddleExtensions] extension packages to provide
  115. * @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules
  116. * @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules
  117. */
  118. export default function Viewer(options) {
  119. options = assign({}, DEFAULT_OPTIONS, options);
  120. this._moddle = this._createModdle(options);
  121. this._container = this._createContainer(options);
  122. /* <project-logo> */
  123. addProjectLogo(this._container);
  124. /* </project-logo> */
  125. this._init(this._container, this._moddle, options);
  126. }
  127. inherits(Viewer, Diagram);
  128. /**
  129. * Parse and render a BPMN 2.0 diagram.
  130. *
  131. * Once finished the viewer reports back the result to the
  132. * provided callback function with (err, warnings).
  133. *
  134. * ## Life-Cycle Events
  135. *
  136. * During import the viewer will fire life-cycle events:
  137. *
  138. * * import.parse.start (about to read model from xml)
  139. * * import.parse.complete (model read; may have worked or not)
  140. * * import.render.start (graphical import start)
  141. * * import.render.complete (graphical import finished)
  142. * * import.done (everything done)
  143. *
  144. * You can use these events to hook into the life-cycle.
  145. *
  146. * @param {String} xml the BPMN 2.0 xml
  147. * @param {ModdleElement<BPMNDiagram>|String} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered)
  148. * @param {Function} [done] invoked with (err, warnings=[])
  149. */
  150. Viewer.prototype.importXML = function(xml, bpmnDiagram, done) {
  151. if (isFunction(bpmnDiagram)) {
  152. done = bpmnDiagram;
  153. bpmnDiagram = null;
  154. }
  155. // done is optional
  156. done = done || function() {};
  157. var self = this;
  158. // hook in pre-parse listeners +
  159. // allow xml manipulation
  160. xml = this._emit('import.parse.start', { xml: xml }) || xml;
  161. this._moddle.fromXML(xml, 'bpmn:Definitions', function(err, definitions, context) {
  162. // hook in post parse listeners +
  163. // allow definitions manipulation
  164. definitions = self._emit('import.parse.complete', {
  165. error: err,
  166. definitions: definitions,
  167. context: context
  168. }) || definitions;
  169. var parseWarnings = context.warnings;
  170. if (err) {
  171. err = checkValidationError(err);
  172. self._emit('import.done', { error: err, warnings: parseWarnings });
  173. return done(err, parseWarnings);
  174. }
  175. self.importDefinitions(definitions, bpmnDiagram, function(err, importWarnings) {
  176. var allWarnings = [].concat(parseWarnings, importWarnings || []);
  177. self._emit('import.done', { error: err, warnings: allWarnings });
  178. done(err, allWarnings);
  179. });
  180. });
  181. };
  182. /**
  183. * Import parsed definitions and render a BPMN 2.0 diagram.
  184. *
  185. * Once finished the viewer reports back the result to the
  186. * provided callback function with (err, warnings).
  187. *
  188. * ## Life-Cycle Events
  189. *
  190. * During import the viewer will fire life-cycle events:
  191. *
  192. * * import.render.start (graphical import start)
  193. * * import.render.complete (graphical import finished)
  194. *
  195. * You can use these events to hook into the life-cycle.
  196. *
  197. * @param {ModdleElement<Definitions>} definitions parsed BPMN 2.0 definitions
  198. * @param {ModdleElement<BPMNDiagram>|String} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered)
  199. * @param {Function} [done] invoked with (err, warnings=[])
  200. */
  201. Viewer.prototype.importDefinitions = function(definitions, bpmnDiagram, done) {
  202. if (isFunction(bpmnDiagram)) {
  203. done = bpmnDiagram;
  204. bpmnDiagram = null;
  205. }
  206. // done is optional
  207. done = done || function() {};
  208. this._setDefinitions(definitions);
  209. return this.open(bpmnDiagram, done);
  210. };
  211. /**
  212. * Open diagram of previously imported XML.
  213. *
  214. * Once finished the viewer reports back the result to the
  215. * provided callback function with (err, warnings).
  216. *
  217. * ## Life-Cycle Events
  218. *
  219. * During switch the viewer will fire life-cycle events:
  220. *
  221. * * import.render.start (graphical import start)
  222. * * import.render.complete (graphical import finished)
  223. *
  224. * You can use these events to hook into the life-cycle.
  225. *
  226. * @param {String|ModdleElement<BPMNDiagram>} [bpmnDiagramOrId] id or the diagram to open
  227. * @param {Function} [done] invoked with (err, warnings=[])
  228. */
  229. Viewer.prototype.open = function(bpmnDiagramOrId, done) {
  230. if (isFunction(bpmnDiagramOrId)) {
  231. done = bpmnDiagramOrId;
  232. bpmnDiagramOrId = null;
  233. }
  234. var definitions = this._definitions;
  235. var bpmnDiagram = bpmnDiagramOrId;
  236. // done is optional
  237. done = done || function() {};
  238. if (!definitions) {
  239. return done(new Error('no XML imported'));
  240. }
  241. if (typeof bpmnDiagramOrId === 'string') {
  242. bpmnDiagram = findBPMNDiagram(definitions, bpmnDiagramOrId);
  243. if (!bpmnDiagram) {
  244. return done(new Error('BPMNDiagram <' + bpmnDiagramOrId + '> not found'));
  245. }
  246. }
  247. // clear existing rendered diagram
  248. // catch synchronous exceptions during #clear()
  249. try {
  250. this.clear();
  251. } catch (error) {
  252. return done(error);
  253. }
  254. // perform graphical import
  255. return importBpmnDiagram(this, definitions, bpmnDiagram, done);
  256. };
  257. /**
  258. * Export the currently displayed BPMN 2.0 diagram as
  259. * a BPMN 2.0 XML document.
  260. *
  261. * ## Life-Cycle Events
  262. *
  263. * During XML saving the viewer will fire life-cycle events:
  264. *
  265. * * saveXML.start (before serialization)
  266. * * saveXML.serialized (after xml generation)
  267. * * saveXML.done (everything done)
  268. *
  269. * You can use these events to hook into the life-cycle.
  270. *
  271. * @param {Object} [options] export options
  272. * @param {Boolean} [options.format=false] output formated XML
  273. * @param {Boolean} [options.preamble=true] output preamble
  274. *
  275. * @param {Function} done invoked with (err, xml)
  276. */
  277. Viewer.prototype.saveXML = function(options, done) {
  278. if (!done) {
  279. done = options;
  280. options = {};
  281. }
  282. var self = this;
  283. var definitions = this._definitions;
  284. if (!definitions) {
  285. return done(new Error('no definitions loaded'));
  286. }
  287. // allow to fiddle around with definitions
  288. definitions = this._emit('saveXML.start', {
  289. definitions: definitions
  290. }) || definitions;
  291. this._moddle.toXML(definitions, options, function(err, xml) {
  292. try {
  293. xml = self._emit('saveXML.serialized', {
  294. error: err,
  295. xml: xml
  296. }) || xml;
  297. self._emit('saveXML.done', {
  298. error: err,
  299. xml: xml
  300. });
  301. } catch (e) {
  302. console.error('error in saveXML life-cycle listener', e);
  303. }
  304. done(err, xml);
  305. });
  306. };
  307. /**
  308. * Export the currently displayed BPMN 2.0 diagram as
  309. * an SVG image.
  310. *
  311. * ## Life-Cycle Events
  312. *
  313. * During SVG saving the viewer will fire life-cycle events:
  314. *
  315. * * saveSVG.start (before serialization)
  316. * * saveSVG.done (everything done)
  317. *
  318. * You can use these events to hook into the life-cycle.
  319. *
  320. * @param {Object} [options]
  321. * @param {Function} done invoked with (err, svgStr)
  322. */
  323. Viewer.prototype.saveSVG = function(options, done) {
  324. if (!done) {
  325. done = options;
  326. options = {};
  327. }
  328. this._emit('saveSVG.start');
  329. var svg, err;
  330. try {
  331. var canvas = this.get('canvas');
  332. var contentNode = canvas.getDefaultLayer(),
  333. defsNode = domQuery('defs', canvas._svg);
  334. var contents = innerSVG(contentNode),
  335. defs = defsNode ? '<defs>' + innerSVG(defsNode) + '</defs>' : '';
  336. var bbox = contentNode.getBBox();
  337. svg =
  338. '<?xml version="1.0" encoding="utf-8"?>\n' +
  339. '<!-- created with bpmn-js / http://bpmn.io -->\n' +
  340. '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
  341. '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
  342. 'width="' + bbox.width + '" height="' + bbox.height + '" ' +
  343. 'viewBox="' + bbox.x + ' ' + bbox.y + ' ' + bbox.width + ' ' + bbox.height + '" version="1.1">' +
  344. defs + contents +
  345. '</svg>';
  346. } catch (e) {
  347. err = e;
  348. }
  349. this._emit('saveSVG.done', {
  350. error: err,
  351. svg: svg
  352. });
  353. done(err, svg);
  354. };
  355. /**
  356. * Get a named diagram service.
  357. *
  358. * @example
  359. *
  360. * var elementRegistry = viewer.get('elementRegistry');
  361. * var startEventShape = elementRegistry.get('StartEvent_1');
  362. *
  363. * @param {String} name
  364. *
  365. * @return {Object} diagram service instance
  366. *
  367. * @method Viewer#get
  368. */
  369. /**
  370. * Invoke a function in the context of this viewer.
  371. *
  372. * @example
  373. *
  374. * viewer.invoke(function(elementRegistry) {
  375. * var startEventShape = elementRegistry.get('StartEvent_1');
  376. * });
  377. *
  378. * @param {Function} fn to be invoked
  379. *
  380. * @return {Object} the functions return value
  381. *
  382. * @method Viewer#invoke
  383. */
  384. Viewer.prototype._setDefinitions = function(definitions) {
  385. this._definitions = definitions;
  386. };
  387. Viewer.prototype.getModules = function() {
  388. return this._modules;
  389. };
  390. /**
  391. * Remove all drawn elements from the viewer.
  392. *
  393. * After calling this method the viewer can still
  394. * be reused for opening another diagram.
  395. *
  396. * @method Viewer#clear
  397. */
  398. Viewer.prototype.clear = function() {
  399. // remove businessObject#di binding
  400. //
  401. // this is necessary, as we establish the bindings
  402. // in the BpmnTreeWalker (and assume none are given
  403. // on reimport)
  404. this.get('elementRegistry').forEach(function(element) {
  405. var bo = element.businessObject;
  406. if (bo && bo.di) {
  407. delete bo.di;
  408. }
  409. });
  410. // remove drawn elements
  411. Diagram.prototype.clear.call(this);
  412. };
  413. /**
  414. * Destroy the viewer instance and remove all its
  415. * remainders from the document tree.
  416. */
  417. Viewer.prototype.destroy = function() {
  418. // diagram destroy
  419. Diagram.prototype.destroy.call(this);
  420. // dom detach
  421. domRemove(this._container);
  422. };
  423. /**
  424. * Register an event listener
  425. *
  426. * Remove a previously added listener via {@link #off(event, callback)}.
  427. *
  428. * @param {String} event
  429. * @param {Number} [priority]
  430. * @param {Function} callback
  431. * @param {Object} [that]
  432. */
  433. Viewer.prototype.on = function(event, priority, callback, target) {
  434. return this.get('eventBus').on(event, priority, callback, target);
  435. };
  436. /**
  437. * De-register an event listener
  438. *
  439. * @param {String} event
  440. * @param {Function} callback
  441. */
  442. Viewer.prototype.off = function(event, callback) {
  443. this.get('eventBus').off(event, callback);
  444. };
  445. Viewer.prototype.attachTo = function(parentNode) {
  446. if (!parentNode) {
  447. throw new Error('parentNode required');
  448. }
  449. // ensure we detach from the
  450. // previous, old parent
  451. this.detach();
  452. // unwrap jQuery if provided
  453. if (parentNode.get && parentNode.constructor.prototype.jquery) {
  454. parentNode = parentNode.get(0);
  455. }
  456. if (typeof parentNode === 'string') {
  457. parentNode = domQuery(parentNode);
  458. }
  459. parentNode.appendChild(this._container);
  460. this._emit('attach', {});
  461. this.get('canvas').resized();
  462. };
  463. Viewer.prototype.getDefinitions = function() {
  464. return this._definitions;
  465. };
  466. Viewer.prototype.detach = function() {
  467. var container = this._container,
  468. parentNode = container.parentNode;
  469. if (!parentNode) {
  470. return;
  471. }
  472. this._emit('detach', {});
  473. parentNode.removeChild(container);
  474. };
  475. Viewer.prototype._init = function(container, moddle, options) {
  476. var baseModules = options.modules || this.getModules(),
  477. additionalModules = options.additionalModules || [],
  478. staticModules = [
  479. {
  480. bpmnjs: [ 'value', this ],
  481. moddle: [ 'value', moddle ]
  482. }
  483. ];
  484. var diagramModules = [].concat(staticModules, baseModules, additionalModules);
  485. var diagramOptions = assign(omit(options, [ 'additionalModules' ]), {
  486. canvas: assign({}, options.canvas, { container: container }),
  487. modules: diagramModules
  488. });
  489. // invoke diagram constructor
  490. Diagram.call(this, diagramOptions);
  491. if (options && options.container) {
  492. this.attachTo(options.container);
  493. }
  494. };
  495. /**
  496. * Emit an event on the underlying {@link EventBus}
  497. *
  498. * @param {String} type
  499. * @param {Object} event
  500. *
  501. * @return {Object} event processing result (if any)
  502. */
  503. Viewer.prototype._emit = function(type, event) {
  504. return this.get('eventBus').fire(type, event);
  505. };
  506. Viewer.prototype._createContainer = function(options) {
  507. var container = domify('<div class="bjs-container"></div>');
  508. assign(container.style, {
  509. width: ensureUnit(options.width),
  510. height: ensureUnit(options.height),
  511. position: options.position
  512. });
  513. return container;
  514. };
  515. Viewer.prototype._createModdle = function(options) {
  516. var moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions);
  517. return new BpmnModdle(moddleOptions);
  518. };
  519. // modules the viewer is composed of
  520. Viewer.prototype._modules = [
  521. CoreModule,
  522. TranslateModule,
  523. SelectionModule,
  524. OverlaysModule
  525. ];
  526. // default moddle extensions the viewer is composed of
  527. Viewer.prototype._moddleExtensions = {};
  528. /* <project-logo> */
  529. import {
  530. open as openPoweredBy,
  531. BPMNIO_IMG
  532. } from './util/PoweredByUtil';
  533. import {
  534. event as domEvent
  535. } from 'min-dom';
  536. /**
  537. * Adds the project logo to the diagram container as
  538. * required by the bpmn.io license.
  539. *
  540. * @see http://bpmn.io/license
  541. *
  542. * @param {Element} container
  543. */
  544. function addProjectLogo(container) {
  545. var img = BPMNIO_IMG;
  546. var linkMarkup =
  547. '<a href="http://bpmn.io" ' +
  548. 'target="_blank" ' +
  549. 'class="bjs-powered-by" ' +
  550. 'title="Powered by bpmn.io" ' +
  551. 'style="position: absolute; bottom: 15px; right: 15px; z-index: 100">' +
  552. img +
  553. '</a>';
  554. var linkElement = domify(linkMarkup);
  555. container.appendChild(linkElement);
  556. domEvent.bind(linkElement, 'click', function(event) {
  557. openPoweredBy();
  558. event.preventDefault();
  559. });
  560. }
  561. /* </project-logo> */