BaseViewer.js 18 KB

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