github.com/v2pro/plz@v0.0.0-20221028024117-e5f9aec5b631/witch/webroot/viz.html (about) 1 <script type="text/x-template" id="viz-template"> 2 <grid-layout 3 :layout="layout" 4 :col-num="24" 5 :row-height="24" 6 :is-draggable="true" 7 :is-resizable="true" 8 :is-mirrored="false" 9 :vertical-compact="false" 10 :margin="[24, 24]" 11 :use-css-transforms="false"> 12 <grid-item v-for="item in layout" 13 v-show="ptrState[item.i]" 14 :x="item.x" :y="item.y" 15 :w="item.w" :h="item.h" 16 :i="item.i" :key="item.i" :ref="item.i" 17 drag-allow-from=".vue-draggable-handle" 18 drag-ignore-from=".no-drag" 19 @move="onMove" @resize="onMove" 20 @moved="onMoved" @resized="onMoved"> 21 <viz-item :addrMap="addrMap" :ptr="item.i" 22 @showPtr="showPtr" @hidePtr="hidePtr"/> 23 </grid-item> 24 <svg style="position:absolute;left:0px;top:0px;pointer-events: none;" 25 width="100%" height="100%"> 26 <defs> 27 <marker id="triangle" viewBox="0 0 10 10" refX="0" refY="5" 28 markerUnits="strokeWidth" markerWidth="10" 29 markerHeight="8" orient="auto"> 30 <path d="M 0 0 L 10 5 L 0 10 z"></path> 31 </marker> 32 </defs> 33 <g v-for="(connector, connectorId) in connectors" 34 :key="connectorId" v-show="!connector.hidden"> 35 <circle :cx="connector.x1" :cy="connector.y1" r="3" fill="#456" 36 stroke="none"/> 37 <path :d="curve(connector)" fill="none" stroke="#456" marker-end="url(#triangle)"/> 38 </g> 39 </svg> 40 </grid-layout> 41 </script> 42 <style> 43 table.viz-item, .viz-item table, .viz-item th, .viz-item td { 44 border: 1px dashed #BBB; 45 } 46 47 table.viz-item, .viz-item table { 48 border-bottom: 0; 49 border-left: 0; 50 } 51 52 .viz-item td, .viz-item th { 53 border-top: 0; 54 border-right: 0; 55 padding-left: 4px; 56 } 57 58 .vue-draggable-handle { 59 position: absolute; 60 width: 24px; 61 height: 24px; 62 top: 0px; 63 left: 0px; 64 background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") no-repeat; 65 background-position: bottom right; 66 padding: 0 8px 8px 0; 67 background-repeat: no-repeat; 68 background-origin: content-box; 69 box-sizing: border-box; 70 cursor: pointer; 71 } 72 </style> 73 <script> 74 Vue.component('viz', { 75 template: '#viz-template', 76 props: ['addrMap'], 77 data: function () { 78 return { 79 layout: [{"x": 0, "y": 0, "w": 6, "h": 3, "i": "__root__"}], 80 connectors: {}, 81 endpoints: {}, 82 ptrState: {'__root__': true} 83 } 84 }, 85 watch: { 86 'addrMap': function() { 87 Object.assign(this.$data, this.$options.data()); 88 } 89 }, 90 methods: { 91 showPtr: function (e) { 92 var connector = this.connectors[e.connectorId]; 93 if (connector) { 94 Vue.set(this.ptrState, e.targetPtr, true); 95 connector.hidden = false; 96 connector.deleted = false; 97 return; 98 } 99 Vue.set(this.ptrState, e.targetPtr, true); 100 if (!this.endpoints[e.targetPtr]) { 101 var srcComp = this.$refs[e.sourcePtr][0]; 102 var targetItem = {"x": 1, "y": srcComp.y + srcComp.h, "w": 2, "h": 2, "i": e.targetPtr}; 103 chooseX(this.layout, targetItem); 104 this.layout.push(targetItem); 105 } 106 var me = this; 107 this.$nextTick(function () { 108 var comp = me.$refs[e.targetPtr][0]; 109 var compEl = comp.$el.getElementsByTagName('table')[0]; 110 var colWidth = document.documentElement.clientWidth / 24; 111 for (var i = 0; i < me.layout.length; i++) { 112 if (me.layout[i].i === e.targetPtr) { 113 me.layout[i].h = 1 + parseInt(compEl.offsetHeight / 48); 114 if (me.layout[i].h < 3) { 115 me.layout[i].h = 3 116 } 117 me.layout[i].w = 2 + parseInt(compEl.offsetWidth / colWidth); 118 } 119 } 120 me.updatePosition(e); 121 e.hidden = false; 122 Vue.set(me.connectors, e.connectorId, e); 123 var endpointConnectors = me.endpoints[e.sourcePtr]; 124 if (endpointConnectors) { 125 endpointConnectors.push(e); 126 } else { 127 Vue.set(me.endpoints, e.sourcePtr, [e]); 128 } 129 endpointConnectors = me.endpoints[e.targetPtr]; 130 if (endpointConnectors) { 131 endpointConnectors.push(e); 132 } else { 133 Vue.set(me.endpoints, e.targetPtr, [e]); 134 } 135 }); 136 }, 137 hidePtr: function (e) { 138 var connector = this.connectors[e.connectorId]; 139 if (!connector) { 140 return; 141 } 142 connector.hidden = true; 143 connector.deleted = true; 144 var endpointConnectors = this.endpoints[e.targetPtr]; 145 if (!endpointConnectors) { 146 return; 147 } 148 var stillAlive = false; 149 for (var i = 0; i < endpointConnectors.length; i++) { 150 var connector = endpointConnectors[i]; 151 if (connector.targetPtr === e.targetPtr && !connector.deleted) { 152 stillAlive = true; 153 break; 154 } 155 } 156 if (!stillAlive) { 157 for (var i = 0; i < endpointConnectors.length; i++) { 158 var connector = endpointConnectors[i]; 159 if (connector.sourcePtr === e.targetPtr) { 160 this.hidePtr(connector); 161 } 162 } 163 Vue.set(this.ptrState, e.targetPtr, false); 164 } 165 }, 166 onMove: function (ptr) { 167 var connectors = this.endpoints[ptr]; 168 if (!connectors) { 169 return; 170 } 171 for (var i = 0; i < connectors.length; i++) { 172 connectors[i].hidden = true; 173 } 174 }, 175 onMoved: function (ptr) { 176 var me = this; 177 window.setTimeout(function () { 178 var connectors = me.endpoints[ptr]; 179 if (!connectors) { 180 return; 181 } 182 for (var i = 0; i < connectors.length; i++) { 183 var connector = connectors[i]; 184 if (!connector.deleted) { 185 connector.hidden = false; 186 } 187 me.updatePosition(connector); 188 } 189 }, 50); 190 }, 191 updatePosition: function (connector) { 192 var sourceComp = this.$refs[connector.sourcePtr][0]; 193 var sourceStyle = sourceComp.style; 194 var pos = relativePos(connector.sourceElem, sourceComp.$el); 195 var x1 = parseInt(sourceStyle.left.slice(0, -2)); 196 connector.x1 = x1 + pos.left + connector.sourceElem.offsetWidth / 2; 197 var y1 = parseInt(sourceStyle.top.slice(0, -2)); 198 connector.y1 = y1 + pos.top + connector.sourceElem.offsetHeight - 8; 199 var targetComp = this.$refs[connector.targetPtr][0]; 200 var targetStyle = targetComp.style; 201 var x2 = parseInt(targetStyle.left.slice(0, -2)); 202 connector.x2 = x2 + 8; 203 var y2 = parseInt(targetStyle.top.slice(0, -2)); 204 connector.y2 = y2; 205 }, 206 curve: function (connector) { 207 var tension = 0; 208 if (connector.x1 < connector.x2) { 209 tension = 0.2 210 } else { 211 tension = -0.2 212 } 213 var x1 = connector.x1; 214 var y1 = connector.y1; 215 var x2 = connector.x2; 216 var y2 = connector.y2; 217 var delta = (x2 - x1) * tension; 218 var hx1 = x1; 219 var hy1 = y1 + delta; 220 var hx2 = x2; 221 var hy2 = y2 - delta; 222 var path = "M " + x1 + " " + y1 + 223 " C " + hx1 + " " + hy1 224 + " " + hx2 + " " + hy2 225 + " " + x2 + " " + y2; 226 return path; 227 } 228 } 229 }); 230 function relativePos(child, ancestor) { 231 if (child === ancestor) { 232 return {left: 0, top: 0} 233 } 234 var offset = relativePos(child.offsetParent, ancestor); 235 offset.left += child.offsetLeft; 236 offset.top += child.offsetTop; 237 return offset; 238 } 239 function chooseX(layout, targetItem) { 240 for (var x = 1; x < 24; x++) { 241 targetItem.x = x; 242 if (!findCollision(layout, targetItem)) { 243 return 244 } 245 } 246 targetItem.x = 1; 247 } 248 function findCollision(layout, targetItem) { 249 for (var i = 0; i < layout.length; i++) { 250 var item = layout[i]; 251 if (collides(item, targetItem)) { 252 return true 253 } 254 } 255 return false 256 } 257 function collides(l1, l2) { 258 if (l1 === l2) return false; // same element 259 if (l1.x + l1.w <= l2.x) return false; // l1 is left of l2 260 if (l1.x >= l2.x + l2.w) return false; // l1 is right of l2 261 if (l1.y + l1.h <= l2.y) return false; // l1 is above l2 262 if (l1.y >= l2.y + l2.h) return false; // l1 is below l2 263 return true; // boxes overlap 264 } 265 </script>