/** * The code in the area * must not be changed. * * @see http://bpmn.io/license for more information. */ 'use strict'; var assign = require('lodash/object/assign'), omit = require('lodash/object/omit'), isString = require('lodash/lang/isString'), isNumber = require('lodash/lang/isNumber'); var domify = require('min-dom/lib/domify'), domQuery = require('min-dom/lib/query'), domRemove = require('min-dom/lib/remove'); var Diagram = require('diagram-js'), BpmnModdle = require('bpmn-moddle'); var Importer = require('./import/Importer'); function initListeners(diagram, listeners) { var events = diagram.get('eventBus'); listeners.forEach(function(l) { events.on(l.event, l.priority, l.callback, l.that); }); } 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', container: 'body' }; /** * Ensure the passed argument is a proper unit (defaulting to px) */ function ensureUnit(val) { return val + (isNumber(val) ? 'px' : ''); } /** * 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} [options.modules] a list of modules to override the default modules * @param {Array} [options.additionalModules] a list of modules to use with the default modules */ function Viewer(options) { this.options = options = assign({}, DEFAULT_OPTIONS, options || {}); var parent = options.container; // support jquery element // unwrap it if passed if (parent.get) { parent = parent.get(0); } // support selector if (isString(parent)) { parent = domQuery(parent); } var container = this.container = domify('
'); parent.appendChild(container); assign(container.style, { width: ensureUnit(options.width), height: ensureUnit(options.height), position: options.position }); /* */ addProjectLogo(container); /* */ } module.exports = Viewer; /** * Import and render a BPMN 2.0 diagram. * * Once finished the viewer reports back the result to the * provided callback function with (err, warnings). * * @param {String} xml the BPMN 2.0 xml * @param {Function} done invoked with (err, warnings=[]) */ Viewer.prototype.importXML = function(xml, done) { var self = this; this.moddle = this.createModdle(); this.moddle.fromXML(xml, 'bpmn:Definitions', function(err, definitions, context) { if (err) { err = checkValidationError(err); return done(err); } var parseWarnings = context.warnings; self.importDefinitions(definitions, function(err, importWarnings) { var allWarnings = parseWarnings.concat(importWarnings || []); done(err, allWarnings); }); }); }; /** * Export the currently displayed BPMN 2.0 diagram as * a BPMN 2.0 XML document. * * @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 definitions = this.definitions; if (!definitions) { return done(new Error('no definitions loaded')); } this.moddle.toXML(definitions, options, done); }; Viewer.prototype.createModdle = function() { return new BpmnModdle(assign({}, this._moddleExtensions, this.options.moddleExtensions)); }; /** * Export the currently displayed BPMN 2.0 diagram as * an SVG image. * * @param {Object} [options] * @param {Function} done invoked with (err, svgStr) */ Viewer.prototype.saveSVG = function(options, done) { if (!done) { done = options; options = {}; } var canvas = this.get('canvas'); var contentNode = canvas.getDefaultLayer(), defsNode = canvas._svg.select('defs'); var contents = contentNode.innerSVG(), defs = (defsNode && defsNode.outerSVG()) || ''; var bbox = contentNode.getBBox(); var svg = '\n' + '\n' + '\n' + '' + defs + contents + ''; done(null, 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 */ Viewer.prototype.get = function(name) { if (!this.diagram) { throw new Error('no diagram loaded'); } return this.diagram.get(name); }; /** * 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 */ Viewer.prototype.invoke = function(fn) { if (!this.diagram) { throw new Error('no diagram loaded'); } return this.diagram.invoke(fn); }; Viewer.prototype.importDefinitions = function(definitions, done) { // use try/catch to not swallow synchronous exceptions // that may be raised during model parsing try { if (this.diagram) { this.clear(); } this.definitions = definitions; var diagram = this.diagram = this._createDiagram(this.options); this._init(diagram); Importer.importBpmnDiagram(diagram, definitions, done); } catch (e) { done(e); } }; Viewer.prototype._init = function(diagram) { initListeners(diagram, this.__listeners || []); }; Viewer.prototype._createDiagram = function(options) { var modules = [].concat(options.modules || this.getModules(), options.additionalModules || []); // add self as an available service modules.unshift({ bpmnjs: [ 'value', this ], moddle: [ 'value', this.moddle ] }); options = omit(options, 'additionalModules'); options = assign(options, { canvas: assign({}, options.canvas, { container: this.container }), modules: modules }); return new Diagram(options); }; 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. */ Viewer.prototype.clear = function() { var diagram = this.diagram; if (diagram) { diagram.destroy(); } }; /** * Destroy the viewer instance and remove all its remainders * from the document tree. */ Viewer.prototype.destroy = function() { // clear underlying diagram this.clear(); // remove container domRemove(this.container); }; /** * Register an event listener on the viewer * * 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, that) { var diagram = this.diagram, listeners = this.__listeners = this.__listeners || []; if (typeof priority === 'function') { that = callback; callback = priority; priority = 1000; } listeners.push({ event: event, priority: priority, callback: callback, that: that }); if (diagram) { return diagram.get('eventBus').on(event, priority, callback, that); } }; /** * De-register an event callback * * @param {String} event * @param {Function} callback */ Viewer.prototype.off = function(event, callback) { var filter, diagram = this.diagram; if (callback) { filter = function(l) { return !(l.event === event && l.callback === callback); }; } else { filter = function(l) { return l.event !== event; }; } this.__listeners = (this.__listeners || []).filter(filter); if (diagram) { diagram.get('eventBus').off(event, callback); } }; // modules the viewer is composed of Viewer.prototype._modules = [ require('./core'), require('diagram-js/lib/features/selection'), require('diagram-js/lib/features/overlays') ]; // default moddle extensions the viewer is composed of Viewer.prototype._moddleExtensions = {}; /* */ var PoweredBy = require('./util/PoweredByUtil'), domEvent = require('min-dom/lib/event'); /** * 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 logoData = PoweredBy.BPMNIO_LOGO; var linkMarkup = '' + '' + ''; var linkElement = domify(linkMarkup); container.appendChild(linkElement); domEvent.bind(linkElement, 'click', function(event) { PoweredBy.open(); event.preventDefault(); }); } /* */