1351 lines
46 KiB
HTML
1351 lines
46 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 {
|
|
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>
|
|
</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="line-numbers"></div>
|
|
<textarea id="editor" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
|
|
|
|
<!-- 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');
|
|
|
|
// -- 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;
|
|
|
|
// -- Initial Content
|
|
editor.value = [
|
|
'Pascar',
|
|
'======',
|
|
'',
|
|
'A faithful offline recreation of the classic MS-DOS Editor',
|
|
'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';
|
|
}
|
|
|
|
// -- 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(); });
|
|
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 (e.target.closest('#menu-bar')) closeAll();
|
|
});
|
|
|
|
// -- Global Keyboard Shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
const shift = e.shiftKey;
|
|
const alt = e.altKey;
|
|
const key = e.key.toLowerCase();
|
|
|
|
if (alt && !ctrl && ALT_MENU_MAP[key]) {
|
|
e.preventDefault();
|
|
const dd = getOpenDropdown();
|
|
if (dd && dd.id === 'menu-' + ALT_MENU_MAP[key]) {
|
|
closeAll();
|
|
} else {
|
|
openMenu(ALT_MENU_MAP[key]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const openDD = getOpenDropdown();
|
|
if (openDD) {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
highlightDDItem(openDD, getFocusedIndex(openDD) + 1);
|
|
return;
|
|
}
|
|
if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
const idx = getFocusedIndex(openDD);
|
|
highlightDDItem(openDD, idx <= 0 ? getDDItems(openDD).length - 1 : idx - 1);
|
|
return;
|
|
}
|
|
if (e.key === 'ArrowLeft') {
|
|
e.preventDefault();
|
|
const menus = ['file','edit','search','view','help'];
|
|
const cur = openDD.id.replace('menu-', '');
|
|
openMenu(menus[(menus.indexOf(cur) - 1 + menus.length) % menus.length]);
|
|
return;
|
|
}
|
|
if (e.key === 'ArrowRight') {
|
|
e.preventDefault();
|
|
const menus = ['file','edit','search','view','help'];
|
|
const cur = openDD.id.replace('menu-', '');
|
|
openMenu(menus[(menus.indexOf(cur) + 1) % menus.length]);
|
|
return;
|
|
}
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
const focused = openDD.querySelector('.dd-item.dd-focused');
|
|
if (findBar.classList.contains('open')
|
|
&& gotoBar.classList.contains('open')
|
|
&& helpOverlay.classList.contains('open')
|
|
&& dialogOv.classList.contains('open')) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
if (focused && focused.dataset.action) {
|
|
const action = focused.dataset.action;
|
|
closeAll();
|
|
handleAction(action);
|
|
}
|
|
return;
|
|
}
|
|
if (!ctrl && !alt && e.key.length === 1) {
|
|
const letter = e.key.toLowerCase();
|
|
const match = getDDItems(openDD).find(item => {
|
|
const ul = item.querySelector('.ul');
|
|
return ul && ul.textContent.toLowerCase() === letter;
|
|
});
|
|
if (match && match.dataset.action) {
|
|
e.preventDefault();
|
|
const action = match.dataset.action;
|
|
closeAll();
|
|
handleAction(action);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e.key === 'Escape') { e.preventDefault(); closeAll(); return; }
|
|
if (e.key === 'F1') { e.preventDefault(); toggleHelp(); return; }
|
|
if (e.key === 'F3') { e.preventDefault(); doFindNext(shift ? -1 : 1); return; }
|
|
if (e.key === 'F5' && !ctrl && !alt) { e.preventDefault(); insertDateTime(); return; }
|
|
|
|
if (ctrl && key === 'n') { e.preventDefault(); doNew(); }
|
|
if (ctrl && key === 'o') { e.preventDefault(); doOpen(); }
|
|
if (ctrl && key === 's' && shift) { e.preventDefault(); doSaveAs(); }
|
|
else if (ctrl && key === 's') { e.preventDefault(); doSave(); }
|
|
if (ctrl && key === 'p') { e.preventDefault(); doPrint(); }
|
|
if (ctrl && key === 'f') { e.preventDefault(); openFindBar(false); }
|
|
if (ctrl && key === 'h') { e.preventDefault(); openFindBar(true); }
|
|
if (ctrl && key === 'g') { e.preventDefault(); openGoto(); }
|
|
if (ctrl && (key === '=' || key === '+')) { e.preventDefault(); zoomChange(1); }
|
|
if (ctrl && key === '-') { e.preventDefault(); zoomChange(-1); }
|
|
if (ctrl && key === '0') { e.preventDefault(); zoomReset(); }
|
|
if (alt && key === 'w') { e.preventDefault(); toggleWordWrap(); }
|
|
if (alt && key === 'l') { e.preventDefault(); toggleLineNumbers(); }
|
|
});
|
|
|
|
// -- Close All Overlays
|
|
function closeAll() {
|
|
document.querySelectorAll('.dropdown').forEach(d => d.classList.remove('open'));
|
|
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
|
|
dialogOv.classList.remove('open');
|
|
helpOverlay.classList.remove('open');
|
|
findBar.classList.remove('open');
|
|
gotoBar.classList.remove('open');
|
|
editor.focus();
|
|
}
|
|
|
|
function toggleHelp() {
|
|
if (helpOverlay.classList.contains('open')) closeAll();
|
|
else { closeAll(); helpOverlay.classList.add('open'); }
|
|
}
|
|
|
|
// -- Action Dispatcher
|
|
function handleAction(a) {
|
|
const map = {
|
|
new: doNew,
|
|
open: doOpen,
|
|
save: doSave,
|
|
saveas: doSaveAs,
|
|
print: doPrint,
|
|
exit: () => showNotif('Close this browser tab to exit.'),
|
|
undo: () => { editor.focus(); document.execCommand('undo'); updateAll(); },
|
|
redo: () => { editor.focus(); document.execCommand('redo'); updateAll(); },
|
|
cut: () => { editor.focus(); document.execCommand('cut'); },
|
|
copy: () => { editor.focus(); document.execCommand('copy'); },
|
|
paste: () => { editor.focus(); document.execCommand('paste'); },
|
|
delete: doDelete,
|
|
selectall: () => { editor.select(); },
|
|
timestamp: insertDateTime,
|
|
find: () => openFindBar(false),
|
|
replace: () => openFindBar(true),
|
|
findnext: () => doFindNext(1),
|
|
findprev: () => doFindNext(-1),
|
|
goto: openGoto,
|
|
wordwrap: toggleWordWrap,
|
|
linenumbers: toggleLineNumbers,
|
|
zoomin: () => zoomChange(1),
|
|
zoomout: () => zoomChange(-1),
|
|
zoomreset: zoomReset,
|
|
shortcuts: toggleHelp,
|
|
about: showAbout,
|
|
};
|
|
if (map[a]) map[a]();
|
|
}
|
|
|
|
// -- File Operations
|
|
let fileHandle = null;
|
|
const hasFS = typeof window.showOpenFilePicker === 'function';
|
|
|
|
function doNew() {
|
|
if (isModified && !confirm('Discard unsaved changes?')) return;
|
|
editor.value = '';
|
|
fileName = 'Untitled';
|
|
fileHandle = null;
|
|
isModified = false;
|
|
updateAll();
|
|
editor.focus();
|
|
}
|
|
|
|
async function doOpen() {
|
|
if (isModified && !confirm('Discard unsaved changes?')) return;
|
|
if (hasFS) {
|
|
try {
|
|
const [handle] = await window.showOpenFilePicker({
|
|
types: [{ description: 'Text files', accept: { 'text/*': ['.txt','.md','.js','.ts','.py','.html','.css','.json','.xml','.csv','.log','.c','.cpp','.h','.java','.rs','.go','.rb','.sh','.bat','.ini','.cfg','.yaml','.yml'] } }],
|
|
multiple: false
|
|
});
|
|
fileHandle = handle;
|
|
const file = await handle.getFile();
|
|
fileName = file.name;
|
|
editor.value = await file.text();
|
|
isModified = false;
|
|
updateAll(); editor.focus();
|
|
showNotif(`Opened: ${fileName}`);
|
|
} catch (err) {
|
|
if (err.name !== 'AbortError') showNotif('Could not open file.');
|
|
}
|
|
} else {
|
|
const inp = document.createElement('input');
|
|
inp.type = 'file';
|
|
inp.accept = '.txt,.md,.js,.ts,.py,.html,.css,.json,.xml,.csv,.log,.c,.cpp,.h,.java,.rs,.go,.rb,.sh,.bat,.ini,.cfg,.yaml,.yml,text/*';
|
|
inp.onchange = (ev) => {
|
|
const file = ev.target.files[0];
|
|
if (!file) return;
|
|
fileHandle = null;
|
|
fileName = file.name;
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
editor.value = e.target.result;
|
|
isModified = false;
|
|
updateAll(); editor.focus();
|
|
showNotif(`Opened: ${fileName}`);
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
inp.click();
|
|
}
|
|
}
|
|
|
|
async function doSave() {
|
|
if (hasFS && fileHandle) {
|
|
try {
|
|
const writable = await fileHandle.createWritable();
|
|
await writable.write(editor.value);
|
|
await writable.close();
|
|
isModified = false;
|
|
updateStatus();
|
|
showNotif(`Saved: ${fileName}`);
|
|
} catch (err) {
|
|
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
|
|
}
|
|
} else if (hasFS && !fileHandle) {
|
|
await doSaveAs();
|
|
} else {
|
|
_downloadFile(fileName === 'Untitled' ? 'untitled.txt' : fileName);
|
|
}
|
|
}
|
|
|
|
async function doSaveAs() {
|
|
if (hasFS) {
|
|
try {
|
|
const handle = await window.showSaveFilePicker({
|
|
suggestedName: fileName === 'Untitled' ? 'untitled.txt' : fileName,
|
|
types: [{ description: 'Text files', accept: { 'text/plain': ['.txt','.md','.js','.ts','.py','.html','.css','.json','.xml','.csv','.log'] } }]
|
|
});
|
|
fileHandle = handle;
|
|
fileName = handle.name;
|
|
const writable = await handle.createWritable();
|
|
await writable.write(editor.value);
|
|
await writable.close();
|
|
isModified = false;
|
|
updateStatus();
|
|
showNotif(`Saved: ${fileName}`);
|
|
} catch (err) {
|
|
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
|
|
}
|
|
} else {
|
|
showDialog('Save As', `
|
|
<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) => { if (e.key === 'Enter') document.getElementById('d-saveas-ok').click(); });
|
|
document.getElementById('d-saveas-cancel').addEventListener('click', closeAll);
|
|
});
|
|
}
|
|
}
|
|
|
|
function _downloadFile(name) {
|
|
const blob = new Blob([editor.value], { type: 'text/plain;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = name; a.click();
|
|
URL.revokeObjectURL(url);
|
|
isModified = false;
|
|
updateStatus();
|
|
showNotif(`Downloaded: ${name}`);
|
|
}
|
|
|
|
function doPrint() { window.print(); }
|
|
|
|
function doDelete() {
|
|
const s = editor.selectionStart, en = editor.selectionEnd;
|
|
if (s !== en) {
|
|
editor.value = editor.value.substring(0, s) + editor.value.substring(en);
|
|
editor.selectionStart = editor.selectionEnd = s;
|
|
isModified = true; updateAll();
|
|
}
|
|
}
|
|
|
|
function insertDateTime() {
|
|
const now = new Date();
|
|
const ts = now.toLocaleString();
|
|
const s = editor.selectionStart, en = editor.selectionEnd;
|
|
editor.value = editor.value.substring(0, s) + ts + editor.value.substring(en);
|
|
editor.selectionStart = editor.selectionEnd = s + ts.length;
|
|
isModified = true; updateAll(); editor.focus();
|
|
}
|
|
|
|
// -- View
|
|
function toggleWordWrap() {
|
|
wordWrap = !wordWrap;
|
|
editor.style.whiteSpace = wordWrap ? 'pre-wrap' : 'pre';
|
|
showNotif('Word Wrap: ' + (wordWrap ? 'ON' : 'OFF'));
|
|
}
|
|
|
|
function toggleLineNumbers() {
|
|
showLN = !showLN;
|
|
updateLineNumbers();
|
|
showNotif('Line Numbers: ' + (showLN ? 'ON' : 'OFF'));
|
|
}
|
|
|
|
function zoomChange(dir) {
|
|
fontSize = Math.min(32, Math.max(9, fontSize + dir * 2));
|
|
applyZoom();
|
|
}
|
|
function zoomReset() { fontSize = 15; applyZoom(); }
|
|
function applyZoom() {
|
|
editor.style.fontSize = fontSize + 'px';
|
|
lineNums.style.fontSize = fontSize + 'px';
|
|
editor.style.lineHeight = lineNums.style.lineHeight = '1.25';
|
|
updateLineNumbers();
|
|
showNotif(`Zoom: ${fontSize}px`);
|
|
}
|
|
|
|
// -- Find / Replace Bar
|
|
function openFindBar(withReplace) {
|
|
replaceMode = withReplace;
|
|
closeAll();
|
|
findBar.classList.add('open');
|
|
document.getElementById('replace-label').style.display = withReplace ? '' : 'none';
|
|
replInput.style.display = withReplace ? '' : 'none';
|
|
document.getElementById('btn-replace-one').style.display = withReplace ? '' : 'none';
|
|
document.getElementById('btn-replace-all').style.display = withReplace ? '' : 'none';
|
|
findInput.value = lastSearch;
|
|
replInput.value = lastReplace;
|
|
findStatus.textContent = '';
|
|
findInput.focus(); findInput.select();
|
|
}
|
|
|
|
findInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); syncSearch(); doFindNext(e.shiftKey ? -1 : 1); }
|
|
if (e.key === 'Escape') closeAll();
|
|
});
|
|
replInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeAll();
|
|
});
|
|
|
|
document.getElementById('btn-find-next').onclick = (e) => { e.preventDefault(); e.stopPropagation(); syncSearch(); doFindNext(1); };
|
|
document.getElementById('btn-find-prev').onclick = (e) => { e.preventDefault(); e.stopPropagation(); syncSearch(); doFindNext(-1); };
|
|
document.getElementById('btn-replace-one').onclick = (e) => { e.preventDefault(); e.stopPropagation(); syncSearch(); doReplaceOne(); };
|
|
document.getElementById('btn-replace-all').onclick = (e) => { e.preventDefault(); e.stopPropagation(); syncSearch(); doReplaceAll(); };
|
|
document.getElementById('btn-find-close').onclick = closeAll;
|
|
|
|
function syncSearch() {
|
|
lastSearch = findInput.value;
|
|
lastReplace = replInput.value;
|
|
}
|
|
|
|
function buildRegex(global) {
|
|
if (!lastSearch) return null;
|
|
let flags = global ? 'g' : '';
|
|
if (!findCase.checked) flags += 'i';
|
|
let pattern = lastSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
if (findWhole.checked) pattern = '\\b' + pattern + '\\b';
|
|
try { return new RegExp(pattern, flags); } catch { return null; }
|
|
}
|
|
|
|
function doFindNext(dir) {
|
|
if (!lastSearch) { openFindBar(false); return; }
|
|
const rx = buildRegex(true);
|
|
if (!rx) { showNotif('Invalid search pattern.'); return; }
|
|
const val = editor.value;
|
|
const matches = [];
|
|
let m;
|
|
while ((m = rx.exec(val)) !== null) matches.push({ i: m.index, len: m[0].length });
|
|
if (!matches.length) {
|
|
findStatus.textContent = 'Not found';
|
|
showNotif(`"${lastSearch}" not found.`);
|
|
return;
|
|
}
|
|
const pos = dir === 1 ? editor.selectionEnd : editor.selectionStart - 1;
|
|
let found;
|
|
if (dir === 1) {
|
|
found = matches.find(x => x.i >= pos) || matches[0];
|
|
} else {
|
|
found = [...matches].reverse().find(x => x.i <= pos) || matches[matches.length - 1];
|
|
}
|
|
editor.setSelectionRange(found.i, found.i + found.len);
|
|
editor.focus();
|
|
findStatus.textContent = `${matches.indexOf(found) + 1} / ${matches.length}`;
|
|
scrollToSelection();
|
|
updateStatus();
|
|
}
|
|
|
|
function doReplaceOne() {
|
|
const s = editor.selectionStart;
|
|
const en = editor.selectionEnd;
|
|
const rx = buildRegex(false);
|
|
if (!rx) return;
|
|
const sel = editor.value.substring(s, en);
|
|
if (rx.test(sel)) {
|
|
editor.value = editor.value.substring(0, s) + lastReplace + editor.value.substring(en);
|
|
editor.setSelectionRange(s, s + lastReplace.length);
|
|
isModified = true; updateAll();
|
|
}
|
|
doFindNext(1);
|
|
}
|
|
|
|
function doReplaceAll() {
|
|
const rx = buildRegex(true);
|
|
if (!rx) return;
|
|
const orig = editor.value;
|
|
const count = (orig.match(rx) || []).length;
|
|
if (!count) { findStatus.textContent = 'Not found'; showNotif(`"${lastSearch}" not found.`); return; }
|
|
editor.value = orig.replace(rx, lastReplace);
|
|
isModified = true; updateAll();
|
|
findStatus.textContent = `Replaced ${count}`;
|
|
showNotif(`Replaced ${count} occurrence(s).`);
|
|
}
|
|
|
|
function scrollToSelection() {
|
|
const lines = editor.value.substring(0, editor.selectionStart).split('\n');
|
|
const lineH = editor.scrollHeight / Math.max(1, editor.value.split('\n').length);
|
|
editor.scrollTop = Math.max(0, (lines.length - 5) * lineH);
|
|
}
|
|
|
|
// -- Go to Line
|
|
function openGoto() {
|
|
closeAll();
|
|
gotoBar.classList.add('open');
|
|
gotoInput.value = '';
|
|
gotoInput.focus();
|
|
}
|
|
|
|
document.getElementById('goto-ok').onclick = execGoto;
|
|
document.getElementById('goto-cancel').onclick = () => { gotoBar.classList.remove('open'); editor.focus(); };
|
|
gotoInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') execGoto();
|
|
if (e.key === 'Escape') { gotoBar.classList.remove('open'); editor.focus(); }
|
|
});
|
|
|
|
function execGoto() {
|
|
const n = parseInt(gotoInput.value, 10);
|
|
if (isNaN(n) || n < 1) { showNotif('Enter a valid line number.'); return; }
|
|
const lines = editor.value.split('\n');
|
|
const lineN = Math.min(n, lines.length);
|
|
let pos = 0;
|
|
for (let i = 0; i < lineN - 1; i++) pos += lines[i].length + 1;
|
|
editor.setSelectionRange(pos, pos);
|
|
editor.focus();
|
|
gotoBar.classList.remove('open');
|
|
updateStatus();
|
|
const lineH = editor.scrollHeight / Math.max(1, lines.length);
|
|
editor.scrollTop = Math.max(0, (lineN - 5) * lineH);
|
|
showNotif(`Jumped to line ${lineN}`);
|
|
}
|
|
|
|
// -- Dialog Helper
|
|
function showDialog(title, bodyHTML, onReady) {
|
|
dialogTitle.textContent = title;
|
|
dialogBody.innerHTML = bodyHTML;
|
|
dialogOv.classList.add('open');
|
|
dialogOv.addEventListener('keydown', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
dialogOv.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
if (onReady) onReady();
|
|
}
|
|
|
|
// -- About Dialog
|
|
function showAbout(e) {
|
|
showDialog('About Pascar', `
|
|
<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>
|
|
<div class="dialog-buttons"><button class="btn primary" id="d-about-ok">OK</button></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> |