diff --git a/lib/Model.js b/lib/Model.js index ff112f65..50273aa2 100644 --- a/lib/Model.js +++ b/lib/Model.js @@ -24,8 +24,22 @@ function instance() { return INSTANCE; } +/** + * Instantiates a BPMN model tree from a given xml string. + * + * @param {String} xmlStr + * @param {String} [typeName] name of the root element, defaults to 'bpmn:Definitions' + * @param {Object} [options] to pass to the underlying reader + * @param {Function} callback the callback that is invoked with (err, result, parseContext) + */ function fromXML(xmlStr, typeName, options, callback) { + if (!_.isString(typeName)) { + callback = options; + options = typeName; + typeName = 'bpmn:Definitions'; + } + if (_.isFunction(options)) { callback = options; options = {}; diff --git a/lib/Modeler.js b/lib/Modeler.js index e11e9aca..bbf927c6 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -3,10 +3,12 @@ var Diagram = require('diagram-js'); var bpmnModule = require('./di').defaultModule, Viewer = require('./Viewer'); +require('./core/BpmnRegistry'); + require('./draw/BpmnRenderer'); -require('diagram-js/src/features/DragUI'); -require('diagram-js/src/features/SelectionUI'); +require('diagram-js/lib/features/dnd/Visuals'); +require('diagram-js/lib/features/selection/Visuals'); function Modeler(container) { @@ -20,7 +22,7 @@ Modeler.prototype.createDiagram = function() { return new Diagram({ canvas: { container: this.container }, modules: [ bpmnModule ], - components: [ 'selectionUI', 'dragUI' ] + components: [ 'selectionVisuals', 'dragVisuals', 'bpmnRegistry'] }); }; diff --git a/lib/Util.js b/lib/Util.js new file mode 100644 index 00000000..8d1bb423 --- /dev/null +++ b/lib/Util.js @@ -0,0 +1,24 @@ +var _ = require('lodash'); + +function failSafeAsync(fn) { + + return function() { + + var args = Array.prototype.slice.call(arguments); + + var done = args[args.length - 1]; + if (!done || !_.isFunction(done)) { + done = function(e) { + throw e; + }; + } + + try { + fn.apply(this, args); + } catch (e) { + done(e); + } + }; +} + +module.exports.failSafeAsync = failSafeAsync; \ No newline at end of file diff --git a/lib/Viewer.js b/lib/Viewer.js index 97cf25ab..3c622d07 100644 --- a/lib/Viewer.js +++ b/lib/Viewer.js @@ -1,11 +1,12 @@ var Diagram = require('diagram-js'); var Importer = require('./import/Importer'), - Model = require('./Model'); + Model = require('./Model'), + failSafeAsync = require('./Util').failSafeAsync; function initListeners(diagram, listeners) { - var events = diagram.get('events'); + var events = diagram.get('eventBus'); listeners.forEach(function(l) { events.on(l.event, l.handler); @@ -72,7 +73,7 @@ Viewer.prototype.saveSVG = function(options, done) { done(null, svg); }; -Viewer.prototype.importDefinitions = function(definitions, done) { +Viewer.prototype.importDefinitions = failSafeAsync(function(definitions, done) { if (this.diagram) { this.clear(); @@ -85,7 +86,7 @@ Viewer.prototype.importDefinitions = function(definitions, done) { this.definitions = definitions; Importer.importBpmnDiagram(this.diagram, definitions, done); -}; +}); Viewer.prototype.createDiagram = function() { return new Diagram({ canvas: { container: this.container }}); diff --git a/lib/core/BpmnRegistry.js b/lib/core/BpmnRegistry.js new file mode 100644 index 00000000..acd5133c --- /dev/null +++ b/lib/core/BpmnRegistry.js @@ -0,0 +1,55 @@ +var bpmnModule = require('../di').defaultModule; + + +/** + * @class + * + * A registry that keeps track of bpmn semantic / di elements and the + * corresponding shapes. + * + * @param {EventBus} events + * @param {ElementRegistry} elementRegistry + */ +function BpmnRegistry(events, elementRegistry) { + + var elements = { + di: {}, + semantic: {}, + diagramElement: {} + }; + + events.on('bpmn.element.add', function(e) { + var semantic = e.semantic, + id = semantic.id; + + elements.di[id] = e.di; + elements.semantic[id] = e.semantic; + elements.diagramElement[id] = e.diagramElement; + }); + + events.on('bpmn.element.removed', function(e) { + var semantic = e.semantic, + id = semantic.id; + + delete elements.di[id]; + delete elements.semantic[id]; + delete elements.diagramElement[id]; + }); + + function get(type) { + var collection = elements[type]; + + return function(id) { + return collection[id]; + }; + } + + // API + this.getSemantic = get('semantic'); + this.getDi = get('di'); + this.getDiagramElement = get('diagramElement'); +} + +bpmnModule.type('bpmnRegistry', [ 'eventBus', 'elementRegistry', BpmnRegistry ]); + +module.exports = BpmnRegistry; \ No newline at end of file diff --git a/lib/draw/BpmnRenderer.js b/lib/draw/BpmnRenderer.js index a65b5b1e..52e06813 100644 --- a/lib/draw/BpmnRenderer.js +++ b/lib/draw/BpmnRenderer.js @@ -1,14 +1,14 @@ var bpmnModule = require('../di').defaultModule; -require('diagram-js/src/core/Events'); -require('diagram-js/src/draw/Styles'); +require('diagram-js/lib/core/EventBus'); +require('diagram-js/lib/draw/Styles'); -var DefaultRenderer = require('diagram-js/src/draw/Renderer'); +var DefaultRenderer = require('diagram-js/lib/draw/Renderer'); var flattenPoints = DefaultRenderer.flattenPoints; -function BpmnRenderer(events, styles) { +function BpmnRenderer(events, styles, bpmnRegistry) { DefaultRenderer.apply(this, [ events, styles ]); @@ -394,6 +394,6 @@ function BpmnRenderer(events, styles) { BpmnRenderer.prototype = Object.create(DefaultRenderer.prototype); -bpmnModule.type('renderer', [ 'events', 'styles', BpmnRenderer ]); +bpmnModule.type('renderer', [ 'eventBus', 'styles', 'bpmnRegistry', BpmnRenderer ]); module.exports = BpmnRenderer; \ No newline at end of file diff --git a/lib/import/Importer.js b/lib/import/Importer.js index 925511e5..f97713c9 100644 --- a/lib/import/Importer.js +++ b/lib/import/Importer.js @@ -1,12 +1,12 @@ var _ = require('lodash'); -var BpmnTreeWalker = require('./BpmnTreeWalker'); +var BpmnTreeWalker = require('./BpmnTreeWalker'), + Util = require('../Util'); function importBpmnDiagram(diagram, definitions, done) { var canvas = diagram.get('canvas'); - - var shapes = {}; + var events = diagram.get('eventBus'); var visitor = { @@ -14,6 +14,12 @@ function importBpmnDiagram(diagram, definitions, done) { var shape; + function fire(type, shape) { + events.fire('bpmn.element.' + type, { + semantic: element, di: di, diagramElement: shape + }); + } + if (di.$type === 'bpmndi:BPMNShape') { var bounds = di.bounds; @@ -24,6 +30,7 @@ function importBpmnDiagram(diagram, definitions, done) { parent: parent }; + fire('add', shape); canvas.addShape(shape); } else { @@ -32,10 +39,12 @@ function importBpmnDiagram(diagram, definitions, done) { }); shape = { id: element.id, type: element.$type, waypoints: waypoints }; + + fire('add', shape); canvas.addConnection(shape); } - shapes[element.id] = shape; + fire('added', shape); return shape; }, @@ -46,10 +55,9 @@ function importBpmnDiagram(diagram, definitions, done) { }; var walker = new BpmnTreeWalker(visitor); - walker.handleDefinitions(definitions); - + done(); } -module.exports.importBpmnDiagram = importBpmnDiagram; \ No newline at end of file +module.exports.importBpmnDiagram = Util.failSafeAsync(importBpmnDiagram); \ No newline at end of file diff --git a/test/spec/browser/ModelerSpec.js b/test/spec/browser/ModelerSpec.js new file mode 100644 index 00000000..571a95ed --- /dev/null +++ b/test/spec/browser/ModelerSpec.js @@ -0,0 +1,71 @@ +var fs = require('fs'); + +var BpmnModel = require('../../../lib/Model'), + Modeler = require('../../../lib/Modeler'); + +var Matchers = require('../Matchers'); + +describe('Modeler', function() { + + var bpmnModel = BpmnModel.instance(); + + function read(xml, opts, callback) { + return BpmnModel.fromXML(xml, 'bpmn:Definitions', opts, callback); + } + + + var container; + + + beforeEach(Matchers.add); + + beforeEach(function() { + container = document.createElement('div'); + document.getElementsByTagName('body')[0].appendChild(container); + }); + + afterEach(function() { + container.parentNode.removeChild(container); + }); + + + it('should import simple process', function(done) { + + var xml = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf8'); + + var renderer = new Modeler(container); + + renderer.importXML(xml, function(err) { + done(err); + }); + }); + + + it('should import empty definitions', function(done) { + + var xml = fs.readFileSync('test/fixtures/bpmn/empty-definitions.bpmn', 'utf8'); + + var renderer = new Modeler(container); + + renderer.importXML(xml, function(err) { + + done(err); + }); + }); + + + it('should handle errors', function(done) { + + var xml = 'invalid stuff'; + + var renderer = new Modeler(container); + + renderer.importXML(xml, function(err) { + + expect(err).toBeDefined(); + + done(); + }); + }); + +}); \ No newline at end of file diff --git a/test/spec/browser/ImporterSpec.js b/test/spec/browser/ViewerSpec.js similarity index 97% rename from test/spec/browser/ImporterSpec.js rename to test/spec/browser/ViewerSpec.js index 37aaf84a..ed30e166 100644 --- a/test/spec/browser/ImporterSpec.js +++ b/test/spec/browser/ViewerSpec.js @@ -5,7 +5,7 @@ var BpmnModel = require('../../../lib/Model'), var Matchers = require('../Matchers'); -describe('Importer', function() { +describe('Viewer', function() { var bpmnModel = BpmnModel.instance(); diff --git a/test/spec/browser/import/ImporterSpec.js b/test/spec/browser/import/ImporterSpec.js new file mode 100644 index 00000000..9335e88c --- /dev/null +++ b/test/spec/browser/import/ImporterSpec.js @@ -0,0 +1,80 @@ +var fs = require('fs'), + Diagram = require('diagram-js/lib/Diagram'); + +var BpmnModel = require('../../../../lib/Model'); +var Importer = require('../../../../lib/import/Importer'); + +var bpmnModule = require('../../../../lib/di').defaultModule; + +require('../../../../lib/core/BpmnRegistry'); +require('../../../../lib/draw/BpmnRenderer'); + +var Matchers = require('../../Matchers'); + +describe('import/Importer', function() { + + var bpmnModel = BpmnModel.instance(); + + function read(xml, opts, callback) { + return BpmnModel.fromXML(xml, 'bpmn:Definitions', opts, callback); + } + + var container; + + + beforeEach(Matchers.add); + + beforeEach(function() { + container = document.createElement('div'); + document.getElementsByTagName('body')[0].appendChild(container); + }); + + afterEach(function() { + container.parentNode.removeChild(container); + }); + + + function createDiagram() { + return new Diagram({ + canvas: { container: container }, + modules: [ bpmnModule ], + components: [ 'bpmnRegistry'] + }); + } + + it('should fire bpmn.element.add during import', function(done) { + + // given + var xml = fs.readFileSync('test/fixtures/bpmn/simple.bpmn', 'utf8'); + + var diagram = createDiagram(); + + var events = []; + + // log events + diagram.get('eventBus').on('bpmn.element.add', function(e) { + events.push({ type: 'add', semantic: e.semantic.id, di: e.di.id, diagramElement: e.diagramElement.id }); + }); + + BpmnModel.fromXML(xml, function(err, definitions) { + if (err) { + return done(err); + } + + // when + Importer.importBpmnDiagram(diagram, definitions, function(err) { + + // then + expect(events).toEqual([ + { type: 'add', semantic: 'SubProcess_1', di: '_BPMNShape_SubProcess_2', diagramElement: 'SubProcess_1' }, + { type: 'add', semantic: 'StartEvent_1', di: '_BPMNShape_StartEvent_2', diagramElement: 'StartEvent_1' }, + { type: 'add', semantic: 'Task_1', di: '_BPMNShape_Task_2', diagramElement: 'Task_1' }, + { type: 'add', semantic: 'SequenceFlow_1', di: 'BPMNEdge_SequenceFlow_1', diagramElement: 'SequenceFlow_1' } + ]); + + done(err); + }); + }); + }); + +}); \ No newline at end of file