Files
pascar/src/index.html
T

2114 lines
60 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Untitled - Pascar</title>
<link rel="manifest" href="/manifest.json" />
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #1c1c1c;
--fg: #ffffff;
--menu-bg: #295991;
--menu-fg: #ffffff;
--menu-sel-bg: #000080;
--menu-sel-fg: #ffffff;
--border: #aaaaaa;
--shadow: #555555;
--dialog-bg: #aaaaaa;
--dialog-title-bg: #295991;
--dialog-title-fg: #ffffff;
--input-bg: #ffffff;
--input-fg: #000000;
--scrollbar-track: #aaaaaa;
--scrollbar-thumb: #555555;
--status-bg: #aaaaaa;
--status-fg: #000000;
--notif-bg: #000080;
--notif-fg: #ffffff;
--help-bg: #000040;
--help-key: #00ffff;
--help-heading: #ffff00;
--ln-fg: #aaaaaa;
}
html,
body {
height: 100%;
overflow: hidden;
}
body {
font-family: "Courier New", Courier, monospace;
font-size: 15px;
background: var(--bg);
color: var(--fg);
display: flex;
flex-direction: column;
height: 100vh;
user-select: none;
}
/* Title Bar */
#title-bar {
background: var(--bg);
color: var(--fg);
text-align: center;
padding: 2px 0;
font-size: 15px;
flex-shrink: 0;
letter-spacing: 0.5px;
}
/* Menu Bar */
#menu-bar {
background: var(--menu-bg);
color: var(--menu-fg);
display: flex;
align-items: stretch;
flex-shrink: 0;
position: relative;
z-index: 50;
}
.menu-item {
padding: 2px 12px;
cursor: default;
position: relative;
white-space: nowrap;
display: flex;
align-items: center;
}
.menu-item:hover,
.menu-item.active {
background: var(--menu-sel-bg);
color: var(--menu-sel-fg);
}
.menu-item .ul {
text-decoration: underline;
}
/* Dropdowns */
.dropdown {
display: none;
position: absolute;
top: 100%;
background: var(--menu-bg);
color: var(--menu-fg);
border: 1px solid #000;
min-width: 220px;
z-index: 200;
box-shadow: 3px 3px 0 var(--shadow);
}
.dropdown.open {
display: block;
}
.dd-item {
padding: 3px 18px 3px 20px;
cursor: default;
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
white-space: nowrap;
font-size: 14px;
}
.dd-item:hover,
.dd-item.dd-focused {
background: var(--menu-sel-bg);
color: var(--menu-sel-fg);
}
.dd-item:hover .dd-key,
.dd-item.dd-focused .dd-key {
color: #aaaaaa;
}
.dd-item.disabled {
color: #777;
cursor: default;
}
.dd-item.disabled:hover {
background: var(--menu-bg);
color: #777;
}
.dd-sep {
border-top: 1px solid #666;
margin: 3px 0;
}
.dd-key {
color: var(--menu-fg);
font-size: 12px;
}
/* Main Editor Area */
#editor-area {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
background: var(--bg);
}
/* Line Numbers */
#line-numbers {
background: var(--bg);
color: var(--ln-fg);
padding: 2px 6px 2px 4px;
text-align: right;
min-width: 44px;
overflow: hidden;
font-family: "Courier New", Courier, monospace;
font-size: 15px;
line-height: 1.25;
border-right: 1px solid var(--border);
pointer-events: none;
white-space: pre;
flex-shrink: 0;
}
/* Textarea */
#editor-panel {
display: flex;
flex: 1;
min-width: 0;
overflow: hidden;
}
#preview-panel {
display: none;
flex: 1;
min-width: 0;
overflow: auto;
border-left: 1px solid var(--border);
background: #fff;
color: #222;
padding: 12px;
}
#preview-panel.active {
display: block;
}
#preview {
width: 100%;
min-height: 100%;
overflow-wrap: break-word;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.5;
}
#preview h1,
#preview h2,
#preview h3,
#preview h4,
#preview h5,
#preview h6 {
color: #222222;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
#preview p,
#preview li,
#preview pre,
#preview code,
#preview blockquote {
color: #222222;
}
#preview pre {
background: #111111;
padding: 10px;
border-radius: 4px;
overflow: auto;
}
#preview code {
color: #ffffff;
padding: 2px 4px;
border-radius: 3px;
}
#preview a {
color: #7db4ff;
text-decoration: none;
}
#preview blockquote {
border-left: 4px solid #555555;
padding-left: 12px;
margin: 0.75rem 0;
}
#preview blockquote p {
color: #555555;
}
#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;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<!-- Title Bar -->
<div id="title-bar">Pascar</div>
<!-- Menu Bar -->
<div id="menu-bar">
<div class="menu-item" data-menu="file"><span class="ul">F</span>ile</div>
<div class="menu-item" data-menu="edit"><span class="ul">E</span>dit</div>
<div class="menu-item" data-menu="search">
<span class="ul">S</span>earch
</div>
<div class="menu-item" data-menu="view"><span class="ul">V</span>iew</div>
<div class="menu-item" data-menu="help"><span class="ul">H</span>elp</div>
<!-- File Menu -->
<div id="menu-file" class="dropdown">
<div class="dd-item" data-action="new">
<span><span class="ul">N</span>ew</span><span class="dd-key">Ctrl+N</span>
</div>
<div class="dd-item" data-action="open">
<span><span class="ul">O</span>pen...</span><span class="dd-key">Ctrl+O</span>
</div>
<div class="dd-item" data-action="save">
<span><span class="ul">S</span>ave</span><span class="dd-key">Ctrl+S</span>
</div>
<div class="dd-item" data-action="saveas">
<span>Save <span class="ul">A</span>s...</span><span class="dd-key">Ctrl+Shift+S</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="print">
<span><span class="ul">P</span>rint...</span><span class="dd-key">Ctrl+P</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="exit">
<span>E<span class="ul">x</span>it</span><span class="dd-key">Alt+F4</span>
</div>
</div>
<!-- Edit Menu -->
<div id="menu-edit" class="dropdown">
<div class="dd-item" data-action="undo">
<span><span class="ul">U</span>ndo</span><span class="dd-key">Ctrl+Z</span>
</div>
<div class="dd-item" data-action="redo">
<span><span class="ul">R</span>edo</span><span class="dd-key">Ctrl+Y</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="cut">
<span>Cu<span class="ul">t</span></span><span class="dd-key">Ctrl+X</span>
</div>
<div class="dd-item" data-action="copy">
<span><span class="ul">C</span>opy</span><span class="dd-key">Ctrl+C</span>
</div>
<div class="dd-item" data-action="paste">
<span><span class="ul">P</span>aste</span><span class="dd-key">Ctrl+V</span>
</div>
<div class="dd-item" data-action="delete">
<span><span class="ul">D</span>elete</span><span class="dd-key">Del</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="selectall">
<span>Select <span class="ul">A</span>ll</span><span class="dd-key">Ctrl+A</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="timestamp">
<span>Time/<span class="ul">D</span>ate</span><span class="dd-key">F5</span>
</div>
</div>
<!-- Search Menu -->
<div id="menu-search" class="dropdown">
<div class="dd-item" data-action="find">
<span><span class="ul">F</span>ind...</span><span class="dd-key">Ctrl+F</span>
</div>
<div class="dd-item" data-action="replace">
<span><span class="ul">R</span>eplace...</span><span class="dd-key">Ctrl+H</span>
</div>
<div class="dd-item" data-action="findnext">
<span>Find <span class="ul">N</span>ext</span><span class="dd-key">F3</span>
</div>
<div class="dd-item" data-action="findprev">
<span>Find <span class="ul">P</span>revious</span><span class="dd-key">Shift+F3</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="goto">
<span><span class="ul">G</span>o to Line...</span><span class="dd-key">Ctrl+G</span>
</div>
</div>
<!-- View Menu -->
<div id="menu-view" class="dropdown">
<div class="dd-item" data-action="wordwrap">
<span><span class="ul">W</span>ord Wrap</span><span class="dd-key">Alt+W</span>
</div>
<div class="dd-item" data-action="linenumbers">
<span><span class="ul">L</span>ine Numbers</span><span class="dd-key">Alt+L</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="zoomin">
<span>Zoom <span class="ul">I</span>n</span><span class="dd-key">Ctrl++</span>
</div>
<div class="dd-item" data-action="zoomout">
<span>Zoom <span class="ul">O</span>ut</span><span class="dd-key">Ctrl+-</span>
</div>
<div class="dd-item" data-action="zoomreset">
<span>Reset Zoo<span class="ul">m</span></span><span class="dd-key">Ctrl+0</span>
</div>
</div>
<!-- Help Menu -->
<div id="menu-help" class="dropdown">
<div class="dd-item" data-action="shortcuts">
<span><span class="ul">K</span>eyboard Shortcuts</span><span class="dd-key">F1</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" data-action="about">
<span><span class="ul">A</span>bout Pascar</span>
</div>
</div>
</div>
<!-- Main Editor Area -->
<div id="editor-area">
<div id="editor-panel">
<div id="line-numbers"></div>
<textarea id="editor" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
</div>
<div id="preview-panel" class="preview-panel">
<div id="preview"></div>
</div>
<!-- Modal Overlay -->
<div id="dialog-overlay">
<div class="dialog" id="dialog-box">
<div class="dialog-title" id="dialog-title">Dialog</div>
<div class="dialog-body" id="dialog-body"></div>
</div>
</div>
<!-- Help Overlay -->
<div id="help-overlay">
<div class="help-title">Pascar - Keyboard Shortcuts</div>
<div class="help-cols">
<div>
<div class="help-section-title">Menu Navigation</div>
<table class="help-table">
<tr>
<td>Alt+F</td>
<td>Open File menu</td>
</tr>
<tr>
<td>Alt+E</td>
<td>Open Edit menu</td>
</tr>
<tr>
<td>Alt+S</td>
<td>Open Search menu</td>
</tr>
<tr>
<td>Alt+V</td>
<td>Open View menu</td>
</tr>
<tr>
<td>Alt+H</td>
<td>Open Help menu</td>
</tr>
<tr>
<td>↑ / ↓</td>
<td>Move through menu items</td>
</tr>
<tr>
<td>← / →</td>
<td>Switch between menus</td>
</tr>
<tr>
<td>Enter</td>
<td>Activate highlighted item</td>
</tr>
<tr>
<td>Letter key</td>
<td>Activate underlined shortcut</td>
</tr>
<tr>
<td>Escape</td>
<td>Close menu</td>
</tr>
</table>
<div class="help-section-title">File Operations</div>
<table class="help-table">
<tr>
<td>Ctrl+N</td>
<td>New file</td>
</tr>
<tr>
<td>Ctrl+O</td>
<td>Open file from disk</td>
</tr>
<tr>
<td>Ctrl+S</td>
<td>Save / download file</td>
</tr>
<tr>
<td>Ctrl+Shift+S</td>
<td>Save As...</td>
</tr>
<tr>
<td>Ctrl+P</td>
<td>Print</td>
</tr>
</table>
<div class="help-section-title">Editing</div>
<table class="help-table">
<tr>
<td>Ctrl+Z</td>
<td>Undo</td>
</tr>
<tr>
<td>Ctrl+Y</td>
<td>Redo</td>
</tr>
<tr>
<td>Ctrl+X</td>
<td>Cut</td>
</tr>
<tr>
<td>Ctrl+C</td>
<td>Copy</td>
</tr>
<tr>
<td>Ctrl+V</td>
<td>Paste</td>
</tr>
<tr>
<td>Ctrl+A</td>
<td>Select All</td>
</tr>
<tr>
<td>Del</td>
<td>Delete selection</td>
</tr>
<tr>
<td>F5</td>
<td>Insert date/time</td>
</tr>
<tr>
<td>Insert</td>
<td>Toggle Insert/Overwrite</td>
</tr>
<tr>
<td>Tab</td>
<td>Indent (4 spaces)</td>
</tr>
<tr>
<td>Shift+Tab</td>
<td>Unindent</td>
</tr>
</table>
</div>
<div>
<div class="help-section-title">Search</div>
<table class="help-table">
<tr>
<td>Ctrl+F</td>
<td>Find...</td>
</tr>
<tr>
<td>Ctrl+H</td>
<td>Find &amp; Replace...</td>
</tr>
<tr>
<td>F3</td>
<td>Find Next</td>
</tr>
<tr>
<td>Shift+F3</td>
<td>Find Previous</td>
</tr>
<tr>
<td>Ctrl+G</td>
<td>Go to Line...</td>
</tr>
</table>
<div class="help-section-title">View</div>
<table class="help-table">
<tr>
<td>Alt+W</td>
<td>Toggle Word Wrap</td>
</tr>
<tr>
<td>Alt+L</td>
<td>Toggle Line Numbers</td>
</tr>
<tr>
<td>Ctrl++</td>
<td>Zoom In</td>
</tr>
<tr>
<td>Ctrl+-</td>
<td>Zoom Out</td>
</tr>
<tr>
<td>Ctrl+0</td>
<td>Reset Zoom</td>
</tr>
</table>
<div class="help-section-title">Navigation</div>
<table class="help-table">
<tr>
<td>Ctrl+Home</td>
<td>Beginning of file</td>
</tr>
<tr>
<td>Ctrl+End</td>
<td>End of file</td>
</tr>
<tr>
<td>Ctrl+Left/Right</td>
<td>Move word by word</td>
</tr>
<tr>
<td>Home / End</td>
<td>Start / end of line</td>
</tr>
<tr>
<td>Page Up / Down</td>
<td>Scroll one page</td>
</tr>
</table>
<div class="help-section-title">General</div>
<table class="help-table">
<tr>
<td>F1</td>
<td>Show this help screen</td>
</tr>
<tr>
<td>Escape</td>
<td>Close menus / dialogs</td>
</tr>
<tr>
<td>Alt+F4</td>
<td>Exit (close tab)</td>
</tr>
</table>
</div>
</div>
<div class="help-close">
Press <strong>Escape</strong> or <strong>F1</strong> to close this
screen
</div>
</div>
<!-- Notification Toast -->
<div id="notif"></div>
</div>
<!-- Find / Replace Bar -->
<div id="find-bar">
<span id="find-label">Find:</span>
<input type="text" id="find-input" placeholder="Search text..." autocomplete="off" />
<span id="replace-label" style="display: none">Replace:</span>
<input type="text" id="replace-input" placeholder="Replace with..." autocomplete="off" style="display: none" />
<label><input type="checkbox" id="find-case" /> Case</label>
<label><input type="checkbox" id="find-whole" /> Word</label>
<button class="btn primary" id="btn-find-next">▼ Next</button>
<button class="btn" id="btn-find-prev">▲ Prev</button>
<button class="btn" id="btn-replace-one" style="display: none">
Replace
</button>
<button class="btn" id="btn-replace-all" style="display: none">
Replace All
</button>
<span id="find-status" style="color: #555; font-size: 12px; margin-left: 4px"></span>
<button class="btn" id="btn-find-close" style="margin-left: auto">
✕ Close
</button>
</div>
<!-- Go to Line Bar -->
<div id="goto-bar">
<span>Go to line:</span>
<input type="text" id="goto-input" maxlength="7" placeholder="Line #" />
<button class="btn primary" id="goto-ok">OK</button>
<button class="btn" id="goto-cancel">Cancel</button>
</div>
<!-- Status Bar -->
<div id="status-bar">
<div class="status-left">
<span id="status-file">Untitled</span>
<span id="status-modified"></span>
<span id="status-enc">UTF-8</span>
</div>
<div class="status-right">
<span id="status-words">Words: 0</span>
<span id="status-chars">Chars: 0</span>
<span id="status-pos">Ln 1, Col 1</span>
<span id="status-mode">INS</span>
</div>
</div>
<script>
(function () {
"use strict";
// -- DOM References
const editor = document.getElementById("editor");
const lineNums = document.getElementById("line-numbers");
const titleBar = document.getElementById("title-bar");
const statusFile = document.getElementById("status-file");
const statusMod = document.getElementById("status-modified");
const statusPos = document.getElementById("status-pos");
const statusMode = document.getElementById("status-mode");
const statusWords = document.getElementById("status-words");
const statusChars = document.getElementById("status-chars");
const notif = document.getElementById("notif");
const helpOverlay = document.getElementById("help-overlay");
const dialogOv = document.getElementById("dialog-overlay");
const dialogTitle = document.getElementById("dialog-title");
const dialogBody = document.getElementById("dialog-body");
const findBar = document.getElementById("find-bar");
const findInput = document.getElementById("find-input");
const replInput = document.getElementById("replace-input");
const findCase = document.getElementById("find-case");
const findWhole = document.getElementById("find-whole");
const findStatus = document.getElementById("find-status");
const gotoBar = document.getElementById("goto-bar");
const gotoInput = document.getElementById("goto-input");
const menuBar = document.getElementById("menu-bar");
const previewPanel = document.getElementById("preview-panel");
const preview = document.getElementById("preview");
// -- State
let fileName = "Untitled";
let isModified = false;
let insertMode = true;
let wordWrap = false;
let showLN = true;
let fontSize = 15;
let lastSearch = "";
let lastReplace = "";
let notifTimer = null;
let replaceMode = false;
let markdownPreview = false;
// -- Initial Content
editor.value = [
"Pascar",
"======",
"",
"A browser-based offline recreation of the classic MS-DOS Editor for the modern needs of today.",
"Runs entirely in your browser - no server, no internet.",
"",
"QUICK START",
"-----------",
" Ctrl+O Open a text file from your computer",
" Ctrl+S Save / download the current file",
" Ctrl+F Find text",
" Ctrl+H Find and Replace",
" Ctrl+G Go to a specific line number",
" F3 / Shift+F3 Find next / previous match",
" F5 Insert current date and time",
" Alt+F/E/S/V/H Open menu (then ↑↓ to navigate, Enter to select)",
" Alt+L Toggle line numbers",
" Ctrl++ / Ctrl-- Zoom in / out",
" Insert Toggle Insert / Overwrite mode",
" F1 Show full keyboard shortcuts",
"",
"Start typing to edit this file.",
].join("\n");
updateAll();
editor.focus();
// -- Core Update Functions
function updateAll() {
updateLineNumbers();
updateStatus();
}
function updateLineNumbers() {
if (!showLN) {
lineNums.style.display = "none";
return;
}
lineNums.style.display = "";
const count = editor.value.split("\n").length;
const digits = String(count).length;
lineNums.style.minWidth = digits * 9 + 16 + "px";
let html = "";
for (let i = 1; i <= count; i++) html += i + "\n";
lineNums.textContent = html;
lineNums.scrollTop = editor.scrollTop;
}
function updateStatus() {
const val = editor.value;
const pos = editor.selectionStart;
const before = val.substring(0, pos);
const lines = before.split("\n");
const ln = lines.length;
const col = lines[lines.length - 1].length + 1;
statusPos.textContent = `Ln ${ln}, Col ${col}`;
statusMode.textContent = insertMode ? "INS" : "OVR";
statusFile.textContent = fileName;
statusMod.textContent = isModified ? "●" : "";
const trimmed = val.trim();
const wordCount = trimmed ? trimmed.split(/\s+/).length : 0;
statusWords.textContent = `Words: ${wordCount}`;
statusChars.textContent = `Chars: ${val.length}`;
titleBar.textContent =
(isModified ? "* " : "") + fileName + " - Pascar";
}
function isMarkdownFile(name) {
return /\.md$/i.test(name);
}
function setMarkdownPreview(active) {
markdownPreview = active;
previewPanel.classList.toggle("active", active);
if (active) {
renderMarkdownPreview();
} else {
preview.innerHTML = "";
}
}
function renderMarkdownPreview() {
if (!markdownPreview) return;
if (typeof marked === "undefined") {
preview.innerHTML =
"<p><em>Marked.js failed to load. Markdown preview is unavailable.</em></p>";
return;
}
try {
preview.innerHTML = marked.parse(editor.value || "");
} catch (err) {
preview.innerHTML =
`<p><em>Error rendering markdown:</em></p><pre>${escHtml(err.message)}</pre>`;
}
}
// -- Notifications
function showNotif(msg, dur = 2500) {
notif.textContent = msg;
notif.classList.add("show");
clearTimeout(notifTimer);
notifTimer = setTimeout(() => notif.classList.remove("show"), dur);
}
// -- Editor Events
editor.addEventListener("input", () => {
isModified = true;
updateAll();
if (markdownPreview) renderMarkdownPreview();
});
editor.addEventListener("keyup", updateStatus);
editor.addEventListener("click", updateStatus);
editor.addEventListener("scroll", () => {
lineNums.scrollTop = editor.scrollTop;
});
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 the click is inside the menu-bar, do not close all
if (e.target.closest("#menu-bar")) return;
// If any menu-item or dropdown is open, close all
if (document.querySelectorAll('.menu-item.active, .dropdown.open').length > 0) {
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;
setMarkdownPreview(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;
setMarkdownPreview(isMarkdownFile(fileName));
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;
setMarkdownPreview(isMarkdownFile(fileName));
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;
setMarkdownPreview(isMarkdownFile(fileName));
updateStatus();
showNotif(`Saved: ${fileName}`);
} catch (err) {
if (err.name !== "AbortError")
showNotif("Save failed: " + err.message);
}
} else {
showDialog(
"Save As",
`
<label>File name:</label>
<input type="text" id="d-savename" value="${escHtml(fileName === "Untitled" ? "untitled.txt" : fileName)}">
<div style="font-size:12px;color:#aaa;margin-bottom:8px;">File System Access API not supported in this browser.<br>File will be downloaded instead.</div>
<div class="dialog-buttons">
<button class="btn primary" id="d-saveas-ok">Download</button>
<button class="btn" id="d-saveas-cancel">Cancel</button>
</div>
`,
() => {
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) => {
e.stopPropagation();
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) => {
if (e.target.id == "source-code-link") {
return;
}
e.preventDefault();
});
if (onReady) onReady();
}
// -- About Dialog
function showAbout(e) {
showDialog(
"About Pascar",
`
<div style="text-align:center;padding:8px 0 4px;">
<div style="font-size:22px;font-weight:bold;margin-bottom:6px;letter-spacing:1px;">Pascar</div>
<div style="margin-bottom:4px;font-size:14px;">Browser based offline text editor</div>
<div style="color:#7f7f7f;font-size:12px;margin-bottom:14px;">Inspired by the classic MS-DOS editor</div>
<div style="font-size:12px;color:#fff;line-height:1.6;">
Runs 100% offline in your browser.<br>
No server. No data sent anywhere.<br>
Open &amp; save files via your local filesystem.
</div>
<div style="font-size:14px;line-height:1.6;text-align:center;">
<a id="source-code-link" style="color:#295991;" href="https://git.ennt.io/mysticmode/pascar" target="_blank">Source code</a>
by Nirmal Kumar R.
</div>
<div class="dialog-buttons"><button class="btn primary" id="d-about-ok">OK</button></div>
</div>
`,
() => {
document
.getElementById("d-about-ok")
.addEventListener("click", closeAll);
},
);
}
// -- Utility
function escHtml(s) {
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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));
});
}
})();
</script>
</body>
</html>