123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- <template>
- <div class="flow-container">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- ref="svg"
- class="flow-view view"
- :class="{
- 'flow-linking':linking || stickySockets,
- 'activity':dragging || pointerLink.active ,
- 'flow-node--activity':nodeActivity,
- 'selecting': !!selector
- }"
- @dragover.prevent
- @drop="managerDrop"
- @mousedown="viewPointerDown"
- :width="width"
- :height="height">
- <svg-defs/>
- <flow-pan-zoom
- ref="panzoom"
- v-model="panzoom">
- <!-- links below nodes -->
- <flow-link
- v-for="(link,i) in nodeData.links"
- :key="i"
- v-bind="linkProps(link)"
- @click="linkPointerClick($event,link)"
- />
- <!-- nodes -->
- <flow-node
- ref="nodes"
- v-for="(n,i) of nodeData.nodes"
- v-bind="nodeProps(n)"
- :key="'node' + n.id"
- :id="n.id"
- :selected="nodeSelection[n.id]?true:false"
- @nodePointerDown.prevent="nodePointerDown($event,i)"
- @socketPointerDown="socketPointerDown(n.id,...arguments)"
- @nodeDoubleClick="$emit('nodeDblClick',n)"
- />
- <!-- mouse link-->
- <flow-link
- :pointer="true"
- v-if="pointerLink.active"
- v-bind="pointerLink.props"
- />
- <rect
- class="flow-selector"
- :class="{'flow-selector--selecting':(selector)?true:false}"
- v-bind="selector"/>
- </flow-pan-zoom>
- </svg>
- <div class="flow-container__control">
- <button @click="$emit('funcsPanelToggle')">Panel</button>
- <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} sockets </button>
- <button @click="nodeActivity=!nodeActivity"> {{ nodeActivity? 'Hide':'Show' }} activity </button>
- <button @click="$emit('documentSave')"> Save </button> <!-- should disable until confirmation -->
- <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">
- Reset view
- </button>
- </div>
- <div class="flow-container__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>
- </template>
- <script>
- import FlowNode from './node'
- import FlowLink from './link'
- import FlowPanZoom from './panzoom'
- import SvgDefs from './svgdefswrapper'
- import utils from '@/utils/utils'
- export default {
- name: 'FlowManager',
- components: {FlowNode, FlowLink, FlowPanZoom, SvgDefs},
- props: {
- 'activity': {type: Object, default: () => {}},
- 'registry': {type: Object, default: () => {}},
- 'width': {type: String, default: '800px'},
- 'height': {type: String, default: '600px'}
- },
- data () {
- // const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
- return {
- panzoom: { x: 0, y: 0, zoom: 1 },
- nodeData: { nodes: [], links: [] },
- dragging: null,
- linking: false,
- pointerLink: {active: false, props: {}, src: {}},
- selector: null,
- nodeSelection: {},
- stickySockets: false,
- nodeActivity: true
- }
- },
- computed: {
- nodeProps () {
- return (node) => {
- let match = {}
- if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
- if (this.pointerLink.src.in !== undefined) {
- match = {out: this.pointerLink.src.type}
- } else {
- match = {in: this.pointerLink.src.type}
- }
- }
- const nodeClass = this.registry[node.src]
- if (!nodeClass) {
- this.nodeRemove(node)
- return
- }
- return {
- transform: `translate(${node.x} ${node.y})`,
- id: node.id,
- label: node.label,
- inputs: nodeClass.inputs,
- output: nodeClass.output,
- match: match,
- dragging: this.dragging && !!this.dragging[node.id],
- activity: this.activity[node.id],
- nodeStyle: nodeClass.style
- }
- }
- },
- linkProps () {
- return (link) => {
- if (!this.$refs.nodes) return
- // For size .x .y
- const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
- const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
- const refFrom = this.$refs.nodes.find(n => n.id === link.from)
- const refTo = this.$refs.nodes.find(n => n.id === link.to)
- if (!refFrom || !refTo) { // delete link
- return {}
- }
- const fromOutput = refFrom.outputPos(0) // only 1 output
- const toInput = refTo.inputPos(link.in)
- return {
- x1: nodeFrom.x + fromOutput.x,
- y1: nodeFrom.y + fromOutput.y,
- x2: nodeTo.x + toInput.x,
- y2: nodeTo.y + toInput.y,
- status: this.activity[nodeFrom.id] && this.activity[nodeFrom.id].status
- }
- }
- }
- },
- mounted () {
- this.$flowService.on('document', (v) => {
- if (!v || !v.data || !v.data.nodes) { return }
- const nodes = []
- for (let node of v.data.nodes) {
- if (this.dragging && !!this.dragging[node.id]) {
- continue
- }
- let localNode = this.nodeData.nodes.find(n => n.id === node.id)
- if (localNode) {
- Object.assign(localNode, node)
- nodes.push(localNode)
- continue
- }
- nodes.push(node)
- }
- if (this.dragging) { // Add our nodes
- for (let nid in this.dragging) {
- nodes.push(this.dragging[nid])
- }
- }
- this.nodeData = {
- nodes: nodes,
- links: v.data.links
- }
- this.$nextTick(() => {
- this.$forceUpdate()
- })
- })
- this.$flowService.on('nodeUpdate', (v) => {
- const nodes = v.data
- const nd = this.nodeData
- for (let nid in nodes) {
- const node = nodes[nid]
- const idx = nd.nodes.findIndex(n => n.id === node.id)
- if (idx === -1) { // new node
- nd.nodes.push(nodes[nid])
- continue
- }
- // if (!this.dragging || !this.dragging[node.id]) {
- this.$set(nd.nodes, idx, node) // new Node
- // }
- }
- })
- this.$nextTick(() => {
- this.$forceUpdate()
- })
- document.addEventListener('keydown', this.keyDown)
- document.addEventListener('keyup', this.keyUp)
- },
- beoreDestroy () {
- document.removeEventListener('keydown', this.keyDown)
- document.removeEventListener('keyup', this.keyUp)
- },
- methods: {
- keyDown (ev) {
- if (document.activeElement && document.activeElement.matches('input,textarea')) { return }
- if (ev.shiftKey) {
- this.linking = true
- }
- let single = null
- const selectionIds = Object.keys(this.nodeSelection)
- if (selectionIds.length === 1) { single = this.nodeSelection[selectionIds[0]] }
- switch (ev.key) {
- case 'Enter':
- if (!single) { return }
- this.$emit('nodeInspect', single)
- break
- case 'Delete':
- if (!this.nodeSelection) { return }
- for (let k in this.nodeSelection) {
- this.nodeRemove(this.nodeSelection[k])
- }
- break
- case 'a':
- if (ev.ctrlKey) {
- ev.preventDefault()
- ev.stopPropagation()
- this.nodeSelection = {}
- for (let n of this.nodeData.nodes) {
- this.nodeSelection[n.id] = n
- }
- }
- break
- }
- },
- keyUp (ev) {
- if (!ev.shiftKey) {
- this.linking = false
- }
- },
- panzoomReset () {
- this.panzoom = {x: 0, y: 0, zoom: 1}
- },
- // XXX: Shrink this function
- // and create some LinkAdd method
- socketPointerDown (nodeId, ev, socket) {
- if (ev.button !== 0) return
- 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(ev.clientX, ev.clientY)
- this.pointerLink.props = {
- x1: node.x + socketPos.x,
- y1: node.y + socketPos.y,
- x2: node.x + socketPos.x,
- y2: node.y + socketPos.y
- }
- this.pointerLink.active = true
- if (isInput) {
- this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in].type, in: socket.in}
- } else {
- this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output.type, out: 0}
- }
- utils.createDrag({
- drag: (ev) => {
- const p = this.transformedPoint(ev.clientX, ev.clientY)
- if (isInput) {
- this.pointerLink.props.x1 = p.x
- this.pointerLink.props.y1 = p.y
- } else {
- this.pointerLink.props.x2 = p.x
- this.pointerLink.props.y2 = p.y
- }
- },
- drop: (ev) => {
- this.pointerLink.active = false
- // find Parent
- var curTarget = ev.target
- for (;curTarget = curTarget.parentNode; curTarget !== document.body) {
- if (curTarget.hasAttribute('data-nodeid')) {
- break
- }
- }
- if (curTarget === document.body) {
- console.error('LINK: target is not a socket')
- return
- }
- const targetNodeId = curTarget.getAttribute('data-nodeid')
- const targetIn = curTarget.getAttribute('data-in')
- const targetOut = curTarget.getAttribute('data-out')
- let link
- // target is input
- if (targetIn && !isInput) {
- link = {
- from: nodeId,
- to: targetNodeId,
- in: parseInt(targetIn)
- }
- } else if (targetOut && isInput) {
- link = {
- from: targetNodeId,
- to: nodeId,
- in: socket.in
- }
- }
- // No link
- if (!link) {
- console.error('LINK: input same direction (in->in/out->out)')
- return
- }
- const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
- const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
- const output = this.registry[nodeFrom.src].output.type
- const input = this.registry[nodeTo.src].inputs[link.in].type
- // Type checking
- if (!(output === 'interface {}' || output === input || input === 'interface {}')) {
- console.error('LINK: Invalid type')
- return
- }
- // Input already exists, replace
- const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
- if (existingInputI !== -1) {
- this.nodeData.links.splice(existingInputI, 1)
- }
- this.linkAdd(link)
- }})
- },
- nodePointerDown (ev, i) {
- document.activeElement && document.activeElement.blur()
- const tnode = this.nodeData.nodes[i]
- if (ev.button === 1) {
- this.nodeRemove(tnode)
- // remove related links
- return
- }
- if (ev.button !== 0) return // first button
- if (ev.shiftKey) {
- this.socketPointerDown(tnode.id, ev, {out: 0})
- return
- }
- this.$emit('nodeInspect', tnode)
- // Switch selection
- if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) this.nodeSelection = {}
- this.nodeSelection[tnode.id] = tnode
- // we can handle with nodeId and a search
- // put the list of nodes in last
- for (let nk in this.nodeSelection) {
- let ni = this.nodeData.nodes.findIndex(n => n.id === this.nodeSelection[nk].id)
- this.nodeData.nodes.splice(ni, 1)
- this.nodeData.nodes.push(this.nodeSelection[nk]) // put in last
- }
- let curP = this.transformedPoint(ev.x, ev.y)
- this.dragging = this.nodeSelection
- utils.createDrag({
- drag: (ev) => {
- if (this.nodeSelection === undefined) {
- console.error('Well something went wrong')
- }
- const dragP = this.transformedPoint(ev.x, ev.y)
- for (let n in this.nodeSelection) {
- this.nodeSelection[n].x += dragP.x - curP.x
- this.nodeSelection[n].y += dragP.y - curP.y
- }
- this.sendFlowEvent('nodeUpdate', this.nodeSelection)
- curP = dragP
- },
- drop: (ev) => {
- this.dragging = null
- this.sendDocumentUpdate()
- }
- })
- },
- nodeRemove (node) {
- const i = this.nodeData.nodes.indexOf(node)
- if (i === -1) return
- this.nodeData.links = this.nodeData.links.filter(l => l.from !== node.id && l.to !== node.id)
- this.nodeData.nodes.splice(i, 1)
- this.sendFlowEvent('nodeRemove', node)
- this.sendDocumentUpdate()
- },
- nodeAdd (src, x = 100, y = 100) {
- const newNode = {
- id: utils.guid(),
- x: x,
- y: y,
- label: src,
- src: src
- }
- // Setup Props
- if (this.registry[src].props) {
- for (let k in this.registry[src].props) {
- newNode.prop || (newNode.prop = {})
- newNode.prop[k] = ''
- }
- }
- this.nodeData.nodes.push(newNode)
- let nu = {}
- this.sendFlowEvent('nodeUpdate', (nu[newNode.id] = newNode, nu))
- this.sendDocumentUpdate()
- },
- linkPointerClick (ev, link) {
- ev.preventDefault()
- this.linkRemove(link)
- },
- linkAdd (link) {
- this.nodeData.links.push(link)
- this.sendFlowEvent('linkUpdate', link)
- this.sendDocumentUpdate()
- },
- linkRemove (link) {
- const i = this.nodeData.links.findIndex(l => l === link)
- if (i === -1) return
- this.nodeData.links.splice(i, 1)
- this.sendFlowEvent('linkRemove', link)
- this.sendDocumentUpdate()
- },
- 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)
- },
- viewPointerDown (ev) {
- if (ev.button !== 0) return
- ev.preventDefault()
- this.nodeSelection = {}
- const p = this.transformedPoint(ev.x, ev.y)
- this.selector = {x: p.x, y: p.y, width: 0, height: 0}
- utils.createDrag({
- drag: (evd) => {
- // transform again in case we changed zoom/pan
- const p = this.transformedPoint(ev.x, ev.y)
- const p2 = this.transformedPoint(evd.x, evd.y)
- const nwidth = p2.x - p.x
- const nheight = p2.y - p.y
- this.selector = {
- x: nwidth < 0 ? p2.x : p.x,
- y: nheight < 0 ? p2.y : p.y,
- width: nwidth < 0 ? -nwidth : nwidth,
- height: nheight < 0 ? -nheight : nheight
- }
- },
- drop: (ev) => {
- for (let n of this.nodeData.nodes) {
- if (n.x > this.selector.x && n.x < (this.selector.x + this.selector.width) &&
- n.y > this.selector.y && n.y < (this.selector.y + this.selector.height)
- ) {
- this.nodeSelection[n.id] = n
- }
- }
- this.selector = null
- }})
- },
- // service events
- sendFlowEvent (type, param) {
- this.$flowService[type](param)
- },
- sendDocumentUpdate (nodeData) {
- this.$flowService.documentUpdate(this.nodeData, this.$route.params.sessId)
- },
- /* sendDocumentRun () {
- console.log('Document running')
- this.$flowService.documentRun(this.nodeData, this.$route.params.sessId)
- }, */
- // HELPERS depending on svg ref
- createSVGPoint (x, y) {
- const p = this.$refs.svg.createSVGPoint()
- p.x = x; p.y = y
- return p
- },
- transformedPoint (x, y, abs) {
- const svgRect = this.$refs.svg.getBoundingClientRect()
- if (!abs) {
- x -= svgRect.left
- y -= svgRect.top
- }
- return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
- }
- }
- }
- </script>
- <style>
- .flow-container {
- display:flex;
- flex-flow:row;
- position:relative;
- }
- .flow-container__control {
- position:absolute;
- top: 20px;
- left: 20px;
- display:flex;
- justify-content: center;
- align-items: center;
- color: #fff;
- }
- .flow-container__control button {
- display:flex;
- justify-content: center;
- margin:0;
- padding:14px;
- color:#333;
- }
- .flow-container__info {
- position:absolute;
- bottom:10px;
- left:10px;
- padding:2px;
- font-size:9px;
- }
- .flow-view {
- border:none;
- position:relative;
- fill:transparent;
- }
- .flow-selector {
- pointer-events:none;
- opacity:0;
- }
- .flow-selector.flow-selector--selecting {
- opacity:1;
- }
- </style>
|