ソースを参照

Panel implementation/ Splitter

luis 7 年 前
コミット
216405e6ed

+ 0 - 7
browser/vue-flow/src/App.vue

@@ -14,13 +14,6 @@ export default {
 * {
   box-sizing:border-box;
 }
-html,body {
-  padding:0;
-  margin:0;
-  height:100vh;
-  max-width:100%;
-  max-height:100%;
-}
 #app {
   position:relative;
   padding:0;

ファイルの差分が大きいため隠しています
+ 1 - 0
browser/vue-flow/src/assets/cog.svg


+ 6 - 0
browser/vue-flow/src/assets/ico/icons8-search.svg

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" version="1.1" width="50px" height="50px">
+<g id="surface1">
+<path style=" " d="M 21 3 C 11.621094 3 4 10.621094 4 20 C 4 29.378906 11.621094 37 21 37 C 24.710938 37 28.140625 35.804688 30.9375 33.78125 L 44.09375 46.90625 L 46.90625 44.09375 L 33.90625 31.0625 C 36.460938 28.085938 38 24.222656 38 20 C 38 10.621094 30.378906 3 21 3 Z M 21 5 C 29.296875 5 36 11.703125 36 20 C 36 28.296875 29.296875 35 21 35 C 12.703125 35 6 28.296875 6 20 C 6 11.703125 12.703125 5 21 5 Z "/>
+</g>
+</svg>

ファイルの差分が大きいため隠しています
+ 4 - 0
browser/vue-flow/src/assets/ico/icons8-settings.svg


ファイルの差分が大きいため隠しています
+ 6 - 0
browser/vue-flow/src/assets/ico/icons8-user-male.svg


+ 27 - 4
browser/vue-flow/src/assets/style.css

@@ -1,3 +1,12 @@
+html,
+body {
+  padding: 0;
+  margin: 0;
+  height: 100vh;
+  max-width: 100%;
+  max-height: 100%;
+}
+
 h4 {
   margin: 8px 0 8px 0;
   font-weight: bold;
@@ -8,18 +17,32 @@ b {
 }
 
 button {
+  background: transparent;
+  position: relative;
   outline: none;
   cursor: pointer;
-  background: #ccc;
   border: none;
   color: #333;
-  margin: 2px 0 2px 2px;
   padding: 10px;
   transition: all 0.3s;
 }
 
-button:hover {
-  background: #b1b1b1;
+button::after,
+.hover::after {
+  content: " ";
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  transition: all 0.3s;
+  background: #222;
+  opacity: 0;
+}
+
+button:hover::after,
+.hover:hover::after {
+  opacity: 0.1;
 }
 
 ul {

ファイルの差分が大きいため隠しています
+ 7 - 0
browser/vue-flow/src/assets/user.svg


+ 75 - 25
browser/vue-flow/src/components/chat.vue

@@ -1,9 +1,10 @@
 <template>
   <div class="flow-chat" :class="{active:active}">
-    <div class="flow-chat__toggle" @click="active=!active">=</div>
+    <button class="flow-chat__toggle" @click="active=!active">=</button>
     <div class="flow-chat__area">
       <div class="flow-chat__container">
-        <input class="handle" type="text" v-model="handle">
+
+        <input class="handle" type="text" v-model="handle" @blur="sendChatRename" @keyup.enter="sendChatRename">
         <div ref="messages" class="flow-chat__messages">
           <div v-for="m in events" class="message">
             <div class="handle">
@@ -13,15 +14,25 @@
             <div class="text">{{ m.message }}</div>
           </div>
         </div>
-      <input ref="input" class="message" @keyup.enter="send" type="text" v-model="input"></div>
+        <input ref="input" class="message" @keyup.enter="send" type="text" v-model="input">
+      </div> <!-- /container -->
+
+      <div class="flow-chat__users">
+        <div class="flow-chat__user" v-for="u in userList">
+          <img src="../assets/user.svg"><span>{{ u }}</span>
+        </div>
+      </div>
+
     </div>
+
   </div>
 </template>
 
 <script>
+
 // init
 let storedHandle = localStorage.getItem('handle')
-if (storedHandle === '') {
+if (!storedHandle || storedHandle === '') {
   storedHandle = 'someone'
 }
 
@@ -30,7 +41,6 @@ export default {
   filters: {
     time (value) {
       const d = new Date(value)
-
       const hours = pad(d.getHours(), 2)
       const minutes = pad(d.getMinutes(), 2)
       let msg = `${hours}:${minutes}`
@@ -39,16 +49,14 @@ export default {
   },
   data () {
     return {
-      handle: storedHandle,
       active: false,
+      handle: storedHandle,
       events: [],
+      userList: [],
       input: ''
     }
   },
   watch: {
-    handle (val) {
-      localStorage.setItem('handle', val)
-    },
     active (val, oldVal) {
       if (val === true && oldVal === false) {
         this.$refs.input.focus()
@@ -68,28 +76,41 @@ export default {
   },
   mounted () {
     // this.$flowService.on('chatEvent', this.addChatEvent)
-    this.$flowService.on('chatEvent', this.addChatEvent)
-    this.$flowService.on('sessionJoin', this.chatJoin)
+    this.$flowService.on('chatEvent', this.recvChatEvent)
+    this.$flowService.on('chatUserList', this.recvChatUserList)
+    this.$flowService.on('sessionJoin', this.sendChatJoin)
     // we might not be joined in a sess
-    this.$flowService.connected(() => {
-      this.$flowService.chatJoin(this.handle, this.$route.params.sessId)
-    })
+    // off
+    this.$flowService.connected(this.sendChatJoin)
+  },
+  beforeDestroy () {
+    this.$flowService.off('chatEvent', this.recvChatEvent)
+    this.$flowService.off('chatUserList', this.recvChatUserList)
+    this.$flowService.off('sessionJoin', this.sendChatJoin)
   },
   methods: {
     send () {
       const msg = this.input
-      if (msg.trim() === '') {
-        return
-      }
+      if (msg.trim() === '') { return }
       this.input = ''
       const msgEvent = {type: 'msg', handle: this.handle, message: msg, time: new Date()}
       this.$flowService.chatEvent(msgEvent)
     },
-    addChatEvent (v) {
+    recvChatUserList (v) {
+      this.userList = v.data
+    },
+    recvChatEvent (v) {
       this.events.push(v.data)
     },
-    chatJoin () {
+
+    sendChatJoin () {
       this.$flowService.chatJoin(this.handle, this.$route.params.sessId)
+    },
+    sendChatRename () {
+      const oldHandle = localStorage.getItem('handle')
+      if (this.handle === oldHandle) return
+      localStorage.setItem('handle', this.handle)
+      this.$flowService.chatRename(this.handle)
     }
   }
 
@@ -107,10 +128,10 @@ function pad (n, width, z) {
   position:relative;
   width:0px;
   transition: all .3s;
-  background: rgba(208,208,208,0.7);
+  background: rgba(208,208,208,0.9);
 }
 .flow-chat.active {
-  width:330px;
+  width:530px;
 }
 .flow-chat__toggle {
   user-select:none;
@@ -132,20 +153,49 @@ function pad (n, width, z) {
 .flow-chat__area {
   height:100%;
   overflow:hidden;
+  display:flex;
+  flex-flow:row;
+  padding:8px 0px;
 }
 
 .flow-chat__container {
-  box-shadow: 0px 0px 1px 2px rgba(50,50,50,0.1);
   height:100%;
   display:flex;
   flex-flow:column;
-  padding:8px;
+  flex:1;
+  padding:0px 8px;
 }
 .flow-chat__container >*:not(last-child) {
   margin-bottom:10px;
   padding:8px;
 }
 
+.flow-chat__users {
+  flex-basis:100px;
+  padding:20px 8px;
+  border-left: solid 1px rgba(150,150,150,0.2);
+
+}
+.flow-chat__user {
+  display:flex;
+  flex-flow:row;
+  justify-content: space-between;
+  align-items: center;
+
+}
+.flow-chat__user img {
+  height:10px;
+  width:auto;
+}
+.flow-chat__user span {
+  text-align:center;
+  flex:1;
+  width: 100px;
+  overflow:hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
 .flow-chat__messages {
   font-size:12px;
   overflow-y:scroll;
@@ -167,7 +217,6 @@ function pad (n, width, z) {
 
 }
 .flow-chat__messages .handle .name {
-  margin-left:-5px;
   font-weight:bold;
   padding-bottom:4px;
 }
@@ -179,11 +228,12 @@ function pad (n, width, z) {
 
 .flow-chat__messages .text {
   padding-top:0px;
+  padding-left:9px;
   color:#777;
 }
 
 .flow-chat input {
-background: #fff;
+  background: #fff;
   width:100%;
   border: none;
   padding:10px;

+ 159 - 75
browser/vue-flow/src/components/flow/manager.vue

@@ -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>

+ 2 - 1
browser/vue-flow/src/components/flow/node.vue

@@ -22,6 +22,7 @@
     />
     <!-- output -->
     <circle
+      v-if="output"
       class="flow-node__socket flow-node__socket--outputs"
       :data-nodeid="id"
       :data-out="0"
@@ -51,7 +52,7 @@ export default {
     'output': {type: String, default: ''},
     'match': {type: Object, default: () => {}},
 
-    'color': {type: String, default: '#444'},
+    'color': {type: String, default: '#777'},
     'textColor': {type: String, default: '#fff'}
     // 'value': {type: Object, default: () => {}}
   },

+ 156 - 0
browser/vue-flow/src/components/flow/panel.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="flow-funcs__container" :class="{active:active}">
+    <!-- Menu thing -->
+    <!-- could be handled in different area -->
+    <div class="flow-funcs__control">
+      <button class="item" @click="funcsViewBlocks=!funcsViewBlocks">
+        {{ funcsViewBlocks ? 'list':'blocks' }} view
+      </button>
+      <button class="item" @click="$emit('toggleResizeable')">
+        resize
+      </button>
+    </div>
+    <div class="flow-funcs__inner">
+
+      <hx-collapsible v-for="g in funcsGroups" :key="g">
+        <div class="flow-funcs__header" slot="header">{{ g }}</div>
+        <div class="flow-funcs__group" :class="{blocks:funcsViewBlocks}">
+          <button
+            :key="k"
+            class="flow-funcs__src hover-anim"
+            draggable="true"
+            v-for="k in funcsGroup(g)"
+            @dragstart="fnDrag($event,k)"
+            :style="{'background':registry[k].style && registry[k].style.color}">
+            {{ k }}
+          </button>
+        </div>
+      </hx-collapsible>
+    </div>
+  </div>
+
+</template>
+<script>
+import HxCollapsible from '@/components/shared/hx-collapsible'
+export default {
+  name: 'FlowPanel',
+  components: {HxCollapsible},
+  props: {
+    active: {type: Boolean, default: true},
+    registry: {type: Object, default: () => {}}
+  },
+  data () {
+    return {
+      funcsViewBlocks: true
+    }
+  },
+  computed: {
+    funcsGroups () {
+      let group = {}
+      for (let r in this.registry) {
+        group[this.registry[r].group] = true
+      }
+      return Object.keys(group)
+    },
+    funcsGroup () {
+      return (g) => {
+        const ret = Object.keys(this.registry).filter(v => this.registry[v].group === g)
+        return ret
+      }
+    }
+
+  },
+  methods: {
+    fnDrag (ev, src) {
+      console.log('Dragging thing', src)
+      ev.dataTransfer.setData('text/plain', src)
+    }
+  }
+}
+</script>
+<style>
+.flow-funcs__container {
+  white-space: nowrap;
+  width:0px;
+  transition: all .3s;
+  height:100%;
+  background:rgba(208,208,208,0.7);
+  overflow-x:hidden;
+  overflow-y:auto;
+}
+.flow-funcs__container.active {
+  width:300px;
+}
+
+.flow-funcs__inner {
+  overflow:hidden;
+}
+.flow-funcs__inner .hx-toggle-arrow {
+  color: rgba(150,150,150,0.9);
+}
+
+.flow-funcs__inner .hx-collapsible:first-child {
+  border-top: solid 1px rgba(150,150,150,0.2);
+}
+.flow-funcs__inner .hx-collapsible {
+  border-bottom: solid 1px rgba(150,150,150,0.2);
+}
+.flow-funcs__inner .hx-collapsible__header {
+  font-size:14px;
+  padding:5px 10px;
+  transition: all .3s;
+}
+.flow-funcs__inner .hx-collapsible__header:hover {
+  background: rgba(150,150,150,0.2);
+}
+
+.flow-funcs__group{
+  display:flex;
+  flex-flow:column;
+}
+.flow-funcs__src {
+  font-size:12px;
+  cursor: move;
+  padding:11px 5px;
+  background: #777;
+  color:#eee;
+  margin-top:1px;
+  text-align:center;
+  transition: all .3s;
+  position:relative;
+}
+.flow-funcs__group.blocks {
+  flex-flow:row;
+  flex-wrap:  wrap;
+
+  justify-content: flex-start;
+  align-content: center;
+}
+.flow-funcs__group.blocks .flow-funcs__src {
+/*Orientation*/
+  text-overflow: ellipsis;
+  margin:2px;
+  width: 60px;
+  height:60px;
+  overflow:hidden;
+}
+.flow-funcs__toggle {
+  margin:0;
+  position:relative;
+}
+
+.flow-funcs__control {
+  display:flex;
+  flex-flow:row;
+
+}
+.flow-funcs__control .item{
+  border-right: solid 1px rgba(208,208,208,0.9);
+  padding:13px;
+  flex:1;
+  height:100%;
+  text-align:center;
+  color:black;
+  font-size:14px;
+}
+</style>

+ 16 - 7
browser/vue-flow/src/components/flow/panzoom.vue

@@ -34,6 +34,16 @@ export default {
       moving: false
     }
   },
+  watch: {
+    value: {
+      handler () {
+        this.zoom = this.value.zoom
+        this.x = this.value.x
+        this.y = this.value.y
+      },
+      deep: true
+    }
+  },
   computed: {
     transformProps () {
       const transString = 'matrix(' + [
@@ -46,7 +56,6 @@ export default {
         class: 'flow-pan-zoom__transform ' + this.moving ? 'moving' : ''
       }
     }
-
   },
   mounted () {
     this.$el.addEventListener('wheel', this.wheel)
@@ -56,16 +65,16 @@ export default {
   },
   methods: {
     // panStart
-    dragStart (e) {
-      if (e.button !== 1) return // first button
-      if (e.target !== this.$refs.transformer) return
+    dragStart (ev) {
       document.activeElement && document.activeElement.blur()
+      if (ev.button !== 0) return // first button
+      if (ev.target !== this.$refs.transformer) return
 
-      const drag = (e) => {
+      const drag = (ev) => {
         this.moving = true
-        this.update(this.x + e.movementX, this.y + e.movementY)
+        this.update(this.x + ev.movementX, this.y + ev.movementY)
       }
-      const drop = (e) => {
+      const drop = (ev) => {
         this.moving = false
         document.removeEventListener('mousemove', drag)
         document.removeEventListener('mouseup', drop)

+ 18 - 11
browser/vue-flow/src/components/flowmain.vue

@@ -9,11 +9,11 @@
         <div class="app-info">
           <h4>HELP</h4>
           <ul>
-            <li><b>Pan</b>: Drag with Middle Mouse button</li>
+            <li><b>Pan</b>: Drag with Mouse button</li>
             <li><b>Zoom</b>: Mouse wheel up and down to zoom in and out</li>
-            <li><b>New Node</b>: Create a node by clicking on fn: buttons</li>
+            <li><b>New Node</b>: Create a node by dragging a fn into area</li>
             <li><b>Remove Node</b>: Middle click in a node to remove a node</li>
-            <li><b>Drag Node</b>: Mouse click and drag</li>
+            <li><b>Move Node</b>: Mouse click and drag</li>
             <li><b>Link two nodes</b>: Drag from any slot to any compatible in another node (in green)</li>
             <li><b>Remove Link</b>: Simple click on the link when it turns red</li>
           </ul>
@@ -33,7 +33,7 @@
         </div>
 
         <!--:value="nodeData"
-          @input="documentUpdate"-->
+        @input="documentUpdate"-->
         <flow-manager
           :registry="registry"
           v-model="nodeData"
@@ -66,12 +66,19 @@ export default {
   data () {
     return {
       registry: {
-        'MatMul': { inputs: [ '[]float32', '[]float32' ], output: '[]float32', style: { 'color': '#789', 'textColor': '#fff' } },
-        'Weights': { inputs: [], output: '[]float32' },
-        'Input': { inputs: [], output: '[]float32', style: { 'color': '#686', 'textColor': '#fff' } },
-        'Activator': { inputs: [ '[]float32' ], output: '[]float32', 'type': 'circle', style: { 'color': '#a44', 'textColor': 'white' } },
-        'test': { inputs: [ '[]float32', 'string' ], output: 'string' },
-        'reverse': { inputs: [ 'string' ], output: 'string' }
+        'MatMul': { group: 'Machine learning', inputs: [ '[]float32', '[]float32' ], output: '[]float32', style: { 'color': '#789', 'textColor': '#fff' } },
+        'Weights': { group: 'Machine learning', inputs: [], output: '[]float32' },
+        'Input': { group: 'Machine learning', inputs: [], output: '[]float32', style: { 'color': '#686', 'textColor': '#fff' } },
+        'Activator': { group: 'Machine learning', inputs: [ '[]float32' ], output: '[]float32', 'type': 'circle', style: { 'color': '#a44', 'textColor': 'white' } },
+
+        'test': { group: 'Text', inputs: [ '[]float32', 'string' ], output: 'string', style: {'color': '#a93'} },
+        'reverse': { group: 'Text', inputs: [ 'string' ], output: 'string', style: {'color': '#a93'} },
+
+        'fetch': { group: 'json', output: 'json', style: {'color': '#99a'}},
+        'jsonExtract': { group: 'json', inputs: ['json'], output: 'string', style: {'color': '#99a'} },
+
+        'string': { group: 'Visualisation', inputs: ['string'], style: {'color': '#9a9'}},
+        'lineGraph': { group: 'Visualisation', inputs: ['[]float32', '[]float32'], style: {'color': '#9a9'} }
       },
       nodeData: {nodes: [], links: []}
     }
@@ -154,7 +161,7 @@ export default {
   margin:20px;
   padding:20px;
   position:absolute;
-  left:0;
+  right:0;
   bottom:3%;
 }
 

+ 80 - 0
browser/vue-flow/src/components/shared/hx-collapsible.vue

@@ -0,0 +1,80 @@
+<template>
+  <div
+    class="hx-collapsible"
+    :class="{active:state.active}">
+    <button
+      class="hx-collapsible__header"
+      @click.stop.prevent="toggle">
+      <slot name="header"/>
+      <div v-if="icon">
+        <hx-toggle-arrow :active="state.active"/>
+      </div>
+    </button>
+
+    <div class="hx-collapsible__content">
+      <slot/>
+    </div>
+  </div>
+</template>
+
+<script>
+import HxToggleArrow from './hx-toggle-arrow'
+export default {
+  components: {HxToggleArrow},
+  props: {
+    active: {type: Boolean, default: true},
+    icon: {type: Boolean, default: true}
+  },
+  data () {
+    return {
+      state: {
+        active: this.active
+      }
+    }
+  },
+  watch: {
+    active (value) {
+      this.state.active = this.active
+    }
+  },
+  methods: {
+    toggle (e) {
+      this.state.active = !this.state.active
+    }
+  }
+}
+</script>
+<style>
+.hx-collapsible {
+  position:relative;
+  display:flex;
+  flex-direction: column;
+}
+/* header */
+.hx-collapsible__header {
+  display:flex;
+  justify-content:space-between;
+  align-items:center;
+  align-content:center;
+  user-select:none;
+  cursor: pointer;
+}
+
+/*content*/
+.hx-collapsible__content{
+  overflow:hidden;
+  opacity:0;
+  max-height:0px;
+  flex-basis: 0px;
+  flex-grow:0;
+  transition: all 300ms;
+}
+.hx-collapsible.active .hx-collapsible__content{
+  flex-basis:100%;
+  flex-grow:1;
+  opacity:1;
+  max-height:900px;
+  transition: all 300ms;
+}
+
+</style>

+ 115 - 0
browser/vue-flow/src/components/shared/hx-split.vue

@@ -0,0 +1,115 @@
+<script>
+export default {
+  name: 'HxSplit',
+  props: {
+    resizeable: {type: Boolean, default: false},
+    dir: {type: String, default: 'horizontal'},
+    split: {type: String, default: '50%'}
+  },
+  data () {
+    return {
+      state: {
+        resizing: false,
+        split: this.split || '50%'
+      }
+    }
+  },
+  computed: {
+    splitClass () {
+      return [
+        'split',
+        this.dir,
+        this.state.resizing ? 'resizing' : '',
+        this.resizeable ? 'resizeable' : ''
+      ]
+    }
+  },
+  watch: {
+    split () {
+      this.state.split = this.split
+    }
+  },
+
+  methods: {
+    startResize (ev) {
+      if (!this.resizeable || event.button !== 0) return
+      event.stopPropagation()
+      event.preventDefault()
+      this.state.resizing = true
+      // Grab delta
+      const parentRect = this.$el.getBoundingClientRect()
+      const splitRect = this.$refs.splitter.getBoundingClientRect()
+      const delta = { x: ev.x - splitRect.left, y: ev.y - splitRect.top }
+
+      const drag = (ev) => {
+        if (event.button !== 0) return
+        const h = this.dir === 'horizontal'
+        var splitter = (h ? this.$el.children[1].clientWidth : this.$el.children[1].clientHeight) / 2
+        var splitSize = h
+          ? ((ev.x - delta.x) - parentRect.left - splitter) / this.$el.clientWidth * 100
+          : ((ev.y - delta.y) - parentRect.top - splitter) / this.$el.clientHeight * 100
+
+        splitSize = Math.max(splitSize, 0)
+        this.state.split = splitSize + '%'
+        this.$emit('onSplitResize', event, this.state.split)
+      }
+      const drop = (ev) => {
+        if (event.button !== 0) return
+        this.state.resizing = false
+        document.removeEventListener('mousemove', drag)
+        document.removeEventListener('mouseup', drop)
+      }
+      document.addEventListener('mousemove', drag)
+      document.addEventListener('mouseup', drop)
+    }
+
+  },
+  render (h) {
+    const children = this.$slots.default.filter(n => n.tag !== undefined)
+    const items = []
+    items.push(h('div', {class: 'content', attrs: {style: 'flex-basis:' + this.state.split}}, [children[0]]))
+    items.push(h('div', {ref: 'splitter', class: 'splitter', on: {mousedown: this.startResize}}))
+    items.push(h('div', {class: 'content'}, [children[1]]))
+    return h('div', {class: this.splitClass}, items)
+  }
+
+}
+</script>
+<style>
+.split {
+  display: flex;
+  flex: 1;
+  height: 100%;
+}
+
+.split > .content {
+  position: relative;
+  display: flex;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.split > .content > * {
+  flex: 1;
+  height: 100%;
+}
+
+.split > .content:last-child {
+  flex: 1;
+}
+
+.split.vertical {
+  flex-direction: column;
+}
+
+.split.horizontal {
+  flex-direction: row;
+}
+
+.split.resizeable.vertical > .splitter {
+  cursor: row-resize;
+}
+.split.resizeable.horizontal > .splitter {
+  cursor: col-resize;
+}
+</style>

+ 31 - 0
browser/vue-flow/src/components/shared/hx-toggle-arrow.vue

@@ -0,0 +1,31 @@
+<template>
+  <div
+    class="hx-toggle-arrow"
+    :class="{active:active}"
+    @click="$emit('click',$event)"
+  >=</div>
+</template>
+<script>
+
+export default {
+  name: 'HxToggleArrow',
+  props: {
+    active: {type: Boolean, default: false}
+  }
+}
+</script>
+
+<style>
+.hx-toggle-arrow {
+  transform: rotate(90deg);
+  transition: all 300ms;
+  color: rgba(155,155,155,0.2);
+  padding:8px;
+  text-shadow: inset 1px 1px 2px #FFF;
+}
+.hx-toggle-arrow.active {
+  content:"=";
+  transform: rotate(0deg);
+}
+
+</style>

+ 3 - 2
browser/vue-flow/src/services/flowservice.js

@@ -41,7 +41,8 @@ export default {
       },
       // event related
       on: eventBus.$on.bind(eventBus),
-      once: eventBus.$once.bind(eventBus)
+      once: eventBus.$once.bind(eventBus),
+      off: eventBus.$off.bind(eventBus)
     };
 
     /// ///////////////
@@ -50,7 +51,7 @@ export default {
     [
       'sessionNew', 'sessionLoad', // SESSION
       'documentUpdate',
-      'chatEvent', 'chatJoin', // CHAT
+      'chatEvent', 'chatJoin', 'chatRename', // CHAT
       'linkAdd', 'linkUpdate', 'linkRemove', // LINK
       'nodeUpdate', 'nodeAdd', 'nodeRemove' // NODE
     ].forEach(ftyp => {

+ 43 - 10
go/src/flowserver/chatroom.go

@@ -1,9 +1,10 @@
 package flowserver
 
 import (
+	"errors"
 	"flowserver/flowmsg"
+	"log"
 	"sync"
-	"time"
 
 	"github.com/gorilla/websocket"
 )
@@ -17,13 +18,13 @@ type ChatClient struct {
 //ChatRoom will have an external ID
 type ChatRoom struct {
 	sync.Mutex
-	clients []ChatClient
+	clients []*ChatClient
 	Events  []interface{} // Persistent chat, temporary datatype
 }
 
 // NewChatRoom create a chatRoom
 func NewChatRoom() *ChatRoom {
-	return &ChatRoom{sync.Mutex{}, []ChatClient{}, []interface{}{}}
+	return &ChatRoom{sync.Mutex{}, []*ChatClient{}, []interface{}{}}
 }
 
 // ClientAdd add a client to session
@@ -38,10 +39,11 @@ func (r *ChatRoom) ClientAdd(c *websocket.Conn, handle string) error {
 		}
 	}
 
-	r.clients = append(r.clients, ChatClient{handle, c})
+	r.clients = append(r.clients, &ChatClient{handle, c})
+
 	// Add event joined
 	//XXX: temp
-	{
+	/*{
 		dataMap := map[string]interface{}{
 			"handle": handle,
 			"type":   "join",
@@ -49,7 +51,8 @@ func (r *ChatRoom) ClientAdd(c *websocket.Conn, handle string) error {
 		}
 		r.Events = append(r.Events, dataMap)
 		r.broadcast(c, flowmsg.SendMessage{OP: "chatEvent", Data: dataMap})
-	}
+	}*/
+	r.broadcastClientList()
 
 	for _, ev := range r.Events {
 		if err := c.WriteJSON(flowmsg.SendMessage{OP: "chatEvent", Data: ev}); err != nil {
@@ -65,16 +68,15 @@ func (r *ChatRoom) ClientRemove(c *websocket.Conn) {
 	r.Lock()
 	defer r.Unlock()
 
-	var cli ChatClient
 	for i, cl := range r.clients {
 		if cl.ws == c {
-			cli = cl
 			r.clients = append(r.clients[:i], r.clients[i+1:]...)
 			break
 		}
 	}
 
-	{
+	r.broadcastClientList()
+	/*{
 		dataMap := map[string]interface{}{
 			"handle": cli.Handle,
 			"type":   "left",
@@ -82,10 +84,33 @@ func (r *ChatRoom) ClientRemove(c *websocket.Conn) {
 		}
 		r.Events = append(r.Events, dataMap)
 		r.broadcast(c, flowmsg.SendMessage{OP: "chatEvent", Data: dataMap})
+	}*/
+}
+
+// ClientRename renames a client
+func (r *ChatRoom) ClientRename(c *websocket.Conn, newHandle string) error {
+	r.Lock()
+	defer r.Unlock()
+
+	var cli *ChatClient
+	for _, cl := range r.clients {
+		if cl.ws == c {
+			cli = cl
+			break
+		}
 	}
+	log.Println("ClientRename from", cli.Handle, " to ", cli.Handle)
+	if cli == nil {
+		log.Println("Cli:Err")
+		return errors.New("Client not found")
+	}
+	cli.Handle = newHandle
+
+	return r.broadcastClientList()
+
 }
 
-// ChatEvent Message broad a cast a message to clients in session
+// Event Message broad a cast a message to clients in session
 func (r *ChatRoom) Event(cl *websocket.Conn, v interface{}) error {
 	r.Lock()
 	defer r.Unlock()
@@ -107,5 +132,13 @@ func (r *ChatRoom) broadcast(c *websocket.Conn, v interface{}) error {
 		}
 	}
 	return nil
+}
+func (r *ChatRoom) broadcastClientList() error {
+
+	clients := make([]string, len(r.clients))
+	for i, cl := range r.clients {
+		clients[i] = cl.Handle
+	}
 
+	return r.broadcast(nil, flowmsg.SendMessage{OP: "chatUserList", Data: clients})
 }

+ 11 - 0
go/src/flowserver/sessionmgr.go

@@ -164,6 +164,17 @@ func (fsm *FlowSessionManager) ServeHTTP(w http.ResponseWriter, r *http.Request)
 				sess.ChatJoin(c, handle)
 				return nil
 			}()
+		case "chatRename":
+			err = func() error {
+				if sess == nil {
+					return nil // just do nothing
+				}
+				var handle string
+				json.Unmarshal(m.Data, &handle)
+				sess.Chat.ClientRename(c, handle)
+				return nil
+			}()
+
 		case "chatEvent":
 			err = func() error {
 				if sess == nil {