UNPKG

vue-diagram-editor-improved

Version:

Highly customizable diagram editor support for Vue.js

262 lines (233 loc) 7.18 kB
import Vue from 'vue'; import Node from '../models/Node'; import Link from '../models/Link'; import Coordinates from '../models/Coordinates'; import {snapToGrip} from '../helpers'; import Size from '../models/Size'; import cloneDeep from 'lodash/cloneDeep'; import isEqual from "lodash/isEqual"; export default () => ({ data: () => ({ nodes: {}, links: {}, draggedNode: null, resizedNode: null, selectedNode: null, selectedLink: null, initialCursorOffset: {x: 0, y: 0}, activePort: null, hoveredPort: null, nodeCoordinatesBeforeDrag: {x: 0, y: 0}, nodeSizeBeforeResize: {width: 0, height: 0}, currentCursorPos: null, }), computed: { newLink() { if (!this.activePort || this.currentCursorPos === null) return null; const {id, type, port} = this.activePort; const startCoordinates = this.nodes[id].getPortCoordinates(type, port); return { x1: startCoordinates.x, y1: startCoordinates.y, x2: this.currentCursorPos.x, y2: this.currentCursorPos.y }; } }, watch: { selectedNode(id) { if (id) { this.$emit('select-node', id); } }, draggedNode(id) { if (id) { const node = this.nodes[id]; this.nodeCoordinatesBeforeDrag = {...node.coordinates}; } this.toggleMoveEventListener(id !== null); }, resizedNode(id) { if (id) { const node = this.nodes[id]; this.nodeSizeBeforeResize = {...node.size}; } this.toggleMoveEventListener(id !== null); }, activePort(v) { this.toggleMoveEventListener(v !== null); } }, methods: { setModel(model) { this.nodes = Object.values(model.nodes || []).reduce((carry, node) => { const nodeModel = new Node(node); return { ...carry, [nodeModel.id]: nodeModel }; }, {}); this.links = Object.values(model.links || []).reduce((carry, link) => { const linkModel = new Link(link); return { ...carry, [linkModel.id]: linkModel }; }, {}); }, beforePan() { return this.pan && !(this.draggedNode || this.resizedNode || this.activePort); }, addNode(node) { const nodeModel = new Node(node); Vue.set(this.nodes, nodeModel.id, nodeModel); }, addLink(link) { const linkModel = new Link(link); Vue.set(this.links, linkModel.id, linkModel); }, deleteNode(id) { const node = this.nodes[id]; if (this.beforeDeleteNode(node) === false) return; Vue.delete(this.nodes, id); for (let linkId in this.links) if (this.links.hasOwnProperty(linkId)) { const link = this.links[linkId]; if (link.start_id === id || link.end_id === id) { Vue.delete(this.links, linkId); } } this.$emit('deleted-node', id); }, deleteLink(id) { if (this.beforeDeleteLink(this.links[id]) === false) return; Vue.delete(this.links, id); this.$emit('deleted-link', id); }, selectLink(id) { this.selectedLink = id; }, clearSelection() { this.selectedNode = null; this.selectedLink = null; }, toggleMoveEventListener(enable) { if (enable) { document.body.addEventListener('mousemove', this.mouseMove); document.body.addEventListener('mouseup', this.mouseUp); } else { document.body.removeEventListener('mousemove', this.mouseMove); document.body.removeEventListener('mouseup', this.mouseUp); } }, mouseMove(pos) { if (this.draggedNode || this.resizedNode) { this.dragMove({ gridSnap: this.gridSnap, x: pos.x, y: pos.y }); } else if (this.activePort) { if (this.activePort.type === 'out') { const {x, y} = this.convertXYtoViewPort(pos.pageX, pos.pageY); this.currentCursorPos = {x, y}; } } }, mouseUp() { if (this.draggedNode || this.resizedNode) { const id = this.draggedNode || this.resizedNode; const node = id ? this.nodes[id] : null; const isDragged = !!this.draggedNode; const hasChanges = (this.draggedNode && !isEqual({...node.coordinates}, {...this.nodeCoordinatesBeforeDrag})) || (this.resizedNode && !isEqual({...node.size}, {...this.nodeSizeBeforeResize})); this.dragEnd(); if (hasChanges) { this.$emit('updated-node', node); } else if (isDragged) { this.selectedNode = id; } } else if (this.activePort) { const activePort = {...this.activePort}; if (this.hoveredPort) { const hoveredPort = {...this.hoveredPort}; if (isEqual(activePort, hoveredPort)) { this.$emit('click-port', activePort); } else { if (this.portAvailable({...hoveredPort, activePort})) { const link = new Link({ start_id: activePort.id, start_port: activePort.port, end_id: hoveredPort.id, end_port: hoveredPort.port }); Vue.set(this.links, link.id, link); this.$emit('created-link', link); } } } this.activePort = null; this.currentCursorPos = null; } }, serialize() { return { nodes: Object.values(this.nodes).map(node => cloneDeep(node)), links: Object.values(this.links).map(link => cloneDeep(link)) }; }, dragStart({id, offset, type}) { if (type === 'drag') { this.draggedNode = id; this.resizedNode = null; } else if (type === 'resize') { this.draggedNode = null; this.resizedNode = id; } this.initialCursorOffset = offset; }, dragMove({gridSnap, x, y}) { const getCoords = (X, Y) => { let {x, y} = this.convertXYtoViewPort(X, Y); x = snapToGrip(x, gridSnap) - (gridSnap / 2); y = snapToGrip(y, gridSnap); x -= this.initialCursorOffset.x; y -= this.initialCursorOffset.y; return {x, y}; }; if (this.draggedNode) { this.updateNode({ id: this.draggedNode, name: 'coordinates', value: new Coordinates(getCoords(x, y)) }); } if (this.resizedNode) { const node = this.nodes[this.resizedNode]; const coords = getCoords(x, y); const size = new Size({ width: Math.abs(coords.x - node.coordinates.x), height: Math.abs(coords.y - node.coordinates.y) }); if (size.width < 30) { size.width = 30; } if (size.height < 30) { size.height = 30; } this.updateNode({ id: this.resizedNode, name: 'size', value: size }); } }, dragEnd() { this.draggedNode = null; this.resizedNode = null; }, updateNode({id, name, value}) { Vue.set(this.nodes[id], name, Node.prepareProp(name, value)); } } });