manager.vue 16 KB

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