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