From 762e33d6546d4d119f613b24b5010d06cb92623e Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Sat, 1 Feb 2025 10:42:24 -0700 Subject: [PATCH] Editor: Save single snippet and alert to changes (#174) * Improved `console` visibility on Light theme * Added new option to export a single snippet and alert for large URL * Added flag to control changes and warn user if closing the tab or selecting other code snippet * Fixed static analysis issues * Fixed code formatting --- src/app/brightscript.js | 12 ++- src/app/css/editor.css | 9 ++- src/app/editor.ejs | 9 ++- src/app/editor.js | 162 ++++++++++++++++++++++++++++++++++------ 4 files changed, 165 insertions(+), 27 deletions(-) diff --git a/src/app/brightscript.js b/src/app/brightscript.js index f12922d..3f6d10d 100644 --- a/src/app/brightscript.js +++ b/src/app/brightscript.js @@ -48,7 +48,17 @@ export function defineMode(CodeMirror) { "stop", "throw", ]; - const endControl = ["next", "endif", "end if", "endfor", "end for", "endwhile", "end while", "endtry", "end try"]; + const endControl = [ + "next", + "endif", + "end if", + "endfor", + "end for", + "endwhile", + "end while", + "endtry", + "end try", + ]; const wordOperators = wordRegexp(["and", "or", "not", "mod"]); const commonkeywords = ["dim", "print", "goto", "library"]; const commontypes = [ diff --git a/src/app/css/editor.css b/src/app/css/editor.css index 6cd3329..ac019f3 100644 --- a/src/app/css/editor.css +++ b/src/app/css/editor.css @@ -68,7 +68,7 @@ --header-color: #333333; --header-background-color: #cccccc; --help-color: #5d5d5d; - --console-color: #333333; + --console-color: #053369; --console-background-color: #eeeeee; --console-selection-color: #aaaaaa; --item-background-color: #094771; @@ -272,6 +272,8 @@ pre { .dropdown li { margin-top: 3px; margin-bottom: 3px; + width: 100%; + white-space: nowrap; } .dropdown li a { @@ -281,6 +283,7 @@ pre { color: var(--control-color); padding: 3px; width: 100%; + box-sizing: border-box; } .dropdown li a i { @@ -371,6 +374,10 @@ pre { color: var(--console-color) !important; } +.VanillaTerm output span { + color: var(--console-color) !important; +} + .VanillaTerm .command { align-items: center; } diff --git a/src/app/editor.ejs b/src/app/editor.ejs index 245433a..199dcae 100644 --- a/src/app/editor.ejs +++ b/src/app/editor.ejs @@ -50,6 +50,7 @@
  • Save as
  • Delete
  • Export
  • +
  • Export All
  • Import
  • @@ -111,12 +112,12 @@ - - + +
    - - + +
    diff --git a/src/app/editor.js b/src/app/editor.js index fd318b4..9d400ad 100644 --- a/src/app/editor.js +++ b/src/app/editor.js @@ -31,7 +31,10 @@ const codeSelect = document.getElementById("code-selector"); const codeDialog = document.getElementById("code-dialog"); const actionType = document.getElementById("actionType"); const codeForm = document.getElementById("code-form"); -const deleteDialog = document.getElementById("delete-dialog"); +const confirmDialog = document.getElementById("confirm-dialog"); +const dialogText = document.getElementById("dialog-text"); +const confirmButton = document.getElementById("confirm-button"); +const cancelButton = document.getElementById("cancel-button"); const moreButton = document.getElementById("more-options"); const dropdown = document.getElementById("more-options-dropdown"); @@ -91,12 +94,14 @@ document.getElementById("rename-option").addEventListener("click", renameCode); document.getElementById("saveas-option").addEventListener("click", saveAsCode); document.getElementById("delete-option").addEventListener("click", deleteCode); document.getElementById("export-option").addEventListener("click", exportCode); +document.getElementById("export-all-option")?.addEventListener("click", exportAllCode); document.getElementById("import-option").addEventListener("click", importCode); let consoleLogsContainer = document.getElementById("console-logs"); let isResizing = false; let editorManager; let currentId = nanoid(10); +let isCodeChanged = false; function main() { updateButtons(); @@ -110,6 +115,16 @@ function main() { const cm = document.querySelector(".CodeMirror"); delete cm.CodeMirror.constructor.keyMap.emacsy["Ctrl-V"]; } + editorManager.editor.on("change", () => { + if (codeSelect.value === "0") { + const code = editorManager.editor.getValue(); + if (code && code.trim() === "") { + isCodeChanged = false; + return; + } + } + markCodeAsChanged(); + }); hideEditor(!(currentApp.title === undefined || currentApp.title.endsWith("editor_code.brs"))); populateCodeSelector(); // Subscribe to Engine events and initialize Console @@ -132,6 +147,7 @@ function main() { endButton.style.display = "inline"; breakButton.style.display = "inline"; } + editorManager.editor.focus(); } function updateButtons() { @@ -212,6 +228,31 @@ function scrollToBottom() { } } +// Code Events + +function markCodeAsChanged() { + isCodeChanged = true; + updateCodeSelector(); +} + +function markCodeAsSaved() { + isCodeChanged = false; + updateCodeSelector(); +} + +function updateCodeSelector() { + const options = Array.from(codeSelect.options); + for (const option of options) { + if (option.value === currentId) { + if (isCodeChanged) { + option.text = `⏺︎ ${option.text.replace(/^⏺︎ /, "")}`; + } else { + option.text = option.text.replace(/^⏺︎ /, ""); + } + } + } +} + function populateCodeSelector(currentId = "") { const arrCode = new Array(); for (let i = 0; i < localStorage.length; i++) { @@ -235,9 +276,44 @@ function populateCodeSelector(currentId = "") { const selected = codeId === currentId; codeSelect.options[i + 1] = new Option(arrCode[i][0], codeId, false, selected); } + updateCodeSelector(); +} + +let savedValue = codeSelect.value; +codeSelect.addEventListener("mousedown", async (e) => { + savedValue = codeSelect.value; +}); + +function showDialog(message) { + return new Promise((resolve) => { + if (message) { + dialogText.innerText = message; + } + confirmDialog.showModal(); + + confirmButton.onclick = () => { + confirmDialog.close(); + resolve(true); + }; + + cancelButton.onclick = () => { + confirmDialog.close(); + resolve(false); + }; + }); } -codeSelect.addEventListener("change", (e) => { +codeSelect.addEventListener("change", async (e) => { + if (isCodeChanged) { + const confirmed = await showDialog( + "There are unsaved changes, do you want to discard and continue?" + ); + if (!confirmed) { + e.preventDefault(); + codeSelect.value = savedValue; + return; + } + } if (codeSelect.value !== "0") { loadCode(codeSelect.value); } else { @@ -254,6 +330,7 @@ function loadCode(id) { code = code.substring(code.indexOf("=@") + 2); } resetApp(id, code); + markCodeAsSaved(); } else { showToast("Could not find the code in the Local Storage!", 3000, true); } @@ -262,7 +339,8 @@ function loadCode(id) { function renameCode() { if (currentId && localStorage.getItem(currentId)) { actionType.value = "rename"; - codeForm.codeName.value = codeSelect.options[codeSelect.selectedIndex].text; + const codeName = codeSelect.options[codeSelect.selectedIndex].text; + codeForm.codeName.value = codeName.replace(/^⏺︎ /, ""); codeDialog.showModal(); } else { showToast("There is no code snippet selected to rename!", 3000, true); @@ -272,22 +350,57 @@ function renameCode() { function saveAsCode() { if (currentId && localStorage.getItem(currentId)) { actionType.value = "saveas"; - codeForm.codeName.value = codeSelect.options[codeSelect.selectedIndex].text + " (Copy)"; + const codeName = codeSelect.options[codeSelect.selectedIndex].text + " (Copy)"; + codeForm.codeName.value = codeName.replace(/^⏺︎ /, ""); codeDialog.showModal(); } else { showToast("There is no code snippet selected to save as!", 3000, true); } } -function deleteCode() { +async function deleteCode() { if (currentId && localStorage.getItem(currentId)) { - deleteDialog.showModal(); + const confirmed = await showDialog("Are you sure you want to delete this code?"); + if (confirmed) { + localStorage.removeItem(currentId); + currentId = nanoid(10); + resetApp(); + showToast("Code deleted from the browser local storage!", 3000); + } } else { showToast("There is no code snippet selected to delete!", 3000, true); } } - function exportCode() { + const codes = {}; + let codeContent = editorManager.editor.getValue(); + if (codeContent && codeContent.trim() !== "") { + if (codeSelect.value !== "0") { + let codeName = codeSelect.options[codeSelect.selectedIndex].text; + codes[currentId] = { name: codeName, content: codeContent }; + const safeFileName = codeName + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/^⏺︎ /, "") + .replace(/[^a-z0-9-]/g, ""); + const json = JSON.stringify(codes, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${safeFileName}.json`; + a.click(); + URL.revokeObjectURL(url); + } else { + showToast("Please save your Code Snipped before exporting!", 3000, true); + return; + } + } else { + showToast("There is no Code Snippet to Export", 3000, true); + } +} + +function exportAllCode() { const codes = {}; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); @@ -324,23 +437,19 @@ function importCode() { localStorage.setItem(id, value); } populateCodeSelector(currentId); - showToast("Code snippets imported to the simulator local storage!", 3000); + if (Object.keys(codes).length === 1) { + showToast("Code snippet imported to the simulator local storage!", 3000); + const loadId = Object.keys(codes)[0]; + loadCode(loadId); + } else { + showToast("Code snippets imported to the simulator local storage!", 3000); + } }; reader.readAsText(file); }; input.click(); } -deleteDialog.addEventListener("close", (e) => { - if (deleteDialog.returnValue === "ok") { - localStorage.removeItem(currentId); - currentId = nanoid(10); - resetApp(); - showToast("Code deleted from the simulator local storage.", 3000); - } - deleteDialog.returnValue = ""; -}); - function resetApp(id = "", code = "") { populateCodeSelector(id); if (currentApp.running) { @@ -349,13 +458,14 @@ function resetApp(id = "", code = "") { } editorManager.editor.setValue(code); editorManager.editor.focus(); + markCodeAsSaved(); } function shareCode() { let code = editorManager.editor.getValue(); if (code && code.trim() !== "") { if (codeSelect.value !== "0") { - let codeName = codeSelect.options[codeSelect.selectedIndex].text; + let codeName = codeSelect.options[codeSelect.selectedIndex].text.replace(/^⏺︎ /, ""); code = `@=${codeName}=@${code}`; } const data = { @@ -364,7 +474,15 @@ function shareCode() { }; getShareUrl(data).then(function (shareLink) { navigator.clipboard.writeText(shareLink); - showToast("brsFiddle.net share URL copied to clipboard."); + if (shareLink.length > 2048) { + showToast( + "URL copied to clipboard, but it's longer than 2048 bytes, consider exporting as a file instead!", + 7000, + true + ); + } else { + showToast("brsFiddle.net share URL copied to clipboard."); + } }); } else { showToast("There is no Source Code to share!", 3000, true); @@ -378,12 +496,13 @@ function saveCode() { actionType.value = "save"; codeDialog.showModal(); } else { - const codeName = codeSelect.options[codeSelect.selectedIndex].text; + const codeName = codeSelect.options[codeSelect.selectedIndex].text.replace(/^⏺︎ /, ""); localStorage.setItem(currentId, `@=${codeName}=@${code}`); showToast( "Code saved in the simulator local storage.\nTo share it use the Share button.", 5000 ); + markCodeAsSaved(); } } else { showToast("There is no Source Code to save", 3000, true); @@ -417,6 +536,7 @@ codeDialog.addEventListener("close", (e) => { 5000 ); } + markCodeAsSaved(); } resetDialog(); });