Files
pascar/src/index.html
T

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 &amp; Replace...</td></tr>
<tr><td>F3</td><td>Find Next</td></tr>
<tr><td>Shift+F3</td><td>Find Previous</td></tr>
<tr><td>Ctrl+G</td><td>Go to Line...</td></tr>
</table>
<div class="help-section-title">View</div>
<table class="help-table">
<tr><td>Alt+W</td><td>Toggle Word Wrap</td></tr>
<tr><td>Alt+L</td><td>Toggle Line Numbers</td></tr>
<tr><td>Ctrl++</td><td>Zoom In</td></tr>
<tr><td>Ctrl+-</td><td>Zoom Out</td></tr>
<tr><td>Ctrl+0</td><td>Reset Zoom</td></tr>
</table>
<div class="help-section-title">Navigation</div>
<table class="help-table">
<tr><td>Ctrl+Home</td><td>Beginning of file</td></tr>
<tr><td>Ctrl+End</td><td>End of file</td></tr>
<tr><td>Ctrl+Left/Right</td><td>Move word by word</td></tr>
<tr><td>Home / End</td><td>Start / end of line</td></tr>
<tr><td>Page Up / Down</td><td>Scroll one page</td></tr>
</table>
<div class="help-section-title">General</div>
<table class="help-table">
<tr><td>F1</td><td>Show this help screen</td></tr>
<tr><td>Escape</td><td>Close menus / dialogs</td></tr>
<tr><td>Alt+F4</td><td>Exit (close tab)</td></tr>
</table>
</div>
</div>
<div class="help-close">Press <strong>Escape</strong> or <strong>F1</strong> to close this screen</div>
</div>
<!-- Notification Toast -->
<div id="notif"></div>
</div>
<!-- Find / Replace Bar -->
<div id="find-bar">
<span id="find-label">Find:</span>
<input type="text" id="find-input" placeholder="Search text..." autocomplete="off">
<span id="replace-label" style="display:none">Replace:</span>
<input type="text" id="replace-input" placeholder="Replace with..." autocomplete="off" style="display:none">
<label><input type="checkbox" id="find-case"> Case</label>
<label><input type="checkbox" id="find-whole"> Word</label>
<button class="btn primary" id="btn-find-next">▼ Next</button>
<button class="btn" id="btn-find-prev">▲ Prev</button>
<button class="btn" id="btn-replace-one" style="display:none">Replace</button>
<button class="btn" id="btn-replace-all" style="display:none">Replace All</button>
<span id="find-status" style="color:#555;font-size:12px;margin-left:4px;"></span>
<button class="btn" id="btn-find-close" style="margin-left:auto">✕ Close</button>
</div>
<!-- Go to Line Bar -->
<div id="goto-bar">
<span>Go to line:</span>
<input type="text" id="goto-input" maxlength="7" placeholder="Line #">
<button class="btn primary" id="goto-ok">OK</button>
<button class="btn" id="goto-cancel">Cancel</button>
</div>
<!-- Status Bar -->
<div id="status-bar">
<div class="status-left">
<span id="status-file">Untitled</span>
<span id="status-modified"></span>
<span id="status-enc">UTF-8</span>
</div>
<div class="status-right">
<span id="status-words">Words: 0</span>
<span id="status-chars">Chars: 0</span>
<span id="status-pos">Ln 1, Col 1</span>
<span id="status-mode">INS</span>
</div>
</div>
<script>
(function () {
'use strict';
// -- DOM References
const editor = document.getElementById('editor');
const lineNums = document.getElementById('line-numbers');
const titleBar = document.getElementById('title-bar');
const statusFile = document.getElementById('status-file');
const statusMod = document.getElementById('status-modified');
const statusPos = document.getElementById('status-pos');
const statusMode = document.getElementById('status-mode');
const statusWords = document.getElementById('status-words');
const statusChars = document.getElementById('status-chars');
const notif = document.getElementById('notif');
const helpOverlay = document.getElementById('help-overlay');
const dialogOv = document.getElementById('dialog-overlay');
const dialogTitle = document.getElementById('dialog-title');
const dialogBody = document.getElementById('dialog-body');
const findBar = document.getElementById('find-bar');
const findInput = document.getElementById('find-input');
const replInput = document.getElementById('replace-input');
const findCase = document.getElementById('find-case');
const findWhole = document.getElementById('find-whole');
const findStatus = document.getElementById('find-status');
const gotoBar = document.getElementById('goto-bar');
const gotoInput = document.getElementById('goto-input');
const menuBar = document.getElementById('menu-bar');
// -- 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 &amp; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
window.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && (e.key === '0' || e.key === '+' || e.key === '=' || e.key === '-')) {
e.preventDefault();
}
}, { capture: true });
window.addEventListener('beforeunload', (e) => {
if (isModified) {
e.preventDefault();
e.returnValue = '';
}
});
// -- Service Worker to make this application work offline
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then(reg => console.log('Service Worker Registered'))
.catch(err => console.log('Registration Failed', err));
});
}
})();
</script>
</body>
</html>