manager.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. <template>
  2. <div class="flow-container">
  3. <svg
  4. xmlns="http://www.w3.org/2000/svg"
  5. xmlns:xlink="http://www.w3.org/1999/xlink"
  6. ref="svg"
  7. class="flow-view view"
  8. :class="{
  9. 'flow-linking':linking || stickySockets,
  10. 'activity':dragging || pointerLink.active ,
  11. 'flow-node--activity':nodeActivity,
  12. 'selecting': !!selector
  13. }"
  14. @dragover.prevent
  15. @drop="managerDrop"
  16. @mousedown="viewPointerDown"
  17. :width="width"
  18. :height="height">
  19. <svg-defs/>
  20. <flow-pan-zoom
  21. ref="panzoom"
  22. v-model="panzoom">
  23. <!-- links below nodes -->
  24. <flow-link
  25. v-for="(link,i) in nodeData.links"
  26. :key="i"
  27. v-bind="linkProps(link)"
  28. @click="linkPointerClick($event,link)"
  29. />
  30. <!-- nodes -->
  31. <flow-node
  32. ref="nodes"
  33. v-for="(n,i) of nodeData.nodes"
  34. v-bind="nodeProps(n)"
  35. :key="'node' + n.id"
  36. :id="n.id"
  37. :selected="nodeSelection[n.id]?true:false"
  38. @nodePointerDown.prevent="nodePointerDown($event,i)"
  39. @socketPointerDown="socketPointerDown(n.id,...arguments)"
  40. @nodeDoubleClick="$emit('nodeDblClick',n)"
  41. />
  42. <!-- mouse link-->
  43. <flow-link
  44. :pointer="true"
  45. v-if="pointerLink.active"
  46. v-bind="pointerLink.props"
  47. />
  48. <rect
  49. class="flow-selector"
  50. :class="{'flow-selector--selecting':(selector)?true:false}"
  51. v-bind="selector"/>
  52. </flow-pan-zoom>
  53. </svg>
  54. <div class="flow-container__control">
  55. <button @click="$emit('funcsPanelToggle')">Panel</button>
  56. <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} sockets </button>
  57. <button @click="nodeActivity=!nodeActivity"> {{ nodeActivity? 'Hide':'Show' }} activity </button>
  58. <button @click="$emit('documentSave')"> Save </button> <!-- should disable until confirmation -->
  59. <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">
  60. Reset view
  61. </button>
  62. </div>
  63. <div class="flow-container__info">
  64. x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }}
  65. nodes: {{ nodeData.nodes.length }}
  66. links: {{ nodeData.links.length }}
  67. </div>
  68. </div>
  69. </template>
  70. <script>
  71. import FlowNode from './node'
  72. import FlowLink from './link'
  73. import FlowPanZoom from './panzoom'
  74. import SvgDefs from './svgdefswrapper'
  75. import utils from '@/utils/utils'
  76. export default {
  77. name: 'FlowManager',
  78. components: {FlowNode, FlowLink, FlowPanZoom, SvgDefs},
  79. props: {
  80. 'activity': {type: Object, default: () => {}},
  81. 'registry': {type: Object, default: () => {}},
  82. 'width': {type: String, default: '800px'},
  83. 'height': {type: String, default: '600px'}
  84. },
  85. data () {
  86. // const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
  87. return {
  88. panzoom: { x: 0, y: 0, zoom: 1 },
  89. nodeData: { nodes: [], links: [] },
  90. dragging: null,
  91. linking: false,
  92. pointerLink: {active: false, props: {}, src: {}},
  93. selector: null,
  94. nodeSelection: {},
  95. stickySockets: false,
  96. nodeActivity: true
  97. }
  98. },
  99. computed: {
  100. nodeProps () {
  101. return (node) => {
  102. let match = {}
  103. if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
  104. if (this.pointerLink.src.in !== undefined) {
  105. match = {out: this.pointerLink.src.type}
  106. } else {
  107. match = {in: this.pointerLink.src.type}
  108. }
  109. }
  110. const nodeClass = this.registry[node.src]
  111. if (!nodeClass) {
  112. this.nodeRemove(node)
  113. return
  114. }
  115. return {
  116. transform: `translate(${node.x} ${node.y})`,
  117. id: node.id,
  118. label: node.label,
  119. inputs: nodeClass.inputs,
  120. output: nodeClass.output,
  121. match: match,
  122. dragging: this.dragging && !!this.dragging[node.id],
  123. activity: this.activity[node.id],
  124. nodeStyle: nodeClass.style
  125. }
  126. }
  127. },
  128. linkProps () {
  129. return (link) => {
  130. if (!this.$refs.nodes) return
  131. // For size .x .y
  132. const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
  133. const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
  134. const refFrom = this.$refs.nodes.find(n => n.id === link.from)
  135. const refTo = this.$refs.nodes.find(n => n.id === link.to)
  136. if (!refFrom || !refTo) { // delete link
  137. return {}
  138. }
  139. const fromOutput = refFrom.outputPos(0) // only 1 output
  140. const toInput = refTo.inputPos(link.in)
  141. return {
  142. x1: nodeFrom.x + fromOutput.x,
  143. y1: nodeFrom.y + fromOutput.y,
  144. x2: nodeTo.x + toInput.x,
  145. y2: nodeTo.y + toInput.y,
  146. status: this.activity[nodeFrom.id] && this.activity[nodeFrom.id].status
  147. }
  148. }
  149. }
  150. },
  151. mounted () {
  152. this.$flowService.on('document', (v) => {
  153. if (!v || !v.data || !v.data.nodes) { return }
  154. const nodes = []
  155. for (let node of v.data.nodes) {
  156. if (this.dragging && !!this.dragging[node.id]) {
  157. continue
  158. }
  159. let localNode = this.nodeData.nodes.find(n => n.id === node.id)
  160. if (localNode) {
  161. Object.assign(localNode, node)
  162. nodes.push(localNode)
  163. continue
  164. }
  165. nodes.push(node)
  166. }
  167. if (this.dragging) { // Add our nodes
  168. for (let nid in this.dragging) {
  169. nodes.push(this.dragging[nid])
  170. }
  171. }
  172. this.nodeData = {
  173. nodes: nodes,
  174. links: v.data.links
  175. }
  176. this.$nextTick(() => {
  177. this.$forceUpdate()
  178. })
  179. })
  180. this.$flowService.on('nodeUpdate', (v) => {
  181. const nodes = v.data
  182. const nd = this.nodeData
  183. for (let nid in nodes) {
  184. const node = nodes[nid]
  185. const idx = nd.nodes.findIndex(n => n.id === node.id)
  186. if (idx === -1) { // new node
  187. nd.nodes.push(nodes[nid])
  188. continue
  189. }
  190. // if (!this.dragging || !this.dragging[node.id]) {
  191. this.$set(nd.nodes, idx, node) // new Node
  192. // }
  193. }
  194. })
  195. this.$nextTick(() => {
  196. this.$forceUpdate()
  197. })
  198. document.addEventListener('keydown', this.keyDown)
  199. document.addEventListener('keyup', this.keyUp)
  200. },
  201. beoreDestroy () {
  202. document.removeEventListener('keydown', this.keyDown)
  203. document.removeEventListener('keyup', this.keyUp)
  204. },
  205. methods: {
  206. keyDown (ev) {
  207. if (document.activeElement && document.activeElement.matches('input,textarea')) { return }
  208. if (ev.shiftKey) {
  209. this.linking = true
  210. }
  211. let single = null
  212. const selectionIds = Object.keys(this.nodeSelection)
  213. if (selectionIds.length === 1) { single = this.nodeSelection[selectionIds[0]] }
  214. switch (ev.key) {
  215. case 'Enter':
  216. if (!single) { return }
  217. this.$emit('nodeInspect', single)
  218. break
  219. case 'Delete':
  220. if (!this.nodeSelection) { return }
  221. for (let k in this.nodeSelection) {
  222. this.nodeRemove(this.nodeSelection[k])
  223. }
  224. break
  225. case 'a':
  226. if (ev.ctrlKey) {
  227. ev.preventDefault()
  228. ev.stopPropagation()
  229. this.nodeSelection = {}
  230. for (let n of this.nodeData.nodes) {
  231. this.nodeSelection[n.id] = n
  232. }
  233. }
  234. break
  235. }
  236. },
  237. keyUp (ev) {
  238. if (!ev.shiftKey) {
  239. this.linking = false
  240. }
  241. },
  242. panzoomReset () {
  243. this.panzoom = {x: 0, y: 0, zoom: 1}
  244. },
  245. // XXX: Shrink this function
  246. // and create some LinkAdd method
  247. socketPointerDown (nodeId, ev, socket) {
  248. if (ev.button !== 0) return
  249. const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
  250. const node = this.nodeData.nodes.find(n => n.id === nodeId)
  251. const isInput = socket.in !== undefined
  252. const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
  253. // const p = this.transformedPoint(ev.clientX, ev.clientY)
  254. this.pointerLink.props = {
  255. x1: node.x + socketPos.x,
  256. y1: node.y + socketPos.y,
  257. x2: node.x + socketPos.x,
  258. y2: node.y + socketPos.y
  259. }
  260. this.pointerLink.active = true
  261. if (isInput) {
  262. this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in].type, in: socket.in}
  263. } else {
  264. this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output.type, out: 0}
  265. }
  266. utils.createDrag({
  267. drag: (ev) => {
  268. const p = this.transformedPoint(ev.clientX, ev.clientY)
  269. if (isInput) {
  270. this.pointerLink.props.x1 = p.x
  271. this.pointerLink.props.y1 = p.y
  272. } else {
  273. this.pointerLink.props.x2 = p.x
  274. this.pointerLink.props.y2 = p.y
  275. }
  276. },
  277. drop: (ev) => {
  278. this.pointerLink.active = false
  279. // find Parent
  280. var curTarget = ev.target
  281. for (;curTarget = curTarget.parentNode; curTarget !== document.body) {
  282. if (curTarget.hasAttribute('data-nodeid')) {
  283. break
  284. }
  285. }
  286. if (curTarget === document.body) {
  287. console.error('LINK: target is not a socket')
  288. return
  289. }
  290. const targetNodeId = curTarget.getAttribute('data-nodeid')
  291. const targetIn = curTarget.getAttribute('data-in')
  292. const targetOut = curTarget.getAttribute('data-out')
  293. let link
  294. // target is input
  295. if (targetIn && !isInput) {
  296. link = {
  297. from: nodeId,
  298. to: targetNodeId,
  299. in: parseInt(targetIn)
  300. }
  301. } else if (targetOut && isInput) {
  302. link = {
  303. from: targetNodeId,
  304. to: nodeId,
  305. in: socket.in
  306. }
  307. }
  308. // No link
  309. if (!link) {
  310. console.error('LINK: input same direction (in->in/out->out)')
  311. return
  312. }
  313. const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
  314. const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
  315. const output = this.registry[nodeFrom.src].output.type
  316. const input = this.registry[nodeTo.src].inputs[link.in].type
  317. // Type checking
  318. if (!(output === 'interface {}' || output === input || input === 'interface {}')) {
  319. console.error('LINK: Invalid type')
  320. return
  321. }
  322. // Input already exists, replace
  323. const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
  324. if (existingInputI !== -1) {
  325. this.nodeData.links.splice(existingInputI, 1)
  326. }
  327. this.linkAdd(link)
  328. }})
  329. },
  330. nodePointerDown (ev, i) {
  331. document.activeElement && document.activeElement.blur()
  332. const tnode = this.nodeData.nodes[i]
  333. if (ev.button === 1) {
  334. this.nodeRemove(tnode)
  335. // remove related links
  336. return
  337. }
  338. if (ev.button !== 0) return // first button
  339. if (ev.shiftKey) {
  340. this.socketPointerDown(tnode.id, ev, {out: 0})
  341. return
  342. }
  343. this.$emit('nodeInspect', tnode)
  344. // Switch selection
  345. if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) this.nodeSelection = {}
  346. this.nodeSelection[tnode.id] = tnode
  347. // we can handle with nodeId and a search
  348. // put the list of nodes in last
  349. for (let nk in this.nodeSelection) {
  350. let ni = this.nodeData.nodes.findIndex(n => n.id === this.nodeSelection[nk].id)
  351. this.nodeData.nodes.splice(ni, 1)
  352. this.nodeData.nodes.push(this.nodeSelection[nk]) // put in last
  353. }
  354. let curP = this.transformedPoint(ev.x, ev.y)
  355. this.dragging = this.nodeSelection
  356. utils.createDrag({
  357. drag: (ev) => {
  358. if (this.nodeSelection === undefined) {
  359. console.error('Well something went wrong')
  360. }
  361. const dragP = this.transformedPoint(ev.x, ev.y)
  362. for (let n in this.nodeSelection) {
  363. this.nodeSelection[n].x += dragP.x - curP.x
  364. this.nodeSelection[n].y += dragP.y - curP.y
  365. }
  366. this.sendFlowEvent('nodeUpdate', this.nodeSelection)
  367. curP = dragP
  368. },
  369. drop: (ev) => {
  370. this.dragging = null
  371. this.sendDocumentUpdate()
  372. }
  373. })
  374. },
  375. nodeRemove (node) {
  376. const i = this.nodeData.nodes.indexOf(node)
  377. if (i === -1) return
  378. this.nodeData.links = this.nodeData.links.filter(l => l.from !== node.id && l.to !== node.id)
  379. this.nodeData.nodes.splice(i, 1)
  380. this.sendFlowEvent('nodeRemove', node)
  381. this.sendDocumentUpdate()
  382. },
  383. nodeAdd (src, x = 100, y = 100) {
  384. const newNode = {
  385. id: utils.guid(),
  386. x: x,
  387. y: y,
  388. label: src,
  389. src: src
  390. }
  391. // Setup Props
  392. if (this.registry[src].props) {
  393. for (let k in this.registry[src].props) {
  394. newNode.prop || (newNode.prop = {})
  395. newNode.prop[k] = ''
  396. }
  397. }
  398. this.nodeData.nodes.push(newNode)
  399. let nu = {}
  400. this.sendFlowEvent('nodeUpdate', (nu[newNode.id] = newNode, nu))
  401. this.sendDocumentUpdate()
  402. },
  403. linkPointerClick (ev, link) {
  404. ev.preventDefault()
  405. this.linkRemove(link)
  406. },
  407. linkAdd (link) {
  408. this.nodeData.links.push(link)
  409. this.sendFlowEvent('linkUpdate', link)
  410. this.sendDocumentUpdate()
  411. },
  412. linkRemove (link) {
  413. const i = this.nodeData.links.findIndex(l => l === link)
  414. if (i === -1) return
  415. this.nodeData.links.splice(i, 1)
  416. this.sendFlowEvent('linkRemove', link)
  417. this.sendDocumentUpdate()
  418. },
  419. managerDrop (ev) {
  420. ev.preventDefault()
  421. const reg = ev.dataTransfer.getData('text')
  422. if (this.registry[reg] === undefined) {
  423. console.error('Registry: Drop src not found in registry')
  424. return
  425. }
  426. const pt = this.transformedPoint(ev.x, ev.y)
  427. this.nodeAdd(reg, pt.x, pt.y)
  428. },
  429. viewPointerDown (ev) {
  430. if (ev.button !== 0) return
  431. ev.preventDefault()
  432. this.nodeSelection = {}
  433. const p = this.transformedPoint(ev.x, ev.y)
  434. this.selector = {x: p.x, y: p.y, width: 0, height: 0}
  435. utils.createDrag({
  436. drag: (evd) => {
  437. // transform again in case we changed zoom/pan
  438. const p = this.transformedPoint(ev.x, ev.y)
  439. const p2 = this.transformedPoint(evd.x, evd.y)
  440. const nwidth = p2.x - p.x
  441. const nheight = p2.y - p.y
  442. this.selector = {
  443. x: nwidth < 0 ? p2.x : p.x,
  444. y: nheight < 0 ? p2.y : p.y,
  445. width: nwidth < 0 ? -nwidth : nwidth,
  446. height: nheight < 0 ? -nheight : nheight
  447. }
  448. },
  449. drop: (ev) => {
  450. for (let n of this.nodeData.nodes) {
  451. if (n.x > this.selector.x && n.x < (this.selector.x + this.selector.width) &&
  452. n.y > this.selector.y && n.y < (this.selector.y + this.selector.height)
  453. ) {
  454. this.nodeSelection[n.id] = n
  455. }
  456. }
  457. this.selector = null
  458. }})
  459. },
  460. // service events
  461. sendFlowEvent (type, param) {
  462. this.$flowService[type](param)
  463. },
  464. sendDocumentUpdate (nodeData) {
  465. this.$flowService.documentUpdate(this.nodeData, this.$route.params.sessId)
  466. },
  467. /* sendDocumentRun () {
  468. console.log('Document running')
  469. this.$flowService.documentRun(this.nodeData, this.$route.params.sessId)
  470. }, */
  471. // HELPERS depending on svg ref
  472. createSVGPoint (x, y) {
  473. const p = this.$refs.svg.createSVGPoint()
  474. p.x = x; p.y = y
  475. return p
  476. },
  477. transformedPoint (x, y, abs) {
  478. const svgRect = this.$refs.svg.getBoundingClientRect()
  479. if (!abs) {
  480. x -= svgRect.left
  481. y -= svgRect.top
  482. }
  483. return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
  484. }
  485. }
  486. }
  487. </script>
  488. <style>
  489. .flow-container {
  490. display:flex;
  491. flex-flow:row;
  492. position:relative;
  493. }
  494. .flow-container__control {
  495. position:absolute;
  496. top: 20px;
  497. left: 20px;
  498. display:flex;
  499. justify-content: center;
  500. align-items: center;
  501. color: #fff;
  502. }
  503. .flow-container__control button {
  504. display:flex;
  505. justify-content: center;
  506. margin:0;
  507. padding:14px;
  508. color:#333;
  509. }
  510. .flow-container__info {
  511. position:absolute;
  512. bottom:10px;
  513. left:10px;
  514. padding:2px;
  515. font-size:9px;
  516. }
  517. .flow-view {
  518. border:none;
  519. position:relative;
  520. fill:transparent;
  521. }
  522. .flow-selector {
  523. pointer-events:none;
  524. opacity:0;
  525. }
  526. .flow-selector.flow-selector--selecting {
  527. opacity:1;
  528. }
  529. </style>