├── .babelrc ├── .babelrc.build ├── .babelrc.dev ├── .gitignore ├── LICENSE ├── README.md ├── bundle.css ├── dist ├── vue-flowchart.css ├── vue-flowchart.esm.js ├── vue-flowchart.esm.js.map ├── vue-flowchart.js └── vue-flowchart.js.map ├── examples ├── server.dev.js └── simple │ ├── app.vue │ ├── index.html │ ├── index.js │ └── scene │ ├── custom │ └── CustomWidget.vue │ └── scene.vue ├── index.js ├── lib ├── helpers │ └── index.js ├── mixins │ └── setState.js └── src │ ├── Engine.js │ ├── components │ ├── BasicNodeWidget.vue │ ├── CanvasWidget.vue │ ├── LinkWidget.vue │ ├── NodeViewWidget.vue │ ├── NodeWidget.vue │ ├── PortWidget.vue │ └── SVGWidget.vue │ └── vue-flowchart.vue ├── package.json ├── rollup.config.js ├── test └── fixtureDatas.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "es2017"], 3 | "plugins": [ 4 | "transform-object-rest-spread" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.babelrc.build: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], "es2017"], 9 | "plugins": [ 10 | "transform-object-rest-spread" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.babelrc.dev: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "es2017"], 3 | "plugins": [ 4 | "transform-object-rest-spread" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Alexandre Bonaventure Geissmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-flowchart 2 | This lib is under heavy development. It is not production-ready and API could change dramatically. Feel free to use it anyway. 3 | # Usage 4 | Soon 5 | 6 | ### Graph Data 7 | ``` 8 | { 9 | links:[ 10 | { 11 | id :String, 12 | source :String, // a node ID 13 | sourcePort :String, 14 | target: node3, 15 | targetPort: 'in' 16 | }, 17 | ... 18 | ], 19 | nodes: [ 20 | { 21 | id :String, 22 | type: :String, // default: 'default' 23 | data: { 24 | name: "Caption", 25 | outVariables: ['out'] 26 | }, 27 | x :Number, // in px 28 | y :Number, // in px 29 | }, 30 | ], 31 | } 32 | ``` 33 | 34 | # Example 35 | 36 | 37 | # TO-DO 38 | 39 | 40 | # Contributions 41 | are welcome :) 42 | -------------------------------------------------------------------------------- /bundle.css: -------------------------------------------------------------------------------- 1 | .vue-flowchart { 2 | width: 100%; 3 | height: 100%; } 4 | 5 | .storm-flow-canvas { 6 | position: relative; 7 | flex-grow: 1; 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | cursor: move; 12 | overflow: hidden; } 13 | .storm-flow-canvas svg { 14 | position: absolute; 15 | height: 100%; 16 | width: 100%; 17 | transform-origin: 0 0; 18 | overflow: visible; } 19 | .storm-flow-canvas .node-view { 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | position: absolute; 25 | pointer-events: none; 26 | transform-origin: 0 0; } 27 | .storm-flow-canvas .node { 28 | position: absolute; 29 | -webkit-touch-callout: none; 30 | /* iOS Safari */ 31 | -webkit-user-select: none; 32 | /* Chrome/Safari/Opera */ 33 | user-select: none; 34 | cursor: move; 35 | pointer-events: all; } 36 | .storm-flow-canvas .node.selected > * { 37 | border-color: #00c0ff; 38 | box-shadow: 0 0 10px rgba(0, 192, 255, 0.5); } 39 | 40 | @keyframes dash { 41 | from { 42 | stroke-dashoffset: 24; } 43 | to { 44 | stroke-dashoffset: 0; } } 45 | .storm-flow-canvas path { 46 | fill: none; 47 | stroke: black; 48 | pointer-events: all; } 49 | .storm-flow-canvas path.selected { 50 | stroke: #00c0ff !important; 51 | stroke-dasharray: 10,2; 52 | animation: dash 1s linear infinite; } 53 | .storm-flow-canvas .port { 54 | width: 15px; 55 | height: 15px; 56 | background: rgba(255, 255, 255, 0.1); } 57 | .storm-flow-canvas .port:hover, .storm-flow-canvas .port.selected { 58 | background: #c0ff00; } 59 | .storm-flow-canvas .basic-node { 60 | background-color: #1e1e1e; 61 | border-radius: 5px; 62 | font-family: Arial; 63 | color: white; 64 | border: solid 2px black; 65 | overflow: hidden; 66 | font-size: 11px; 67 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } 68 | .storm-flow-canvas .basic-node .title { 69 | /* background-image: linear-gradient(rgba(black,0.1),rgba(black,0.2));*/ 70 | background: rgba(0, 0, 0, 0.3); 71 | display: flex; 72 | white-space: nowrap; } 73 | .storm-flow-canvas .basic-node .title > * { 74 | align-self: center; } 75 | .storm-flow-canvas .basic-node .title .fa { 76 | padding: 5px; 77 | opacity: 0.2; 78 | cursor: pointer; } 79 | .storm-flow-canvas .basic-node .title .fa:hover { 80 | opacity: 1.0; } 81 | .storm-flow-canvas .basic-node .title .name { 82 | flex-grow: 1; 83 | padding: 5px 5px; } 84 | .storm-flow-canvas .basic-node .ports { 85 | display: flex; 86 | background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.2)); } 87 | .storm-flow-canvas .basic-node .ports .in, .storm-flow-canvas .basic-node .ports .out { 88 | flex-grow: 1; 89 | display: flex; 90 | flex-direction: column; } 91 | .storm-flow-canvas .basic-node .ports .in-port, .storm-flow-canvas .basic-node .ports .out-port { 92 | display: flex; 93 | margin-top: 1px; } 94 | .storm-flow-canvas .basic-node .ports .in-port > *, .storm-flow-canvas .basic-node .ports .out-port > * { 95 | align-self: center; } 96 | .storm-flow-canvas .basic-node .ports .in-port .name, .storm-flow-canvas .basic-node .ports .out-port .name { 97 | padding: 0 5px; } 98 | .storm-flow-canvas .basic-node .ports .out-port { 99 | justify-content: flex-end; } 100 | .storm-flow-canvas .basic-node .ports .out-port .name { 101 | justify-content: flex-end; 102 | text-align: right; } 103 | 104 | .iframe { 105 | pointer-events: none; 106 | background-color: grey; } 107 | -------------------------------------------------------------------------------- /dist/vue-flowchart.css: -------------------------------------------------------------------------------- 1 | .vue-flowchart { 2 | width: 100%; 3 | height: 100%; } 4 | 5 | .storm-flow-canvas { 6 | position: relative; 7 | flex-grow: 1; 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | cursor: move; 12 | overflow: hidden; } 13 | .storm-flow-canvas svg { 14 | position: absolute; 15 | height: 100%; 16 | width: 100%; 17 | transform-origin: 0 0; 18 | overflow: visible; } 19 | .storm-flow-canvas .node-view { 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | position: absolute; 25 | pointer-events: none; 26 | transform-origin: 0 0; } 27 | .storm-flow-canvas .node { 28 | position: absolute; 29 | -webkit-touch-callout: none; 30 | /* iOS Safari */ 31 | -webkit-user-select: none; 32 | /* Chrome/Safari/Opera */ 33 | user-select: none; 34 | cursor: move; 35 | pointer-events: all; } 36 | .storm-flow-canvas .node.selected > * { 37 | border-color: #00c0ff; 38 | box-shadow: 0 0 10px rgba(0, 192, 255, 0.5); } 39 | 40 | @keyframes dash { 41 | from { 42 | stroke-dashoffset: 24; } 43 | to { 44 | stroke-dashoffset: 0; } } 45 | .storm-flow-canvas path { 46 | fill: none; 47 | stroke: black; 48 | pointer-events: all; } 49 | .storm-flow-canvas path.selected { 50 | stroke: #00c0ff !important; 51 | stroke-dasharray: 10,2; 52 | animation: dash 1s linear infinite; } 53 | .storm-flow-canvas .port { 54 | width: 15px; 55 | height: 15px; 56 | background: rgba(255, 255, 255, 0.1); } 57 | .storm-flow-canvas .port:hover, .storm-flow-canvas .port.selected { 58 | background: #c0ff00; } 59 | .storm-flow-canvas .basic-node { 60 | background-color: #1e1e1e; 61 | border-radius: 5px; 62 | font-family: Arial; 63 | color: white; 64 | border: solid 2px black; 65 | overflow: hidden; 66 | font-size: 11px; 67 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } 68 | .storm-flow-canvas .basic-node .title { 69 | /* background-image: linear-gradient(rgba(black,0.1),rgba(black,0.2));*/ 70 | background: rgba(0, 0, 0, 0.3); 71 | display: flex; 72 | white-space: nowrap; } 73 | .storm-flow-canvas .basic-node .title > * { 74 | align-self: center; } 75 | .storm-flow-canvas .basic-node .title .fa { 76 | padding: 5px; 77 | opacity: 0.2; 78 | cursor: pointer; } 79 | .storm-flow-canvas .basic-node .title .fa:hover { 80 | opacity: 1.0; } 81 | .storm-flow-canvas .basic-node .title .name { 82 | flex-grow: 1; 83 | padding: 5px 5px; } 84 | .storm-flow-canvas .basic-node .ports { 85 | display: flex; 86 | background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.2)); } 87 | .storm-flow-canvas .basic-node .ports .in, .storm-flow-canvas .basic-node .ports .out { 88 | flex-grow: 1; 89 | display: flex; 90 | flex-direction: column; } 91 | .storm-flow-canvas .basic-node .ports .in-port, .storm-flow-canvas .basic-node .ports .out-port { 92 | display: flex; 93 | margin-top: 1px; } 94 | .storm-flow-canvas .basic-node .ports .in-port > *, .storm-flow-canvas .basic-node .ports .out-port > * { 95 | align-self: center; } 96 | .storm-flow-canvas .basic-node .ports .in-port .name, .storm-flow-canvas .basic-node .ports .out-port .name { 97 | padding: 0 5px; } 98 | .storm-flow-canvas .basic-node .ports .out-port { 99 | justify-content: flex-end; } 100 | .storm-flow-canvas .basic-node .ports .out-port .name { 101 | justify-content: flex-end; 102 | text-align: right; } 103 | 104 | .iframe { 105 | pointer-events: none; 106 | background-color: grey; } 107 | -------------------------------------------------------------------------------- /examples/server.dev.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandreBonaventure/vue-flowchart/c66eebe90747cd0944344d4f7b7eb828aebe6c48/examples/server.dev.js -------------------------------------------------------------------------------- /examples/simple/app.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue Flowchart 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | //PLUGINS 4 | 5 | 6 | import app from './app.vue' 7 | 8 | const App = Vue.extend(app) 9 | const vm = new App().$mount('#app') 10 | -------------------------------------------------------------------------------- /examples/simple/scene/custom/CustomWidget.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 74 | 75 | 91 | -------------------------------------------------------------------------------- /examples/simple/scene/scene.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 86 | 87 | 94 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // export default {} 2 | export { default } from './lib/src/vue-flowchart.vue' 3 | export { default as portWidget } from './lib/src/components/PortWidget.vue' 4 | // export Engine from './lib/Engine.js' 5 | -------------------------------------------------------------------------------- /lib/helpers/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandreBonaventure/vue-flowchart/c66eebe90747cd0944344d4f7b7eb828aebe6c48/lib/helpers/index.js -------------------------------------------------------------------------------- /lib/mixins/setState.js: -------------------------------------------------------------------------------- 1 | import { forEach } from 'lodash-es' 2 | 3 | export default { 4 | methods: { 5 | setState(data) { 6 | forEach(data, (val, key) => this.$set(this, key, val)) 7 | } 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/Engine.js: -------------------------------------------------------------------------------- 1 | import { defaults, filter, values, forEach, flatMap, find, mapValues } from 'lodash-es' 2 | import Vue from 'vue' 3 | /** 4 | * @author Dylan Vorster 5 | */ 6 | 7 | const validatorNoop = (node) => { return Promise.resolve(true) } 8 | 9 | export default function() { 10 | return { 11 | state:{ 12 | links:{}, 13 | nodes:{}, 14 | factories: {}, 15 | canvas: null, 16 | offsetX:0, 17 | offsetY:0, 18 | zoom: 100, 19 | listeners:{}, 20 | selectedLink: null, 21 | selectedNode: null, 22 | 23 | updatingNodes: null, 24 | updatingLinks: null, 25 | validators: { 26 | onNodeRemove: validatorNoop, 27 | onEdgeRemove: validatorNoop, 28 | onEdgeUpdate: validatorNoop, 29 | }, 30 | }, 31 | 32 | repaintLinks: function(links){ 33 | Vue.set(this.state, 'updatingNodes', {}); 34 | Vue.set(this.state, 'updatingLinks', {}); 35 | links.forEach((link) => { 36 | Vue.set(this.state.updatingLinks, link.id, link); 37 | }) 38 | this.update() 39 | }, 40 | 41 | repaintNodes: function(nodes){ 42 | // this.state.updatingNodes = {}; 43 | // this.state.updatingLinks = {}; 44 | Vue.set(this.state, 'updatingNodes', {}); 45 | Vue.set(this.state, 'updatingLinks', {}); 46 | 47 | //store the updating node is's 48 | nodes.forEach((node) => { 49 | Vue.set(this.state.updatingNodes, node.id, node); 50 | this.getNodeLinks(node).forEach((link) => { 51 | Vue.set(this.state.updatingLinks, link.id, link) 52 | if(link.points.length < 2) { 53 | return; 54 | }else{ 55 | if(link.source !== null) { 56 | Vue.set(link.points, 0, this.getPortCenter(this.getNode(link.source),link.sourcePort)) 57 | } 58 | if(link.target !== null) { 59 | Vue.set(link.points, link.points.length-1, this.getPortCenter(this.getNode(link.target),link.targetPort)) 60 | } 61 | } 62 | }) 63 | }) 64 | 65 | this.update(); 66 | }, 67 | 68 | update: function(){ 69 | this.fireEvent({type:'repaint'}); 70 | }, 71 | 72 | getNodeDimensions(){ 73 | const nodes = this.state.nodes 74 | const dimensions = mapValues(nodes, (node, id) => { 75 | const el = this.getNodeElement(id) 76 | const { width, height } = el.getBoundingClientRect() 77 | return { width, height } 78 | }) 79 | return dimensions 80 | }, 81 | 82 | getRelativeMousePoint: function(event){ 83 | var point = this.getRelativePoint(event.pageX,event.pageY); 84 | return { 85 | x:(point.x/(this.state.zoom/100.0))-this.state.offsetX, 86 | y:(point.y/(this.state.zoom/100.0))-this.state.offsetY 87 | }; 88 | }, 89 | 90 | getRelativePoint: function(x,y){ 91 | var canvasRect = this.state.canvas.getBoundingClientRect(); 92 | return {x: x-canvasRect.left,y:y-canvasRect.top}; 93 | }, 94 | 95 | fireEvent: function(event){ 96 | forEach(this.state.listeners,function(listener){ 97 | listener(event); 98 | }); 99 | }, 100 | 101 | removeListener: function(id){ 102 | Vue.delete(this.state.listeners, id) 103 | }, 104 | 105 | removeAllListeners: function(id){ 106 | this.state.listeners = {} 107 | }, 108 | 109 | registerListener: function(cb){ 110 | var id = this.UID(); 111 | this.state.listeners[id] = cb; 112 | return id; 113 | }, 114 | 115 | setZoom: function(zoom){ 116 | this.state.zoom = zoom; 117 | this.update(); 118 | }, 119 | 120 | setOffset: function(x,y){ 121 | this.state.offsetX = x; 122 | this.state.offsetY = y; 123 | this.update(); 124 | }, 125 | 126 | loadModel: function(model){ 127 | this.state.links = {}; 128 | this.state.nodes = {}; 129 | 130 | model.nodes.forEach(function(node){ 131 | this.addNode(node); 132 | }.bind(this)); 133 | 134 | model.links.forEach(function(link){ 135 | this.addLink(link); 136 | }.bind(this)); 137 | }, 138 | 139 | generateLinkPoints(){ 140 | forEach(this.state.links, (link) => { 141 | if(link.points.length === 0){ 142 | link.points.push({ 143 | ...this.getPortCenter(this.getNode(link.source),link.sourcePort), 144 | id: this.UID() 145 | }); 146 | link.points.push({ 147 | ...this.getPortCenter(this.getNode(link.target),link.targetPort), 148 | id: this.UID() 149 | }); 150 | } 151 | }) 152 | }, 153 | 154 | 155 | 156 | updateNode: function(node){ 157 | 158 | //find the links and move those as well 159 | this.getNodeLinks(node); 160 | this.fireEvent({type:'repaint'}); 161 | }, 162 | 163 | UID: function(){ 164 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 165 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 166 | return v.toString(16); 167 | }); 168 | }, 169 | 170 | getNodeElement: function(id){ 171 | return this.state.canvas.querySelector(`.node[data-nodeid="${id}"]`); 172 | }, 173 | getNodePortElement: function(node,port){ 174 | return this.state.canvas.querySelector('.port[data-name="'+port+'"][data-nodeid="'+node.id+'"]'); 175 | }, 176 | 177 | getNodePortLinks: function(node,port){ 178 | var nodeID = this.getNodeID(node); 179 | var links = this.getNodeLinks(nodeID); 180 | return links.filter(function(link){ 181 | if(link.target === nodeID && link.targetPort === port){ 182 | return true; 183 | } 184 | else if(link.source === nodeID && link.sourcePort === port){ 185 | return true; 186 | } 187 | return false; 188 | }); 189 | }, 190 | 191 | getNodeID: function(node){ 192 | if(typeof node === 'object'){ 193 | node = node.id; 194 | } 195 | return node; 196 | }, 197 | 198 | getNodeLinks: function(node){ 199 | var nodeID = this.getNodeID(node); 200 | return values(filter(this.state.links,function(link,index){ 201 | return link.source == nodeID || link.target == nodeID; 202 | })); 203 | }, 204 | 205 | removeLink(link, bypassValidation = false) { 206 | if (typeof link !== 'object') link = this.getLink(link) 207 | const validator$ = bypassValidation ? validatorNoop() : this.state.validators.onEdgeRemove(link) 208 | validator$.then(valid => { 209 | if (valid) { 210 | Vue.delete(this.state.links, link.id) 211 | this.update(); 212 | this.fireEvent({ 213 | type:'link:remove', 214 | data: link 215 | }) 216 | } 217 | }) 218 | 219 | }, 220 | 221 | removeNode(node, bypassValidation = false){ 222 | if (typeof node !== 'object') node = this.getNode(node) 223 | const validator$ = bypassValidation ? validatorNoop() : this.state.validators.onNodeRemove(node) 224 | validator$.then(valid => { 225 | if (valid) { 226 | //remove the links 227 | var links = this.getNodeLinks(node) 228 | links.forEach((link) => { 229 | this.removeLink(link, true) 230 | }) 231 | 232 | //remove the node 233 | Vue.delete(this.state.nodes, node.id) 234 | // this.update() 235 | this.fireEvent({ 236 | type:'node:remove', 237 | data: node 238 | }) 239 | } 240 | }) 241 | }, 242 | 243 | getPortCenter: function(node,port){ 244 | var sourceElement = this.getNodePortElement(node,port); 245 | var sourceRect = sourceElement.getBoundingClientRect(); 246 | 247 | var rel = this.getRelativePoint(sourceRect.left,sourceRect.top); 248 | 249 | return { 250 | x: ((sourceElement.offsetWidth/2)+rel.x/(this.state.zoom/100.0)) -(this.state.offsetX), 251 | y: ((sourceElement.offsetHeight/2)+rel.y/(this.state.zoom/100.0)) -(this.state.offsetY) 252 | }; 253 | }, 254 | 255 | setSelectedNode: function(node){ 256 | // this.state.selectedLink = null; 257 | this.state.selectedNode = node; 258 | if (node) { 259 | this.fireEvent({ 260 | type:'node:select', 261 | data: node 262 | }) 263 | } 264 | 265 | // this.state.updatingNodes = null; 266 | // this.state.updatingLinks = null; 267 | this.update(); 268 | }, 269 | 270 | setSelectedLink: function(link){ 271 | // this.state.selectedNode = null; 272 | this.state.selectedLink = link; 273 | this.fireEvent({ 274 | type:'link:select', 275 | data: link 276 | }) 277 | // this.state.updatingNodes = null; 278 | // this.state.updatingLinks = null; 279 | this.update(); 280 | }, 281 | 282 | addLink: function(link){ 283 | var FinalLink = link = { 284 | id: this.UID(), 285 | source: null, 286 | sourcePort: null, 287 | target: null, 288 | targetPort: null, 289 | points: [], 290 | ...link, 291 | } 292 | 293 | Vue.set(this.state.links, FinalLink.id, FinalLink) 294 | this.fireEvent({ 295 | type:'link:add', 296 | data: FinalLink 297 | }) 298 | return FinalLink; 299 | }, 300 | 301 | addNode: function(node,event){ 302 | var point = {x:0,y:0}; 303 | if(event !== undefined){ 304 | point = this.getRelativeMousePoint(event); 305 | } 306 | 307 | var FinalNode = defaults(node,{ 308 | id: this.UID(), 309 | type: 'default', 310 | data:{}, 311 | x: point.x, 312 | y: point.y 313 | }); 314 | Vue.set(this.state.nodes, FinalNode.id, FinalNode) 315 | this.fireEvent({ 316 | type:'node:add', 317 | data: FinalNode, 318 | }) 319 | 320 | }, 321 | 322 | getLink: function(id){ 323 | return this.state.links[id]; 324 | }, 325 | 326 | getPoint: function(id){ 327 | const allPoints = flatMap(this.state.links, ({ points }) => points) 328 | const point = find(allPoints, { id }); 329 | console.log(point); 330 | return point 331 | }, 332 | 333 | getNode: function(id){ 334 | return this.state.nodes[id]; 335 | }, 336 | 337 | getNodeFactory: function(type){ 338 | if(this.state.factories[type] === undefined){ 339 | throw "Cannot find node factory for: "+type; 340 | } 341 | return this.state.factories[type]; 342 | }, 343 | 344 | registerNodeFactory: function(factory){ 345 | var FinalModel = defaults(factory,{ 346 | type: "factory", 347 | isPortAllowed: function(sourceNode,sourceport,targetNode,targetPort){ 348 | return true; 349 | }, 350 | generateModel: function(model,engine){ 351 | return null; 352 | } 353 | }); 354 | this.state.factories[FinalModel.type] = FinalModel; 355 | }, 356 | 357 | registerValidators: function(validators){ 358 | this.state.validators = defaults( 359 | validators, 360 | this.state.validators 361 | ) 362 | } 363 | }; 364 | }; 365 | -------------------------------------------------------------------------------- /lib/src/components/BasicNodeWidget.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 89 | 90 | 93 | -------------------------------------------------------------------------------- /lib/src/components/CanvasWidget.vue: -------------------------------------------------------------------------------- 1 | 242 | 243 | 256 | 257 | 393 | -------------------------------------------------------------------------------- /lib/src/components/LinkWidget.vue: -------------------------------------------------------------------------------- 1 | 204 | 205 | 210 | 211 | 214 | -------------------------------------------------------------------------------- /lib/src/components/NodeViewWidget.vue: -------------------------------------------------------------------------------- 1 | 66 | 73 | 74 | 75 | 78 | -------------------------------------------------------------------------------- /lib/src/components/NodeWidget.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 48 | 49 | 52 | -------------------------------------------------------------------------------- /lib/src/components/PortWidget.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 41 | 42 | 45 | -------------------------------------------------------------------------------- /lib/src/components/SVGWidget.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 44 | 45 | 51 | -------------------------------------------------------------------------------- /lib/src/vue-flowchart.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 114 | 115 | 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-flowchart", 3 | "version": "0.1.1", 4 | "description": "vue-flowchart", 5 | "main": "dist/vue-flowchart.js", 6 | "module": "dist/vue-flowchart.esm.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build:umd": "rollup -c --name vueFlowchart --input index.js --output dist/vue-flowchart.js --format umd", 10 | "build:es": "rollup -c --name vueFlowchart --input index.js --output dist/vue-flowchart.esm.js --format es", 11 | "build": "npm run build:umd && npm run build:es", 12 | "serve": "NODE_PATH=$NODE_PATH:lib:examples/simple budo examples/simple/index.js --open -d . --live -- -t [ vueify ] -g babelify", 13 | "//dist": "NODE_PATH=$NODE_PATH:examples/simple browserify -e examples/simple/index.js -o dist/index.js -t vueify -t [ babelify ] -t uglifyify -p [ minifyify --no-map ]" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/AlexandreBonaventure/vue-flowchart.git" 18 | }, 19 | "keywords": [ 20 | "vue", 21 | "vue.js", 22 | "flow", 23 | "chart", 24 | "diagram" 25 | ], 26 | "author": "Alexandre Bonaventure Geissmann", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/AlexandreBonaventure/vue-flowchart/issues" 30 | }, 31 | "homepage": "https://github.com/AlexandreBonaventure/vue-flowchart#readme", 32 | "peerDependencies": { 33 | "vue": "2.x" 34 | }, 35 | "dependencies": { 36 | "lodash-es": "^4.17.4" 37 | }, 38 | "devDependencies": { 39 | "babel-plugin-external-helpers": "^6.8.0", 40 | "babel-plugin-transform-object-rest-spread": "^6.22.0", 41 | "babel-plugin-transform-runtime": "^6.15.0", 42 | "babel-preset-es2015": "^6.18.0", 43 | "babel-preset-es2017": "^6.22.0", 44 | "babelify": "^7.3.0", 45 | "babelify-external-helpers": "^1.1.0", 46 | "browserify": "^13.1.1", 47 | "browserify-shim": "^3.8.12", 48 | "budo": "^9.2.2", 49 | "bundle-collapser": "^1.2.1", 50 | "jade": "^1.11.0", 51 | "js-data": "^2.9.0", 52 | "minifyify": "^7.3.4", 53 | "node-sass": "^4.2.0", 54 | "rollup": "^0.36.1", 55 | "rollup-plugin-babel": "^2.7.1", 56 | "rollup-plugin-buble": "^0.15.0", 57 | "rollup-plugin-commonjs": "^7.0.0", 58 | "rollup-plugin-node-resolve": "^2.0.0", 59 | "rollup-plugin-scss": "^0.2.0", 60 | "rollup-plugin-vue": "2.3.1", 61 | "uglifyify": "^3.0.4", 62 | "vue": "2.2.4", 63 | "vue-hot-reload-api": "^1.3.3", 64 | "vueify": "^9.4.0", 65 | "vueify-insert-css": "^1.0.0", 66 | "vuex": "^2.0.0" 67 | }, 68 | "browserify-shim": {} 69 | } 70 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import vue from 'rollup-plugin-vue' 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | import scss from 'rollup-plugin-scss'; 6 | import buble from 'rollup-plugin-buble'; 7 | 8 | export default { 9 | plugins: [ 10 | vue({ 11 | styleToImports: true, 12 | // css: null, 13 | }), 14 | scss({ 15 | //Choose *one* of these possible "output:..." options 16 | // Default behaviour is to write all styles to the bundle destination where .js is replaced by .css 17 | // output: true, 18 | // 19 | // // Filename to write all styles to 20 | output: 'dist/vue-flowchart.css', 21 | // 22 | // // Callback that will be called ongenerate with two arguments: 23 | // // - styles: the contents of all style tags combined: 'body { color: green }' 24 | // // - styleNodes: an array of style objects: { filename: 'body { ... }' } 25 | // output: function (styles, styleNodes) { 26 | // writeFileSync('bundle.css', styles) 27 | // }, 28 | // 29 | // // Disable any style output or callbacks, import as string 30 | // output: false 31 | }), 32 | // babel({ 33 | // exclude: ['node_modules/**', '*.vue'], 34 | // }), 35 | buble({objectAssign: 'Object.assign'}), 36 | nodeResolve({ 37 | browser: true, 38 | jsnext: true, 39 | main: true 40 | }), 41 | commonjs({ 42 | include: 'node_modules/**', 43 | }), 44 | ], 45 | format: 'umd', 46 | external: [ 47 | 'vue', 48 | ], 49 | sourceMap: true, 50 | } 51 | -------------------------------------------------------------------------------- /test/fixtureDatas.js: -------------------------------------------------------------------------------- 1 | import EngineCore from '../lib/src/Engine.js' 2 | 3 | const Engine = EngineCore() 4 | 5 | export default function factory() { 6 | const model = {links:[],nodes: []}; 7 | 8 | generateSet(model,0,0); 9 | // generateSet(model,800,0); 10 | // generateSet(model,1600,0); 11 | // generateSet(model,2400,0); 12 | // 13 | // generateSet(model,0,300); 14 | // generateSet(model,800,300); 15 | // generateSet(model,1600,300); 16 | // generateSet(model,2400,300); 17 | // 18 | // generateSet(model,0,600); 19 | // generateSet(model,800,600); 20 | // generateSet(model,1600,600); 21 | // generateSet(model,2400,600); 22 | // 23 | // generateSet(model,0,900); 24 | // generateSet(model,800,900); 25 | // generateSet(model,1600,900); 26 | // generateSet(model,2400,900); 27 | 28 | return model 29 | 30 | 31 | function generateSet(model,offsetX,offsetY) { 32 | var node1 = Engine.UID(); 33 | var node2 = Engine.UID(); 34 | var node3 = Engine.UID(); 35 | var node4 = Engine.UID(); 36 | var node5 = Engine.UID(); 37 | 38 | 39 | model.links = model.links.concat([ 40 | { 41 | id: Engine.UID(), 42 | source: node1, 43 | sourcePort: 'inout', 44 | target: node2, 45 | targetPort: 'in', 46 | }, 47 | { 48 | id: Engine.UID(), 49 | source: node1, 50 | sourcePort: 'inout', 51 | target: node3, 52 | targetPort: 'in' 53 | }, 54 | { 55 | id: Engine.UID(), 56 | source: node2, 57 | sourcePort: 'out', 58 | target: node4, 59 | targetPort: 'in' 60 | }, 61 | { 62 | id: Engine.UID(), 63 | source: node4, 64 | sourcePort: 'out', 65 | target: node5, 66 | targetPort: 'default' 67 | }, 68 | { 69 | id: Engine.UID(), 70 | source: node2, 71 | sourcePort: 'out', 72 | target: node5, 73 | targetPort: 'default' 74 | } 75 | ]); 76 | 77 | model.nodes = model.nodes.concat([ 78 | { 79 | id:node1, 80 | type: 'custom', 81 | data: { 82 | name: "I'm custom", 83 | inOutVariables: ['inout'] 84 | }, 85 | x: Math.random(50) * 10 + offsetX, 86 | y: Math.random(50) * 10 + offsetY 87 | }, 88 | { 89 | id:node2, 90 | type: 'default', 91 | data: { 92 | name: "Add Card to User", 93 | inPorts: ['in','in 2'], 94 | outPorts: ['out'] 95 | }, 96 | x:250 +offsetX, 97 | y:50 + offsetY 98 | }, 99 | { 100 | id:node3, 101 | type: 'default', 102 | data: { 103 | color: 'rgb(0,192,255)', 104 | name: "Remove User", 105 | inPorts: ['in'] 106 | }, 107 | x:250 + offsetX, 108 | y:150 + offsetY 109 | }, 110 | { 111 | id:node4, 112 | type: 'default', 113 | data: { 114 | color: 'rgb(0,192,255)', 115 | name: "Remove User", 116 | inPorts: ['in'], 117 | outPorts: ['out'] 118 | }, 119 | x:500 + offsetX, 120 | y:150 + offsetY 121 | }, 122 | { 123 | id:node5, 124 | type: 'default', 125 | data: { 126 | color: 'rgb(192,255,0)', 127 | name: "Complex Action 2", 128 | // port: ['in','in2','in3'] 129 | }, 130 | x:800 + offsetX, 131 | y:100 + offsetY 132 | }, 133 | ]); 134 | } 135 | 136 | } 137 | --------------------------------------------------------------------------------