diff --git a/src/index.html b/src/index.html index c7b98e1..2ab1db4 100644 --- a/src/index.html +++ b/src/index.html @@ -1,1355 +1,1981 @@ - + + - - -Untitled - Pascar - - + /* Textarea */ + #editor { + flex: 1; + background: var(--bg); + color: var(--fg); + border: none; + outline: none; + padding: 2px 8px; + font-family: "Courier New", Courier, monospace; + font-size: 15px; + line-height: 1.25; + resize: none; + overflow-y: scroll; + overflow-x: auto; + white-space: pre; + caret-color: #ffffff; + tab-size: 4; + -moz-tab-size: 4; + user-select: text; + -webkit-user-select: text; + } + + #editor::selection { + background: #ffffff; + color: #000080; + } + + #editor::-webkit-scrollbar { + width: 14px; + height: 14px; + } + + #editor::-webkit-scrollbar-track { + background: var(--scrollbar-track); + } + + #editor::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border: 1px solid #333; + } + + #editor::-webkit-scrollbar-corner { + background: var(--scrollbar-track); + } + + /* Go-to-Line Bar */ + #goto-bar { + display: none; + background: var(--status-bg); + color: var(--status-fg); + padding: 3px 10px; + font-size: 13px; + align-items: center; + gap: 8px; + flex-shrink: 0; + border-top: 1px solid #555; + } + + #goto-bar.open { + display: flex; + } + + #goto-bar input { + background: #fff; + color: #000; + border: 1px solid #000; + padding: 1px 5px; + font-family: "Courier New", monospace; + font-size: 13px; + width: 70px; + outline: none; + } + + #goto-bar input:focus { + outline: 2px solid #000080; + } + + /* Find Bar (inline) */ + #find-bar { + display: none; + background: var(--status-bg); + color: var(--status-fg); + padding: 3px 10px; + font-size: 13px; + align-items: center; + gap: 8px; + flex-shrink: 0; + border-top: 1px solid #555; + flex-wrap: wrap; + } + + #find-bar.open { + display: flex; + } + + #find-bar input[type="text"] { + background: #fff; + color: #000; + border: 1px solid #000; + padding: 1px 5px; + font-family: "Courier New", monospace; + font-size: 13px; + width: 160px; + outline: none; + } + + #find-bar input[type="text"]:focus { + outline: 2px solid #000080; + } + + #find-bar label { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + } + + /* Status Bar */ + #status-bar { + background: var(--status-bg); + color: var(--status-fg); + display: flex; + justify-content: space-between; + align-items: center; + padding: 2px 8px; + font-size: 13px; + flex-shrink: 0; + border-top: 1px solid #555; + } + + .status-left { + display: flex; + gap: 16px; + align-items: center; + } + + .status-right { + display: flex; + gap: 16px; + align-items: center; + } + + #status-modified { + color: #cc0000; + font-weight: bold; + } + + /* Modal Dialog */ + #dialog-overlay { + display: none; + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.55); + z-index: 300; + align-items: center; + justify-content: center; + } + + #dialog-overlay.open { + display: flex; + } + + .dialog { + background: var(--bg); + color: var(--menu-fg); + border: 2px solid #ffffff; + min-width: 340px; + max-width: 480px; + box-shadow: 5px 5px 0 #000000; + } + + .dialog-title { + background: var(--dialog-title-bg); + color: var(--dialog-title-fg); + padding: 4px 10px; + font-weight: bold; + font-size: 14px; + } + + .dialog-body { + padding: 14px 18px 10px; + } + + .dialog-body label { + display: block; + margin-bottom: 4px; + font-size: 13px; + } + + .dialog-body input[type="text"] { + width: 100%; + background: var(--input-bg); + color: var(--input-fg); + border: 1px solid #000; + padding: 3px 5px; + font-family: "Courier New", monospace; + font-size: 13px; + margin-bottom: 10px; + outline: none; + } + + .dialog-body input[type="text"]:focus { + outline: 2px solid #000080; + } + + .dialog-check { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + margin-bottom: 6px; + cursor: pointer; + } + + .dialog-buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; + } + + /* Buttons */ + .btn { + background: var(--menu-bg); + color: #000; + border: 1px solid #000; + padding: 3px 16px; + font-family: "Courier New", monospace; + font-size: 13px; + cursor: pointer; + box-shadow: 2px 2px 0 #555; + outline: none; + } + + .btn:hover { + background: #cccccc; + } + + .btn:active { + box-shadow: none; + transform: translate(1px, 1px); + } + + .btn.primary { + background: (--menu-bg); + color: #fff; + border-color: #fff; + } + + .btn.primary:hover { + background: #0000aa; + } + + .btn:focus { + outline: 2px solid #000080; + } + + /* Notification */ + #notif { + position: absolute; + bottom: 32px; + left: 50%; + transform: translateX(-50%); + background: var(--notif-bg); + color: var(--notif-fg); + border: 1px solid #ffffff; + padding: 5px 18px; + font-size: 13px; + display: none; + z-index: 400; + white-space: nowrap; + pointer-events: none; + } + + #notif.show { + display: block; + } + + /* Help Screen */ + #help-overlay { + display: none; + position: absolute; + inset: 0; + background: rgba(0, 0, 64, 0.96); + z-index: 300; + overflow-y: auto; + color: #ffffff; + padding: 24px 32px; + } + + #help-overlay.open { + display: block; + } + + .help-title { + text-align: center; + font-size: 17px; + margin-bottom: 16px; + color: var(--help-heading); + letter-spacing: 1px; + } + + .help-cols { + display: flex; + gap: 40px; + justify-content: center; + flex-wrap: wrap; + } + + .help-section-title { + color: var(--help-heading); + font-weight: bold; + margin: 12px 0 4px; + font-size: 13px; + } + + .help-table { + border-collapse: collapse; + font-size: 13px; + } + + .help-table td { + padding: 2px 12px 2px 0; + vertical-align: top; + } + + .help-table td:first-child { + color: var(--help-key); + min-width: 150px; + } + + .help-close { + text-align: center; + margin-top: 18px; + color: #aaaaaa; + font-size: 13px; + } + + - - -
Pascar
- - - - - -
-
- - - -
-
-
Dialog
-
+ +
Pascar
+ + + - - -
-
Pascar - Keyboard Shortcuts
-
-
-
Menu Navigation
- - - - - - - - - - - -
Alt+FOpen File menu
Alt+EOpen Edit menu
Alt+SOpen Search menu
Alt+VOpen View menu
Alt+HOpen Help menu
↑ / ↓Move through menu items
← / →Switch between menus
EnterActivate highlighted item
Letter keyActivate underlined shortcut
EscapeClose menu
- -
File Operations
- - - - - - -
Ctrl+NNew file
Ctrl+OOpen file from disk
Ctrl+SSave / download file
Ctrl+Shift+SSave As...
Ctrl+PPrint
- -
Editing
- - - - - - - - - - - - -
Ctrl+ZUndo
Ctrl+YRedo
Ctrl+XCut
Ctrl+CCopy
Ctrl+VPaste
Ctrl+ASelect All
DelDelete selection
F5Insert date/time
InsertToggle Insert/Overwrite
TabIndent (4 spaces)
Shift+TabUnindent
+ + + + + - - -
-
- - -
- Find: - - - - - - - - - - - -
- - -
- Go to line: - - - -
- - -
-
- Untitled - - UTF-8 + + +
+
+ + + +
+
+
Dialog
+
+
+
+ + +
+
Pascar - Keyboard Shortcuts
+
+
+
Menu Navigation
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Alt+FOpen File menu
Alt+EOpen Edit menu
Alt+SOpen Search menu
Alt+VOpen View menu
Alt+HOpen Help menu
↑ / ↓Move through menu items
← / →Switch between menus
EnterActivate highlighted item
Letter keyActivate underlined shortcut
EscapeClose menu
+ +
File Operations
+ + + + + + + + + + + + + + + + + + + + + +
Ctrl+NNew file
Ctrl+OOpen file from disk
Ctrl+SSave / download file
Ctrl+Shift+SSave As...
Ctrl+PPrint
+ +
Editing
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Ctrl+ZUndo
Ctrl+YRedo
Ctrl+XCut
Ctrl+CCopy
Ctrl+VPaste
Ctrl+ASelect All
DelDelete selection
F5Insert date/time
InsertToggle Insert/Overwrite
TabIndent (4 spaces)
Shift+TabUnindent
+
+
+
Search
+ + + + + + + + + + + + + + + + + + + + + +
Ctrl+FFind...
Ctrl+HFind & Replace...
F3Find Next
Shift+F3Find Previous
Ctrl+GGo to Line...
+ +
View
+ + + + + + + + + + + + + + + + + + + + + +
Alt+WToggle Word Wrap
Alt+LToggle Line Numbers
Ctrl++Zoom In
Ctrl+-Zoom Out
Ctrl+0Reset Zoom
+ +
Navigation
+ + + + + + + + + + + + + + + + + + + + + +
Ctrl+HomeBeginning of file
Ctrl+EndEnd of file
Ctrl+Left/RightMove word by word
Home / EndStart / end of line
Page Up / DownScroll one page
+ +
General
+ + + + + + + + + + + + + +
F1Show this help screen
EscapeClose menus / dialogs
Alt+F4Exit (close tab)
+
+
+
+ Press Escape or F1 to close this + screen +
+
+ + +
-
- Words: 0 - Chars: 0 - Ln 1, Col 1 - INS + + +
+ Find: + + + + + + + + + + +
-
- - + editor.addEventListener("keydown", (e) => { + if (e.key === "Insert" && !e.ctrlKey && !e.altKey) { + insertMode = !insertMode; + updateStatus(); + return; + } + if (e.key === "Tab" && !e.ctrlKey && !e.altKey) { + e.preventDefault(); + const s = editor.selectionStart, + en = editor.selectionEnd; + if (e.shiftKey) { + const lineStart = editor.value.lastIndexOf("\n", s - 1) + 1; + const lineText = editor.value.substring(lineStart, s); + const spaces = lineText.match(/^ {1,4}/); + if (spaces) { + editor.value = + editor.value.substring(0, lineStart) + + editor.value.substring(lineStart + spaces[0].length); + editor.selectionStart = editor.selectionEnd = + s - spaces[0].length; + isModified = true; + updateAll(); + } + } else { + editor.value = + editor.value.substring(0, s) + + " " + + editor.value.substring(en); + editor.selectionStart = editor.selectionEnd = s + 4; + isModified = true; + updateAll(); + } + } + }); + + // -- Menu System + const ALT_MENU_MAP = { + f: "file", + e: "edit", + s: "search", + v: "view", + h: "help", + }; + + function getOpenDropdown() { + return document.querySelector(".dropdown.open") || null; + } + + // Position the dropdown directly below its menu-item trigger + function positionDropdown(menuName) { + const item = document.querySelector( + `.menu-item[data-menu="${menuName}"]`, + ); + const dd = document.getElementById("menu-" + menuName); + if (!item || !dd) return; + const itemRect = item.getBoundingClientRect(); + const barRect = menuBar.getBoundingClientRect(); + dd.style.left = itemRect.left - barRect.left + "px"; + } + + function openMenu(name) { + const item = document.querySelector( + `.menu-item[data-menu="${name}"]`, + ); + const dd = document.getElementById("menu-" + name); + if (!item || !dd) return; + document + .querySelectorAll(".dropdown") + .forEach((d) => d.classList.remove("open")); + document + .querySelectorAll(".menu-item") + .forEach((m) => m.classList.remove("active")); + positionDropdown(name); + dd.classList.add("open"); + item.classList.add("active"); + highlightDDItem(dd, 0); + } + + function getDDItems(dd) { + return Array.from(dd.querySelectorAll(".dd-item:not(.disabled)")); + } + + function highlightDDItem(dd, index) { + const items = getDDItems(dd); + items.forEach((i) => i.classList.remove("dd-focused")); + if (items.length === 0) return; + const idx = ((index % items.length) + items.length) % items.length; + items[idx].classList.add("dd-focused"); + } + + function getFocusedIndex(dd) { + const items = getDDItems(dd); + return items.findIndex((i) => i.classList.contains("dd-focused")); + } + + document.querySelectorAll(".menu-item").forEach((item) => { + item.addEventListener("click", (e) => { + e.stopPropagation(); + const name = item.dataset.menu; + const dd = document.getElementById("menu-" + name); + const wasOpen = dd.classList.contains("open"); + document + .querySelectorAll(".dropdown") + .forEach((d) => d.classList.remove("open")); + document + .querySelectorAll(".menu-item") + .forEach((m) => m.classList.remove("active")); + if (!wasOpen) { + positionDropdown(name); + dd.classList.add("open"); + item.classList.add("active"); + } + }); + item.addEventListener("mouseenter", () => { + const anyOpen = document.querySelector(".dropdown.open"); + if (anyOpen) { + const name = item.dataset.menu; + document + .querySelectorAll(".dropdown") + .forEach((d) => d.classList.remove("open")); + document + .querySelectorAll(".menu-item") + .forEach((m) => m.classList.remove("active")); + positionDropdown(name); + document.getElementById("menu-" + name).classList.add("open"); + item.classList.add("active"); + } + }); + }); + + document.querySelectorAll(".dd-item:not(.disabled)").forEach((item) => { + item.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + closeAll(); + handleAction(item.dataset.action); + }); + }); + + document.addEventListener("click", (e) => { + if (e.target.closest("#menu-bar")) closeAll(); + }); + + // -- Global Keyboard Shortcuts + document.addEventListener("keydown", (e) => { + const ctrl = e.ctrlKey || e.metaKey; + const shift = e.shiftKey; + const alt = e.altKey; + const key = e.key.toLowerCase(); + + if (alt && !ctrl && ALT_MENU_MAP[key]) { + e.preventDefault(); + const dd = getOpenDropdown(); + if (dd && dd.id === "menu-" + ALT_MENU_MAP[key]) { + closeAll(); + } else { + openMenu(ALT_MENU_MAP[key]); + } + return; + } + + const openDD = getOpenDropdown(); + if (openDD) { + if (e.key === "ArrowDown") { + e.preventDefault(); + highlightDDItem(openDD, getFocusedIndex(openDD) + 1); + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + const idx = getFocusedIndex(openDD); + highlightDDItem( + openDD, + idx <= 0 ? getDDItems(openDD).length - 1 : idx - 1, + ); + return; + } + if (e.key === "ArrowLeft") { + e.preventDefault(); + const menus = ["file", "edit", "search", "view", "help"]; + const cur = openDD.id.replace("menu-", ""); + openMenu( + menus[(menus.indexOf(cur) - 1 + menus.length) % menus.length], + ); + return; + } + if (e.key === "ArrowRight") { + e.preventDefault(); + const menus = ["file", "edit", "search", "view", "help"]; + const cur = openDD.id.replace("menu-", ""); + openMenu(menus[(menus.indexOf(cur) + 1) % menus.length]); + return; + } + if (e.key === "Enter") { + e.preventDefault(); + const focused = openDD.querySelector(".dd-item.dd-focused"); + if ( + findBar.classList.contains("open") && + gotoBar.classList.contains("open") && + helpOverlay.classList.contains("open") && + dialogOv.classList.contains("open") + ) { + e.preventDefault(); + e.stopPropagation(); + return; + } + if (focused && focused.dataset.action) { + const action = focused.dataset.action; + closeAll(); + handleAction(action); + } + return; + } + if (!ctrl && !alt && e.key.length === 1) { + const letter = e.key.toLowerCase(); + const match = getDDItems(openDD).find((item) => { + const ul = item.querySelector(".ul"); + return ul && ul.textContent.toLowerCase() === letter; + }); + if (match && match.dataset.action) { + e.preventDefault(); + const action = match.dataset.action; + closeAll(); + handleAction(action); + return; + } + } + } + + if (e.key === "Escape") { + e.preventDefault(); + closeAll(); + return; + } + if (e.key === "F1") { + e.preventDefault(); + toggleHelp(); + return; + } + if (e.key === "F3") { + e.preventDefault(); + doFindNext(shift ? -1 : 1); + return; + } + if (e.key === "F5" && !ctrl && !alt) { + e.preventDefault(); + insertDateTime(); + return; + } + + if (ctrl && key === "n") { + e.preventDefault(); + doNew(); + } + if (ctrl && key === "o") { + e.preventDefault(); + doOpen(); + } + if (ctrl && key === "s" && shift) { + e.preventDefault(); + doSaveAs(); + } else if (ctrl && key === "s") { + e.preventDefault(); + doSave(); + } + if (ctrl && key === "p") { + e.preventDefault(); + doPrint(); + } + if (ctrl && key === "f") { + e.preventDefault(); + openFindBar(false); + } + if (ctrl && key === "h") { + e.preventDefault(); + openFindBar(true); + } + if (ctrl && key === "g") { + e.preventDefault(); + openGoto(); + } + if (ctrl && (key === "=" || key === "+")) { + e.preventDefault(); + zoomChange(1); + } + if (ctrl && key === "-") { + e.preventDefault(); + zoomChange(-1); + } + if (ctrl && key === "0") { + e.preventDefault(); + zoomReset(); + } + if (alt && key === "w") { + e.preventDefault(); + toggleWordWrap(); + } + if (alt && key === "l") { + e.preventDefault(); + toggleLineNumbers(); + } + }); + + // -- Close All Overlays + function closeAll() { + document + .querySelectorAll(".dropdown") + .forEach((d) => d.classList.remove("open")); + document + .querySelectorAll(".menu-item") + .forEach((m) => m.classList.remove("active")); + dialogOv.classList.remove("open"); + helpOverlay.classList.remove("open"); + findBar.classList.remove("open"); + gotoBar.classList.remove("open"); + editor.focus(); + } + + function toggleHelp() { + if (helpOverlay.classList.contains("open")) closeAll(); + else { + closeAll(); + helpOverlay.classList.add("open"); + } + } + + // -- Action Dispatcher + function handleAction(a) { + const map = { + new: doNew, + open: doOpen, + save: doSave, + saveas: doSaveAs, + print: doPrint, + exit: () => showNotif("Close this browser tab to exit."), + undo: () => { + editor.focus(); + document.execCommand("undo"); + updateAll(); + }, + redo: () => { + editor.focus(); + document.execCommand("redo"); + updateAll(); + }, + cut: () => { + editor.focus(); + document.execCommand("cut"); + }, + copy: () => { + editor.focus(); + document.execCommand("copy"); + }, + paste: () => { + editor.focus(); + document.execCommand("paste"); + }, + delete: doDelete, + selectall: () => { + editor.select(); + }, + timestamp: insertDateTime, + find: () => openFindBar(false), + replace: () => openFindBar(true), + findnext: () => doFindNext(1), + findprev: () => doFindNext(-1), + goto: openGoto, + wordwrap: toggleWordWrap, + linenumbers: toggleLineNumbers, + zoomin: () => zoomChange(1), + zoomout: () => zoomChange(-1), + zoomreset: zoomReset, + shortcuts: toggleHelp, + about: showAbout, + }; + if (map[a]) map[a](); + } + + // -- File Operations + let fileHandle = null; + const hasFS = typeof window.showOpenFilePicker === "function"; + + function doNew() { + if (isModified && !confirm("Discard unsaved changes?")) return; + editor.value = ""; + fileName = "Untitled"; + fileHandle = null; + isModified = false; + updateAll(); + editor.focus(); + } + + async function doOpen() { + if (isModified && !confirm("Discard unsaved changes?")) return; + if (hasFS) { + try { + const [handle] = await window.showOpenFilePicker({ + types: [ + { + description: "Text files", + accept: { + "text/*": [ + ".txt", + ".md", + ".js", + ".ts", + ".py", + ".html", + ".css", + ".json", + ".xml", + ".csv", + ".log", + ".c", + ".cpp", + ".h", + ".java", + ".rs", + ".go", + ".rb", + ".sh", + ".bat", + ".ini", + ".cfg", + ".yaml", + ".yml", + ], + }, + }, + ], + multiple: false, + }); + fileHandle = handle; + const file = await handle.getFile(); + fileName = file.name; + editor.value = await file.text(); + isModified = false; + updateAll(); + editor.focus(); + showNotif(`Opened: ${fileName}`); + } catch (err) { + if (err.name !== "AbortError") showNotif("Could not open file."); + } + } else { + const inp = document.createElement("input"); + inp.type = "file"; + inp.accept = + ".txt,.md,.js,.ts,.py,.html,.css,.json,.xml,.csv,.log,.c,.cpp,.h,.java,.rs,.go,.rb,.sh,.bat,.ini,.cfg,.yaml,.yml,text/*"; + inp.onchange = (ev) => { + const file = ev.target.files[0]; + if (!file) return; + fileHandle = null; + fileName = file.name; + const reader = new FileReader(); + reader.onload = (e) => { + editor.value = e.target.result; + isModified = false; + updateAll(); + editor.focus(); + showNotif(`Opened: ${fileName}`); + }; + reader.readAsText(file); + }; + inp.click(); + } + } + + async function doSave() { + if (hasFS && fileHandle) { + try { + const writable = await fileHandle.createWritable(); + await writable.write(editor.value); + await writable.close(); + isModified = false; + updateStatus(); + showNotif(`Saved: ${fileName}`); + } catch (err) { + if (err.name !== "AbortError") + showNotif("Save failed: " + err.message); + } + } else if (hasFS && !fileHandle) { + await doSaveAs(); + } else { + _downloadFile(fileName === "Untitled" ? "untitled.txt" : fileName); + } + } + + async function doSaveAs() { + if (hasFS) { + try { + const handle = await window.showSaveFilePicker({ + suggestedName: + fileName === "Untitled" ? "untitled.txt" : fileName, + types: [ + { + description: "Text files", + accept: { + "text/plain": [ + ".txt", + ".md", + ".js", + ".ts", + ".py", + ".html", + ".css", + ".json", + ".xml", + ".csv", + ".log", + ], + }, + }, + ], + }); + fileHandle = handle; + fileName = handle.name; + const writable = await handle.createWritable(); + await writable.write(editor.value); + await writable.close(); + isModified = false; + updateStatus(); + showNotif(`Saved: ${fileName}`); + } catch (err) { + if (err.name !== "AbortError") + showNotif("Save failed: " + err.message); + } + } else { + showDialog( + "Save As", + ` + + +
File System Access API not supported in this browser.
File will be downloaded instead.
+
+ + +
+ `, + () => { + const inp = document.getElementById("d-savename"); + inp.focus(); + inp.select(); + document + .getElementById("d-saveas-ok") + .addEventListener("click", () => { + const name = inp.value.trim(); + if (name) { + fileName = name; + closeAll(); + _downloadFile(name); + } + }); + inp.addEventListener("keydown", (e) => { + if (e.key === "Enter") + document.getElementById("d-saveas-ok").click(); + }); + document + .getElementById("d-saveas-cancel") + .addEventListener("click", closeAll); + }, + ); + } + } + + function _downloadFile(name) { + const blob = new Blob([editor.value], { + type: "text/plain;charset=utf-8", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = name; + a.click(); + URL.revokeObjectURL(url); + isModified = false; + updateStatus(); + showNotif(`Downloaded: ${name}`); + } + + function doPrint() { + window.print(); + } + + function doDelete() { + const s = editor.selectionStart, + en = editor.selectionEnd; + if (s !== en) { + editor.value = + editor.value.substring(0, s) + editor.value.substring(en); + editor.selectionStart = editor.selectionEnd = s; + isModified = true; + updateAll(); + } + } + + function insertDateTime() { + const now = new Date(); + const ts = now.toLocaleString(); + const s = editor.selectionStart, + en = editor.selectionEnd; + editor.value = + editor.value.substring(0, s) + ts + editor.value.substring(en); + editor.selectionStart = editor.selectionEnd = s + ts.length; + isModified = true; + updateAll(); + editor.focus(); + } + + // -- View + function toggleWordWrap() { + wordWrap = !wordWrap; + editor.style.whiteSpace = wordWrap ? "pre-wrap" : "pre"; + showNotif("Word Wrap: " + (wordWrap ? "ON" : "OFF")); + } + + function toggleLineNumbers() { + showLN = !showLN; + updateLineNumbers(); + showNotif("Line Numbers: " + (showLN ? "ON" : "OFF")); + } + + function zoomChange(dir) { + fontSize = Math.min(32, Math.max(9, fontSize + dir * 2)); + applyZoom(); + } + function zoomReset() { + fontSize = 15; + applyZoom(); + } + function applyZoom() { + editor.style.fontSize = fontSize + "px"; + lineNums.style.fontSize = fontSize + "px"; + editor.style.lineHeight = lineNums.style.lineHeight = "1.25"; + updateLineNumbers(); + showNotif(`Zoom: ${fontSize}px`); + } + + // -- Find / Replace Bar + function openFindBar(withReplace) { + replaceMode = withReplace; + closeAll(); + findBar.classList.add("open"); + document.getElementById("replace-label").style.display = withReplace + ? "" + : "none"; + replInput.style.display = withReplace ? "" : "none"; + document.getElementById("btn-replace-one").style.display = withReplace + ? "" + : "none"; + document.getElementById("btn-replace-all").style.display = withReplace + ? "" + : "none"; + findInput.value = lastSearch; + replInput.value = lastReplace; + findStatus.textContent = ""; + findInput.focus(); + findInput.select(); + } + + findInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + syncSearch(); + doFindNext(e.shiftKey ? -1 : 1); + } + if (e.key === "Escape") closeAll(); + }); + replInput.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeAll(); + }); + + document.getElementById("btn-find-next").onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + syncSearch(); + doFindNext(1); + }; + document.getElementById("btn-find-prev").onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + syncSearch(); + doFindNext(-1); + }; + document.getElementById("btn-replace-one").onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + syncSearch(); + doReplaceOne(); + }; + document.getElementById("btn-replace-all").onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + syncSearch(); + doReplaceAll(); + }; + document.getElementById("btn-find-close").onclick = closeAll; + + function syncSearch() { + lastSearch = findInput.value; + lastReplace = replInput.value; + } + + function buildRegex(global) { + if (!lastSearch) return null; + let flags = global ? "g" : ""; + if (!findCase.checked) flags += "i"; + let pattern = lastSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + if (findWhole.checked) pattern = "\\b" + pattern + "\\b"; + try { + return new RegExp(pattern, flags); + } catch { + return null; + } + } + + function doFindNext(dir) { + if (!lastSearch) { + openFindBar(false); + return; + } + const rx = buildRegex(true); + if (!rx) { + showNotif("Invalid search pattern."); + return; + } + const val = editor.value; + const matches = []; + let m; + while ((m = rx.exec(val)) !== null) + matches.push({ i: m.index, len: m[0].length }); + if (!matches.length) { + findStatus.textContent = "Not found"; + showNotif(`"${lastSearch}" not found.`); + return; + } + const pos = + dir === 1 ? editor.selectionEnd : editor.selectionStart - 1; + let found; + if (dir === 1) { + found = matches.find((x) => x.i >= pos) || matches[0]; + } else { + found = + [...matches].reverse().find((x) => x.i <= pos) || + matches[matches.length - 1]; + } + editor.setSelectionRange(found.i, found.i + found.len); + editor.focus(); + findStatus.textContent = `${matches.indexOf(found) + 1} / ${matches.length}`; + scrollToSelection(); + updateStatus(); + } + + function doReplaceOne() { + const s = editor.selectionStart; + const en = editor.selectionEnd; + const rx = buildRegex(false); + if (!rx) return; + const sel = editor.value.substring(s, en); + if (rx.test(sel)) { + editor.value = + editor.value.substring(0, s) + + lastReplace + + editor.value.substring(en); + editor.setSelectionRange(s, s + lastReplace.length); + isModified = true; + updateAll(); + } + doFindNext(1); + } + + function doReplaceAll() { + const rx = buildRegex(true); + if (!rx) return; + const orig = editor.value; + const count = (orig.match(rx) || []).length; + if (!count) { + findStatus.textContent = "Not found"; + showNotif(`"${lastSearch}" not found.`); + return; + } + editor.value = orig.replace(rx, lastReplace); + isModified = true; + updateAll(); + findStatus.textContent = `Replaced ${count}`; + showNotif(`Replaced ${count} occurrence(s).`); + } + + function scrollToSelection() { + const lines = editor.value + .substring(0, editor.selectionStart) + .split("\n"); + const lineH = + editor.scrollHeight / Math.max(1, editor.value.split("\n").length); + editor.scrollTop = Math.max(0, (lines.length - 5) * lineH); + } + + // -- Go to Line + function openGoto() { + closeAll(); + gotoBar.classList.add("open"); + gotoInput.value = ""; + gotoInput.focus(); + } + + document.getElementById("goto-ok").onclick = execGoto; + document.getElementById("goto-cancel").onclick = () => { + gotoBar.classList.remove("open"); + editor.focus(); + }; + gotoInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") execGoto(); + if (e.key === "Escape") { + gotoBar.classList.remove("open"); + editor.focus(); + } + }); + + function execGoto() { + const n = parseInt(gotoInput.value, 10); + if (isNaN(n) || n < 1) { + showNotif("Enter a valid line number."); + return; + } + const lines = editor.value.split("\n"); + const lineN = Math.min(n, lines.length); + let pos = 0; + for (let i = 0; i < lineN - 1; i++) pos += lines[i].length + 1; + editor.setSelectionRange(pos, pos); + editor.focus(); + gotoBar.classList.remove("open"); + updateStatus(); + const lineH = editor.scrollHeight / Math.max(1, lines.length); + editor.scrollTop = Math.max(0, (lineN - 5) * lineH); + showNotif(`Jumped to line ${lineN}`); + } + + // -- Dialog Helper + function showDialog(title, bodyHTML, onReady) { + dialogTitle.textContent = title; + dialogBody.innerHTML = bodyHTML; + dialogOv.classList.add("open"); + dialogOv.addEventListener("keydown", (e) => { + e.preventDefault(); + }); + dialogOv.addEventListener("click", (e) => { + e.preventDefault(); + }); + if (onReady) onReady(); + } + + // -- About Dialog + function showAbout(e) { + showDialog( + "About Pascar", + ` +
+
Pascar
+
Browser based offline text editor
+
Inspired by the classic MS-DOS editor
+
+ Runs 100% offline in your browser.
+ No server. No data sent anywhere.
+ Open & save files via your local filesystem. +
+
+ Source code + by Nirmal Kumar R. +
+
+
+ `, + () => { + document + .getElementById("d-about-ok") + .addEventListener("click", closeAll); + }, + ); + } + + // -- Utility + function escHtml(s) { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + window.addEventListener( + "keydown", + (e) => { + if ( + (e.ctrlKey || e.metaKey) && + (e.key === "0" || e.key === "+" || e.key === "=" || e.key === "-") + ) { + e.preventDefault(); + } + }, + { capture: true }, + ); + + window.addEventListener("beforeunload", (e) => { + if (isModified) { + e.preventDefault(); + e.returnValue = ""; + } + }); + + // -- Service Worker to make this application work offline + if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker + .register("sw.js") + .then((reg) => console.log("Service Worker Registered")) + .catch((err) => console.log("Registration Failed", err)); + }); + } + })(); + - + \ No newline at end of file