flowblocks
Version:
Reusable flow diagram blocks
344 lines (286 loc) • 13 kB
JavaScript
const jointjs = require("jointjs")
const helper = require('./helper')
const EVENTS_DICT = require('./events-dict')
const shortid = require('shortid');
class Flow {
constructor(options) {
this.options = {
};
this.graph = {};
this.paper = {};
this._blocks = [];
Object.assign(this.options, options);
this._initialize();
this.emitter = undefined;
}
_initialize() {
}
create(paperDivId, emitter, name, bId) {
var self = this;
this.emitter = emitter;
this.graph = new jointjs.dia.Graph;
this.paper = new jointjs.dia.Paper({
el: document.getElementById(paperDivId),
width: 1400,
height: 960,
gridSize: 1,
model: self.graph,
// background: {
// color: '#F2EAD7'
// },
snapLinks: true,
linkPinning: false,
embeddingMode: true,
clickThreshold: 5,
defaultLink: new jointjs.dia.Link({
attrs: {
'.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' },
'.connection': { stroke: 'blue' },
'.marker-source': { d: 'M 15 0 L 0 5 L 15 15 z', opacity: '0', stroke: 'orange' },
'.marker-arrowhead[end="source"]': { fill: 'red', d: 'M 10 0 L 0 5 L 10 10 z', opacity: '0' }
},
router: { name: 'metro' },
}),
defaultConnectionPoint: { name: 'boundary' },
highlighting: {
'default': {
name: 'stroke',
options: {
padding: 6
}
},
'embedding': {
name: 'addClass',
options: {
className: 'highlighted-parent'
}
}
},
validateEmbedding: function (childView, parentView) {
return parentView.model instanceof jointjs.shapes.devs.Coupled;
},
validateConnection: function (cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
// console.log('Source', cellViewS, magnetS )
var sourceElement = cellViewS.model;
// console.log('Target', cellViewT, magnetT )
var targetElement = cellViewT.model;
var freeInPorts = targetElement.freePorts('in');
var freePort = freeInPorts.find(port=>{
return port.id == magnetT.getAttribute('port');
})
var portIsFree = freePort ? true : false;
return magnetS != magnetT && magnetT.getAttribute('port-group') == 'in' && cellViewS != cellViewT && portIsFree;
},
validateMagnet: function (cellView, magnet, evt) {
// by default passive magnets cant create connections
if (magnet.getAttribute('magnet') == 'passive') {
return false;
}
// check if there is a free output port
var sourceElement = cellView.model;
var freePorts = sourceElement.freePorts('out')
return freePorts.length > 0;
}
});
joint.dia.Link.prototype.toolMarkup = [
'<g class="link-tool">',
'<g class="tool-remove" event="remove">',
'<circle r="11" />',
'<path transform="scale(.8) translate(-16, -16)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z" />',
'<title>Remove link.</title>',
'</g>',
'</g>' // <-- missing
].join('');
var graphId = bId ? bId : shortid.generate();
var graphName = name ? name : 'Flowblocks #'+graphId;
this.graph.set('name', graphName);
this.graph.set('id', graphId);
this.graph.set('created', Date.now());
this._bindConnectionEvents();
this._bindToolsEvents();
this._bindInteractionEvents();
this.emitter.emit(EVENTS_DICT.EVENTS.FLOWBLOCKS_CREATE_SUCCESS, this.graph.get('name'), this.graph.get('id'), this.graph.get('version'));
return this;
}
removeAllBlocks(){
this.graph.removeCells(this._blocks);
this._blocks = [];
}
/**
* Traverser current model specification that is presented in Flowblocks
* Assumes linear/sequential graph.
* @returns {Array} Array of {p: previous, c: current, n: next} objects holding blocks
*/
//(previousBlock, currentBlock, nextBlock)
traverseSequential(){
var result = []; // {p: previous, c: current, n: next}
// find start block
var inputBlock = undefined;
inputBlock = this._blocks.find(block=>{
return block.get('_template') == 'Start';
})
if(!inputBlock){
return;
console.warn('No input block found.');
}
// traverse graph
var blocks = [];
this.graph.bfs(inputBlock, function(block){
blocks.push(block)
})
// build result object
for(var i=0; i<blocks.length;i++){
var pIdx = i-1>=0?i-1:i;
var nIdx = i+1<blocks.length?i+1:i;
var previous = blocks[pIdx];
var current = blocks[i];
var next = blocks[nIdx];
result.push({
p: previous.get('blockId') != current.get('blockId')?previous:undefined,
c: current,
n: next.get('blockId') != current.get('blockId')?next:undefined
})
}
return result;
}
/**
* Imports flowblocks graph
* When ty
* @param {*} graphJson
* @param {Object} types - when provided, each cell validation will be first searched in types definition, when not found validation from cell itself
* will be used
*/
import(graphJson, typesForValidation /* object holding each type */){
this.removeAllBlocks();
this.graph.fromJSON(graphJson);
// now build _blocks array
var cells = this.graph.getCells();
cells.forEach(cell=>{
if(cell.isElement()){
// reinstantiate custom validation functions
// here goes the change - validation will be reapplied from types provided instead of validation sitting already on block/cell
const typeName = cell.get("_type");
cell._reApplyValidation(typesForValidation&&typesForValidation[typeName]&&typesForValidation[typeName].validation?typesForValidation[typeName].validation:undefined);
this.addBlock(cell, true);
// reinitialize custom validations
}
})
this.emitter.emit(EVENTS_DICT.EVENTS.FLOWBLOCKS_IMPORT_SUCCESS, this.graph.get('name'), this.graph.get('id'), this.graph.get('version'));
this.emitter.emit(EVENTS_DICT.EVENTS.FLOWBLOCKS_DONE_SUCCESS);
}
_bindInteractionEvents(){
var self = this;
this.paper.on('element:pointerdblclick', function (toolView, evt) {
self.emitter.emit(EVENTS_DICT.EVENTS.BLOCK_DBLCLICK, toolView.model, evt)
});
}
_bindToolsEvents() {
this.paper.on('element:mouseenter', function (view) {
view.showTools();
});
this.paper.on('element:mouseleave', function (view) {
view.hideTools();
});
}
_bindConnectionEvents() {
var self = this;
this.paper.on('link:connect', function (linkView, evt, elementViewConnected, magnet, arrowhead) {
var newParticipants = helper.linkGetParticipants(linkView.model, self);
var sourceElement = newParticipants.sourceElement;
var targetElement = newParticipants.targetElement;
var sourcePort = newParticipants.sourcePort;
var targetPort = newParticipants.targetPort;
var previousTargetElement = elementViewConnected.model
var previousTargetPort = magnet.getAttribute('port');
// console.log('CONNECTED ', sourceElement, sourcePort, targetElement, targetPort, previousTargetElement, previousTargetPort);
sourceElement._handleConnectTo(targetElement, sourcePort, targetPort, linkView.model.id);
targetElement._handleConnectFrom(sourceElement, targetPort, sourcePort, linkView.model.id);
self.emitter.emit(EVENTS_DICT.EVENTS.CONNECTION_REMOVED,sourceElement, sourcePort, targetElement, targetPort);
})
this.paper.on('link:disconnect', function (link, evt, elementViewDisconnected, magnet, arrowhead) {
// console.log(magnet.getAttribute('port'), magnet);
var participants = helper.linkGetParticipants(link.model, self);
var sourceElement = participants.sourceElement;
var targetElement = elementViewDisconnected.model
// var targetPort = magnet.getAttribute('port');
var targetPort = participants.targetPort;
var sourcePort = participants.sourcePort;
var newTargetElement = participants.targetElement;
// console.log(sourceElement, targetElement, newTargetElement);
if (targetElement != undefined && sourceElement != undefined) {
sourceElement._handleDisconnect(targetElement, sourcePort, link.model.id);
//targetElement._handleDisconnect(sourceElement, targetPort, link.model.id);
targetElement._handleDisconnect(sourceElement, magnet.getAttribute('port'), link.model.id);
self.emitter.emit(EVENTS_DICT.EVENTS.CONNECTION_REMOVED,sourceElement, sourcePort, targetElement, magnet.getAttribute('port'));
}
})
this.graph.on('remove', function (cell) {
if (cell.isLink()) {
var participants = helper.linkGetParticipants(cell, self);
var sourceElement = participants.sourceElement;
var targetElement = participants.targetElement;
var sourcePort = participants.sourcePort;
var targetPort = participants.targetPort;
if (targetElement != undefined && sourceElement != undefined) {
sourceElement._handleDisconnect(targetElement, sourcePort, cell.id);
targetElement._handleDisconnect(sourceElement, targetPort, cell.id);
self.emitter.emit(EVENTS_DICT.EVENTS.CONNECTION_REMOVED,sourceElement, sourcePort, targetElement, targetPort);
}
} else {
var blockToDelete = cell;
// remove the block
self._blocks = self._blocks.filter(block => {
return block.id != blockToDelete.id
})
self._blocks.forEach(block => {
block._handleDelete(blockToDelete);
})
self.emitter.emit(EVENTS_DICT.EVENTS.BLOCK_REMOVED,blockToDelete);
}
})
}
addBlock(block, omitGraph) {
this._blocks.push(block);
if(!omitGraph)
this.graph.addCell(block);
block._enableRemoval(this.paper);
this.emitter.emit(EVENTS_DICT.EVENTS.BLOCK_ADDED,block);
}
enablePanAndZoom(panAndZoom) {
var pzController = panAndZoom(document.querySelector('[joint-selector=svg]'), {
fit: false,
panEnabled: false,
controlIconsEnabled: true,
center: false,
dblClickZoomEnabled: false,
minZoom: 0.3
});
//Enable pan when a blank area is click (held) on
this.paper.on('blank:pointerdown', function (evt, x, y) {
pzController.enablePan();
});
//Disable pan when the mouse button is released
this.paper.on('cell:pointerup blank:pointerup', function (cellView, event) {
pzController.disablePan();
});
}
validate() {
var isOK = true;
var errorBlocks = [];
this._blocks.forEach(block => {
var blockStatus = block.getStatus();
if (!blockStatus.valid) {
errorBlocks.push({
blockId: block.get('blockId'),
errors: blockStatus.errors
});
}
isOK = isOK && blockStatus.valid;
})
return {
valid: isOK,
errorBlocks: errorBlocks,
};
}
}
module.exports = new Flow({});