2020-12-04 04:27:02 +01:00

277 lines
8.0 KiB
Vue

<template>
<div class="flowchart-container"
@mousemove="handleMove"
@mouseup="handleUp"
@mousedown="handleDown">
<svg width="100%" height="100%">
<flowchart-link v-bind.sync="link"
v-for="(link, index) in lines"
:key="`link${index}`"
@deleteLink="linkDelete(link.id)">
</flowchart-link>
</svg>
<flowchart-node v-bind.sync="node"
v-for="(node, index) in scene.nodes"
:key="`node${index}`"
:options="nodeOptions"
@linkingStart="linkingStart(node.id)"
@linkingStop="linkingStop(node.id)"
@nodeSelected="nodeSelected(node.id, $event)">
</flowchart-node>
</div>
</template>
<script>
import FlowchartLink from './FlowchartLink.vue';
import FlowchartNode from './FlowchartNode.vue';
import { getMousePosition } from '../assets/utilty/position';
export default {
name: 'VueFlowchart',
props: {
scene: {
type: Object,
default() {
return {
centerX: 0,
scale: 1,
centerY: 0,
nodes: [],
links: [],
}
}
}
},
data() {
return {
action: {
linking: false,
dragging: false,
scrolling: false,
selected: 0,
},
mouse: {
x: 0,
y: 0,
lastX: 0,
lastY: 0,
},
draggingLink: null,
rootDivOffset: {
top: 0,
left: 0
},
};
},
components: {
FlowchartLink,
FlowchartNode,
},
computed: {
nodeOptions() {
return {
centerY: this.scene.centerY,
centerX: this.scene.centerX,
scale: this.scene.scale,
offsetTop: this.rootDivOffset.top,
offsetLeft: this.rootDivOffset.left,
selected: this.action.selected,
}
},
lines() {
const lines = this.scene.links.map((link) => {
const fromNode = this.findNodeWithID(link.from)
const toNode = this.findNodeWithID(link.to)
let x, y, cy, cx, ex, ey;
x = this.scene.centerX + fromNode.x;
y = this.scene.centerY + fromNode.y;
[cx, cy] = this.getPortPosition('bottom', x, y);
x = this.scene.centerX + toNode.x;
y = this.scene.centerY + toNode.y;
[ex, ey] = this.getPortPosition('top', x, y);
return {
start: [cx, cy],
end: [ex, ey],
id: link.id,
};
})
if (this.draggingLink) {
let x, y, cy, cx;
const fromNode = this.findNodeWithID(this.draggingLink.from)
x = this.scene.centerX + fromNode.x;
y = this.scene.centerY + fromNode.y;
[cx, cy] = this.getPortPosition('bottom', x, y);
// push temp dragging link, mouse cursor postion = link end postion
lines.push({
start: [cx, cy],
end: [this.draggingLink.mx, this.draggingLink.my],
})
}
return lines;
}
},
mounted() {
this.rootDivOffset.top = this.$el ? this.$el.offsetTop : 0;
this.rootDivOffset.left = this.$el ? this.$el.offsetLeft : 0;
},
methods: {
findNodeWithID(id) {
return this.scene.nodes.find((item) => {
return id === item.id
})
},
getPortPosition(type, x, y) {
// Line start and end points set here
if (type === 'top') {
return [x + 100, y];
}
else if (type === 'bottom') {
return [x + 100, y + 150];
}
},
linkingStart(index) {
this.action.linking = true;
// Dirty fix, so that links won't glitch when being created
const node = this.findNodeWithID(index);
const [x,y] = this.getPortPosition('bottom',node.x + this.scene.centerX,node.y + this.scene.centerY);
this.draggingLink = {
from: index,
mx: x, // Yes, those are magic numbers
my: y,
};
},
linkingStop(index) {
// add new Link
if (this.draggingLink && this.draggingLink.from !== index) {
// check link existence
const existed = this.scene.links.find((link) => {
return link.from === this.draggingLink.from && link.to === index;
})
if (!existed) {
let maxID = Math.max(0, ...this.scene.links.map((link) => {
return link.id
}))
const newLink = {
id: maxID + 1,
from: this.draggingLink.from,
to: index,
};
this.scene.links.push(newLink)
this.$emit('linkAdded', newLink)
}
}
this.draggingLink = null
},
linkDelete(id) {
const deletedLink = this.scene.links.find((item) => {
return item.id === id;
});
if (deletedLink) {
this.scene.links = this.scene.links.filter((item) => {
return item.id !== id;
});
this.$emit('linkBreak', deletedLink);
}
},
nodeSelected(id, e) {
this.action.dragging = id;
this.action.selected = id;
this.$emit('nodeClick', id);
this.mouse.lastX = e.pageX || e.clientX + document.documentElement.scrollLeft
this.mouse.lastY = e.pageY || e.clientY + document.documentElement.scrollTop
},
handleMove(e) {
if (this.action.linking) {
[this.mouse.x, this.mouse.y] = getMousePosition(this.$el, e);
[this.draggingLink.mx, this.draggingLink.my] = [this.mouse.x, this.mouse.y];
}
if (this.action.dragging) {
this.mouse.x = e.pageX || e.clientX + document.documentElement.scrollLeft
this.mouse.y = e.pageY || e.clientY + document.documentElement.scrollTop
let diffX = this.mouse.x - this.mouse.lastX;
let diffY = this.mouse.y - this.mouse.lastY;
this.mouse.lastX = this.mouse.x;
this.mouse.lastY = this.mouse.y;
this.moveSelectedNode(diffX, diffY);
}
if (this.action.scrolling) {
[this.mouse.x, this.mouse.y] = getMousePosition(this.$el, e);
let diffX = this.mouse.x - this.mouse.lastX;
let diffY = this.mouse.y - this.mouse.lastY;
this.mouse.lastX = this.mouse.x;
this.mouse.lastY = this.mouse.y;
this.scene.centerX += diffX;
this.scene.centerY += diffY;
// this.hasDragged = true
}
},
handleUp(e) {
const target = e.target || e.srcElement;
if (this.$el.contains(target)) {
if (typeof target.className !== 'string' || target.className.indexOf('node-input') < 0) {
this.draggingLink = null;
}
if (typeof target.className === 'string' && target.className.indexOf('node-delete') > -1) {
// console.log('delete2', this.action.dragging);
this.nodeDelete(this.action.dragging);
}
}
this.action.linking = false;
this.action.dragging = null;
this.action.scrolling = false;
},
handleDown(e) {
const target = e.target || e.srcElement;
// console.log('for scroll', target, e.keyCode, e.which)
if ((target === this.$el || target.matches('svg, svg *')) && e.which === 1) {
this.action.scrolling = true;
[this.mouse.lastX, this.mouse.lastY] = getMousePosition(this.$el, e);
this.action.selected = null; // deselectAll
}
this.$emit('canvasClick', e);
},
moveSelectedNode(dx, dy) {
let index = this.scene.nodes.findIndex((item) => {
return item.id === this.action.dragging
})
let left = this.scene.nodes[index].x + dx / this.scene.scale;
let top = this.scene.nodes[index].y + dy / this.scene.scale;
this.$set(this.scene.nodes, index, Object.assign(this.scene.nodes[index], {
x: left,
y: top,
}));
},
nodeDelete(id) {
this.scene.nodes = this.scene.nodes.filter((node) => {
return node.id !== id;
})
this.scene.links = this.scene.links.filter((link) => {
return link.from !== id && link.to !== id
})
this.$emit('nodeDelete', id)
}
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.flowchart-container {
margin: 0;
background: #f3f3f3;
position: relative;
overflow: hidden;
height: 100%;
svg {
cursor: grab;
}
}
</style>