diff --git a/Gruntfile.js b/Gruntfile.js index 6f071280..78dab1e7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -57,7 +57,8 @@ module.exports = function(grunt) { options: { alias: [ '<%= config.sources %>/main.js:bpmn', - '<%= config.sources %>/Model.js:bpmn/Model' + '<%= config.sources %>/Model.js:bpmn/Model', + '<%= config.sources %>/Diagram.js:bpmn/Diagram' ] }, sources: { diff --git a/example/index.html b/example/index.html index c9093419..9062e7f8 100644 --- a/example/index.html +++ b/example/index.html @@ -1,18 +1,20 @@ bpmn-js demo +
Drop files here
-
+
@@ -36,6 +38,11 @@ if (err) { console.error(err); } else { + var diagram = new Diagram(document.getElementById('canvas')); + diagram.importDefinitions(definitions, function() { + console.log("import done!!"); + }); + console.log(definitions); } }); diff --git a/example/style.css b/example/style.css new file mode 100644 index 00000000..de79351a --- /dev/null +++ b/example/style.css @@ -0,0 +1,160 @@ +/** + * outline styles + */ +.djs-outline { + fill: none; + visibility: hidden; +} + +.djs-group.hover .djs-outline, +.djs-group.selected .djs-outline { + visibility: visible; + stroke-dasharray: 2,4; +} + +.djs-group.selected .djs-outline { + stroke: blue; + stroke-width: 1px; +} + +.djs-group.hover .djs-outline { + stroke: red; + stroke-width: 1px; +} + +.djs-group.drop-ok .djs-visual { + fill: green; +} + +.djs-group.drop-not-ok .djs-visual { + fill: red; +} + +/** + * drag styles + */ +.djs-dragger { + fill: white; + opacity: 0.3; + stroke: #333; + stroke-dasharray: 2,1; +} + +.djs-shape.djs-dragging { + visibility: hidden; +} + +/** + * hit shape styles + */ +.djs-hit { + stroke-opacity: 0.0; + stroke-width: 10px; + stroke: fuchsia; + fill: none; +} + +/** + * shape / connection basic styles + */ +.djs-shape .djs-visual { + stroke: black; + stroke-width: 2px; + fill: #F3F3F3; +} + +.djs-connection .djs-visual { + stroke-width: 2px; + stroke: #333; + fill: none; +} + +.djs-connection .djs-bendpoint { + visibility: hidden; +} + +.djs-connection:hover .djs-bendpoint { + visibility: visible; +} + +.djs-connection .djs-bendpoint:hover { + stroke: #CCC; + stroke-width: 1px; + fill: yellow; +} + +.djs-connection-marker { + fill: black; + stroke: none; +} + +.djs-menu-vertical, .djs-menu-horizontal { + height: 35px; + background-color: #52B415; + padding-left: 10px; +} +.djs-menu-vertical > button { + display:inline-block; + margin-bottom:0; + font-weight:400; + text-align:center; + vertical-align:middle; + cursor:pointer; + background: #52B415 none; + color: white; + border: 0px #ffffff; + border-right-width: 1px; + border-bottom-style: solid; + border-left-width: 1px; + white-space:nowrap; + padding:5px 12px; + font-size:15px; + line-height:1.428571429; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; +} +.djs-menu-vertical > .button:focus{ + outline:thin dotted #333; + outline:5px auto -webkit-focus-ring-color; + outline-offset:-2px; +} +.djs-menu-vertical > .btn:hover,.btn:focus { + color:#707070;text-decoration:none; +} + +.djs-menu-brand { + background: url("logo.png") scroll no-repeat transparent; + background-size: 35px 35px; + width: 35px; + height: 35px; + float: right; +} + +/* Button icons*/ +.djs-undo-button > i:before { + font-family: awesomesymbols; + content: "\f0e2"; + font-style: normal; +} + +.djs-redo-button > i:before { + font-family: awesomesymbols; + content: "\f01e"; + font-style: normal; +} + +.djs-rect-button > i { + width: 10px; + height: 10px; + border: 1px solid; + display: block; +} + +/* Fonts */ +@font-face { + font-family: "awesomesymbols"; + src: url("../fonts/fontawesome-webfont.woff") format('woff'); + font-weight: normal; +} diff --git a/lib/BpmnTreeWalker.js b/lib/BpmnTreeWalker.js new file mode 100644 index 00000000..f890581a --- /dev/null +++ b/lib/BpmnTreeWalker.js @@ -0,0 +1,157 @@ +var _ = require('lodash'); + +function BpmnTraverser(visitor) { + + var elementDiMap = {}; + var elementGfxMap = {}; + + ///// Helpers ///////////////////////////////// + + function contextual(fn, ctx) { + return function(e) { + fn(e, ctx); + }; + } + + function is(element, type) { + return element.__isInstanceOf(type); + } + + function visit(element, di, ctx) { + + // call visitor + var gfx = visitor(element, di, ctx); + + // and log returned result + elementGfxMap[element.id] = gfx; + + return gfx; + } + + ////// DI handling //////////////////////////// + + function buildDiMap(definitions) { + _.forEach(definitions.diagrams, handleDiagram); + } + + function registerDi(element) { + var bpmnElement = element.bpmnElement; + elementDiMap[bpmnElement.id] = element; + } + + function getDi(bpmnElement) { + var id = bpmnElement.id; + return id ? elementDiMap[id] : null; + } + + function handleDiagram(diagram) { + handlePlane(diagram.plane); + } + + function handlePlane(plane) { + registerDi(plane); + + _.forEach(plane.planeElement, handlePlaneElement); + } + + function handlePlaneElement(planeElement) { + registerDi(planeElement); + } + + + ////// Semantic handling ////////////////////// + + function handleDefinitions(definitions, diagram) { + buildDiMap(definitions); + + // make sure we walk the correct bpmnElement + + var diagrams = definitions.diagrams; + + if (diagram && diagrams.indexOf(diagram) === -1) { + throw new Error('diagram not part of bpmn:Definitions'); + } + + if (!diagram) { + diagram = diagrams[0]; + } + + var rootElement = diagram.plane.bpmnElement; + + if (is(rootElement, 'bpmn:Process')) { + handleProcess(rootElement); + } else + if (is(rootElement, 'bpmn:Collaboration')) { + handleCollaboration(rootElement); + } else { + throw new Error('unsupported root element for bpmndi:Diagram: ' + type.name); + } + } + + function handleFlowNode(flowNode, context) { + var di = getDi(flowNode); + + var childCtx = visit(flowNode, di, context); + + if (is(flowNode, 'bpmn:FlowElementsContainer')) { + handleFlowElementsContainer(flowNode, childCtx); + } + } + + function handleSequenceFlow(sequenceFlow, context) { + var di = getDi(sequenceFlow); + + visit(sequenceFlow, di, context); + } + + function handleFlowElementsContainer(container, context) { + + var sequenceFlows = []; + + // handle FlowNode(s) + _.forEach(container.flowElements, function(e) { + if (is(e, 'bpmn:SequenceFlow')) { + sequenceFlows.push(e); + } else + if (is(e, 'bpmn:FlowNode')) { + handleFlowNode(e, context); + } else { + throw new Error('unrecognized element: ' + e); + } + }); + + // handle SequenceFlows + _.forEach(sequenceFlows, contextual(handleSequenceFlow, context)); + } + + function handleParticipant(participant, context) { + var di = getDi(participant); + + var newCtx = visit(participant, di, context); + + var process = participant.processRef; + if (process) { + handleProcess(process, newCtx); + } + } + + function handleProcess(process, context) { + handleFlowElementsContainer(process, context); + } + + function handleCollaboration(collaboration) { + + _.forEach(collaboration.participants, contextual(handleParticipant)); + + // TODO: handle message flows + } + + + ///// API //////////////////////////////// + + return { + handleDefinitions: handleDefinitions + }; +} + +module.exports = BpmnTraverser; \ No newline at end of file diff --git a/lib/Diagram.js b/lib/Diagram.js new file mode 100644 index 00000000..0d120df8 --- /dev/null +++ b/lib/Diagram.js @@ -0,0 +1,15 @@ +var DiagramJS = require('diagram-js'); +var Importer = require('./Importer'); + +function Diagram(container) { + this.container = container; +} + +Diagram.prototype.importDefinitions = function(definitions, done) { + + this.diagram = new DiagramJS({ canvas: { container: this.container }}); + + Importer.importBpmnDiagram(this.diagram, definitions, done); +}; + +module.exports = Diagram; \ No newline at end of file diff --git a/lib/Importer.js b/lib/Importer.js new file mode 100644 index 00000000..438bddb8 --- /dev/null +++ b/lib/Importer.js @@ -0,0 +1,23 @@ +var BpmnTreeWalker = require('./BpmnTreeWalker'); + +function importBpmnDiagram(diagram, definitions, done) { + + var canvas = diagram.resolve('canvas'); + + var visitor = function(element, di, parent) { + + if (di.$type === 'bpmndi:BPMNShape') { + var bounds = di.bounds; + + canvas.addShape({ id: element.id, type: element.$type, x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }); + } + }; + + var walker = new BpmnTreeWalker(visitor); + + walker.handleDefinitions(definitions); + + done(); +} + +module.exports.importBpmnDiagram = importBpmnDiagram; \ No newline at end of file diff --git a/lib/main.js b/lib/main.js index e69de29b..68b5c21e 100644 --- a/lib/main.js +++ b/lib/main.js @@ -0,0 +1,6 @@ +var Diagram = require('./Diagram'), + Model = require('./Model'); + + +module.exports.Diagram = Diagram; +module.exports.Model = Model; \ No newline at end of file diff --git a/package.json b/package.json index 07f99621..ea17380a 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "xsd-schema-validator": "0.0.3", "sax": "~0.6.0", "lodash": "~2.4.0", - "eve": "0.4.1" + "eve": "0.4.1", + "brfs": "~1.0.0" }, "dependencies": { "moddle": "~0.0.1", diff --git a/test/config/karma.unit.js b/test/config/karma.unit.js index a49e8f3f..94b501ef 100644 --- a/test/config/karma.unit.js +++ b/test/config/karma.unit.js @@ -23,7 +23,8 @@ module.exports = function(karma) { // browserify configuration browserify: { debug: true, - watch: true + watch: true, + transform: [ 'debowerify', 'brfs' ] } }); }; diff --git a/test/fixtures/bpmn/collaboration.bpmn b/test/fixtures/bpmn/collaboration.bpmn new file mode 100644 index 00000000..7ff95ad4 --- /dev/null +++ b/test/fixtures/bpmn/collaboration.bpmn @@ -0,0 +1,51 @@ + + + + + + + + + + + + + Task_1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/browser/ImporterSpec.js b/test/spec/browser/ImporterSpec.js new file mode 100644 index 00000000..b33a40dd --- /dev/null +++ b/test/spec/browser/ImporterSpec.js @@ -0,0 +1,39 @@ +var fs = require('fs'); + +var BpmnModel = require('../../../lib/Model'), + Diagram = require('../../../lib/Diagram'); + +var Matchers = require('../Matchers'); + +describe('Importer', function() { + + var bpmnModel = BpmnModel.instance(); + + function read(xml, opts, callback) { + return BpmnModel.fromXML(xml, 'bpmn:Definitions', opts, callback); + } + + beforeEach(Matchers.add); + + var container; + + 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'); + + read(xml, function(err, result) { + var diagram = new Diagram(container); + + diagram.importDefinitions(result, done); + }); + }); +}); \ No newline at end of file diff --git a/test/spec/node/import/BpmnTreeWalkerSpec.js b/test/spec/node/import/BpmnTreeWalkerSpec.js new file mode 100644 index 00000000..9c1174e0 --- /dev/null +++ b/test/spec/node/import/BpmnTreeWalkerSpec.js @@ -0,0 +1,96 @@ +var _ = require('lodash'); + +var BpmnModel = require('../../../../lib/Model'), + BpmnTreeWalker = require('../../../../lib/BpmnTreeWalker'); + +var Helper = require('../Helper'), + Matchers = require('../../Matchers'); + + +describe('BpmnTreeWalker', function() { + + function readBpmnDiagram(file) { + return Helper.readFile('test/fixtures/bpmn/' + file); + } + + function read(xml, root, opts, callback) { + return BpmnModel.fromXML(xml, root, opts, callback); + } + + function readFile(file, root, opts, callback) { + return read(readBpmnDiagram(file), root, opts, callback); + } + + beforeEach(Matchers.add); + + var bpmnModel = BpmnModel.instance(); + + + it('should walk simple process', function(done) { + + readFile('simple.bpmn', 'bpmn:Definitions', function(err, definitions) { + + if (err) { + done(err); + } else { + + var drawn = []; + + var visitor = function(element, di, ctx) { + var id = element.id; + + drawn.push({ id: id, parent: ctx }); + + return id; + }; + + new BpmnTreeWalker(visitor).handleDefinitions(definitions); + + var expectedDrawn = [ + { id: 'SubProcess_1', parent: undefined }, + { id: 'StartEvent_1', parent: 'SubProcess_1' }, + { id: 'Task_1', parent: 'SubProcess_1' }, + { id: 'SequenceFlow_1', parent: 'SubProcess_1' } ]; + + + expect(drawn).toDeepEqual(expectedDrawn); + + done(); + } + }); + }); + + it('should walk collaboration', function(done) { + + readFile('collaboration.bpmn', 'bpmn:Definitions', function(err, definitions) { + + if (err) { + done(err); + } else { + + var drawn = []; + + var visitor = function(element, di, ctx) { + var id = element.id; + + drawn.push({ id: id, parent: ctx }); + + return id; + }; + + new BpmnTreeWalker(visitor).handleDefinitions(definitions); + + var expectedDrawn = [ + { id : '_Participant_2', parent : undefined }, + { id : 'Task_1', parent : '_Participant_2' }, + { id : 'Participant_1', parent : undefined }, + { id : 'StartEvent_1', parent : 'Participant_1' } ]; + + + expect(drawn).toDeepEqual(expectedDrawn); + + done(); + } + }); + }); +}); \ No newline at end of file