self modifying website

feat: add save and load modals

dunkirk.sh 21462111 e383616f

verified
Changed files
+688 -78
+688 -78
index.html
··· 179 179 white-space: pre; 180 180 margin: 10px 0; 181 181 } 182 + 183 + /* Modal styles */ 184 + .modal { 185 + display: none; 186 + position: fixed; 187 + z-index: 9999; 188 + left: 0; 189 + top: 0; 190 + width: 100%; 191 + height: 100%; 192 + background-color: rgba(0, 0, 0, 0.7); 193 + overflow: auto; 194 + opacity: 1; 195 + transition: opacity 0.2s ease-in-out; 196 + } 197 + 198 + .modal-content { 199 + background-color: #ffffff; 200 + border: 2px solid #808080; 201 + box-shadow: 202 + inset -2px -2px #c0c0c0, 203 + inset 2px 2px #404040, 204 + 0 0 10px rgba(0, 0, 0, 0.5); 205 + margin: 10% auto; 206 + width: 80%; 207 + max-width: 600px; 208 + animation: fadeIn 0.2s ease-in-out; 209 + position: relative; /* Ensure proper positioning of close button */ 210 + } 211 + 212 + @keyframes fadeIn { 213 + from { 214 + opacity: 0; 215 + transform: translateY(-20px); 216 + } 217 + to { 218 + opacity: 1; 219 + transform: translateY(0); 220 + } 221 + } 222 + 223 + .modal-body { 224 + padding: 15px; 225 + max-height: 70vh; 226 + overflow-y: auto; 227 + } 228 + 229 + .input-group { 230 + margin: 10px 0; 231 + } 232 + 233 + .input-group label { 234 + display: inline-block; 235 + width: 80px; 236 + } 237 + 238 + .input-group input { 239 + font-family: "Courier Prime", "Courier New", monospace; 240 + padding: 5px; 241 + background: #ffffff; 242 + border: 2px inset #c0c0c0; 243 + } 244 + 245 + .save-list { 246 + max-height: 200px; 247 + overflow-y: auto; 248 + border: 1px solid #c0c0c0; 249 + margin: 10px 0; 250 + padding: 5px; 251 + } 252 + 253 + .save-item { 254 + display: flex; 255 + justify-content: space-between; 256 + margin: 5px 0; 257 + padding: 8px; 258 + border-bottom: 1px solid #c0c0c0; 259 + background-color: #f0f0f0; 260 + } 261 + 262 + .hotkey { 263 + cursor: pointer; 264 + padding: 2px 5px; 265 + margin: 0 2px; 266 + border: 1px solid #c0c0c0; 267 + border-radius: 3px; 268 + } 269 + 270 + .hotkey:hover { 271 + background-color: #c0c0c0; 272 + } 273 + 274 + .modal-close { 275 + position: absolute; 276 + right: 10px; 277 + top: 2px; 278 + cursor: pointer; 279 + font-size: 16px; 280 + font-weight: bold; 281 + color: #000000; 282 + background: #d0d0d0; 283 + border: 1px solid #808080; 284 + border-radius: 3px; 285 + width: 20px; 286 + height: 20px; 287 + text-align: center; 288 + line-height: 18px; 289 + box-shadow: 290 + inset -1px -1px #404040, 291 + inset 1px 1px #ffffff; 292 + } 293 + 294 + .modal-close:hover { 295 + background-color: #c0c0c0; 296 + color: #ff0000; 297 + } 298 + 299 + .modal-close:active { 300 + box-shadow: 301 + inset 1px 1px #404040, 302 + inset -1px -1px #ffffff; 303 + } 182 304 </style> 183 305 </head> 184 306 <body> 307 + <!-- Help Modal --> 308 + <div id="help-modal" class="modal"> 309 + <div class="modal-content"> 310 + <div class="title-bar"> 311 + PLASTIC.EXE - Help 312 + <span 313 + class="modal-close" 314 + onclick="hideModal('help-modal')" 315 + tabindex="0" 316 + role="button" 317 + aria-label="Close help modal" 318 + >✕</span 319 + > 320 + </div> 321 + <div class="modal-body"> 322 + <h2>Keyboard Shortcuts:</h2> 323 + <ul> 324 + <li><strong>F1</strong>: Show this help screen</li> 325 + <li> 326 + <strong>F2</strong>: Save the current page state 327 + </li> 328 + <li><strong>F3</strong>: Load a saved page state</li> 329 + <li><strong>Esc</strong>: Close modals</li> 330 + <li> 331 + <strong>Ctrl+Enter</strong>: Execute code in the 332 + editor 333 + </li> 334 + </ul> 335 + 336 + <h2>How to Use PLASTIC:</h2> 337 + <ol> 338 + <li> 339 + Type a description of what you want to change in the 340 + editor 341 + </li> 342 + <li> 343 + Press Ctrl+Enter or click the "GENERATE & EXECUTE" 344 + button 345 + </li> 346 + <li> 347 + PLASTIC will modify itself according to your 348 + description 349 + </li> 350 + <li>Use F2 to save your modified system</li> 351 + <li>Use F3 to load a previously saved state</li> 352 + </ol> 353 + 354 + <h2>About PLASTIC:</h2> 355 + <p> 356 + PLASTIC.EXE is a self-modifying system that lets you 357 + transform its interface and functionality through 358 + natural language commands. 359 + </p> 360 + </div> 361 + <div style="text-align: center; margin-top: 15px"> 362 + <div class="button" onclick="hideModal('help-modal')"> 363 + CLOSE 364 + </div> 365 + </div> 366 + </div> 367 + </div> 368 + 369 + <!-- Save Modal --> 370 + <div id="save-modal" class="modal"> 371 + <div class="modal-content"> 372 + <div class="title-bar"> 373 + PLASTIC.EXE - Save 374 + <span 375 + class="modal-close" 376 + onclick="hideModal('save-modal')" 377 + tabindex="0" 378 + role="button" 379 + aria-label="Close save modal" 380 + >✕</span 381 + > 382 + </div> 383 + <div class="modal-body"> 384 + <h2>Save Current State</h2> 385 + <div class="input-group"> 386 + <label for="save-name">Name:</label> 387 + <input 388 + type="text" 389 + id="save-name" 390 + placeholder="Enter save name" 391 + /> 392 + </div> 393 + <div 394 + style=" 395 + display: flex; 396 + justify-content: center; 397 + gap: 10px; 398 + margin-top: 15px; 399 + " 400 + > 401 + <div class="button" onclick="saveCurrentHTML()"> 402 + SAVE 403 + </div> 404 + <div class="button" onclick="hideModal('save-modal')"> 405 + CANCEL 406 + </div> 407 + </div> 408 + <div id="save-message"></div> 409 + 410 + <h2>Saved States</h2> 411 + <div id="saved-list" class="save-list"></div> 412 + </div> 413 + </div> 414 + </div> 415 + 416 + <!-- Load Modal --> 417 + <div id="load-modal" class="modal"> 418 + <div class="modal-content"> 419 + <div class="title-bar"> 420 + PLASTIC.EXE - Load 421 + <span 422 + class="modal-close" 423 + onclick="hideModal('load-modal')" 424 + tabindex="0" 425 + role="button" 426 + aria-label="Close load modal" 427 + >✕</span 428 + > 429 + </div> 430 + <div class="modal-body"> 431 + <h2>Load Saved State</h2> 432 + <div id="load-list" class="save-list"></div> 433 + <div id="load-message"></div> 434 + <div style="text-align: center; margin-top: 15px"> 435 + <div class="button" onclick="hideModal('load-modal')"> 436 + CANCEL 437 + </div> 438 + </div> 439 + </div> 440 + </div> 441 + </div> 185 442 <div class="terminal"> 186 443 <div class="title-bar"> 187 - C:\PLASTIC.EXE - [Self-Modifying System v2.1] 444 + C:\PLASTIC.EXE - [Self-Modifying System v3.0] 188 445 </div> 189 446 190 447 <div class="content"> ··· 200 457 > 201 458 202 459 <h2>System Information:</h2> 203 - <p>PLASTIC v2.1 - Self-Modifying Code System</p> 460 + <p>PLASTIC v3.0 - Self-Modifying Code System</p> 204 461 <p>Copyright (C) 1995 DUNKIRK Corp.</p> 205 462 <p> 206 463 All rights reserved. Licensed to: REGISTERED USER under MIT ··· 257 514 </div> 258 515 259 516 <div class="status-bar"> 260 - <span>F1=Help F2=Save F3=Load F10=Menu</span> 261 - <span>12:00</span> 517 + <span 518 + ><span class="hotkey" onclick="showHelp()">F1=Help</span> 519 + <span class="hotkey" onclick="showSaveModal()" 520 + >F2=Save</span 521 + > 522 + <span class="hotkey" onclick="showLoadModal()" 523 + >F3=Load</span 524 + > 525 + F10=Menu</span 526 + > 527 + <span id="time">12:00</span> 262 528 </div> 263 529 </div> 264 530 ··· 399 665 args.position, 400 666 ); 401 667 } else { 402 - // Fallback: use all argument values in order 403 668 return window.toolCallbacks[func]( 404 669 ...Object.values(args), 405 670 ); ··· 495 760 console.log("Cleaned response:", cleanResponse); 496 761 497 762 // For safety, preprocess JavaScript code in JSON to escape problematic characters 498 - cleanResponse = cleanResponse.replace(/"code"\s*:\s*(`|")([^`"]*?)(`|")/g, function(match, q1, code, q3) { 499 - // Replace all literal backslashes with double backslashes in the code string 500 - const escapedCode = code.replace(/\\/g, "\\\\"); 501 - return `"code":${q1}${escapedCode}${q3}`; 502 - }); 763 + cleanResponse = cleanResponse.replace( 764 + /"code"\s*:\s*(`|")([^`"]*?)(`|")/g, 765 + function (match, q1, code, q3) { 766 + // Replace all literal backslashes with double backslashes in the code string 767 + const escapedCode = code.replace(/\\/g, "\\\\"); 768 + return `"code":${q1}${escapedCode}${q3}`; 769 + }, 770 + ); 503 771 504 772 // Check if response contains tool calls 505 773 try { ··· 635 903 ); 636 904 if (jsonMatch) { 637 905 // Preprocess JavaScript code in JSON to escape problematic characters 638 - let cleanJson = jsonMatch[0].replace(/"code"\s*:\s*(`|")([^`"]*?)(`|")/g, function(match, q1, code, q3) { 639 - // Replace all literal backslashes with double backslashes in the code string 640 - const escapedCode = code.replace(/\\/g, "\\\\"); 641 - return `"code":${q1}${escapedCode}${q3}`; 642 - }); 643 - 906 + let cleanJson = jsonMatch[0].replace( 907 + /"code"\s*:\s*(`|")([^`"]*?)(`|")/g, 908 + function (match, q1, code, q3) { 909 + // Replace all literal backslashes with double backslashes in the code string 910 + const escapedCode = code.replace( 911 + /\\/g, 912 + "\\\\", 913 + ); 914 + return `"code":${q1}${escapedCode}${q3}`; 915 + }, 916 + ); 917 + 644 918 const toolResponse = safeJsonParse(cleanJson); 645 919 if ( 646 920 toolResponse.tool_calls && ··· 688 962 console.error("JSON parse error:", error); 689 963 console.error("Problem JSON:", jsonString); 690 964 // Try to escape any unescaped control characters 691 - const escapedJson = jsonString.replace(/[\u0000-\u001F]/g, match => { 692 - return '\\u' + ('0000' + match.charCodeAt(0).toString(16)).slice(-4); 693 - }); 965 + const escapedJson = jsonString.replace( 966 + /[\u0000-\u001F]/g, 967 + (match) => { 968 + return ( 969 + "\\u" + 970 + ( 971 + "0000" + match.charCodeAt(0).toString(16) 972 + ).slice(-4) 973 + ); 974 + }, 975 + ); 694 976 try { 695 977 return JSON.parse(escapedJson); 696 978 } catch (secondError) { 697 - console.error("Second parse attempt failed:", secondError); 979 + console.error( 980 + "Second parse attempt failed:", 981 + secondError, 982 + ); 698 983 throw error; // Throw the original error 699 984 } 700 985 } 701 986 } 702 - 703 - // Handle Ctrl+Enter in textarea 704 - function setupCtrlEnterHandler() { 987 + 988 + function deleteSave(key) { 989 + if (confirm("Are you sure you want to delete this save?")) { 990 + localStorage.removeItem(key); 991 + // Refresh the load modal to show updated list 992 + showLoadModal(); 993 + } 994 + } 995 + 996 + // Handle keyboard shortcuts 997 + function setupKeyboardHandlers() { 705 998 const codeEditor = document.getElementById("codeEditor"); 706 999 if (codeEditor) { 707 1000 // Remove any previous event listeners first (to avoid duplicates) 708 - codeEditor.removeEventListener("keydown", ctrlEnterHandler); 1001 + codeEditor.removeEventListener("keydown", editorKeyHandler); 709 1002 // Add a new event listener 710 - codeEditor.addEventListener("keydown", ctrlEnterHandler); 1003 + codeEditor.addEventListener("keydown", editorKeyHandler); 711 1004 } else { 712 1005 console.error("codeEditor element not found, will retry"); 713 1006 // Retry after a short delay 714 - setTimeout(setupCtrlEnterHandler, 100); 1007 + setTimeout(setupKeyboardHandlers, 100); 715 1008 } 1009 + 1010 + // Global keyboard shortcuts 1011 + document.addEventListener("keydown", function (e) { 1012 + // F1 key - Help 1013 + if (e.key === "F1") { 1014 + e.preventDefault(); 1015 + showHelp(); 1016 + } 1017 + // F2 key - Save 1018 + else if (e.key === "F2") { 1019 + e.preventDefault(); 1020 + showSaveModal(); 1021 + } 1022 + // F3 key - Load 1023 + else if (e.key === "F3") { 1024 + e.preventDefault(); 1025 + showLoadModal(); 1026 + } 1027 + // Escape key - Close any open modal 1028 + else if (e.key === "Escape") { 1029 + document.querySelectorAll(".modal").forEach((modal) => { 1030 + if (modal.style.display === "block") { 1031 + hideModal(modal.id); 1032 + // Return focus to the editor after closing modal 1033 + const editor = 1034 + document.getElementById("codeEditor"); 1035 + if (editor) { 1036 + editor.focus(); 1037 + } 1038 + } 1039 + }); 1040 + } 1041 + }); 716 1042 } 717 - 718 - // Define the handler function separately so it can be added/removed consistently 719 - function ctrlEnterHandler(e) { 1043 + 1044 + // Define the editor key handler function 1045 + function editorKeyHandler(e) { 720 1046 if (e.ctrlKey && e.key === "Enter") { 721 1047 e.preventDefault(); 722 1048 generateAndExecute(); 723 1049 } 724 1050 } 725 - 726 - // Set up the handler immediately 727 - setupCtrlEnterHandler(); 728 - 729 - // Also ensure it's set up when the DOM is fully loaded 730 - window.addEventListener("DOMContentLoaded", setupCtrlEnterHandler); 731 - window.addEventListener("load", setupCtrlEnterHandler); 1051 + 1052 + // Set up the handlers immediately 1053 + setupKeyboardHandlers(); 1054 + 1055 + // Also ensure they're set up when the DOM is fully loaded 1056 + // Modal functions 1057 + function showModal(modalId) { 1058 + const modal = document.getElementById(modalId); 1059 + if (modal) { 1060 + modal.style.display = "block"; 1061 + // Prevent body scrolling when modal is open 1062 + document.body.style.overflow = "hidden"; 1063 + 1064 + // Make the modal appear with a nice fade-in effect 1065 + modal.style.opacity = "0"; 1066 + setTimeout(() => { 1067 + modal.style.opacity = "1"; 1068 + }, 10); 1069 + 1070 + // Add/refresh click handler for closing when clicking outside 1071 + const outsideClickHandler = function (e) { 1072 + if (e.target === modal) { 1073 + hideModal(modalId); 1074 + } 1075 + }; 1076 + // Remove any existing handlers to avoid duplicates 1077 + modal.removeEventListener("click", outsideClickHandler); 1078 + modal.addEventListener("click", outsideClickHandler); 1079 + 1080 + // Trap focus within the modal 1081 + const focusableElements = modal.querySelectorAll( 1082 + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), .modal-close, .button', 1083 + ); 1084 + if (focusableElements.length > 0) { 1085 + const firstElement = focusableElements[0]; 1086 + const lastElement = 1087 + focusableElements[focusableElements.length - 1]; 1088 + 1089 + // Focus the first element 1090 + setTimeout(() => { 1091 + firstElement.focus(); 1092 + }, 100); 1093 + 1094 + // Add key event handler for tab key to trap focus 1095 + const handleTabKey = function (e) { 1096 + if (e.key === "Tab") { 1097 + if ( 1098 + e.shiftKey && 1099 + document.activeElement === firstElement 1100 + ) { 1101 + e.preventDefault(); 1102 + lastElement.focus(); 1103 + } else if ( 1104 + !e.shiftKey && 1105 + document.activeElement === lastElement 1106 + ) { 1107 + e.preventDefault(); 1108 + firstElement.focus(); 1109 + } 1110 + } 1111 + }; 1112 + 1113 + // Remove any existing handler first 1114 + modal.removeEventListener("keydown", handleTabKey); 1115 + modal.addEventListener("keydown", handleTabKey); 1116 + 1117 + // Store the handler on the modal object for later cleanup 1118 + modal._tabHandler = handleTabKey; 1119 + } 1120 + } 1121 + } 1122 + 1123 + function hideModal(modalId) { 1124 + const modal = document.getElementById(modalId); 1125 + if (modal) { 1126 + // Fade out effect 1127 + modal.style.opacity = "0"; 1128 + 1129 + // Hide the modal after animation 1130 + setTimeout(() => { 1131 + modal.style.display = "none"; 1132 + // Restore body scrolling when modal is closed 1133 + document.body.style.overflow = "auto"; 1134 + }, 200); 1135 + 1136 + // Return focus to editor when modal is closed 1137 + const editor = document.getElementById("codeEditor"); 1138 + if (editor) { 1139 + editor.focus(); 1140 + } 1141 + 1142 + // Clear any error messages 1143 + const messageElements = document.querySelectorAll( 1144 + "#save-message, #load-message", 1145 + ); 1146 + messageElements.forEach((el) => { 1147 + if (el) el.textContent = ""; 1148 + }); 1149 + } 1150 + } 1151 + 1152 + function showHelp() { 1153 + showModal("help-modal"); 1154 + } 1155 + 1156 + function showSaveModal() { 1157 + document.getElementById("save-name").value = ""; 1158 + showModal("save-modal"); 1159 + // Focus the input field and select any existing text 1160 + setTimeout(() => { 1161 + const saveNameInput = document.getElementById("save-name"); 1162 + saveNameInput.focus(); 1163 + saveNameInput.select(); 1164 + }, 100); 1165 + } 1166 + 1167 + function showLoadModal() { 1168 + // Populate the load list 1169 + const loadList = document.getElementById("load-list"); 1170 + loadList.innerHTML = ""; 1171 + 1172 + // Get all keys in localStorage that start with 'plastic-save-' 1173 + const saves = []; 1174 + for (let i = 0; i < localStorage.length; i++) { 1175 + const key = localStorage.key(i); 1176 + if (key && key.startsWith("plastic-save-")) { 1177 + const saveName = key.replace("plastic-save-", ""); 1178 + let timestamp = null; 1179 + 1180 + // Try to extract timestamp if it's a JSON object 1181 + try { 1182 + const saveData = JSON.parse( 1183 + localStorage.getItem(key), 1184 + ); 1185 + timestamp = saveData.timestamp; 1186 + } catch (e) { 1187 + // Old format, no timestamp available 1188 + } 1189 + 1190 + saves.push({ 1191 + key, 1192 + name: saveName, 1193 + timestamp: timestamp, 1194 + }); 1195 + } 1196 + } 1197 + 1198 + if (saves.length === 0) { 1199 + loadList.innerHTML = 1200 + '<div class="save-item">No saves found</div>'; 1201 + } else { 1202 + // Sort saves by timestamp (newest first) or alphabetically if no timestamp 1203 + saves.sort((a, b) => { 1204 + if (a.timestamp && b.timestamp) { 1205 + return b.timestamp - a.timestamp; // Newest first 1206 + } else if (a.timestamp) { 1207 + return -1; // a comes first 1208 + } else if (b.timestamp) { 1209 + return 1; // b comes first 1210 + } else { 1211 + return a.name.localeCompare(b.name); // Alphabetically 1212 + } 1213 + }); 1214 + 1215 + saves.forEach((save) => { 1216 + const saveItem = document.createElement("div"); 1217 + saveItem.className = "save-item"; 1218 + 1219 + // Format timestamp if available 1220 + let dateStr = ""; 1221 + if (save.timestamp) { 1222 + const date = new Date(save.timestamp); 1223 + dateStr = ` <span style="font-size: 12px; color: #808080;">(${date.toLocaleString()})</span>`; 1224 + } 1225 + 1226 + saveItem.innerHTML = ` 1227 + <span>${save.name}${dateStr}</span> 1228 + <div> 1229 + <span class="hotkey" onclick="loadSave('${save.key}')" tabindex="0" role="button" aria-label="Load save ${save.name}">Load</span> 1230 + <span class="separator">|</span> 1231 + <span class="hotkey" onclick="deleteSave('${save.key}')" tabindex="0" role="button" aria-label="Delete save ${save.name}">Delete</span> 1232 + </div> 1233 + `; 1234 + // Add double-click support to load the save directly 1235 + saveItem.addEventListener("dblclick", () => 1236 + loadSave(save.key), 1237 + ); 1238 + loadList.appendChild(saveItem); 1239 + }); 1240 + } 1241 + 1242 + showModal("load-modal"); 1243 + } 732 1244 733 - // Update time in status bar 734 - setInterval(() => { 735 - const now = new Date(); 736 - const timeStr = now.toLocaleTimeString([], { 737 - hour: "2-digit", 738 - minute: "2-digit", 739 - hour12: false, 740 - }); 741 - const statusBarTime = document.querySelector( 742 - ".status-bar span:last-child", 743 - ); 744 - if (statusBarTime) { 745 - statusBarTime.textContent = timeStr; 1245 + function saveCurrentHTML() { 1246 + const saveName = document 1247 + .getElementById("save-name") 1248 + .value.trim(); 1249 + if (!saveName) { 1250 + const saveMessage = document.getElementById("save-message"); 1251 + saveMessage.textContent = 1252 + "Please enter a name for your save"; 1253 + saveMessage.style.color = "#ff0000"; 1254 + setTimeout(() => { 1255 + saveMessage.textContent = ""; 1256 + }, 3000); 1257 + return; 746 1258 } 747 1259 748 - // Update memory in status bar based on actual tab memory usage 749 - const memoryUsage = Math.round( 750 - performance.memory 751 - ? performance.memory.usedJSHeapSize / 1024 752 - : 0, 753 - ); 1260 + try { 1261 + // Ensure all modals are closed in the saved HTML 1262 + // First make a clone of the current document state 1263 + const tempDiv = document.createElement("div"); 1264 + tempDiv.innerHTML = document.documentElement.outerHTML; 754 1265 755 - const memoryStr = `${589 - Math.min(Math.round(memoryUsage / 1024), 500)}K free`; 756 - const memory = document.getElementById("memory"); 757 - if (memory) { 758 - memory.textContent = memoryStr; 1266 + // Find and hide all modals in the clone 1267 + const modalElements = tempDiv.querySelectorAll(".modal"); 1268 + modalElements.forEach((modal) => { 1269 + modal.style.display = "none"; 1270 + }); 1271 + 1272 + // Save the current state 1273 + const saveData = { 1274 + html: tempDiv.innerHTML, 1275 + timestamp: new Date().getTime(), 1276 + name: saveName, 1277 + }; 1278 + 1279 + localStorage.setItem( 1280 + `plastic-save-${saveName}`, 1281 + JSON.stringify(saveData), 1282 + ); 1283 + 1284 + const saveMessage = document.getElementById("save-message"); 1285 + saveMessage.textContent = `Saved as "${saveName}"`; 1286 + saveMessage.style.color = "#008000"; 1287 + setTimeout(() => { 1288 + saveMessage.textContent = ""; 1289 + hideModal("save-modal"); 1290 + }, 1500); 1291 + } catch (e) { 1292 + const saveMessage = document.getElementById("save-message"); 1293 + saveMessage.textContent = `Error saving: ${e.message}`; 1294 + saveMessage.style.color = "#ff0000"; 1295 + setTimeout(() => { 1296 + saveMessage.textContent = ""; 1297 + }, 3000); 1298 + } 1299 + } 1300 + 1301 + // Add keyboard support for modals 1302 + document.addEventListener("DOMContentLoaded", function () { 1303 + const saveNameInput = document.getElementById("save-name"); 1304 + if (saveNameInput) { 1305 + // Remove any existing listeners to prevent duplicates 1306 + const saveInputHandler = function (e) { 1307 + if (e.key === "Enter") { 1308 + e.preventDefault(); 1309 + saveCurrentHTML(); 1310 + } 1311 + }; 1312 + 1313 + saveNameInput.removeEventListener( 1314 + "keydown", 1315 + saveInputHandler, 1316 + ); 1317 + saveNameInput.addEventListener("keydown", saveInputHandler); 759 1318 } 760 1319 761 - // Update runtime in status bar - based on how long the page has been open 762 - const pageOpenTime = performance.timing 763 - ? performance.timing.navigationStart || Date.now() 764 - : Date.now(); 765 - const timeOpen = Math.floor((Date.now() - pageOpenTime) / 1000); 766 - const hours = Math.floor(timeOpen / 3600) 767 - .toString() 768 - .padStart(2, "0"); 769 - const minutes = Math.floor((timeOpen % 3600) / 60) 770 - .toString() 771 - .padStart(2, "0"); 772 - const seconds = Math.floor(timeOpen % 60) 773 - .toString() 774 - .padStart(2, "0"); 775 - const runtimeStr = `${hours}:${minutes}:${seconds}`; 776 - const runtime = document.getElementById("runtime"); 777 - if (runtime) { 778 - runtime.textContent = runtimeStr; 1320 + document 1321 + .querySelectorAll(".modal-close") 1322 + .forEach((closeBtn) => { 1323 + closeBtn.addEventListener("keydown", function (e) { 1324 + if (e.key === "Enter" || e.key === " ") { 1325 + e.preventDefault(); 1326 + const modalId = this.closest(".modal").id; 1327 + hideModal(modalId); 1328 + } 1329 + }); 1330 + }); 1331 + }); 1332 + 1333 + function loadSave(key) { 1334 + try { 1335 + const savedData = localStorage.getItem(key); 1336 + if (savedData) { 1337 + let saveObj; 1338 + 1339 + try { 1340 + saveObj = JSON.parse(savedData); 1341 + } catch (e) { 1342 + saveObj = { html: savedData }; 1343 + } 1344 + 1345 + if ( 1346 + confirm( 1347 + "Loading will replace the current page. Continue?", 1348 + ) 1349 + ) { 1350 + hideModal("load-modal"); 1351 + 1352 + const scriptToAdd = ` 1353 + <script> 1354 + document.addEventListener('DOMContentLoaded', function() { 1355 + // Ensure all modals are hidden 1356 + document.querySelectorAll('.modal').forEach(modal => { 1357 + modal.style.display = "none"; 1358 + }); 1359 + 1360 + // Re-setup keyboard handlers 1361 + if (typeof setupKeyboardHandlers === 'function') { 1362 + setupKeyboardHandlers(); 1363 + } 1364 + }); 1365 + <\/script> 1366 + `; 1367 + 1368 + // Load the saved HTML with the added script 1369 + document.open(); 1370 + document.write(saveObj.html + scriptToAdd); 1371 + document.close(); 1372 + } 1373 + } else { 1374 + const loadMessage = 1375 + document.getElementById("load-message"); 1376 + loadMessage.textContent = "Save not found"; 1377 + loadMessage.style.color = "#ff0000"; 1378 + setTimeout(() => { 1379 + loadMessage.textContent = ""; 1380 + }, 3000); 1381 + } 1382 + } catch (e) { 1383 + const loadMessage = document.getElementById("load-message"); 1384 + loadMessage.textContent = `Error loading: ${e.message}`; 1385 + loadMessage.style.color = "#ff0000"; 1386 + setTimeout(() => { 1387 + loadMessage.textContent = ""; 1388 + }, 3000); 779 1389 } 780 - }, 1000); 1390 + } 781 1391 </script> 782 1392 </body> 783 1393 </html>