bpmn-js/lib/Viewer.js

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> */