UNPKG

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