chore(Viewer+Modeler): inherit from Diagram

* simplifies event handling
* relies on Diagram#clear to reset the diagram before
  successive imports
* allows diagram services to be re-used across imports
* allows diagram services to be injected (or retrieved)
  before import

Closes #237
This commit is contained in:
Nico Rehwaldt 2016-03-16 23:11:05 +01:00
parent ebfc2a40ea
commit ff0d88bb90
4 changed files with 291 additions and 182 deletions

View File

@ -109,8 +109,8 @@ Modeler.prototype.createDiagram = function(done) {
return this.importXML(initialDiagram, done); return this.importXML(initialDiagram, done);
}; };
Modeler.prototype.createModdle = function() { Modeler.prototype._createModdle = function(options) {
var moddle = Viewer.prototype.createModdle.call(this); var moddle = Viewer.prototype._createModdle.call(this, options);
moddle.ids = new Ids([ 32, 36, 1 ]); moddle.ids = new Ids([ 32, 36, 1 ]);

View File

@ -18,17 +18,12 @@ var domify = require('min-dom/lib/domify'),
var Diagram = require('diagram-js'), var Diagram = require('diagram-js'),
BpmnModdle = require('bpmn-moddle'); BpmnModdle = require('bpmn-moddle');
var inherits = require('inherits');
var Importer = require('./import/Importer'); 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) { function checkValidationError(err) {
// check if we can help the user by indicating wrong BPMN 2.0 xml // check if we can help the user by indicating wrong BPMN 2.0 xml
@ -110,39 +105,23 @@ function ensureUnit(val) {
*/ */
function Viewer(options) { function Viewer(options) {
this.options = options = assign({}, DEFAULT_OPTIONS, options || {}); options = assign({}, DEFAULT_OPTIONS, options);
this.moddle = this.createModdle(); this.moddle = this._createModdle(options);
var parent = options.container; this.container = this._createContainer(options);
// 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('<div class="bjs-container"></div>');
parent.appendChild(container);
assign(container.style, {
width: ensureUnit(options.width),
height: ensureUnit(options.height),
position: options.position
});
/* <project-logo> */ /* <project-logo> */
addProjectLogo(container); addProjectLogo(this.container);
/* </project-logo> */ /* </project-logo> */
this._init(this.container, this.moddle, options);
} }
inherits(Viewer, Diagram);
module.exports = Viewer; module.exports = Viewer;
@ -206,10 +185,6 @@ Viewer.prototype.saveXML = function(options, done) {
this.moddle.toXML(definitions, options, done); 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 * Export the currently displayed BPMN 2.0 diagram as
* an SVG image. * an SVG image.
@ -258,15 +233,9 @@ Viewer.prototype.saveSVG = function(options, done) {
* @param {String} name * @param {String} name
* *
* @return {Object} diagram service instance * @return {Object} diagram service instance
*
* @method Viewer#get
*/ */
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. * Invoke a function in the context of this viewer.
@ -280,94 +249,61 @@ Viewer.prototype.get = function(name) {
* @param {Function} fn to be invoked * @param {Function} fn to be invoked
* *
* @return {Object} the functions return value * @return {Object} the functions return value
*
* @method Viewer#invoke
*/ */
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. * Remove all drawn elements from the viewer.
* *
* After calling this method the viewer can still * After calling this method the viewer can still
* be reused for opening another diagram. * be reused for opening another diagram.
*
* @method Viewer#clear
*/ */
Viewer.prototype.clear = function() {
var diagram = this.diagram;
if (diagram) { Viewer.prototype.importDefinitions = function(definitions, done) {
diagram.destroy();
// use try/catch to not swallow synchronous exceptions
// that may be raised during model parsing
try {
if (this.definitions) {
// clear existing rendered diagram
this.clear();
}
// update definitions
this.definitions = definitions;
// perform graphical import
Importer.importBpmnDiagram(this, definitions, done);
} catch (e) {
// handle synchronous errors
done(e);
} }
}; };
Viewer.prototype.getModules = function() {
return this._modules;
};
/** /**
* Destroy the viewer instance and remove all its remainders * Destroy the viewer instance and remove all its
* from the document tree. * remainders from the document tree.
*/ */
Viewer.prototype.destroy = function() { Viewer.prototype.destroy = function() {
// clear underlying diagram
this.clear();
// remove container // diagram destroy
Diagram.prototype.destroy.call(this);
// dom detach
domRemove(this.container); domRemove(this.container);
}; };
/** /**
* Register an event listener on the viewer * Register an event listener
* *
* Remove a previously added listener via {@link #off(event, callback)}. * Remove a previously added listener via {@link #off(event, callback)}.
* *
@ -376,50 +312,79 @@ Viewer.prototype.destroy = function() {
* @param {Function} callback * @param {Function} callback
* @param {Object} [that] * @param {Object} [that]
*/ */
Viewer.prototype.on = function(event, priority, callback, that) { Viewer.prototype.on = function(event, priority, callback, target) {
var diagram = this.diagram, return this.get('eventBus').on(event, priority, callback, target);
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 * De-register an event listener
* *
* @param {String} event * @param {String} event
* @param {Function} callback * @param {Function} callback
*/ */
Viewer.prototype.off = function(event, callback) { Viewer.prototype.off = function(event, callback) {
var filter, this.get('eventBus').off(event, callback);
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);
}
}; };
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);
};
Viewer.prototype._createContainer = function(options) {
var parent = options.container,
container;
// support jquery element
// unwrap it if passed
if (parent.get) {
parent = parent.get(0);
}
// support selector
if (isString(parent)) {
parent = domQuery(parent);
}
container = domify('<div class="bjs-container"></div>');
assign(container.style, {
width: ensureUnit(options.width),
height: ensureUnit(options.height),
position: options.position
});
parent.appendChild(container);
return container;
};
Viewer.prototype._createModdle = function(options) {
var moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions);
return new BpmnModdle(moddleOptions);
};
// modules the viewer is composed of // modules the viewer is composed of
Viewer.prototype._modules = [ Viewer.prototype._modules = [
require('./core'), require('./core'),

View File

@ -107,37 +107,6 @@ describe('Modeler', function() {
}); });
describe('ids', function() {
it('should populate ids on import', function(done) {
// given
var xml = require('../fixtures/bpmn/simple.bpmn');
var modeler = new Modeler({ container: container });
// when
modeler.importXML(xml, function(err) {
var moddle = modeler.get('moddle');
var elementRegistry = modeler.get('elementRegistry');
var subProcess = elementRegistry.get('SubProcess_1').businessObject;
var bpmnEdge = elementRegistry.get('SequenceFlow_3').businessObject.di;
// then
expect(moddle.ids.assigned('SubProcess_1')).to.eql(subProcess);
expect(moddle.ids.assigned('BPMNEdge_SequenceFlow_3')).to.eql(bpmnEdge);
done();
});
});
});
describe('translate support', function() { describe('translate support', function() {
var xml = require('../fixtures/bpmn/simple.bpmn'); var xml = require('../fixtures/bpmn/simple.bpmn');
@ -275,6 +244,81 @@ describe('Modeler', function() {
}); });
describe('ids', function() {
it('should provide ids with moddle', function() {
// given
var modeler = new Modeler({ container: container });
// when
var moddle = modeler.get('moddle');
// then
expect(moddle.ids).to.exist;
});
it('should populate ids on import', function(done) {
// given
var xml = require('../fixtures/bpmn/simple.bpmn');
var modeler = new Modeler({ container: container });
var moddle = modeler.get('moddle');
var elementRegistry = modeler.get('elementRegistry');
// when
modeler.importXML(xml, function(err) {
var subProcess = elementRegistry.get('SubProcess_1').businessObject;
var bpmnEdge = elementRegistry.get('SequenceFlow_3').businessObject.di;
// then
expect(moddle.ids.assigned('SubProcess_1')).to.eql(subProcess);
expect(moddle.ids.assigned('BPMNEdge_SequenceFlow_3')).to.eql(bpmnEdge);
done();
});
});
it('should clear ids before re-import', function(done) {
// given
var someXML = require('../fixtures/bpmn/simple.bpmn'),
otherXML = require('../fixtures/bpmn/basic.bpmn');
var modeler = new Modeler({ container: container });
var moddle = modeler.get('moddle');
var elementRegistry = modeler.get('elementRegistry');
// when
modeler.importXML(someXML, function() {
modeler.importXML(otherXML, function() {
var task = elementRegistry.get('Task_1').businessObject;
// then
// not in other.bpmn
expect(moddle.ids.assigned('SubProcess_1')).to.be.false;
// in other.bpmn
expect(moddle.ids.assigned('Task_1')).to.eql(task);
done();
});
});
});
});
it('should handle errors', function(done) { it('should handle errors', function(done) {
var xml = 'invalid stuff'; var xml = 'invalid stuff';
@ -298,7 +342,7 @@ describe('Modeler', function() {
describe('dependency injection', function() { describe('dependency injection', function() {
it('should be available via di as <bpmnjs>', function(done) { it('should provide self as <bpmnjs>', function(done) {
var xml = require('../fixtures/bpmn/simple.bpmn'); var xml = require('../fixtures/bpmn/simple.bpmn');
@ -310,6 +354,49 @@ describe('Modeler', function() {
}); });
}); });
it('should allow Diagram#get before import', function() {
// when
var modeler = new Modeler({ container: container });
// then
var eventBus = modeler.get('eventBus');
expect(eventBus).to.exist;
});
it('should keep references to services across re-import', function(done) {
// given
var someXML = require('../fixtures/bpmn/simple.bpmn'),
otherXML = require('../fixtures/bpmn/basic.bpmn');
var modeler = new Modeler({ container: container });
var eventBus = modeler.get('eventBus'),
canvas = modeler.get('canvas');
// when
modeler.importXML(someXML, function() {
// then
expect(modeler.get('canvas')).to.equal(canvas);
expect(modeler.get('eventBus')).to.equal(eventBus);
modeler.importXML(otherXML, function() {
// then
expect(modeler.get('canvas')).to.equal(canvas);
expect(modeler.get('eventBus')).to.equal(eventBus);
done();
});
});
});
}); });
}); });

View File

@ -2,6 +2,8 @@
var TestContainer = require('mocha-test-container-support'); var TestContainer = require('mocha-test-container-support');
var Diagram = require('diagram-js/lib/Diagram');
var Viewer = require('../../lib/Viewer'); var Viewer = require('../../lib/Viewer');
var inherits = require('inherits'); var inherits = require('inherits');
@ -46,8 +48,11 @@ describe('Viewer', function() {
// mimic re-import of same diagram // mimic re-import of same diagram
viewer.importXML(xml, function(err, warnings) { viewer.importXML(xml, function(err, warnings) {
if (err) {
return done(err);
}
// then // then
expect(err).to.exist;
expect(warnings.length).to.equal(0); expect(warnings.length).to.equal(0);
done(); done();
@ -57,6 +62,16 @@ describe('Viewer', function() {
}); });
it('should be instance of Diagram', function() {
// when
var viewer = new Viewer({ container: container });
// then
expect(viewer).to.be.instanceof(Diagram);
});
describe('defaults', function() { describe('defaults', function() {
it('should use <body> as default parent', function(done) { it('should use <body> as default parent', function(done) {
@ -259,7 +274,7 @@ describe('Viewer', function() {
describe('dependency injection', function() { describe('dependency injection', function() {
it('should be available via di as <bpmnjs>', function(done) { it('should provide self as <bpmnjs>', function(done) {
var xml = require('../fixtures/bpmn/simple.bpmn'); var xml = require('../fixtures/bpmn/simple.bpmn');
@ -271,6 +286,48 @@ describe('Viewer', function() {
}); });
}); });
it('should allow Diagram#get before import', function() {
// when
var viewer = new Viewer({ container: container });
// then
var eventBus = viewer.get('eventBus');
expect(eventBus).to.exist;
});
it('should keep references to services across re-import', function(done) {
// given
var someXML = require('../fixtures/bpmn/simple.bpmn'),
otherXML = require('../fixtures/bpmn/basic.bpmn');
var viewer = new Viewer({ container: container });
var eventBus = viewer.get('eventBus'),
canvas = viewer.get('canvas');
// when
viewer.importXML(someXML, function() {
// then
expect(viewer.get('canvas')).to.equal(canvas);
expect(viewer.get('eventBus')).to.equal(eventBus);
viewer.importXML(otherXML, function() {
// then
expect(viewer.get('canvas')).to.equal(canvas);
expect(viewer.get('eventBus')).to.equal(eventBus);
done();
});
});
});
}); });