From f37291196b423b542fc273d07955868484422f4d Mon Sep 17 00:00:00 2001 From: Nirmal Kumar R Date: Sun, 31 May 2026 00:43:24 +0530 Subject: [PATCH] fix: Menu dropdown should be in right x position --- index.html | 2 +- src/index.html | 218 +++++++++++++++++++++++-------------------------- 2 files changed, 102 insertions(+), 118 deletions(-) diff --git a/index.html b/index.html index 54af0d5..224ec56 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -Untitled - Pascar
Pascar
Dialog
Pascar - Keyboard Shortcuts
Menu Navigation
Alt+FOpen File menu
Alt+EOpen Edit menu
Alt+SOpen Search menu
Alt+VOpen View menu
Alt+HOpen Help menu
↑ / ↓Move through menu items
← / →Switch between menus
EnterActivate highlighted item
Letter keyActivate underlined shortcut
EscapeClose menu
File Operations
Ctrl+NNew file
Ctrl+OOpen file from disk
Ctrl+SSave / download file
Ctrl+Shift+SSave As...
Ctrl+PPrint
Editing
Ctrl+ZUndo
Ctrl+YRedo
Ctrl+XCut
Ctrl+CCopy
Ctrl+VPaste
Ctrl+ASelect All
DelDelete selection
F5Insert date/time
InsertToggle Insert/Overwrite
TabIndent (4 spaces)
Shift+TabUnindent
Search
Ctrl+FFind...
Ctrl+HFind & Replace...
F3Find Next
Shift+F3Find Previous
Ctrl+GGo to Line...
View
Alt+WToggle Word Wrap
Alt+LToggle Line Numbers
Ctrl++Zoom In
Ctrl+-Zoom Out
Ctrl+0Reset Zoom
Navigation
Ctrl+HomeBeginning of file
Ctrl+EndEnd of file
Ctrl+Left/RightMove word by word
Home / EndStart / end of line
Page Up / DownScroll one page
General
F1Show this help screen
EscapeClose menus / dialogs
Alt+F4Exit (close tab)
Press Escape or F1 to close this screen
Find:
Go to line:
Untitled UTF-8
Words: 0 Chars: 0 Ln 1, Col 1 INS
\ No newline at end of file +Untitled - Pascar
Pascar
Dialog
Pascar - Keyboard Shortcuts
Menu Navigation
Alt+FOpen File menu
Alt+EOpen Edit menu
Alt+SOpen Search menu
Alt+VOpen View menu
Alt+HOpen Help menu
↑ / ↓Move through menu items
← / →Switch between menus
EnterActivate highlighted item
Letter keyActivate underlined shortcut
EscapeClose menu
File Operations
Ctrl+NNew file
Ctrl+OOpen file from disk
Ctrl+SSave / download file
Ctrl+Shift+SSave As...
Ctrl+PPrint
Editing
Ctrl+ZUndo
Ctrl+YRedo
Ctrl+XCut
Ctrl+CCopy
Ctrl+VPaste
Ctrl+ASelect All
DelDelete selection
F5Insert date/time
InsertToggle Insert/Overwrite
TabIndent (4 spaces)
Shift+TabUnindent
Search
Ctrl+FFind...
Ctrl+HFind & Replace...
F3Find Next
Shift+F3Find Previous
Ctrl+GGo to Line...
View
Alt+WToggle Word Wrap
Alt+LToggle Line Numbers
Ctrl++Zoom In
Ctrl+-Zoom Out
Ctrl+0Reset Zoom
Navigation
Ctrl+HomeBeginning of file
Ctrl+EndEnd of file
Ctrl+Left/RightMove word by word
Home / EndStart / end of line
Page Up / DownScroll one page
General
F1Show this help screen
EscapeClose menus / dialogs
Alt+F4Exit (close tab)
Press Escape or F1 to close this screen
Find:
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.
@@ -1125,7 +1116,7 @@ isModified = true; updateAll(); editor.focus(); } - // View + // -- View function toggleWordWrap() { wordWrap = !wordWrap; editor.style.whiteSpace = wordWrap ? 'pre-wrap' : 'pre'; @@ -1144,26 +1135,22 @@ } 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'; + 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 + // -- Find / Replace Bar function openFindBar(withReplace) { replaceMode = withReplace; closeAll(); findBar.classList.add('open'); - - // Toggle replace row 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'; - document.getElementById('find-label').textContent = withReplace ? 'Find:' : 'Find:'; - findInput.value = lastSearch; replInput.value = lastReplace; findStatus.textContent = ''; @@ -1252,12 +1239,12 @@ } function scrollToSelection() { - const lines = editor.value.substring(0, editor.selectionStart).split('\n'); - const lineH = editor.scrollHeight / Math.max(1, editor.value.split('\n').length); + 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 + // -- Go to Line function openGoto() { closeAll(); gotoBar.classList.add('open'); @@ -1275,8 +1262,8 @@ 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); + 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); @@ -1288,12 +1275,11 @@ showNotif(`Jumped to line ${lineN}`); } - // Dialog Helper + // -- Dialog Helper function showDialog(title, bodyHTML, onReady) { dialogTitle.textContent = title; dialogBody.innerHTML = bodyHTML; dialogOv.classList.add('open'); - // Run callback after DOM is updated so getElementById finds the elements if (onReady) onReady(); } @@ -1301,14 +1287,14 @@ if (e.target === dialogOv) closeAll(); }); - // About Dialog + // -- About Dialog function showAbout() { showDialog('About Pascar', `
Pascar
-
MSDOS Editor Browser Edition - Offline Recreation
-
Inspired by the classic MS-DOS TUI text editor
-
+
Browser based offline text editor
+
Inspired by the classic MS-DOS editor
+
Runs 100% offline in your browser.
No server. No data sent anywhere.
Open & save files via your local filesystem. @@ -1321,19 +1307,17 @@ }); } - // Utility + // -- Utility function escHtml(s) { return s.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } - // Prevent browser zoom from interfering with Ctrl+0/+/- window.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && (e.key === '0' || e.key === '+' || e.key === '=' || e.key === '-')) { e.preventDefault(); } }, { capture: true }); - // Warn before closing with unsaved changes window.addEventListener('beforeunload', (e) => { if (isModified) { e.preventDefault();