diff --git a/index.html b/index.html
index 54af0d5..224ec56 100644
--- a/index.html
+++ b/index.html
@@ -1 +1 @@
-
Untitled - PascarPascar
Pascar - Keyboard Shortcuts
Menu Navigation
| Alt+F | Open File menu |
| Alt+E | Open Edit menu |
| Alt+S | Open Search menu |
| Alt+V | Open View menu |
| Alt+H | Open Help menu |
| ↑ / ↓ | Move through menu items |
| ← / → | Switch between menus |
| Enter | Activate highlighted item |
| Letter key | Activate underlined shortcut |
| Escape | Close menu |
File Operations
| Ctrl+N | New file |
| Ctrl+O | Open file from disk |
| Ctrl+S | Save / download file |
| Ctrl+Shift+S | Save As... |
| Ctrl+P | Print |
Editing
| Ctrl+Z | Undo |
| Ctrl+Y | Redo |
| Ctrl+X | Cut |
| Ctrl+C | Copy |
| Ctrl+V | Paste |
| Ctrl+A | Select All |
| Del | Delete selection |
| F5 | Insert date/time |
| Insert | Toggle Insert/Overwrite |
| Tab | Indent (4 spaces) |
| Shift+Tab | Unindent |
Search
| Ctrl+F | Find... |
| Ctrl+H | Find & Replace... |
| F3 | Find Next |
| Shift+F3 | Find Previous |
| Ctrl+G | Go to Line... |
View
| Alt+W | Toggle Word Wrap |
| Alt+L | Toggle Line Numbers |
| Ctrl++ | Zoom In |
| Ctrl+- | Zoom Out |
| Ctrl+0 | Reset Zoom |
Navigation
| Ctrl+Home | Beginning of file |
| Ctrl+End | End of file |
| Ctrl+Left/Right | Move word by word |
| Home / End | Start / end of line |
| Page Up / Down | Scroll one page |
General
| F1 | Show this help screen |
| Escape | Close menus / dialogs |
| Alt+F4 | Exit (close tab) |
Press Escape or F1 to close this screen
Find: Replace:
Go to line:
Untitled UTF-8
Words: 0 Chars: 0 Ln 1, Col 1 INS
\ No newline at end of file
+Untitled - PascarPascar
Pascar - Keyboard Shortcuts
Menu Navigation
| Alt+F | Open File menu |
| Alt+E | Open Edit menu |
| Alt+S | Open Search menu |
| Alt+V | Open View menu |
| Alt+H | Open Help menu |
| ↑ / ↓ | Move through menu items |
| ← / → | Switch between menus |
| Enter | Activate highlighted item |
| Letter key | Activate underlined shortcut |
| Escape | Close menu |
File Operations
| Ctrl+N | New file |
| Ctrl+O | Open file from disk |
| Ctrl+S | Save / download file |
| Ctrl+Shift+S | Save As... |
| Ctrl+P | Print |
Editing
| Ctrl+Z | Undo |
| Ctrl+Y | Redo |
| Ctrl+X | Cut |
| Ctrl+C | Copy |
| Ctrl+V | Paste |
| Ctrl+A | Select All |
| Del | Delete selection |
| F5 | Insert date/time |
| Insert | Toggle Insert/Overwrite |
| Tab | Indent (4 spaces) |
| Shift+Tab | Unindent |
Search
| Ctrl+F | Find... |
| Ctrl+H | Find & Replace... |
| F3 | Find Next |
| Shift+F3 | Find Previous |
| Ctrl+G | Go to Line... |
View
| Alt+W | Toggle Word Wrap |
| Alt+L | Toggle Line Numbers |
| Ctrl++ | Zoom In |
| Ctrl+- | Zoom Out |
| Ctrl+0 | Reset Zoom |
Navigation
| Ctrl+Home | Beginning of file |
| Ctrl+End | End of file |
| Ctrl+Left/Right | Move word by word |
| Home / End | Start / end of line |
| Page Up / Down | Scroll one page |
General
| F1 | Show this help screen |
| Escape | Close menus / dialogs |
| Alt+F4 | Exit (close tab) |
Press Escape or F1 to close this screen
Find: Replace:
Go to line:
Untitled UTF-8
Words: 0 Chars: 0 Ln 1, Col 1 INS
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
index 881aee2..2d46b62 100644
--- a/src/index.html
+++ b/src/index.html
@@ -91,7 +91,6 @@
display: none;
position: absolute;
top: 100%;
- left: 0;
background: var(--menu-bg);
color: var(--menu-fg);
border: 1px solid #000;
@@ -261,7 +260,7 @@
#dialog-overlay.open { display: flex; }
.dialog {
- background: var(--dialog-bg);
+ background: var(--bg);
color: var(--menu-fg);
border: 2px solid #ffffff;
min-width: 340px;
@@ -325,7 +324,7 @@
.btn:hover { background: #cccccc; }
.btn:active { box-shadow: none; transform: translate(1px, 1px); }
.btn.primary {
- background: #000080;
+ background: (--menu-bg);
color: #fff;
border-color: #fff;
}
@@ -602,7 +601,7 @@
(function () {
'use strict';
- // DOM References
+ // -- DOM References
const editor = document.getElementById('editor');
const lineNums = document.getElementById('line-numbers');
const titleBar = document.getElementById('title-bar');
@@ -625,8 +624,9 @@
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
+ // -- State
let fileName = 'Untitled';
let isModified = false;
let insertMode = true;
@@ -638,28 +638,28 @@
let notifTimer = null;
let replaceMode = false;
- // Initial Content
+ // -- 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',
+ ' 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');
@@ -667,7 +667,7 @@
updateAll();
editor.focus();
- // Core Update Functions
+ // -- Core Update Functions
function updateAll() {
updateLineNumbers();
updateStatus();
@@ -706,7 +706,7 @@
titleBar.textContent = (isModified ? '* ' : '') + fileName + ' - Pascar';
}
- // Notifications
+ // -- Notifications
function showNotif(msg, dur = 2500) {
notif.textContent = msg;
notif.classList.add('show');
@@ -714,25 +714,22 @@
notifTimer = setTimeout(() => notif.classList.remove('show'), dur);
}
- // Editor Events
+ // -- 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) => {
- // Insert / Overwrite toggle
if (e.key === 'Insert' && !e.ctrlKey && !e.altKey) {
insertMode = !insertMode;
updateStatus();
return;
}
- // Tab - 4 spaces
if (e.key === 'Tab' && !e.ctrlKey && !e.altKey) {
e.preventDefault();
const s = editor.selectionStart, en = editor.selectionEnd;
if (e.shiftKey) {
- // unindent
const lineStart = editor.value.lastIndexOf('\n', s - 1) + 1;
const lineText = editor.value.substring(lineStart, s);
const spaces = lineText.match(/^ {1,4}/);
@@ -750,29 +747,35 @@
}
});
- // Menu keyboard navigation state
- // Maps Alt+letter - menu name
+ // -- Menu System
const ALT_MENU_MAP = { f: 'file', e: 'edit', s: 'search', v: 'view', h: 'help' };
- // Maps letter - action for items within an open dropdown
- // Built dynamically from .ul spans inside each dropdown
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');
- // Highlight first non-separator item
highlightDDItem(dd, 0);
}
- // Highlight a dd-item by index among visible (non-separator, non-disabled) items
function getDDItems(dd) {
return Array.from(dd.querySelectorAll('.dd-item:not(.disabled)'));
}
@@ -790,18 +793,55 @@
return items.findIndex(i => i.classList.contains('dd-focused'));
}
- // Global Keyboard Shortcuts
+ 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.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();
- // Alt+letter: open a menu
if (alt && !ctrl && ALT_MENU_MAP[key]) {
e.preventDefault();
const dd = getOpenDropdown();
- // If same menu already open, close it; otherwise open the requested one
if (dd && dd.id === 'menu-' + ALT_MENU_MAP[key]) {
closeAll();
} else {
@@ -810,13 +850,11 @@
return;
}
- // Arrow navigation inside open dropdown
const openDD = getOpenDropdown();
if (openDD) {
if (e.key === 'ArrowDown') {
e.preventDefault();
- const idx = getFocusedIndex(openDD);
- highlightDDItem(openDD, idx + 1);
+ highlightDDItem(openDD, getFocusedIndex(openDD) + 1);
return;
}
if (e.key === 'ArrowUp') {
@@ -827,19 +865,16 @@
}
if (e.key === 'ArrowLeft') {
e.preventDefault();
- // Move to previous menu
const menus = ['file','edit','search','view','help'];
const cur = openDD.id.replace('menu-', '');
- const i = menus.indexOf(cur);
- openMenu(menus[(i - 1 + menus.length) % menus.length]);
+ 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-', '');
- const i = menus.indexOf(cur);
- openMenu(menus[(i + 1) % menus.length]);
+ openMenu(menus[(menus.indexOf(cur) + 1) % menus.length]);
return;
}
if (e.key === 'Enter') {
@@ -852,11 +887,9 @@
}
return;
}
- // Letter shortcut inside menu: match underlined letter
if (!ctrl && !alt && e.key.length === 1) {
const letter = e.key.toLowerCase();
- const items = getDDItems(openDD);
- const match = items.find(item => {
+ const match = getDDItems(openDD).find(item => {
const ul = item.querySelector('.ul');
return ul && ul.textContent.toLowerCase() === letter;
});
@@ -883,7 +916,6 @@
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 === 'a') { /* browser default */ }
if (ctrl && (key === '=' || key === '+')) { e.preventDefault(); zoomChange(1); }
if (ctrl && key === '-') { e.preventDefault(); zoomChange(-1); }
if (ctrl && key === '0') { e.preventDefault(); zoomReset(); }
@@ -891,7 +923,7 @@
if (alt && key === 'l') { e.preventDefault(); toggleLineNumbers(); }
});
- // Close All Overlays
+ // -- Close All Overlays
function closeAll() {
document.querySelectorAll('.dropdown').forEach(d => d.classList.remove('open'));
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
@@ -907,42 +939,7 @@
else { closeAll(); helpOverlay.classList.add('open'); }
}
- // Menu System
- document.querySelectorAll('.menu-item').forEach(item => {
- item.addEventListener('click', (e) => {
- e.stopPropagation();
- const dd = document.getElementById('menu-' + item.dataset.menu);
- 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) { dd.classList.add('open'); item.classList.add('active'); }
- });
- // Keep menu open when hovering to other menus while one is open
- item.addEventListener('mouseenter', () => {
- const anyOpen = document.querySelector('.dropdown.open');
- if (anyOpen) {
- document.querySelectorAll('.dropdown').forEach(d => d.classList.remove('open'));
- document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
- const dd = document.getElementById('menu-' + item.dataset.menu);
- dd.classList.add('open');
- item.classList.add('active');
- }
- });
- });
-
- document.querySelectorAll('.dd-item:not(.disabled)').forEach(item => {
- item.addEventListener('click', (e) => {
- e.stopPropagation();
- closeAll();
- handleAction(item.dataset.action);
- });
- });
-
- document.addEventListener('click', (e) => {
- if (!e.target.closest('#menu-bar')) closeAll();
- });
-
- // Action Dispatcher
+ // -- Action Dispatcher
function handleAction(a) {
const map = {
new: doNew,
@@ -975,7 +972,10 @@
if (map[a]) map[a]();
}
- // File Operations
+ // -- File Operations
+ let fileHandle = null;
+ const hasFS = typeof window.showOpenFilePicker === 'function';
+
function doNew() {
if (isModified && !confirm('Discard unsaved changes?')) return;
editor.value = '';
@@ -986,10 +986,6 @@
editor.focus();
}
- // File System Access API handle (Chrome/Edge only) - lets us save in-place
- let fileHandle = null;
- const hasFS = typeof window.showOpenFilePicker === 'function';
-
async function doOpen() {
if (isModified && !confirm('Discard unsaved changes?')) return;
if (hasFS) {
@@ -1009,7 +1005,6 @@
if (err.name !== 'AbortError') showNotif('Could not open file.');
}
} else {
- // Fallback: classic
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/*';
@@ -1033,7 +1028,6 @@
async function doSave() {
if (hasFS && fileHandle) {
- // Write directly back to the original file - no download dialog
try {
const writable = await fileHandle.createWritable();
await writable.write(editor.value);
@@ -1045,10 +1039,8 @@
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
}
} else if (hasFS && !fileHandle) {
- // No handle yet (new file) - show Save As picker
await doSaveAs();
} else {
- // Fallback: trigger download
_downloadFile(fileName === 'Untitled' ? 'untitled.txt' : fileName);
}
}
@@ -1072,11 +1064,10 @@
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
}
} else {
- // Fallback: ask name then download
showDialog('Save As', `
- File System Access API not supported in this browser.
File will be downloaded instead.
+ File System Access API not supported in this browser.
File will be downloaded instead.