2114 lines
60 KiB
HTML
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 & 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 & 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, "&")
|
|
.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));
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|