|
@@ -1,68 +1,78 @@
|
|
|
<template>
|
|
|
<div class="flow-container">
|
|
|
- <div>
|
|
|
- fn:
|
|
|
- <button
|
|
|
- :key="k"
|
|
|
- v-for="(r,k) of registry"
|
|
|
- @click="nodeAdd(k)">
|
|
|
- {{ k }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
|
|
|
- <svg
|
|
|
- xmlns="http://www.w3.org/2000/svg"
|
|
|
- xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
|
- ref="svg"
|
|
|
- class="view"
|
|
|
- :width="width"
|
|
|
- :height="height">
|
|
|
- <defs>
|
|
|
- <pattern id="smallGrid" width="8" height="8" pattern-units="userSpaceOnUse">
|
|
|
- <path d="M 8 0 L 0 0 0 8" fill="none" stroke="gray" stroke-width="0.5"/>
|
|
|
- </pattern>
|
|
|
- <pattern id="grid" width="80" height="80" pattern-units="userSpaceOnUse">
|
|
|
- <rect width="80" height="80" fill="url(#smallGrid)"/>
|
|
|
- <path d="M 80 0 L 0 0 0 80" fill="none" stroke="gray" stroke-width="1"/>
|
|
|
- </pattern>
|
|
|
- </defs>
|
|
|
+ <hx-split
|
|
|
+ dir="horizontal"
|
|
|
+ :resizeable="funcsResizeable"
|
|
|
+ :split="funcsActive?funcsSize:'0px'"
|
|
|
+ @onSplitResize="funcsSizeUpdate"
|
|
|
+ >
|
|
|
+ <flow-panel
|
|
|
+ :registry="registry"
|
|
|
+ @toggleResizeable="funcsResizeable=!funcsResizeable"
|
|
|
+ />
|
|
|
+ <div class="flow-svgcontainer">
|
|
|
+ <svg
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
|
+ ref="svg"
|
|
|
+ class="view"
|
|
|
+ @dragover.prevent="() => {}"
|
|
|
+ @drop="managerDrop"
|
|
|
+ :width="width"
|
|
|
+ :height="height">
|
|
|
+ <flow-pan-zoom
|
|
|
+ ref="panzoom"
|
|
|
+ v-model="panzoom">
|
|
|
+ <flow-link
|
|
|
+ v-for="(link,i) in nodeData.links"
|
|
|
+ :key="i"
|
|
|
+ v-bind="linkProps(link)"
|
|
|
+ @click="linkRemove(link)"
|
|
|
+ />
|
|
|
+ <!-- mouse link-->
|
|
|
+ <flow-link
|
|
|
+ v-if="pointerLink.active"
|
|
|
+ v-bind="pointerLink.props"
|
|
|
+ />
|
|
|
+ <flow-node
|
|
|
+ ref="nodes"
|
|
|
+ v-for="(n,i) of nodeData.nodes"
|
|
|
+ v-bind="nodeProps(n)"
|
|
|
+ :key="'node' + n.id"
|
|
|
+ :id="n.id"
|
|
|
+ @nodePointerDown.prevent="nodeDragStart($event,i)"
|
|
|
+ @socketPointerDown="socketPointerDown(n.id,...arguments)"
|
|
|
+ />
|
|
|
+ </flow-pan-zoom>
|
|
|
+ </svg>
|
|
|
+ <div class="flow-svgcontainer__control">
|
|
|
+ <button
|
|
|
+ class="flow-funcs__toggle"
|
|
|
+ @click="funcsActive=!funcsActive">Panel</button>
|
|
|
+ <button
|
|
|
+ @click="panzoomReset">Reset view</button>
|
|
|
|
|
|
- <flow-pan-zoom
|
|
|
- ref="panzoom"
|
|
|
- v-model="panzoom">
|
|
|
- <flow-link
|
|
|
- v-for="(link,i) in nodeData.links"
|
|
|
- :key="i"
|
|
|
- v-bind="linkProps(link)"
|
|
|
- @click="linkRemove(link)"
|
|
|
- />
|
|
|
- <!-- mouse link-->
|
|
|
- <flow-link
|
|
|
- v-if="pointerLink.active"
|
|
|
- v-bind="pointerLink.props"
|
|
|
- />
|
|
|
- <flow-node
|
|
|
- ref="nodes"
|
|
|
- v-for="(n,i) of nodeData.nodes"
|
|
|
- v-bind="nodeProps(n)"
|
|
|
- :key="'node' + n.id"
|
|
|
- :id="n.id"
|
|
|
- @nodePointerDown.prevent="nodeDragStart($event,i)"
|
|
|
- @socketPointerDown="socketPointerDown(n.id,...arguments)"
|
|
|
- />
|
|
|
- </flow-pan-zoom>
|
|
|
- </svg>
|
|
|
+ </div>
|
|
|
+ <div class="flow-svgcontainer__info">
|
|
|
+ x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }}
|
|
|
+ nodes: {{ nodeData.nodes.length }}
|
|
|
+ links: {{ nodeData.links.length }}
|
|
|
+ </div>
|
|
|
+ </div> <!-- /svg-container-->
|
|
|
+ </hx-split>
|
|
|
</div>
|
|
|
</template>
|
|
|
<script>
|
|
|
-// import nodeData from '../nodedata'
|
|
|
+import FlowPanel from './panel'
|
|
|
import FlowNode from './node'
|
|
|
import FlowLink from './link'
|
|
|
import FlowPanZoom from './panzoom'
|
|
|
+import HxSplit from '@/components/shared/hx-split'
|
|
|
|
|
|
export default {
|
|
|
name: 'FlowManager',
|
|
|
- components: {FlowNode, FlowLink, FlowPanZoom},
|
|
|
+ components: {FlowNode, FlowLink, FlowPanZoom, FlowPanel, HxSplit},
|
|
|
props: {
|
|
|
'value': {type: Object, default: () => {}},
|
|
|
'registry': {type: Object, default: () => {}},
|
|
@@ -72,6 +82,9 @@ export default {
|
|
|
data () {
|
|
|
// const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
|
|
|
return {
|
|
|
+ funcsSize: '200px',
|
|
|
+ funcsActive: true,
|
|
|
+ funcsResizeable: false,
|
|
|
panzoom: { x: 0, y: 0, zoom: 1 },
|
|
|
nodeData: { nodes: [], links: [] },
|
|
|
dragging: null,
|
|
@@ -124,7 +137,6 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
},
|
|
|
watch: {
|
|
|
value: {
|
|
@@ -157,16 +169,24 @@ export default {
|
|
|
})
|
|
|
},
|
|
|
methods: {
|
|
|
+ funcsSizeUpdate (e, size) {
|
|
|
+ this.funcsSize = size
|
|
|
+ },
|
|
|
+ panzoomReset () {
|
|
|
+ this.panzoom.x = 0
|
|
|
+ this.panzoom.y = 0
|
|
|
+ this.panzoom.zoom = 1
|
|
|
+ },
|
|
|
// XXX: Shrink this function
|
|
|
// and create some LinkAdd method
|
|
|
- socketPointerDown (nodeId, e, socket) {
|
|
|
+ socketPointerDown (nodeId, ev, socket) {
|
|
|
const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
|
|
|
const node = this.nodeData.nodes.find(n => n.id === nodeId)
|
|
|
|
|
|
const isInput = socket.in !== undefined
|
|
|
const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
|
|
|
|
|
|
- const p = this.transformedPoint(e.clientX, e.clientY)
|
|
|
+ const p = this.transformedPoint(ev.clientX, ev.clientY)
|
|
|
if (isInput) {
|
|
|
this.pointerLink.props = {
|
|
|
x1: node.x + socketPos.x,
|
|
@@ -192,8 +212,8 @@ export default {
|
|
|
|
|
|
// What socket is this
|
|
|
// Create a temporary link
|
|
|
- const drag = (e) => {
|
|
|
- const p = this.transformedPoint(e.clientX, e.clientY)
|
|
|
+ const drag = (ev) => {
|
|
|
+ const p = this.transformedPoint(ev.clientX, ev.clientY)
|
|
|
if (isInput) {
|
|
|
this.pointerLink.props.x1 = p.x
|
|
|
this.pointerLink.props.y1 = p.y
|
|
@@ -202,14 +222,14 @@ export default {
|
|
|
this.pointerLink.props.y2 = p.y
|
|
|
}
|
|
|
}
|
|
|
- const drop = (e) => {
|
|
|
+ const drop = (ev) => {
|
|
|
document.removeEventListener('mousemove', drag)
|
|
|
document.removeEventListener('mouseup', drop)
|
|
|
this.pointerLink.active = false
|
|
|
|
|
|
- const targetNodeId = e.target.getAttribute('data-nodeid')
|
|
|
- const targetIn = e.target.getAttribute('data-in')
|
|
|
- const targetOut = e.target.getAttribute('data-out')
|
|
|
+ const targetNodeId = ev.target.getAttribute('data-nodeid')
|
|
|
+ const targetIn = ev.target.getAttribute('data-in')
|
|
|
+ const targetOut = ev.target.getAttribute('data-out')
|
|
|
// Not a node or same node
|
|
|
if (targetNodeId === undefined || targetNodeId === nodeId) {
|
|
|
console.error('LINK: target is not a socket/ same socket')
|
|
@@ -256,10 +276,11 @@ export default {
|
|
|
document.addEventListener('mousemove', drag)
|
|
|
document.addEventListener('mouseup', drop)
|
|
|
},
|
|
|
- nodeDragStart (e, i) {
|
|
|
- e.preventDefault()
|
|
|
+ nodeDragStart (ev, i) {
|
|
|
+ document.activeElement && document.activeElement.blur()
|
|
|
+ ev.preventDefault()
|
|
|
var tnode = this.nodeData.nodes[i]
|
|
|
- if (e.button === 1) {
|
|
|
+ if (ev.button === 1) {
|
|
|
// remove related links
|
|
|
this.nodeData.links = this.nodeData.links.filter(l => l.from !== tnode.id && l.to !== tnode.id)
|
|
|
this.nodeData.nodes.splice(i, 1)
|
|
@@ -268,26 +289,26 @@ export default {
|
|
|
this.$emit('shouldPersist')
|
|
|
return
|
|
|
}
|
|
|
- if (e.button !== 0) return // first button
|
|
|
+ if (ev.button !== 0) return // first button
|
|
|
// we can handle with nodeId and a search
|
|
|
this.nodeData.nodes.splice(i, 1)
|
|
|
this.nodeData.nodes.push(tnode) // put in last
|
|
|
|
|
|
this.dragging = tnode
|
|
|
// transform CTM
|
|
|
- const delta = this.transformedPoint(e.clientX, e.clientY)
|
|
|
+ const delta = this.transformedPoint(ev.clientX, ev.clientY)
|
|
|
delta.x -= tnode.x
|
|
|
delta.y -= tnode.y
|
|
|
|
|
|
- const drag = (e) => {
|
|
|
- const point = this.transformedPoint(e.clientX, e.clientY)
|
|
|
+ const drag = (ev) => {
|
|
|
+ const point = this.transformedPoint(ev.clientX, ev.clientY)
|
|
|
tnode.x = point.x - delta.x
|
|
|
tnode.y = point.y - delta.y
|
|
|
// Bad possibly
|
|
|
this.$emit('input', this.nodeData)
|
|
|
this.$emit('nodeUpdate', tnode)
|
|
|
}
|
|
|
- const drop = (e) => {
|
|
|
+ const drop = (ev) => {
|
|
|
document.removeEventListener('mousemove', drag)
|
|
|
document.removeEventListener('mouseup', drop)
|
|
|
this.dragging = null
|
|
@@ -299,13 +320,13 @@ export default {
|
|
|
document.addEventListener('mousemove', drag)
|
|
|
document.addEventListener('mouseup', drop)
|
|
|
},
|
|
|
- nodeAdd (k) {
|
|
|
+ nodeAdd (src, x = 100, y = 100) {
|
|
|
const newNode = {
|
|
|
id: guid(),
|
|
|
- x: 100,
|
|
|
- y: 100,
|
|
|
- label: k,
|
|
|
- src: k
|
|
|
+ x: x,
|
|
|
+ y: y,
|
|
|
+ label: src,
|
|
|
+ src: src
|
|
|
}
|
|
|
this.nodeData.nodes.push(newNode)
|
|
|
this.$emit('input', this.nodeData)
|
|
@@ -327,7 +348,17 @@ export default {
|
|
|
this.$emit('linkRemove', link)
|
|
|
this.$emit('shouldPersist')
|
|
|
},
|
|
|
+ managerDrop (ev) {
|
|
|
+ ev.preventDefault()
|
|
|
+ const reg = ev.dataTransfer.getData('text')
|
|
|
+ if (this.registry[reg] === undefined) {
|
|
|
+ console.error('Registry: Drop src not found in registry')
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
+ const pt = this.transformedPoint(ev.x, ev.y)
|
|
|
+ this.nodeAdd(reg, pt.x, pt.y)
|
|
|
+ },
|
|
|
// HELPERS depending on svg ref
|
|
|
createSVGPoint (x, y) {
|
|
|
const p = this.$refs.svg.createSVGPoint()
|
|
@@ -344,6 +375,7 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+// utils
|
|
|
function guid () {
|
|
|
function s4 () {
|
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
@@ -357,11 +389,63 @@ function guid () {
|
|
|
<style>
|
|
|
.flow-container {
|
|
|
display:flex;
|
|
|
- flex-flow:column;
|
|
|
+ flex-flow:row;
|
|
|
+}
|
|
|
+
|
|
|
+.flow-svgcontainer {
|
|
|
+ width:100%;
|
|
|
+ height:100%;
|
|
|
+ position:relative;
|
|
|
+}
|
|
|
+.flow-svgcontainer__control {
|
|
|
+ position:absolute;
|
|
|
+ top: 10px;
|
|
|
+ left: 10px;
|
|
|
+ display:flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.flow-svgcontainer__control button {
|
|
|
+ display:flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.flow-svgcontainer__info {
|
|
|
+ position:absolute;
|
|
|
+ bottom:10px;
|
|
|
+ left:10px;
|
|
|
+ padding:2px;
|
|
|
+ font-size:8px;
|
|
|
+ color: #777;
|
|
|
}
|
|
|
svg.view {
|
|
|
border:none;
|
|
|
position:relative;
|
|
|
}
|
|
|
|
|
|
+.split .splitter{
|
|
|
+ flex-basis:0px;
|
|
|
+ position:relative;
|
|
|
+ background: rgba(208,208,208,0.9);
|
|
|
+}
|
|
|
+.split:not(.resizeable) .content:first-child {
|
|
|
+ transition: all .3s;
|
|
|
+}
|
|
|
+
|
|
|
+.split.resizeable.horizontal .splitter::after {
|
|
|
+ display:flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index:100;
|
|
|
+ content:" ";
|
|
|
+ position:absolute;
|
|
|
+
|
|
|
+ top:20%;
|
|
|
+ bottom:20%;
|
|
|
+ left:0px;
|
|
|
+ width:20px;
|
|
|
+
|
|
|
+ background: rgba(0,0,0,0.4);
|
|
|
+ transition: all .3s;
|
|
|
+}
|
|
|
+
|
|
|
</style>
|