/ editor.html
editor.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Ethereum Transaction Lifecycle Visualizer</title> 7 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> 8 <style> 9 body, html { 10 margin: 0; 11 padding: 0; 12 width: 100%; 13 height: 100%; 14 overflow: hidden; 15 font-family: Arial, sans-serif; 16 } 17 #visualization-container { 18 width: 100%; 19 height: 100%; 20 overflow: auto; 21 border: 1px solid #ccc; 22 resize: both; 23 } 24 #visualization { 25 width: 100%; 26 height: 100%; 27 } 28 #controls { 29 position: fixed; 30 bottom: 20px; 31 right: 20px; 32 width: 150px; 33 height: 150px; 34 } 35 .control-button { 36 position: absolute; 37 width: 60px; 38 height: 30px; 39 background-color: rgba(255, 255, 255, 0.7); 40 border: 1px solid #999; 41 border-radius: 5px; 42 } 43 #upBtn { top: 0; left: 45px; } 44 #leftBtn { top: 60px; left: 0; } 45 #rightBtn { top: 60px; right: 0; } 46 #downBtn { bottom: 0; left: 45px; } 47 #metadata, #addNodeForm { 48 position: fixed; 49 background-color: rgba(255, 255, 255, 0.9); 50 padding: 10px; 51 border-radius: 5px; 52 max-width: 300px; 53 max-height: 80%; 54 overflow-y: auto; 55 } 56 #metadata { 57 bottom: 20px; 58 left: 20px; 59 } 60 #addNodeForm { 61 top: 20px; 62 left: 20px; 63 display: none; /* Initially hidden */ 64 } 65 #metadata input, #metadata textarea, #addNodeForm input, #addNodeForm select { 66 width: 100%; 67 margin-bottom: 10px; 68 } 69 #metadata button, #addNodeForm button { 70 margin-right: 10px; 71 } 72 #editToggle { 73 position: fixed; 74 top: 20px; 75 right: 20px; 76 } 77 </style> 78 </head> 79 <body onload="onLoadPopulateNodesFromURl()"> 80 <div id="visualization-container"> 81 <div id="visualization"></div> 82 </div> 83 <div id="controls"> 84 <button id="upBtn" class="control-button">Up</button> 85 <button id="leftBtn" class="control-button">Previous</button> 86 <button id="rightBtn" class="control-button">Next</button> 87 <button id="downBtn" class="control-button">Down</button> 88 </div> 89 <div id="metadata"></div> 90 <div id="addNodeForm"> 91 <h3>Add New Node</h3> 92 <input type="text" id="newNodeName" placeholder="Node Name"> 93 <select id="newNodeDirection"> 94 <option value="next">Next</option> 95 <option value="up">Up</option> 96 <option value="down">Down</option> 97 </select> 98 <button onclick="addNewNode()">Add Node</button> 99 </div> 100 <button id="editToggle" onclick="toggleEditMode()">Edit Mode</button> 101 102 <script> 103 let nodeIds = ["User"]; 104 let connectionMatrix = { 105 "User": {} 106 }; 107 let nodeMetadata = { 108 "User": { 109 "content": "Initiates and signs the transaction", 110 "url": "https://ethereum.org/en/developers/docs/transactions/" 111 } 112 }; 113 114 const directionToGridDelta = { 115 "next": [1, 0], 116 "previous": [-1, 0], 117 "up": [0, -1], 118 "down": [0, 1] 119 }; 120 121 let isEditMode = false; 122 123 function assignGridPositions(startNode) { 124 const gridPositions = {}; 125 const queue = [[startNode, 0, 0]]; 126 const visited = new Set(); 127 128 while (queue.length > 0) { 129 const [node, x, y] = queue.shift(); 130 if (visited.has(node)) continue; 131 visited.add(node); 132 gridPositions[node] = [x, y]; 133 134 for (let neighbor in connectionMatrix[node]) { 135 const connection = connectionMatrix[node][neighbor]; 136 if (connection) { 137 const [dx, dy] = directionToGridDelta[connection[0]]; 138 queue.push([neighbor, x + dx, y + dy]); 139 } 140 } 141 } 142 143 return gridPositions; 144 } 145 146 function createVisualization() { 147 const width = window.innerWidth; 148 const height = window.innerHeight; 149 const nodeRadius = Math.min(width, height) * 0.05; 150 const transactionRadius = nodeRadius * 0.3; 151 const gridSize = Math.min(width, height) * 0.2; 152 153 d3.select("#visualization").selectAll("*").remove(); 154 155 const gridPositions = assignGridPositions(nodeIds[0]); 156 157 let nodes = nodeIds.map(id => ({ 158 id: id, 159 x: (gridPositions[id][0] + 2) * gridSize, 160 y: (gridPositions[id][1] + 3) * gridSize 161 })); 162 163 const links = []; 164 for (let source in connectionMatrix) { 165 for (let target in connectionMatrix[source]) { 166 if (connectionMatrix[source][target]) { 167 links.push({ 168 source: nodes.find(n => n.id === source), 169 target: nodes.find(n => n.id === target), 170 type: connectionMatrix[source][target][0] 171 }); 172 } 173 } 174 } 175 176 const svg = d3.select("#visualization") 177 .append("svg") 178 .attr("width", width) 179 .attr("height", height); 180 181 const link = svg.append("g") 182 .selectAll("line") 183 .data(links) 184 .join("line") 185 .attr("stroke", "#999") 186 .attr("stroke-opacity", 0.6) 187 .attr("x1", d => d.source.x) 188 .attr("y1", d => d.source.y) 189 .attr("x2", d => d.target.x) 190 .attr("y2", d => d.target.y); 191 192 const node = svg.append("g") 193 .selectAll("circle") 194 .data(nodes) 195 .join("circle") 196 .attr("r", nodeRadius) 197 .attr("fill", "lightblue") 198 .attr("cx", d => d.x) 199 .attr("cy", d => d.y); 200 201 const label = svg.append("g") 202 .selectAll("text") 203 .data(nodes) 204 .join("text") 205 .text(d => d.id) 206 .attr("font-size", nodeRadius * 0.3) 207 .attr("text-anchor", "middle") 208 .attr("dominant-baseline", "central") 209 .attr("x", d => d.x) 210 .attr("y", d => d.y); 211 212 const transaction = svg.append("circle") 213 .attr("r", transactionRadius) 214 .attr("fill", "red"); 215 216 return { nodes, transaction }; 217 } 218 219 let { nodes, transaction } = createVisualization(); 220 let currentNode = nodes[0]; 221 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 222 223 const directionMapping = { 224 "up": "up", 225 "down": "down", 226 "next": "right", 227 "previous": "left" 228 }; 229 230 function updateMetadataDisplay(nodeId) { 231 const metadata = nodeMetadata[nodeId]; 232 let html = `<h3><a href="${metadata.url}" target="_blank">${nodeId}</a></h3>`; 233 if (isEditMode) { 234 html += ` 235 <form id="metadataForm"> 236 <input type="text" id="nodeNameInput" value="${nodeId}"> 237 <textarea id="content" name="content" placeholder="Content">${metadata.content}</textarea> 238 <input type="url" id="url" name="url" value="${metadata.url}" placeholder="URL"> 239 <button type="button" onclick="saveMetadata('${nodeId}')">Save</button> 240 <button type="button" onclick="deleteNode('${nodeId}')">Delete</button> 241 <button type="button" onclick="uploadNodeData()">Upload</button> 242 <button type="button" onclick="downloadNodeData()">Download</button> 243 </form>`; 244 } else { 245 html += `<p>${metadata.content}</p>`; 246 } 247 document.getElementById("metadata").innerHTML = html; 248 } 249 250 function saveMetadata(oldNodeId) { 251 const newNodeId = document.getElementById("nodeNameInput").value; 252 const content = document.getElementById("content").value; 253 const url = document.getElementById("url").value; 254 255 const newMetadata = { 256 content: content, 257 url: url 258 }; 259 260 if (oldNodeId !== newNodeId) { 261 nodeMetadata[newNodeId] = newMetadata; 262 delete nodeMetadata[oldNodeId]; 263 nodeIds = nodeIds.map(id => id === oldNodeId ? newNodeId : id); 264 265 // Update connectionMatrix 266 connectionMatrix[newNodeId] = connectionMatrix[oldNodeId]; 267 delete connectionMatrix[oldNodeId]; 268 for (let node in connectionMatrix) { 269 if (connectionMatrix[node][oldNodeId]) { 270 connectionMatrix[node][newNodeId] = connectionMatrix[node][oldNodeId]; 271 delete connectionMatrix[node][oldNodeId]; 272 } 273 } 274 275 currentNode = { id: newNodeId, x: currentNode.x, y: currentNode.y }; 276 //Update the nodes object 277 nodes = nodes.map(n => n.id === oldNodeId ? currentNode : n); 278 console.log(nodes); 279 } else { 280 nodeMetadata[newNodeId] = newMetadata; 281 } 282 283 ({ nodes, transaction } = createVisualization()); 284 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 285 updateButtons(); 286 updateMetadataDisplay(newNodeId); 287 } 288 289 function resetMetadata(nodeId) { 290 nodeMetadata[nodeId] = { 291 "content": "", 292 "url": "" 293 }; 294 updateMetadataDisplay(nodeId); 295 } 296 297 function moveTransaction(direction) { 298 for (let targetId in connectionMatrix[currentNode.id]) { 299 const connection = connectionMatrix[currentNode.id][targetId]; 300 if (connection && connection[0] === direction) { 301 const targetNode = nodes.find(n => n.id === targetId); 302 if (targetNode) { 303 currentNode = targetNode; 304 transaction.transition() 305 .duration(500) 306 .attr("cx", currentNode.x) 307 .attr("cy", currentNode.y); 308 updateButtons(); 309 updateMetadataDisplay(currentNode.id); 310 break; 311 } 312 } 313 } 314 } 315 316 function updateButtons() { 317 for (let direction in directionMapping) { 318 const buttonId = `#${directionMapping[direction]}Btn`; 319 let isEnabled = false; 320 for (let targetId in connectionMatrix[currentNode.id]) { 321 const connection = connectionMatrix[currentNode.id][targetId]; 322 if (connection && connection[0] === direction) { 323 isEnabled = true; 324 break; 325 } 326 } 327 d3.select(buttonId).property("disabled", !isEnabled); 328 } 329 } 330 331 function toggleEditMode() { 332 isEditMode = !isEditMode; 333 updateMetadataDisplay(currentNode.id); 334 document.getElementById("editToggle").textContent = isEditMode ? "View Mode" : "Edit Mode"; 335 document.getElementById("addNodeForm").style.display = isEditMode ? "block" : "none"; 336 } 337 338 function addNewNode() { 339 const newNodeName = document.getElementById("newNodeName").value; 340 const direction = document.getElementById("newNodeDirection").value; 341 342 if (!newNodeName || nodeIds.includes(newNodeName)) { 343 alert("Please enter a unique node name."); 344 return; 345 } 346 347 nodeIds.push(newNodeName); 348 connectionMatrix[newNodeName] = {}; 349 nodeMetadata[newNodeName] = { 350 "content": "", 351 "url": "" 352 }; 353 354 connectionMatrix[currentNode.id][newNodeName] = [direction, directionToGridDelta[direction]]; 355 const oppositeDirection = direction === "next" ? "previous" : (direction === "up" ? "down" : "up"); 356 connectionMatrix[newNodeName][currentNode.id] = [oppositeDirection, directionToGridDelta[oppositeDirection]]; 357 358 ({ nodes, transaction } = createVisualization()); 359 360 // Move the transaction to the new node 361 currentNode = nodes.find(n => n.id === newNodeName); 362 transaction.transition() 363 .duration(500) 364 .attr("cx", currentNode.x) 365 .attr("cy", currentNode.y); 366 367 updateButtons(); 368 updateMetadataDisplay(currentNode.id); 369 370 // Clear the input field 371 document.getElementById("newNodeName").value = ""; 372 } 373 374 d3.select("#upBtn").on("click", () => moveTransaction("up")); 375 d3.select("#downBtn").on("click", () => moveTransaction("down")); 376 d3.select("#leftBtn").on("click", () => moveTransaction("previous")); 377 d3.select("#rightBtn").on("click", () => moveTransaction("next")); 378 379 updateButtons(); 380 updateMetadataDisplay(currentNode.id); 381 382 window.addEventListener('resize', () => { 383 ({ nodes, transaction } = createVisualization()); 384 currentNode = nodes.find(n => n.id === currentNode.id); 385 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 386 updateButtons(); 387 updateMetadataDisplay(currentNode.id); 388 }); 389 390 function deleteNode(nodeId) { 391 if (nodeId === "User") { 392 alert("Cannot delete the initial node."); 393 return; 394 } 395 396 delete nodeMetadata[nodeId]; 397 delete connectionMatrix[nodeId]; 398 nodeIds = nodeIds.filter(id => id !== nodeId); 399 for (let node in connectionMatrix) { 400 delete connectionMatrix[node][nodeId]; 401 } 402 403 ({ nodes, transaction } = createVisualization()); 404 currentNode = nodes[0]; 405 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 406 updateButtons(); 407 updateMetadataDisplay(currentNode.id); 408 } 409 410 function downloadNodeData() { 411 const data = { 412 nodeIds: nodeIds, 413 connectionMatrix: connectionMatrix, 414 nodeMetadata: nodeMetadata 415 }; 416 const blob = new Blob([JSON.stringify(data)], { type: "application/json" }); 417 const url = URL.createObjectURL(blob); 418 const a = document.createElement("a"); 419 a.href = url; 420 a.download = "nodedata.json"; 421 a.click(); 422 } 423 424 function uploadNodeData() { 425 const input = document.createElement("input"); 426 input.type = "file"; 427 input.accept = ".json"; 428 input.onchange = e => { 429 const file = e.target.files[0]; 430 const reader = new FileReader(); 431 reader.onload = e => { 432 const data = JSON.parse(e.target.result); 433 nodeIds = data.nodeIds; 434 connectionMatrix = data.connectionMatrix; 435 nodeMetadata = data.nodeMetadata; 436 ({ nodes, transaction } = createVisualization()); 437 currentNode = nodes[0]; 438 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 439 updateButtons(); 440 updateMetadataDisplay(currentNode.id); 441 }; 442 reader.readAsText(file); 443 }; 444 input.click(); 445 } 446 447 function onLoadPopulateNodesFromURl() { 448 //Fetch json file from relative URL, check if it exists, if so populate the nodes 449 fetch('start.json') 450 .then(response => response.json()) 451 .then(data => { 452 nodeIds = data.nodeIds; 453 connectionMatrix = data.connectionMatrix; 454 nodeMetadata = data.nodeMetadata; 455 ({ nodes, transaction } = createVisualization()); 456 currentNode = nodes[0]; 457 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 458 updateButtons(); 459 updateMetadataDisplay(currentNode.id); 460 }) 461 .catch((error) => { 462 console.error('Error:', error); 463 }); 464 } 465 466 </script> 467 </body> 468 </html>