Files
bpmn-js/lib/BaseViewer.js
2023-03-09 09:02:01 +01:00

803 lines
20 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,
isNumber,
omit
} from 'min-dash';
import {
domify,
assignStyle,
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-browser';
import {
importBpmnDiagram
} from './import/Importer';
import {
wrapForCompatibility
} from './util/CompatibilityUtil';
/**
* @typedef {import('didi').ModuleDeclaration} ModuleDeclaration
*
* @typedef {import('./BaseViewer').BaseModelerOptions} BaseModelerOptions
* @typedef {import('./BaseViewer').ModdleElement} ModdleElement
* @typedef {import('./BaseViewer').ImportXMLResult} ImportXMLResult
* @typedef {import('./BaseViewer').ImportXMLError} ImportXMLError
* @typedef {import('./BaseViewer').ImportDefinitionsResult} ImportDefinitionsResult
* @typedef {import('./BaseViewer').ImportDefinitionsError} ImportDefinitionsError
* @typedef {import('./BaseViewer').ModdleElement} ModdleElement
* @typedef {import('./BaseViewer').ModdleElementsById} ModdleElementsById
* @typedef {import('./BaseViewer').OpenResult} OpenResult
* @typedef {import('./BaseViewer').OpenError} OpenError
* @typedef {import('./BaseViewer').SaveXMLOptions} SaveXMLOptions
* @typedef {import('./BaseViewer').SaveXMLResult} SaveXMLResult
*/
/**
* A base viewer for BPMN 2.0 diagrams.
*
* Have a look at {@link Viewer}, {@link NavigatedViewer} or {@link Modeler} for
* bundles that include actual features.
*
* @param {BaseModelerOptions} [options] The options to configure the viewer.
*/
export default function BaseViewer(options) {
/**
* @type {BaseModelerOptions}
*/
options = assign({}, DEFAULT_OPTIONS, options);
this._moddle = this._createModdle(options);
/**
* @type {HTMLElement}
*/
this._container = this._createContainer(options);
/* <project-logo> */
addProjectLogo(this._container);
/* </project-logo> */
this._init(this._container, this._moddle, options);
}
inherits(BaseViewer, 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.
*
* @throws {ImportXMLError} An error thrown during the import of the XML.
*
* @fires BaseViewer#ImportParseStart
* @fires BaseViewer#ImportParseComplete
* @fires Importer#ImportRenderStart
* @fires Importer#ImportRenderComplete
* @fires BaseViewer#ImportDone
*
* @param {string} xml The BPMN 2.0 XML to be imported.
* @param {ModdleElement|string} [bpmnDiagram] The optional diagram or Id of the BPMN diagram to open.
*
* @return {Promise<ImportXMLResult>} A promise resolving with warnings that were produced during the import.
*/
BaseViewer.prototype.importXML = wrapForCompatibility(async function importXML(xml, bpmnDiagram) {
const self = this;
function ParseCompleteEvent(data) {
const event = self.get('eventBus').createEvent(data);
// TODO(nikku): remove with future bpmn-js version
Object.defineProperty(event, 'context', {
enumerable: true,
get: function() {
console.warn(new Error(
'import.parse.complete <context> is deprecated ' +
'and will be removed in future library versions'
));
return {
warnings: data.warnings,
references: data.references,
elementsById: data.elementsById
};
}
});
return event;
}
let aggregatedWarnings = [];
try {
// hook in pre-parse listeners +
// allow xml manipulation
/**
* A `import.parse.start` event.
*
* @event BaseViewer#ImportParseStart
* @type {Object}
* @property {string} xml The XML that is to be parsed.
*/
xml = this._emit('import.parse.start', { xml: xml }) || xml;
let parseResult;
try {
parseResult = await this._moddle.fromXML(xml, 'bpmn:Definitions');
} catch (error) {
this._emit('import.parse.complete', {
error
});
throw error;
}
let definitions = parseResult.rootElement;
const references = parseResult.references;
const parseWarnings = parseResult.warnings;
const elementsById = parseResult.elementsById;
aggregatedWarnings = aggregatedWarnings.concat(parseWarnings);
// hook in post parse listeners +
// allow definitions manipulation
/**
* A `import.parse.complete` event.
*
* @event BaseViewer#ImportParseComplete
* @type {Object}
* @property {Error|null} error An error thrown when parsing the XML.
* @property {ModdleElement} definitions The definitions model element.
* @property {ModdleElementsById} elementsById The model elements by ID.
* @property {ModdleElement[]} references The referenced model elements.
* @property {string[]} warnings The warnings produced when parsing the XML.
*/
definitions = this._emit('import.parse.complete', ParseCompleteEvent({
error: null,
definitions: definitions,
elementsById: elementsById,
references: references,
warnings: aggregatedWarnings
})) || definitions;
const importResult = await this.importDefinitions(definitions, bpmnDiagram);
aggregatedWarnings = aggregatedWarnings.concat(importResult.warnings);
/**
* A `import.parse.complete` event.
*
* @event BaseViewer#ImportDone
* @type {Object}
* @property {ImportXMLError|null} error An error thrown during import.
* @property {string[]} warnings The warnings.
*/
this._emit('import.done', { error: null, warnings: aggregatedWarnings });
return { warnings: aggregatedWarnings };
} catch (err) {
let error = err;
aggregatedWarnings = aggregatedWarnings.concat(error.warnings || []);
addWarningsToError(error, aggregatedWarnings);
error = checkValidationError(error);
this._emit('import.done', { error, warnings: error.warnings });
throw error;
}
});
/**
* 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.
*
* @throws {ImportDefinitionsError} An error thrown during the import of the definitions.
*
* @param {ModdleElement} definitions The definitions.
* @param {ModdleElement|string} [bpmnDiagram] The optional diagram or ID of the BPMN diagram to open.
*
* @return {Promise<ImportDefinitionsResult>} A promise resolving with warnings that were produced during the import.
*/
BaseViewer.prototype.importDefinitions = wrapForCompatibility(async function importDefinitions(definitions, bpmnDiagram) {
this._setDefinitions(definitions);
const result = await this.open(bpmnDiagram);
return { warnings: result.warnings };
});
/**
* 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.
*
* @throws {OpenError} An error thrown during opening.
*
* @param {ModdleElement|string} bpmnDiagramOrId The diagram or Id of the BPMN diagram to open.
*
* @return {Promise<OpenResult>} A promise resolving with warnings that were produced during opening.
*/
BaseViewer.prototype.open = wrapForCompatibility(async function open(bpmnDiagramOrId) {
const definitions = this._definitions;
let bpmnDiagram = bpmnDiagramOrId;
if (!definitions) {
const error = new Error('no XML imported');
addWarningsToError(error, []);
throw error;
}
if (typeof bpmnDiagramOrId === 'string') {
bpmnDiagram = findBPMNDiagram(definitions, bpmnDiagramOrId);
if (!bpmnDiagram) {
const error = new Error('BPMNDiagram <' + bpmnDiagramOrId + '> not found');
addWarningsToError(error, []);
throw error;
}
}
// clear existing rendered diagram
// catch synchronous exceptions during #clear()
try {
this.clear();
} catch (error) {
addWarningsToError(error, []);
throw error;
}
// perform graphical import
const { warnings } = await importBpmnDiagram(this, definitions, bpmnDiagram);
return { warnings };
});
/**
* 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.
*
* @throws {Error} An error thrown during export.
*
* @fires BaseViewer#SaveXMLStart
* @fires BaseViewer#SaveXMLDone
*
* @param {SaveXMLOptions} [options] The options.
*
* @return {Promise<SaveXMLResult>} A promise resolving with the XML.
*/
BaseViewer.prototype.saveXML = wrapForCompatibility(async function saveXML(options) {
options = options || {};
let definitions = this._definitions,
error, xml;
try {
if (!definitions) {
throw new Error('no definitions loaded');
}
// allow to fiddle around with definitions
/**
* A `saveXML.start` event.
*
* @event BaseViewer#SaveXMLStart
* @type {Object}
* @property {ModdleElement} definitions The definitions model element.
*/
definitions = this._emit('saveXML.start', {
definitions
}) || definitions;
const result = await this._moddle.toXML(definitions, options);
xml = result.xml;
xml = this._emit('saveXML.serialized', {
xml
}) || xml;
} catch (err) {
error = err;
}
const result = error ? { error } : { xml };
/**
* A `saveXML.done` event.
*
* @event BaseViewer#SaveXMLDone
* @type {Object}
* @property {Error} [error] An error thrown when saving the XML.
* @property {string} [xml] The saved XML.
*/
this._emit('saveXML.done', result);
if (error) {
throw error;
}
return result;
});
/**
* 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.
*
* @throws {Error} An error thrown during export.
*
* @fires BaseViewer#SaveSVGDone
*
* @return {Promise<SaveSVGResult>} A promise resolving with the SVG.
*/
BaseViewer.prototype.saveSVG = wrapForCompatibility(async function saveSVG() {
this._emit('saveSVG.start');
let svg, err;
try {
const canvas = this.get('canvas');
const contentNode = canvas.getActiveLayer(),
defsNode = domQuery('defs', canvas._svg);
const contents = innerSVG(contentNode),
defs = defsNode ? '<defs>' + innerSVG(defsNode) + '</defs>' : '';
const 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;
}
/**
* A `saveSVG.done` event.
*
* @event BaseViewer#SaveSVGDone
* @type {Object}
* @property {Error} [error] An error thrown when saving the SVG.
* @property {string} [svg] The saved SVG.
*/
this._emit('saveSVG.done', {
error: err,
svg: svg
});
if (err) {
throw err;
}
return { svg };
});
/**
* Get a named diagram service.
*
* @example
*
* const elementRegistry = viewer.get('elementRegistry');
* const startEventShape = elementRegistry.get('StartEvent_1');
*
* @param {string} name
*
* @return {Object} diagram service instance
*
* @method BaseViewer#get
*/
/**
* Invoke a function in the context of this viewer.
*
* @example
*
* viewer.invoke(function(elementRegistry) {
* const startEventShape = elementRegistry.get('StartEvent_1');
* });
*
* @param {Function} fn to be invoked
*
* @return {Object} the functions return value
*
* @method BaseViewer#invoke
*/
BaseViewer.prototype._setDefinitions = function(definitions) {
this._definitions = definitions;
};
/**
* Return modules to instantiate with.
*
* @return {ModuleDeclaration[]} The modules.
*/
BaseViewer.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.
*/
BaseViewer.prototype.clear = function() {
if (!this.getDefinitions()) {
// no diagram to clear
return;
}
// remove drawn elements
Diagram.prototype.clear.call(this);
};
/**
* Destroy the viewer instance and remove all its remainders from the document
* tree.
*/
BaseViewer.prototype.destroy = function() {
// diagram destroy
Diagram.prototype.destroy.call(this);
// dom detach
domRemove(this._container);
};
/**
* Register an event listener.
*
* Remove an event listener via {@link BaseViewer#off}.
*
* @param {string|string[]} events The event(s) to listen to.
* @param {number} [priority] The priority with which to listen.
* @param {EventCallback} callback The callback.
* @param {*} [that] Value of `this` the callback will be called with.
*/
BaseViewer.prototype.on = function(events, priority, callback, that) {
return this.get('eventBus').on(events, priority, callback, that);
};
/**
* Remove an event listener.
*
* @param {string|string[]} events The event(s).
* @param {Function} [callback] The callback.
*/
BaseViewer.prototype.off = function(events, callback) {
this.get('eventBus').off(events, callback);
};
/**
* Attach the viewer to an HTML element.
*
* @param {HTMLElement} parentNode The parent node to attach to.
*/
BaseViewer.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();
};
/**
* Get the definitions model element.
*
* @returns {ModdleElement} The definitions model element.
*/
BaseViewer.prototype.getDefinitions = function() {
return this._definitions;
};
/**
* Detach the viewer.
*
* @fires BaseViewer#DetachEvent
*/
BaseViewer.prototype.detach = function() {
const container = this._container,
parentNode = container.parentNode;
if (!parentNode) {
return;
}
/**
* A `detach` event.
*
* @event BaseViewer#DetachEvent
* @type {Object}
*/
this._emit('detach', {});
parentNode.removeChild(container);
};
BaseViewer.prototype._init = function(container, moddle, options) {
const baseModules = options.modules || this.getModules(options),
additionalModules = options.additionalModules || [],
staticModules = [
{
bpmnjs: [ 'value', this ],
moddle: [ 'value', moddle ]
}
];
const diagramModules = [].concat(staticModules, baseModules, additionalModules);
const 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} The return value after calling all event listeners.
*/
BaseViewer.prototype._emit = function(type, event) {
return this.get('eventBus').fire(type, event);
};
BaseViewer.prototype._createContainer = function(options) {
const container = domify('<div class="bjs-container"></div>');
assignStyle(container, {
width: ensureUnit(options.width),
height: ensureUnit(options.height),
position: options.position
});
return container;
};
BaseViewer.prototype._createModdle = function(options) {
const moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions);
return new BpmnModdle(moddleOptions);
};
BaseViewer.prototype._modules = [];
// helpers ///////////////
function addWarningsToError(err, warningsAry) {
err.warnings = warningsAry;
return err;
}
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)
const pattern = /unparsable content <([^>]+)> detected([\s\S]*)$/;
const 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;
}
const 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;
}
/* <project-logo> */
import {
open as openPoweredBy,
BPMNIO_IMG,
LOGO_STYLES,
LINK_STYLES
} 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) {
const img = BPMNIO_IMG;
const linkMarkup =
'<a href="http://bpmn.io" ' +
'target="_blank" ' +
'class="bjs-powered-by" ' +
'title="Powered by bpmn.io" ' +
'>' +
img +
'</a>';
const linkElement = domify(linkMarkup);
assignStyle(domQuery('svg', linkElement), LOGO_STYLES);
assignStyle(linkElement, LINK_STYLES, {
position: 'absolute',
bottom: '15px',
right: '15px',
zIndex: '100'
});
container.appendChild(linkElement);
domEvent.bind(linkElement, 'click', function(event) {
openPoweredBy();
event.preventDefault();
});
}
/* </project-logo> */