UNPKG

19.2 kBJavaScriptView Raw
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 */
7import {
8 assign,
9 find,
10 isNumber,
11 omit
12} from 'min-dash';
13
14import {
15 domify,
16 assignStyle,
17 query as domQuery,
18 remove as domRemove
19} from 'min-dom';
20
21import {
22 innerSVG
23} from 'tiny-svg';
24
25import Diagram from 'diagram-js';
26import BpmnModdle from 'bpmn-moddle';
27
28import inherits from 'inherits-browser';
29
30import {
31 importBpmnDiagram
32} from './import/Importer';
33
34/**
35 * @template T
36 *
37 * @typedef {import('diagram-js/lib/core/EventBus').EventBusEventCallback<T>} EventBusEventCallback
38 */
39
40/**
41 * @typedef {import('didi').ModuleDeclaration} ModuleDeclaration
42 *
43 * @typedef {import('./model/Types').Moddle} Moddle
44 * @typedef {import('./model/Types').ModdleElement} ModdleElement
45 * @typedef {import('./model/Types').ModdleExtension} ModdleExtension
46 *
47 * @typedef { {
48 * width?: number|string;
49 * height?: number|string;
50 * position?: string;
51 * container?: string|HTMLElement;
52 * moddleExtensions?: ModdleExtensions;
53 * additionalModules?: ModuleDeclaration[];
54 * } & Record<string, any> } BaseViewerOptions
55 *
56 * @typedef {Record<string, ModdleElement>} ModdleElementsById
57 *
58 * @typedef { {
59 * [key: string]: ModdleExtension;
60 * } } ModdleExtensions
61 *
62 * @typedef { {
63 * warnings: string[];
64 * } } ImportXMLResult
65 *
66 * @typedef {ImportXMLResult & Error} ImportXMLError
67 *
68 * @typedef {ImportXMLResult} ImportDefinitionsResult
69 *
70 * @typedef {ImportXMLError} ImportDefinitionsError
71 *
72 * @typedef {ImportXMLResult} OpenResult
73 *
74 * @typedef {ImportXMLError} OpenError
75 *
76 * @typedef { {
77 * format?: boolean;
78 * preamble?: boolean;
79 * } } SaveXMLOptions
80 *
81 * @typedef { {
82 * xml?: string;
83 * error?: Error;
84 * } } SaveXMLResult
85 *
86 * @typedef { {
87 * svg: string;
88 * } } SaveSVGResult
89 *
90 * @typedef { {
91 * xml: string;
92 * } } ImportParseStartEvent
93 *
94 * @typedef { {
95 * error?: ImportXMLError;
96 * definitions?: ModdleElement;
97 * elementsById?: ModdleElementsById;
98 * references?: ModdleElement[];
99 * warnings: string[];
100 * } } ImportParseCompleteEvent
101 *
102 * @typedef { {
103 * error?: ImportXMLError;
104 * warnings: string[];
105 * } } ImportDoneEvent
106 *
107 * @typedef { {
108 * definitions: ModdleElement;
109 * } } SaveXMLStartEvent
110 *
111 * @typedef {SaveXMLResult} SaveXMLDoneEvent
112 *
113 * @typedef { {
114 * error?: Error;
115 * svg: string;
116 * } } SaveSVGDoneEvent
117 */
118
119/**
120 * A base viewer for BPMN 2.0 diagrams.
121 *
122 * Have a look at {@link Viewer}, {@link NavigatedViewer} or {@link Modeler} for
123 * bundles that include actual features.
124 *
125 * @param {BaseViewerOptions} [options] The options to configure the viewer.
126 */
127export default function BaseViewer(options) {
128
129 /**
130 * @type {BaseViewerOptions}
131 */
132 options = assign({}, DEFAULT_OPTIONS, options);
133
134 /**
135 * @type {Moddle}
136 */
137 this._moddle = this._createModdle(options);
138
139 /**
140 * @type {HTMLElement}
141 */
142 this._container = this._createContainer(options);
143
144 /* <project-logo> */
145
146 addProjectLogo(this._container);
147
148 /* </project-logo> */
149
150 this._init(this._container, this._moddle, options);
151}
152
153inherits(BaseViewer, Diagram);
154
155/**
156 * Parse and render a BPMN 2.0 diagram.
157 *
158 * Once finished the viewer reports back the result to the
159 * provided callback function with (err, warnings).
160 *
161 * ## Life-Cycle Events
162 *
163 * During import the viewer will fire life-cycle events:
164 *
165 * * import.parse.start (about to read model from XML)
166 * * import.parse.complete (model read; may have worked or not)
167 * * import.render.start (graphical import start)
168 * * import.render.complete (graphical import finished)
169 * * import.done (everything done)
170 *
171 * You can use these events to hook into the life-cycle.
172 *
173 * @throws {ImportXMLError} An error thrown during the import of the XML.
174 *
175 * @fires BaseViewer#ImportParseStartEvent
176 * @fires BaseViewer#ImportParseCompleteEvent
177 * @fires Importer#ImportRenderStartEvent
178 * @fires Importer#ImportRenderCompleteEvent
179 * @fires BaseViewer#ImportDoneEvent
180 *
181 * @param {string} xml The BPMN 2.0 XML to be imported.
182 * @param {ModdleElement|string} [bpmnDiagram] The optional diagram or Id of the BPMN diagram to open.
183 *
184 * @return {Promise<ImportXMLResult>} A promise resolving with warnings that were produced during the import.
185 */
186BaseViewer.prototype.importXML = async function importXML(xml, bpmnDiagram) {
187
188 const self = this;
189
190 function ParseCompleteEvent(data) {
191 return self.get('eventBus').createEvent(data);
192 }
193
194 let aggregatedWarnings = [];
195 try {
196
197 // hook in pre-parse listeners +
198 // allow xml manipulation
199
200 /**
201 * A `import.parse.start` event.
202 *
203 * @event BaseViewer#ImportParseStartEvent
204 * @type {ImportParseStartEvent}
205 */
206 xml = this._emit('import.parse.start', { xml: xml }) || xml;
207
208 let parseResult;
209 try {
210 parseResult = await this._moddle.fromXML(xml, 'bpmn:Definitions');
211 } catch (error) {
212 this._emit('import.parse.complete', {
213 error
214 });
215
216 throw error;
217 }
218
219 let definitions = parseResult.rootElement;
220 const references = parseResult.references;
221 const parseWarnings = parseResult.warnings;
222 const elementsById = parseResult.elementsById;
223
224 aggregatedWarnings = aggregatedWarnings.concat(parseWarnings);
225
226 // hook in post parse listeners +
227 // allow definitions manipulation
228
229 /**
230 * A `import.parse.complete` event.
231 *
232 * @event BaseViewer#ImportParseCompleteEvent
233 * @type {ImportParseCompleteEvent}
234 */
235 definitions = this._emit('import.parse.complete', ParseCompleteEvent({
236 error: null,
237 definitions: definitions,
238 elementsById: elementsById,
239 references: references,
240 warnings: aggregatedWarnings
241 })) || definitions;
242
243 const importResult = await this.importDefinitions(definitions, bpmnDiagram);
244
245 aggregatedWarnings = aggregatedWarnings.concat(importResult.warnings);
246
247 /**
248 * A `import.parse.complete` event.
249 *
250 * @event BaseViewer#ImportDoneEvent
251 * @type {ImportDoneEvent}
252 */
253 this._emit('import.done', { error: null, warnings: aggregatedWarnings });
254
255 return { warnings: aggregatedWarnings };
256 } catch (err) {
257 let error = err;
258 aggregatedWarnings = aggregatedWarnings.concat(error.warnings || []);
259 addWarningsToError(error, aggregatedWarnings);
260
261 error = checkValidationError(error);
262
263 this._emit('import.done', { error, warnings: error.warnings });
264
265 throw error;
266 }
267};
268
269
270/**
271 * Import parsed definitions and render a BPMN 2.0 diagram.
272 *
273 * Once finished the viewer reports back the result to the
274 * provided callback function with (err, warnings).
275 *
276 * ## Life-Cycle Events
277 *
278 * During import the viewer will fire life-cycle events:
279 *
280 * * import.render.start (graphical import start)
281 * * import.render.complete (graphical import finished)
282 *
283 * You can use these events to hook into the life-cycle.
284 *
285 * @throws {ImportDefinitionsError} An error thrown during the import of the definitions.
286 *
287 * @param {ModdleElement} definitions The definitions.
288 * @param {ModdleElement|string} [bpmnDiagram] The optional diagram or ID of the BPMN diagram to open.
289 *
290 * @return {Promise<ImportDefinitionsResult>} A promise resolving with warnings that were produced during the import.
291 */
292BaseViewer.prototype.importDefinitions = async function importDefinitions(definitions, bpmnDiagram) {
293 this._setDefinitions(definitions);
294 const result = await this.open(bpmnDiagram);
295
296 return { warnings: result.warnings };
297};
298
299
300/**
301 * Open diagram of previously imported XML.
302 *
303 * Once finished the viewer reports back the result to the
304 * provided callback function with (err, warnings).
305 *
306 * ## Life-Cycle Events
307 *
308 * During switch the viewer will fire life-cycle events:
309 *
310 * * import.render.start (graphical import start)
311 * * import.render.complete (graphical import finished)
312 *
313 * You can use these events to hook into the life-cycle.
314 *
315 * @throws {OpenError} An error thrown during opening.
316 *
317 * @param {ModdleElement|string} bpmnDiagramOrId The diagram or Id of the BPMN diagram to open.
318 *
319 * @return {Promise<OpenResult>} A promise resolving with warnings that were produced during opening.
320 */
321BaseViewer.prototype.open = async function open(bpmnDiagramOrId) {
322
323 const definitions = this._definitions;
324 let bpmnDiagram = bpmnDiagramOrId;
325
326 if (!definitions) {
327 const error = new Error('no XML imported');
328 addWarningsToError(error, []);
329
330 throw error;
331 }
332
333 if (typeof bpmnDiagramOrId === 'string') {
334 bpmnDiagram = findBPMNDiagram(definitions, bpmnDiagramOrId);
335
336 if (!bpmnDiagram) {
337 const error = new Error('BPMNDiagram <' + bpmnDiagramOrId + '> not found');
338 addWarningsToError(error, []);
339
340 throw error;
341 }
342 }
343
344 // clear existing rendered diagram
345 // catch synchronous exceptions during #clear()
346 try {
347 this.clear();
348 } catch (error) {
349 addWarningsToError(error, []);
350
351 throw error;
352 }
353
354 // perform graphical import
355 const { warnings } = await importBpmnDiagram(this, definitions, bpmnDiagram);
356
357 return { warnings };
358};
359
360/**
361 * Export the currently displayed BPMN 2.0 diagram as
362 * a BPMN 2.0 XML document.
363 *
364 * ## Life-Cycle Events
365 *
366 * During XML saving the viewer will fire life-cycle events:
367 *
368 * * saveXML.start (before serialization)
369 * * saveXML.serialized (after xml generation)
370 * * saveXML.done (everything done)
371 *
372 * You can use these events to hook into the life-cycle.
373 *
374 * @throws {Error} An error thrown during export.
375 *
376 * @fires BaseViewer#SaveXMLStart
377 * @fires BaseViewer#SaveXMLDone
378 *
379 * @param {SaveXMLOptions} [options] The options.
380 *
381 * @return {Promise<SaveXMLResult>} A promise resolving with the XML.
382 */
383BaseViewer.prototype.saveXML = async function saveXML(options) {
384
385 options = options || {};
386
387 let definitions = this._definitions,
388 error, xml;
389
390 try {
391 if (!definitions) {
392 throw new Error('no definitions loaded');
393 }
394
395 // allow to fiddle around with definitions
396
397 /**
398 * A `saveXML.start` event.
399 *
400 * @event BaseViewer#SaveXMLStartEvent
401 * @type {SaveXMLStartEvent}
402 */
403 definitions = this._emit('saveXML.start', {
404 definitions
405 }) || definitions;
406
407 const result = await this._moddle.toXML(definitions, options);
408 xml = result.xml;
409
410 xml = this._emit('saveXML.serialized', {
411 xml
412 }) || xml;
413 } catch (err) {
414 error = err;
415 }
416
417 const result = error ? { error } : { xml };
418
419 /**
420 * A `saveXML.done` event.
421 *
422 * @event BaseViewer#SaveXMLDoneEvent
423 * @type {SaveXMLDoneEvent}
424 */
425 this._emit('saveXML.done', result);
426
427 if (error) {
428 throw error;
429 }
430
431 return result;
432};
433
434
435/**
436 * Export the currently displayed BPMN 2.0 diagram as
437 * an SVG image.
438 *
439 * ## Life-Cycle Events
440 *
441 * During SVG saving the viewer will fire life-cycle events:
442 *
443 * * saveSVG.start (before serialization)
444 * * saveSVG.done (everything done)
445 *
446 * You can use these events to hook into the life-cycle.
447 *
448 * @throws {Error} An error thrown during export.
449 *
450 * @fires BaseViewer#SaveSVGDone
451 *
452 * @return {Promise<SaveSVGResult>} A promise resolving with the SVG.
453 */
454BaseViewer.prototype.saveSVG = async function saveSVG() {
455 this._emit('saveSVG.start');
456
457 let svg, err;
458
459 try {
460 const canvas = this.get('canvas');
461
462 const contentNode = canvas.getActiveLayer(),
463 defsNode = domQuery('defs', canvas._svg);
464
465 const contents = innerSVG(contentNode),
466 defs = defsNode ? '<defs>' + innerSVG(defsNode) + '</defs>' : '';
467
468 const bbox = contentNode.getBBox();
469
470 svg =
471 '<?xml version="1.0" encoding="utf-8"?>\n' +
472 '<!-- created with bpmn-js / http://bpmn.io -->\n' +
473 '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
474 '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
475 'width="' + bbox.width + '" height="' + bbox.height + '" ' +
476 'viewBox="' + bbox.x + ' ' + bbox.y + ' ' + bbox.width + ' ' + bbox.height + '" version="1.1">' +
477 defs + contents +
478 '</svg>';
479 } catch (e) {
480 err = e;
481 }
482
483 /**
484 * A `saveSVG.done` event.
485 *
486 * @event BaseViewer#SaveSVGDoneEvent
487 * @type {SaveSVGDoneEvent}
488 */
489 this._emit('saveSVG.done', {
490 error: err,
491 svg: svg
492 });
493
494 if (err) {
495 throw err;
496 }
497
498 return { svg };
499};
500
501BaseViewer.prototype._setDefinitions = function(definitions) {
502 this._definitions = definitions;
503};
504
505/**
506 * Return modules to instantiate with.
507 *
508 * @return {ModuleDeclaration[]} The modules.
509 */
510BaseViewer.prototype.getModules = function() {
511 return this._modules;
512};
513
514/**
515 * Remove all drawn elements from the viewer.
516 *
517 * After calling this method the viewer can still be reused for opening another
518 * diagram.
519 */
520BaseViewer.prototype.clear = function() {
521 if (!this.getDefinitions()) {
522
523 // no diagram to clear
524 return;
525 }
526
527 // remove drawn elements
528 Diagram.prototype.clear.call(this);
529};
530
531/**
532 * Destroy the viewer instance and remove all its remainders from the document
533 * tree.
534 */
535BaseViewer.prototype.destroy = function() {
536
537 // diagram destroy
538 Diagram.prototype.destroy.call(this);
539
540 // dom detach
541 domRemove(this._container);
542};
543
544/**
545 * Register an event listener.
546 *
547 * Remove an event listener via {@link BaseViewer#off}.
548 *
549 * @template T
550 *
551 * @param {string|string[]} events The event(s) to listen to.
552 * @param {number} [priority] The priority with which to listen.
553 * @param {EventBusEventCallback<T>} callback The callback.
554 * @param {any} [that] Value of `this` the callback will be called with.
555 */
556BaseViewer.prototype.on = function(events, priority, callback, that) {
557 return this.get('eventBus').on(events, priority, callback, that);
558};
559
560/**
561 * Remove an event listener.
562 *
563 * @param {string|string[]} events The event(s).
564 * @param {Function} [callback] The callback.
565 */
566BaseViewer.prototype.off = function(events, callback) {
567 this.get('eventBus').off(events, callback);
568};
569
570/**
571 * Attach the viewer to an HTML element.
572 *
573 * @param {HTMLElement} parentNode The parent node to attach to.
574 */
575BaseViewer.prototype.attachTo = function(parentNode) {
576
577 if (!parentNode) {
578 throw new Error('parentNode required');
579 }
580
581 // ensure we detach from the
582 // previous, old parent
583 this.detach();
584
585 // unwrap jQuery if provided
586 if (parentNode.get && parentNode.constructor.prototype.jquery) {
587 parentNode = parentNode.get(0);
588 }
589
590 if (typeof parentNode === 'string') {
591 parentNode = domQuery(parentNode);
592 }
593
594 parentNode.appendChild(this._container);
595
596 this._emit('attach', {});
597
598 this.get('canvas').resized();
599};
600
601/**
602 * Get the definitions model element.
603 *
604 * @return {ModdleElement} The definitions model element.
605 */
606BaseViewer.prototype.getDefinitions = function() {
607 return this._definitions;
608};
609
610/**
611 * Detach the viewer.
612 *
613 * @fires BaseViewer#DetachEvent
614 */
615BaseViewer.prototype.detach = function() {
616
617 const container = this._container,
618 parentNode = container.parentNode;
619
620 if (!parentNode) {
621 return;
622 }
623
624 /**
625 * A `detach` event.
626 *
627 * @event BaseViewer#DetachEvent
628 * @type {Object}
629 */
630 this._emit('detach', {});
631
632 parentNode.removeChild(container);
633};
634
635BaseViewer.prototype._init = function(container, moddle, options) {
636
637 const baseModules = options.modules || this.getModules(options),
638 additionalModules = options.additionalModules || [],
639 staticModules = [
640 {
641 bpmnjs: [ 'value', this ],
642 moddle: [ 'value', moddle ]
643 }
644 ];
645
646 const diagramModules = [].concat(staticModules, baseModules, additionalModules);
647
648 const diagramOptions = assign(omit(options, [ 'additionalModules' ]), {
649 canvas: assign({}, options.canvas, { container: container }),
650 modules: diagramModules
651 });
652
653 // invoke diagram constructor
654 Diagram.call(this, diagramOptions);
655
656 if (options && options.container) {
657 this.attachTo(options.container);
658 }
659};
660
661/**
662 * Emit an event on the underlying {@link EventBus}
663 *
664 * @param {string} type
665 * @param {Object} event
666 *
667 * @return {Object} The return value after calling all event listeners.
668 */
669BaseViewer.prototype._emit = function(type, event) {
670 return this.get('eventBus').fire(type, event);
671};
672
673/**
674 * @param {BaseViewerOptions} options
675 *
676 * @return {HTMLElement}
677 */
678BaseViewer.prototype._createContainer = function(options) {
679
680 const container = domify('<div class="bjs-container"></div>');
681
682 assignStyle(container, {
683 width: ensureUnit(options.width),
684 height: ensureUnit(options.height),
685 position: options.position
686 });
687
688 return container;
689};
690
691/**
692 * @param {BaseViewerOptions} options
693 *
694 * @return {Moddle}
695 */
696BaseViewer.prototype._createModdle = function(options) {
697 const moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions);
698
699 return new BpmnModdle(moddleOptions);
700};
701
702BaseViewer.prototype._modules = [];
703
704// helpers ///////////////
705
706function addWarningsToError(err, warningsAry) {
707 err.warnings = warningsAry;
708 return err;
709}
710
711function checkValidationError(err) {
712
713 // check if we can help the user by indicating wrong BPMN 2.0 xml
714 // (in case he or the exporting tool did not get that right)
715
716 const pattern = /unparsable content <([^>]+)> detected([\s\S]*)$/;
717 const match = pattern.exec(err.message);
718
719 if (match) {
720 err.message =
721 'unparsable content <' + match[1] + '> detected; ' +
722 'this may indicate an invalid BPMN 2.0 diagram file' + match[2];
723 }
724
725 return err;
726}
727
728const DEFAULT_OPTIONS = {
729 width: '100%',
730 height: '100%',
731 position: 'relative'
732};
733
734
735/**
736 * Ensure the passed argument is a proper unit (defaulting to px)
737 */
738function ensureUnit(val) {
739 return val + (isNumber(val) ? 'px' : '');
740}
741
742
743/**
744 * Find BPMNDiagram in definitions by ID
745 *
746 * @param {ModdleElement<Definitions>} definitions
747 * @param {string} diagramId
748 *
749 * @return {ModdleElement<BPMNDiagram>|null}
750 */
751function findBPMNDiagram(definitions, diagramId) {
752 if (!diagramId) {
753 return null;
754 }
755
756 return find(definitions.diagrams, function(element) {
757 return element.id === diagramId;
758 }) || null;
759}
760
761
762/* <project-logo> */
763
764import {
765 open as openPoweredBy,
766 BPMNIO_IMG,
767 LOGO_STYLES,
768 LINK_STYLES
769} from './util/PoweredByUtil';
770
771import {
772 event as domEvent
773} from 'min-dom';
774
775/**
776 * Adds the project logo to the diagram container as
777 * required by the bpmn.io license.
778 *
779 * @see http://bpmn.io/license
780 *
781 * @param {Element} container
782 */
783function addProjectLogo(container) {
784 const img = BPMNIO_IMG;
785
786 const linkMarkup =
787 '<a href="http://bpmn.io" ' +
788 'target="_blank" ' +
789 'class="bjs-powered-by" ' +
790 'title="Powered by bpmn.io" ' +
791 '>' +
792 img +
793 '</a>';
794
795 const linkElement = domify(linkMarkup);
796
797 assignStyle(domQuery('svg', linkElement), LOGO_STYLES);
798 assignStyle(linkElement, LINK_STYLES, {
799 position: 'absolute',
800 bottom: '15px',
801 right: '15px',
802 zIndex: '100'
803 });
804
805 container.appendChild(linkElement);
806
807 domEvent.bind(linkElement, 'click', function(event) {
808 openPoweredBy();
809
810 event.preventDefault();
811 });
812}
813
814/* </project-logo> */