BaseViewer.js 15 KB

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