mirror of
https://github.com/sartography/bpmn-js.git
synced 2025-01-10 17:16:02 +00:00
9c063f867b
This was removed in f8f9334, but it occurred that the method is used productively by some of the clients. This commit restores the method as part of public API. Use `Viewer#importDefinitions` to import previously parsed definitions. Now it is also possible to pass the diagram or diagram id to be rendered during graphical import.
691 lines
16 KiB
JavaScript
691 lines
16 KiB
JavaScript
/**
|
|
* The code in the <project-logo></project-logo> area
|
|
* must not be changed.
|
|
*
|
|
* @see http://bpmn.io/license for more information.
|
|
*/
|
|
import {
|
|
assign,
|
|
find,
|
|
isFunction,
|
|
isNumber,
|
|
omit
|
|
} from 'min-dash';
|
|
|
|
import {
|
|
domify,
|
|
query as domQuery,
|
|
remove as domRemove
|
|
} from 'min-dom';
|
|
|
|
import {
|
|
innerSVG
|
|
} from 'tiny-svg';
|
|
|
|
import Diagram from 'diagram-js';
|
|
import BpmnModdle from 'bpmn-moddle';
|
|
|
|
import inherits from 'inherits';
|
|
|
|
import {
|
|
importBpmnDiagram
|
|
} from './import/Importer';
|
|
|
|
import CoreModule from './core';
|
|
import TranslateModule from 'diagram-js/lib/i18n/translate';
|
|
import SelectionModule from 'diagram-js/lib/features/selection';
|
|
import OverlaysModule from 'diagram-js/lib/features/overlays';
|
|
|
|
|
|
function checkValidationError(err) {
|
|
|
|
// check if we can help the user by indicating wrong BPMN 2.0 xml
|
|
// (in case he or the exporting tool did not get that right)
|
|
|
|
var pattern = /unparsable content <([^>]+)> detected([\s\S]*)$/;
|
|
var match = pattern.exec(err.message);
|
|
|
|
if (match) {
|
|
err.message =
|
|
'unparsable content <' + match[1] + '> detected; ' +
|
|
'this may indicate an invalid BPMN 2.0 diagram file' + match[2];
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
var DEFAULT_OPTIONS = {
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'relative'
|
|
};
|
|
|
|
|
|
/**
|
|
* Ensure the passed argument is a proper unit (defaulting to px)
|
|
*/
|
|
function ensureUnit(val) {
|
|
return val + (isNumber(val) ? 'px' : '');
|
|
}
|
|
|
|
|
|
/**
|
|
* Find BPMNDiagram in definitions by ID
|
|
*
|
|
* @param {ModdleElement<Definitions>} definitions
|
|
* @param {String} diagramId
|
|
*
|
|
* @return {ModdleElement<BPMNDiagram>|null}
|
|
*/
|
|
function findBPMNDiagram(definitions, diagramId) {
|
|
if (!diagramId) {
|
|
return null;
|
|
}
|
|
|
|
return find(definitions.diagrams, function(element) {
|
|
return element.id === diagramId;
|
|
}) || null;
|
|
}
|
|
|
|
/**
|
|
* A viewer for BPMN 2.0 diagrams.
|
|
*
|
|
* Have a look at {@link NavigatedViewer} or {@link Modeler} for bundles that include
|
|
* additional features.
|
|
*
|
|
*
|
|
* ## Extending the Viewer
|
|
*
|
|
* In order to extend the viewer pass extension modules to bootstrap via the
|
|
* `additionalModules` option. An extension module is an object that exposes
|
|
* named services.
|
|
*
|
|
* The following example depicts the integration of a simple
|
|
* logging component that integrates with interaction events:
|
|
*
|
|
*
|
|
* ```javascript
|
|
*
|
|
* // logging component
|
|
* function InteractionLogger(eventBus) {
|
|
* eventBus.on('element.hover', function(event) {
|
|
* console.log()
|
|
* })
|
|
* }
|
|
*
|
|
* InteractionLogger.$inject = [ 'eventBus' ]; // minification save
|
|
*
|
|
* // extension module
|
|
* var extensionModule = {
|
|
* __init__: [ 'interactionLogger' ],
|
|
* interactionLogger: [ 'type', InteractionLogger ]
|
|
* };
|
|
*
|
|
* // extend the viewer
|
|
* var bpmnViewer = new Viewer({ additionalModules: [ extensionModule ] });
|
|
* bpmnViewer.importXML(...);
|
|
* ```
|
|
*
|
|
* @param {Object} [options] configuration options to pass to the viewer
|
|
* @param {DOMElement} [options.container] the container to render the viewer in, defaults to body.
|
|
* @param {String|Number} [options.width] the width of the viewer
|
|
* @param {String|Number} [options.height] the height of the viewer
|
|
* @param {Object} [options.moddleExtensions] extension packages to provide
|
|
* @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules
|
|
* @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules
|
|
*/
|
|
export default function Viewer(options) {
|
|
|
|
options = assign({}, DEFAULT_OPTIONS, options);
|
|
|
|
this._moddle = this._createModdle(options);
|
|
|
|
this._container = this._createContainer(options);
|
|
|
|
/* <project-logo> */
|
|
|
|
addProjectLogo(this._container);
|
|
|
|
/* </project-logo> */
|
|
|
|
this._init(this._container, this._moddle, options);
|
|
}
|
|
|
|
inherits(Viewer, Diagram);
|
|
|
|
|
|
/**
|
|
* Parse and render a BPMN 2.0 diagram.
|
|
*
|
|
* Once finished the viewer reports back the result to the
|
|
* provided callback function with (err, warnings).
|
|
*
|
|
* ## Life-Cycle Events
|
|
*
|
|
* During import the viewer will fire life-cycle events:
|
|
*
|
|
* * import.parse.start (about to read model from xml)
|
|
* * import.parse.complete (model read; may have worked or not)
|
|
* * import.render.start (graphical import start)
|
|
* * import.render.complete (graphical import finished)
|
|
* * import.done (everything done)
|
|
*
|
|
* You can use these events to hook into the life-cycle.
|
|
*
|
|
* @param {String} xml the BPMN 2.0 xml
|
|
* @param {ModdleElement<BPMNDiagram>|String} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered)
|
|
* @param {Function} [done] invoked with (err, warnings=[])
|
|
*/
|
|
Viewer.prototype.importXML = function(xml, bpmnDiagram, done) {
|
|
|
|
if (isFunction(bpmnDiagram)) {
|
|
done = bpmnDiagram;
|
|
bpmnDiagram = null;
|
|
}
|
|
|
|
// done is optional
|
|
done = done || function() {};
|
|
|
|
var self = this;
|
|
|
|
// hook in pre-parse listeners +
|
|
// allow xml manipulation
|
|
xml = this._emit('import.parse.start', { xml: xml }) || xml;
|
|
|
|
this._moddle.fromXML(xml, 'bpmn:Definitions', function(err, definitions, context) {
|
|
|
|
// hook in post parse listeners +
|
|
// allow definitions manipulation
|
|
definitions = self._emit('import.parse.complete', {
|
|
error: err,
|
|
definitions: definitions,
|
|
context: context
|
|
}) || definitions;
|
|
|
|
var parseWarnings = context.warnings;
|
|
|
|
if (err) {
|
|
err = checkValidationError(err);
|
|
|
|
self._emit('import.done', { error: err, warnings: parseWarnings });
|
|
|
|
return done(err, parseWarnings);
|
|
}
|
|
|
|
self.importDefinitions(definitions, bpmnDiagram, function(err, importWarnings) {
|
|
var allWarnings = [].concat(parseWarnings, importWarnings || []);
|
|
|
|
self._emit('import.done', { error: err, warnings: allWarnings });
|
|
|
|
done(err, allWarnings);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Import parsed definitions and render a BPMN 2.0 diagram.
|
|
*
|
|
* Once finished the viewer reports back the result to the
|
|
* provided callback function with (err, warnings).
|
|
*
|
|
* ## Life-Cycle Events
|
|
*
|
|
* During import the viewer will fire life-cycle events:
|
|
*
|
|
* * import.render.start (graphical import start)
|
|
* * import.render.complete (graphical import finished)
|
|
*
|
|
* You can use these events to hook into the life-cycle.
|
|
*
|
|
* @param {ModdleElement<Definitions>} definitions parsed BPMN 2.0 definitions
|
|
* @param {ModdleElement<BPMNDiagram>|String} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered)
|
|
* @param {Function} [done] invoked with (err, warnings=[])
|
|
*/
|
|
Viewer.prototype.importDefinitions = function(definitions, bpmnDiagram, done) {
|
|
|
|
if (isFunction(bpmnDiagram)) {
|
|
done = bpmnDiagram;
|
|
bpmnDiagram = null;
|
|
}
|
|
|
|
// done is optional
|
|
done = done || function() {};
|
|
|
|
this._setDefinitions(definitions);
|
|
|
|
return this.open(bpmnDiagram, done);
|
|
};
|
|
|
|
/**
|
|
* Open diagram of previously imported XML.
|
|
*
|
|
* Once finished the viewer reports back the result to the
|
|
* provided callback function with (err, warnings).
|
|
*
|
|
* ## Life-Cycle Events
|
|
*
|
|
* During switch the viewer will fire life-cycle events:
|
|
*
|
|
* * import.render.start (graphical import start)
|
|
* * import.render.complete (graphical import finished)
|
|
*
|
|
* You can use these events to hook into the life-cycle.
|
|
*
|
|
* @param {String|ModdleElement<BPMNDiagram>} [bpmnDiagramOrId] id or the diagram to open
|
|
* @param {Function} [done] invoked with (err, warnings=[])
|
|
*/
|
|
Viewer.prototype.open = function(bpmnDiagramOrId, done) {
|
|
|
|
if (isFunction(bpmnDiagramOrId)) {
|
|
done = bpmnDiagramOrId;
|
|
bpmnDiagramOrId = null;
|
|
}
|
|
|
|
var definitions = this._definitions;
|
|
var bpmnDiagram = bpmnDiagramOrId;
|
|
|
|
// done is optional
|
|
done = done || function() {};
|
|
|
|
if (!definitions) {
|
|
return done(new Error('no XML imported'));
|
|
}
|
|
|
|
if (typeof bpmnDiagramOrId === 'string') {
|
|
bpmnDiagram = findBPMNDiagram(definitions, bpmnDiagramOrId);
|
|
|
|
if (!bpmnDiagram) {
|
|
return done(new Error('BPMNDiagram <' + bpmnDiagramOrId + '> not found'));
|
|
}
|
|
}
|
|
|
|
// clear existing rendered diagram
|
|
// catch synchronous exceptions during #clear()
|
|
try {
|
|
this.clear();
|
|
} catch (error) {
|
|
return done(error);
|
|
}
|
|
|
|
// perform graphical import
|
|
return importBpmnDiagram(this, definitions, bpmnDiagram, done);
|
|
};
|
|
|
|
/**
|
|
* Export the currently displayed BPMN 2.0 diagram as
|
|
* a BPMN 2.0 XML document.
|
|
*
|
|
* ## Life-Cycle Events
|
|
*
|
|
* During XML saving the viewer will fire life-cycle events:
|
|
*
|
|
* * saveXML.start (before serialization)
|
|
* * saveXML.serialized (after xml generation)
|
|
* * saveXML.done (everything done)
|
|
*
|
|
* You can use these events to hook into the life-cycle.
|
|
*
|
|
* @param {Object} [options] export options
|
|
* @param {Boolean} [options.format=false] output formated XML
|
|
* @param {Boolean} [options.preamble=true] output preamble
|
|
*
|
|
* @param {Function} done invoked with (err, xml)
|
|
*/
|
|
Viewer.prototype.saveXML = function(options, done) {
|
|
|
|
if (!done) {
|
|
done = options;
|
|
options = {};
|
|
}
|
|
|
|
var self = this;
|
|
|
|
var definitions = this._definitions;
|
|
|
|
if (!definitions) {
|
|
return done(new Error('no definitions loaded'));
|
|
}
|
|
|
|
// allow to fiddle around with definitions
|
|
definitions = this._emit('saveXML.start', {
|
|
definitions: definitions
|
|
}) || definitions;
|
|
|
|
this._moddle.toXML(definitions, options, function(err, xml) {
|
|
|
|
try {
|
|
xml = self._emit('saveXML.serialized', {
|
|
error: err,
|
|
xml: xml
|
|
}) || xml;
|
|
|
|
self._emit('saveXML.done', {
|
|
error: err,
|
|
xml: xml
|
|
});
|
|
} catch (e) {
|
|
console.error('error in saveXML life-cycle listener', e);
|
|
}
|
|
|
|
done(err, xml);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Export the currently displayed BPMN 2.0 diagram as
|
|
* an SVG image.
|
|
*
|
|
* ## Life-Cycle Events
|
|
*
|
|
* During SVG saving the viewer will fire life-cycle events:
|
|
*
|
|
* * saveSVG.start (before serialization)
|
|
* * saveSVG.done (everything done)
|
|
*
|
|
* You can use these events to hook into the life-cycle.
|
|
*
|
|
* @param {Object} [options]
|
|
* @param {Function} done invoked with (err, svgStr)
|
|
*/
|
|
Viewer.prototype.saveSVG = function(options, done) {
|
|
|
|
if (!done) {
|
|
done = options;
|
|
options = {};
|
|
}
|
|
|
|
this._emit('saveSVG.start');
|
|
|
|
var svg, err;
|
|
|
|
try {
|
|
var canvas = this.get('canvas');
|
|
|
|
var contentNode = canvas.getDefaultLayer(),
|
|
defsNode = domQuery('defs', canvas._svg);
|
|
|
|
var contents = innerSVG(contentNode),
|
|
defs = defsNode ? '<defs>' + innerSVG(defsNode) + '</defs>' : '';
|
|
|
|
var bbox = contentNode.getBBox();
|
|
|
|
svg =
|
|
'<?xml version="1.0" encoding="utf-8"?>\n' +
|
|
'<!-- created with bpmn-js / http://bpmn.io -->\n' +
|
|
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
|
|
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
|
|
'width="' + bbox.width + '" height="' + bbox.height + '" ' +
|
|
'viewBox="' + bbox.x + ' ' + bbox.y + ' ' + bbox.width + ' ' + bbox.height + '" version="1.1">' +
|
|
defs + contents +
|
|
'</svg>';
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
this._emit('saveSVG.done', {
|
|
error: err,
|
|
svg: svg
|
|
});
|
|
|
|
done(err, svg);
|
|
};
|
|
|
|
/**
|
|
* Get a named diagram service.
|
|
*
|
|
* @example
|
|
*
|
|
* var elementRegistry = viewer.get('elementRegistry');
|
|
* var startEventShape = elementRegistry.get('StartEvent_1');
|
|
*
|
|
* @param {String} name
|
|
*
|
|
* @return {Object} diagram service instance
|
|
*
|
|
* @method Viewer#get
|
|
*/
|
|
|
|
/**
|
|
* Invoke a function in the context of this viewer.
|
|
*
|
|
* @example
|
|
*
|
|
* viewer.invoke(function(elementRegistry) {
|
|
* var startEventShape = elementRegistry.get('StartEvent_1');
|
|
* });
|
|
*
|
|
* @param {Function} fn to be invoked
|
|
*
|
|
* @return {Object} the functions return value
|
|
*
|
|
* @method Viewer#invoke
|
|
*/
|
|
|
|
|
|
Viewer.prototype._setDefinitions = function(definitions) {
|
|
this._definitions = definitions;
|
|
};
|
|
|
|
Viewer.prototype.getModules = function() {
|
|
return this._modules;
|
|
};
|
|
|
|
/**
|
|
* Remove all drawn elements from the viewer.
|
|
*
|
|
* After calling this method the viewer can still
|
|
* be reused for opening another diagram.
|
|
*
|
|
* @method Viewer#clear
|
|
*/
|
|
Viewer.prototype.clear = function() {
|
|
|
|
// remove businessObject#di binding
|
|
//
|
|
// this is necessary, as we establish the bindings
|
|
// in the BpmnTreeWalker (and assume none are given
|
|
// on reimport)
|
|
this.get('elementRegistry').forEach(function(element) {
|
|
var bo = element.businessObject;
|
|
|
|
if (bo && bo.di) {
|
|
delete bo.di;
|
|
}
|
|
});
|
|
|
|
// remove drawn elements
|
|
Diagram.prototype.clear.call(this);
|
|
};
|
|
|
|
/**
|
|
* Destroy the viewer instance and remove all its
|
|
* remainders from the document tree.
|
|
*/
|
|
Viewer.prototype.destroy = function() {
|
|
|
|
// diagram destroy
|
|
Diagram.prototype.destroy.call(this);
|
|
|
|
// dom detach
|
|
domRemove(this._container);
|
|
};
|
|
|
|
/**
|
|
* Register an event listener
|
|
*
|
|
* Remove a previously added listener via {@link #off(event, callback)}.
|
|
*
|
|
* @param {String} event
|
|
* @param {Number} [priority]
|
|
* @param {Function} callback
|
|
* @param {Object} [that]
|
|
*/
|
|
Viewer.prototype.on = function(event, priority, callback, target) {
|
|
return this.get('eventBus').on(event, priority, callback, target);
|
|
};
|
|
|
|
/**
|
|
* De-register an event listener
|
|
*
|
|
* @param {String} event
|
|
* @param {Function} callback
|
|
*/
|
|
Viewer.prototype.off = function(event, callback) {
|
|
this.get('eventBus').off(event, callback);
|
|
};
|
|
|
|
Viewer.prototype.attachTo = function(parentNode) {
|
|
|
|
if (!parentNode) {
|
|
throw new Error('parentNode required');
|
|
}
|
|
|
|
// ensure we detach from the
|
|
// previous, old parent
|
|
this.detach();
|
|
|
|
// unwrap jQuery if provided
|
|
if (parentNode.get && parentNode.constructor.prototype.jquery) {
|
|
parentNode = parentNode.get(0);
|
|
}
|
|
|
|
if (typeof parentNode === 'string') {
|
|
parentNode = domQuery(parentNode);
|
|
}
|
|
|
|
parentNode.appendChild(this._container);
|
|
|
|
this._emit('attach', {});
|
|
|
|
this.get('canvas').resized();
|
|
};
|
|
|
|
Viewer.prototype.getDefinitions = function() {
|
|
return this._definitions;
|
|
};
|
|
|
|
Viewer.prototype.detach = function() {
|
|
|
|
var container = this._container,
|
|
parentNode = container.parentNode;
|
|
|
|
if (!parentNode) {
|
|
return;
|
|
}
|
|
|
|
this._emit('detach', {});
|
|
|
|
parentNode.removeChild(container);
|
|
};
|
|
|
|
Viewer.prototype._init = function(container, moddle, options) {
|
|
|
|
var baseModules = options.modules || this.getModules(),
|
|
additionalModules = options.additionalModules || [],
|
|
staticModules = [
|
|
{
|
|
bpmnjs: [ 'value', this ],
|
|
moddle: [ 'value', moddle ]
|
|
}
|
|
];
|
|
|
|
var diagramModules = [].concat(staticModules, baseModules, additionalModules);
|
|
|
|
var diagramOptions = assign(omit(options, [ 'additionalModules' ]), {
|
|
canvas: assign({}, options.canvas, { container: container }),
|
|
modules: diagramModules
|
|
});
|
|
|
|
// invoke diagram constructor
|
|
Diagram.call(this, diagramOptions);
|
|
|
|
if (options && options.container) {
|
|
this.attachTo(options.container);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Emit an event on the underlying {@link EventBus}
|
|
*
|
|
* @param {String} type
|
|
* @param {Object} event
|
|
*
|
|
* @return {Object} event processing result (if any)
|
|
*/
|
|
Viewer.prototype._emit = function(type, event) {
|
|
return this.get('eventBus').fire(type, event);
|
|
};
|
|
|
|
Viewer.prototype._createContainer = function(options) {
|
|
|
|
var container = domify('<div class="bjs-container"></div>');
|
|
|
|
assign(container.style, {
|
|
width: ensureUnit(options.width),
|
|
height: ensureUnit(options.height),
|
|
position: options.position
|
|
});
|
|
|
|
return container;
|
|
};
|
|
|
|
Viewer.prototype._createModdle = function(options) {
|
|
var moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions);
|
|
|
|
return new BpmnModdle(moddleOptions);
|
|
};
|
|
|
|
// modules the viewer is composed of
|
|
Viewer.prototype._modules = [
|
|
CoreModule,
|
|
TranslateModule,
|
|
SelectionModule,
|
|
OverlaysModule
|
|
];
|
|
|
|
// default moddle extensions the viewer is composed of
|
|
Viewer.prototype._moddleExtensions = {};
|
|
|
|
/* <project-logo> */
|
|
|
|
import {
|
|
open as openPoweredBy,
|
|
BPMNIO_IMG
|
|
} from './util/PoweredByUtil';
|
|
|
|
import {
|
|
event as domEvent
|
|
} from 'min-dom';
|
|
|
|
/**
|
|
* Adds the project logo to the diagram container as
|
|
* required by the bpmn.io license.
|
|
*
|
|
* @see http://bpmn.io/license
|
|
*
|
|
* @param {Element} container
|
|
*/
|
|
function addProjectLogo(container) {
|
|
var img = BPMNIO_IMG;
|
|
|
|
var linkMarkup =
|
|
'<a href="http://bpmn.io" ' +
|
|
'target="_blank" ' +
|
|
'class="bjs-powered-by" ' +
|
|
'title="Powered by bpmn.io" ' +
|
|
'style="position: absolute; bottom: 15px; right: 15px; z-index: 100">' +
|
|
img +
|
|
'</a>';
|
|
|
|
var linkElement = domify(linkMarkup);
|
|
|
|
container.appendChild(linkElement);
|
|
|
|
domEvent.bind(linkElement, 'click', function(event) {
|
|
openPoweredBy();
|
|
|
|
event.preventDefault();
|
|
});
|
|
}
|
|
|
|
/* </project-logo> */ |