fix: Menu dropdown should be in right x position
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+101
-117
@@ -91,7 +91,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
|
||||||
background: var(--menu-bg);
|
background: var(--menu-bg);
|
||||||
color: var(--menu-fg);
|
color: var(--menu-fg);
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
@@ -261,7 +260,7 @@
|
|||||||
#dialog-overlay.open { display: flex; }
|
#dialog-overlay.open { display: flex; }
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
background: var(--dialog-bg);
|
background: var(--bg);
|
||||||
color: var(--menu-fg);
|
color: var(--menu-fg);
|
||||||
border: 2px solid #ffffff;
|
border: 2px solid #ffffff;
|
||||||
min-width: 340px;
|
min-width: 340px;
|
||||||
@@ -325,7 +324,7 @@
|
|||||||
.btn:hover { background: #cccccc; }
|
.btn:hover { background: #cccccc; }
|
||||||
.btn:active { box-shadow: none; transform: translate(1px, 1px); }
|
.btn:active { box-shadow: none; transform: translate(1px, 1px); }
|
||||||
.btn.primary {
|
.btn.primary {
|
||||||
background: #000080;
|
background: (--menu-bg);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #fff;
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
@@ -602,7 +601,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// DOM References
|
// -- DOM References
|
||||||
const editor = document.getElementById('editor');
|
const editor = document.getElementById('editor');
|
||||||
const lineNums = document.getElementById('line-numbers');
|
const lineNums = document.getElementById('line-numbers');
|
||||||
const titleBar = document.getElementById('title-bar');
|
const titleBar = document.getElementById('title-bar');
|
||||||
@@ -625,8 +624,9 @@
|
|||||||
const findStatus = document.getElementById('find-status');
|
const findStatus = document.getElementById('find-status');
|
||||||
const gotoBar = document.getElementById('goto-bar');
|
const gotoBar = document.getElementById('goto-bar');
|
||||||
const gotoInput = document.getElementById('goto-input');
|
const gotoInput = document.getElementById('goto-input');
|
||||||
|
const menuBar = document.getElementById('menu-bar');
|
||||||
|
|
||||||
// State
|
// -- State
|
||||||
let fileName = 'Untitled';
|
let fileName = 'Untitled';
|
||||||
let isModified = false;
|
let isModified = false;
|
||||||
let insertMode = true;
|
let insertMode = true;
|
||||||
@@ -638,28 +638,28 @@
|
|||||||
let notifTimer = null;
|
let notifTimer = null;
|
||||||
let replaceMode = false;
|
let replaceMode = false;
|
||||||
|
|
||||||
// Initial Content
|
// -- Initial Content
|
||||||
editor.value = [
|
editor.value = [
|
||||||
'Pascar',
|
'Pascar',
|
||||||
'===================================',
|
'======',
|
||||||
'',
|
'',
|
||||||
'A faithful offline recreation of the classic MS-DOS Editor',
|
'A faithful offline recreation of the classic MS-DOS Editor',
|
||||||
'Runs entirely in your browser - no server, no internet.',
|
'Runs entirely in your browser - no server, no internet.',
|
||||||
'',
|
'',
|
||||||
'QUICK START',
|
'QUICK START',
|
||||||
'-----------',
|
'-----------',
|
||||||
' Ctrl+O Open a text file from your computer',
|
' Ctrl+O Open a text file from your computer',
|
||||||
' Ctrl+S Save / download the current file',
|
' Ctrl+S Save / download the current file',
|
||||||
' Ctrl+F Find text',
|
' Ctrl+F Find text',
|
||||||
' Ctrl+H Find and Replace',
|
' Ctrl+H Find and Replace',
|
||||||
' Ctrl+G Go to a specific line number',
|
' Ctrl+G Go to a specific line number',
|
||||||
' F3 / Shift+F3 Find next / previous match',
|
' F3 / Shift+F3 Find next / previous match',
|
||||||
' F5 Insert current date and time',
|
' F5 Insert current date and time',
|
||||||
' Alt+F/E/S/V/H Open menu (then ↑↓ to navigate, Enter to select)',
|
' Alt+F/E/S/V/H Open menu (then ↑↓ to navigate, Enter to select)',
|
||||||
' Alt+L Toggle line numbers',
|
' Alt+L Toggle line numbers',
|
||||||
' Ctrl++ / Ctrl+- Zoom in / out',
|
' Ctrl++ / Ctrl-- Zoom in / out',
|
||||||
' Insert Toggle Insert / Overwrite mode',
|
' Insert Toggle Insert / Overwrite mode',
|
||||||
' F1 Show full keyboard shortcuts',
|
' F1 Show full keyboard shortcuts',
|
||||||
'',
|
'',
|
||||||
'Start typing to edit this file.',
|
'Start typing to edit this file.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -667,7 +667,7 @@
|
|||||||
updateAll();
|
updateAll();
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
|
||||||
// Core Update Functions
|
// -- Core Update Functions
|
||||||
function updateAll() {
|
function updateAll() {
|
||||||
updateLineNumbers();
|
updateLineNumbers();
|
||||||
updateStatus();
|
updateStatus();
|
||||||
@@ -706,7 +706,7 @@
|
|||||||
titleBar.textContent = (isModified ? '* ' : '') + fileName + ' - Pascar';
|
titleBar.textContent = (isModified ? '* ' : '') + fileName + ' - Pascar';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications
|
// -- Notifications
|
||||||
function showNotif(msg, dur = 2500) {
|
function showNotif(msg, dur = 2500) {
|
||||||
notif.textContent = msg;
|
notif.textContent = msg;
|
||||||
notif.classList.add('show');
|
notif.classList.add('show');
|
||||||
@@ -714,25 +714,22 @@
|
|||||||
notifTimer = setTimeout(() => notif.classList.remove('show'), dur);
|
notifTimer = setTimeout(() => notif.classList.remove('show'), dur);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Editor Events
|
// -- Editor Events
|
||||||
editor.addEventListener('input', () => { isModified = true; updateAll(); });
|
editor.addEventListener('input', () => { isModified = true; updateAll(); });
|
||||||
editor.addEventListener('keyup', updateStatus);
|
editor.addEventListener('keyup', updateStatus);
|
||||||
editor.addEventListener('click', updateStatus);
|
editor.addEventListener('click', updateStatus);
|
||||||
editor.addEventListener('scroll', () => { lineNums.scrollTop = editor.scrollTop; });
|
editor.addEventListener('scroll', () => { lineNums.scrollTop = editor.scrollTop; });
|
||||||
|
|
||||||
editor.addEventListener('keydown', (e) => {
|
editor.addEventListener('keydown', (e) => {
|
||||||
// Insert / Overwrite toggle
|
|
||||||
if (e.key === 'Insert' && !e.ctrlKey && !e.altKey) {
|
if (e.key === 'Insert' && !e.ctrlKey && !e.altKey) {
|
||||||
insertMode = !insertMode;
|
insertMode = !insertMode;
|
||||||
updateStatus();
|
updateStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Tab - 4 spaces
|
|
||||||
if (e.key === 'Tab' && !e.ctrlKey && !e.altKey) {
|
if (e.key === 'Tab' && !e.ctrlKey && !e.altKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const s = editor.selectionStart, en = editor.selectionEnd;
|
const s = editor.selectionStart, en = editor.selectionEnd;
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
// unindent
|
|
||||||
const lineStart = editor.value.lastIndexOf('\n', s - 1) + 1;
|
const lineStart = editor.value.lastIndexOf('\n', s - 1) + 1;
|
||||||
const lineText = editor.value.substring(lineStart, s);
|
const lineText = editor.value.substring(lineStart, s);
|
||||||
const spaces = lineText.match(/^ {1,4}/);
|
const spaces = lineText.match(/^ {1,4}/);
|
||||||
@@ -750,29 +747,35 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Menu keyboard navigation state
|
// -- Menu System
|
||||||
// Maps Alt+letter - menu name
|
|
||||||
const ALT_MENU_MAP = { f: 'file', e: 'edit', s: 'search', v: 'view', h: 'help' };
|
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() {
|
function getOpenDropdown() {
|
||||||
return document.querySelector('.dropdown.open') || null;
|
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) {
|
function openMenu(name) {
|
||||||
const item = document.querySelector(`.menu-item[data-menu="${name}"]`);
|
const item = document.querySelector(`.menu-item[data-menu="${name}"]`);
|
||||||
const dd = document.getElementById('menu-' + name);
|
const dd = document.getElementById('menu-' + name);
|
||||||
if (!item || !dd) return;
|
if (!item || !dd) return;
|
||||||
document.querySelectorAll('.dropdown').forEach(d => d.classList.remove('open'));
|
document.querySelectorAll('.dropdown').forEach(d => d.classList.remove('open'));
|
||||||
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
|
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
|
||||||
|
positionDropdown(name);
|
||||||
dd.classList.add('open');
|
dd.classList.add('open');
|
||||||
item.classList.add('active');
|
item.classList.add('active');
|
||||||
// Highlight first non-separator item
|
|
||||||
highlightDDItem(dd, 0);
|
highlightDDItem(dd, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight a dd-item by index among visible (non-separator, non-disabled) items
|
|
||||||
function getDDItems(dd) {
|
function getDDItems(dd) {
|
||||||
return Array.from(dd.querySelectorAll('.dd-item:not(.disabled)'));
|
return Array.from(dd.querySelectorAll('.dd-item:not(.disabled)'));
|
||||||
}
|
}
|
||||||
@@ -790,18 +793,55 @@
|
|||||||
return items.findIndex(i => i.classList.contains('dd-focused'));
|
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) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
const ctrl = e.ctrlKey || e.metaKey;
|
const ctrl = e.ctrlKey || e.metaKey;
|
||||||
const shift = e.shiftKey;
|
const shift = e.shiftKey;
|
||||||
const alt = e.altKey;
|
const alt = e.altKey;
|
||||||
const key = e.key.toLowerCase();
|
const key = e.key.toLowerCase();
|
||||||
|
|
||||||
// Alt+letter: open a menu
|
|
||||||
if (alt && !ctrl && ALT_MENU_MAP[key]) {
|
if (alt && !ctrl && ALT_MENU_MAP[key]) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const dd = getOpenDropdown();
|
const dd = getOpenDropdown();
|
||||||
// If same menu already open, close it; otherwise open the requested one
|
|
||||||
if (dd && dd.id === 'menu-' + ALT_MENU_MAP[key]) {
|
if (dd && dd.id === 'menu-' + ALT_MENU_MAP[key]) {
|
||||||
closeAll();
|
closeAll();
|
||||||
} else {
|
} else {
|
||||||
@@ -810,13 +850,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow navigation inside open dropdown
|
|
||||||
const openDD = getOpenDropdown();
|
const openDD = getOpenDropdown();
|
||||||
if (openDD) {
|
if (openDD) {
|
||||||
if (e.key === 'ArrowDown') {
|
if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const idx = getFocusedIndex(openDD);
|
highlightDDItem(openDD, getFocusedIndex(openDD) + 1);
|
||||||
highlightDDItem(openDD, idx + 1);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'ArrowUp') {
|
if (e.key === 'ArrowUp') {
|
||||||
@@ -827,19 +865,16 @@
|
|||||||
}
|
}
|
||||||
if (e.key === 'ArrowLeft') {
|
if (e.key === 'ArrowLeft') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Move to previous menu
|
|
||||||
const menus = ['file','edit','search','view','help'];
|
const menus = ['file','edit','search','view','help'];
|
||||||
const cur = openDD.id.replace('menu-', '');
|
const cur = openDD.id.replace('menu-', '');
|
||||||
const i = menus.indexOf(cur);
|
openMenu(menus[(menus.indexOf(cur) - 1 + menus.length) % menus.length]);
|
||||||
openMenu(menus[(i - 1 + menus.length) % menus.length]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'ArrowRight') {
|
if (e.key === 'ArrowRight') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const menus = ['file','edit','search','view','help'];
|
const menus = ['file','edit','search','view','help'];
|
||||||
const cur = openDD.id.replace('menu-', '');
|
const cur = openDD.id.replace('menu-', '');
|
||||||
const i = menus.indexOf(cur);
|
openMenu(menus[(menus.indexOf(cur) + 1) % menus.length]);
|
||||||
openMenu(menus[(i + 1) % menus.length]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
@@ -852,11 +887,9 @@
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Letter shortcut inside menu: match underlined letter
|
|
||||||
if (!ctrl && !alt && e.key.length === 1) {
|
if (!ctrl && !alt && e.key.length === 1) {
|
||||||
const letter = e.key.toLowerCase();
|
const letter = e.key.toLowerCase();
|
||||||
const items = getDDItems(openDD);
|
const match = getDDItems(openDD).find(item => {
|
||||||
const match = items.find(item => {
|
|
||||||
const ul = item.querySelector('.ul');
|
const ul = item.querySelector('.ul');
|
||||||
return ul && ul.textContent.toLowerCase() === letter;
|
return ul && ul.textContent.toLowerCase() === letter;
|
||||||
});
|
});
|
||||||
@@ -883,7 +916,6 @@
|
|||||||
if (ctrl && key === 'f') { e.preventDefault(); openFindBar(false); }
|
if (ctrl && key === 'f') { e.preventDefault(); openFindBar(false); }
|
||||||
if (ctrl && key === 'h') { e.preventDefault(); openFindBar(true); }
|
if (ctrl && key === 'h') { e.preventDefault(); openFindBar(true); }
|
||||||
if (ctrl && key === 'g') { e.preventDefault(); openGoto(); }
|
if (ctrl && key === 'g') { e.preventDefault(); openGoto(); }
|
||||||
if (ctrl && key === 'a') { /* browser default */ }
|
|
||||||
if (ctrl && (key === '=' || key === '+')) { e.preventDefault(); zoomChange(1); }
|
if (ctrl && (key === '=' || key === '+')) { e.preventDefault(); zoomChange(1); }
|
||||||
if (ctrl && key === '-') { e.preventDefault(); zoomChange(-1); }
|
if (ctrl && key === '-') { e.preventDefault(); zoomChange(-1); }
|
||||||
if (ctrl && key === '0') { e.preventDefault(); zoomReset(); }
|
if (ctrl && key === '0') { e.preventDefault(); zoomReset(); }
|
||||||
@@ -891,7 +923,7 @@
|
|||||||
if (alt && key === 'l') { e.preventDefault(); toggleLineNumbers(); }
|
if (alt && key === 'l') { e.preventDefault(); toggleLineNumbers(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close All Overlays
|
// -- Close All Overlays
|
||||||
function closeAll() {
|
function closeAll() {
|
||||||
document.querySelectorAll('.dropdown').forEach(d => d.classList.remove('open'));
|
document.querySelectorAll('.dropdown').forEach(d => d.classList.remove('open'));
|
||||||
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
|
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
|
||||||
@@ -907,42 +939,7 @@
|
|||||||
else { closeAll(); helpOverlay.classList.add('open'); }
|
else { closeAll(); helpOverlay.classList.add('open'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu System
|
// -- Action Dispatcher
|
||||||
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
|
|
||||||
function handleAction(a) {
|
function handleAction(a) {
|
||||||
const map = {
|
const map = {
|
||||||
new: doNew,
|
new: doNew,
|
||||||
@@ -975,7 +972,10 @@
|
|||||||
if (map[a]) map[a]();
|
if (map[a]) map[a]();
|
||||||
}
|
}
|
||||||
|
|
||||||
// File Operations
|
// -- File Operations
|
||||||
|
let fileHandle = null;
|
||||||
|
const hasFS = typeof window.showOpenFilePicker === 'function';
|
||||||
|
|
||||||
function doNew() {
|
function doNew() {
|
||||||
if (isModified && !confirm('Discard unsaved changes?')) return;
|
if (isModified && !confirm('Discard unsaved changes?')) return;
|
||||||
editor.value = '';
|
editor.value = '';
|
||||||
@@ -986,10 +986,6 @@
|
|||||||
editor.focus();
|
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() {
|
async function doOpen() {
|
||||||
if (isModified && !confirm('Discard unsaved changes?')) return;
|
if (isModified && !confirm('Discard unsaved changes?')) return;
|
||||||
if (hasFS) {
|
if (hasFS) {
|
||||||
@@ -1009,7 +1005,6 @@
|
|||||||
if (err.name !== 'AbortError') showNotif('Could not open file.');
|
if (err.name !== 'AbortError') showNotif('Could not open file.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: classic <input type=file>
|
|
||||||
const inp = document.createElement('input');
|
const inp = document.createElement('input');
|
||||||
inp.type = 'file';
|
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.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() {
|
async function doSave() {
|
||||||
if (hasFS && fileHandle) {
|
if (hasFS && fileHandle) {
|
||||||
// Write directly back to the original file - no download dialog
|
|
||||||
try {
|
try {
|
||||||
const writable = await fileHandle.createWritable();
|
const writable = await fileHandle.createWritable();
|
||||||
await writable.write(editor.value);
|
await writable.write(editor.value);
|
||||||
@@ -1045,10 +1039,8 @@
|
|||||||
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
|
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
|
||||||
}
|
}
|
||||||
} else if (hasFS && !fileHandle) {
|
} else if (hasFS && !fileHandle) {
|
||||||
// No handle yet (new file) - show Save As picker
|
|
||||||
await doSaveAs();
|
await doSaveAs();
|
||||||
} else {
|
} else {
|
||||||
// Fallback: trigger download
|
|
||||||
_downloadFile(fileName === 'Untitled' ? 'untitled.txt' : fileName);
|
_downloadFile(fileName === 'Untitled' ? 'untitled.txt' : fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1072,11 +1064,10 @@
|
|||||||
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
|
if (err.name !== 'AbortError') showNotif('Save failed: ' + err.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: ask name then download
|
|
||||||
showDialog('Save As', `
|
showDialog('Save As', `
|
||||||
<label>File name:</label>
|
<label>File name:</label>
|
||||||
<input type="text" id="d-savename" value="${escHtml(fileName === 'Untitled' ? 'untitled.txt' : fileName)}">
|
<input type="text" id="d-savename" value="${escHtml(fileName === 'Untitled' ? 'untitled.txt' : fileName)}">
|
||||||
<div style="font-size:12px;color:#555;margin-bottom:8px;">File System Access API not supported in this browser.<br>File will be downloaded instead.</div>
|
<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">
|
<div class="dialog-buttons">
|
||||||
<button class="btn primary" id="d-saveas-ok">Download</button>
|
<button class="btn primary" id="d-saveas-ok">Download</button>
|
||||||
<button class="btn" id="d-saveas-cancel">Cancel</button>
|
<button class="btn" id="d-saveas-cancel">Cancel</button>
|
||||||
@@ -1125,7 +1116,7 @@
|
|||||||
isModified = true; updateAll(); editor.focus();
|
isModified = true; updateAll(); editor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// View
|
// -- View
|
||||||
function toggleWordWrap() {
|
function toggleWordWrap() {
|
||||||
wordWrap = !wordWrap;
|
wordWrap = !wordWrap;
|
||||||
editor.style.whiteSpace = wordWrap ? 'pre-wrap' : 'pre';
|
editor.style.whiteSpace = wordWrap ? 'pre-wrap' : 'pre';
|
||||||
@@ -1144,26 +1135,22 @@
|
|||||||
}
|
}
|
||||||
function zoomReset() { fontSize = 15; applyZoom(); }
|
function zoomReset() { fontSize = 15; applyZoom(); }
|
||||||
function applyZoom() {
|
function applyZoom() {
|
||||||
editor.style.fontSize = fontSize + 'px';
|
editor.style.fontSize = fontSize + 'px';
|
||||||
lineNums.style.fontSize = fontSize + 'px';
|
lineNums.style.fontSize = fontSize + 'px';
|
||||||
editor.style.lineHeight = lineNums.style.lineHeight = '1.25';
|
editor.style.lineHeight = lineNums.style.lineHeight = '1.25';
|
||||||
updateLineNumbers();
|
updateLineNumbers();
|
||||||
showNotif(`Zoom: ${fontSize}px`);
|
showNotif(`Zoom: ${fontSize}px`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find / Replace Bar
|
// -- Find / Replace Bar
|
||||||
function openFindBar(withReplace) {
|
function openFindBar(withReplace) {
|
||||||
replaceMode = withReplace;
|
replaceMode = withReplace;
|
||||||
closeAll();
|
closeAll();
|
||||||
findBar.classList.add('open');
|
findBar.classList.add('open');
|
||||||
|
|
||||||
// Toggle replace row
|
|
||||||
document.getElementById('replace-label').style.display = withReplace ? '' : 'none';
|
document.getElementById('replace-label').style.display = withReplace ? '' : 'none';
|
||||||
replInput.style.display = withReplace ? '' : 'none';
|
replInput.style.display = withReplace ? '' : 'none';
|
||||||
document.getElementById('btn-replace-one').style.display = withReplace ? '' : 'none';
|
document.getElementById('btn-replace-one').style.display = withReplace ? '' : 'none';
|
||||||
document.getElementById('btn-replace-all').style.display = withReplace ? '' : 'none';
|
document.getElementById('btn-replace-all').style.display = withReplace ? '' : 'none';
|
||||||
document.getElementById('find-label').textContent = withReplace ? 'Find:' : 'Find:';
|
|
||||||
|
|
||||||
findInput.value = lastSearch;
|
findInput.value = lastSearch;
|
||||||
replInput.value = lastReplace;
|
replInput.value = lastReplace;
|
||||||
findStatus.textContent = '';
|
findStatus.textContent = '';
|
||||||
@@ -1252,12 +1239,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scrollToSelection() {
|
function scrollToSelection() {
|
||||||
const lines = editor.value.substring(0, editor.selectionStart).split('\n');
|
const lines = editor.value.substring(0, editor.selectionStart).split('\n');
|
||||||
const lineH = editor.scrollHeight / Math.max(1, editor.value.split('\n').length);
|
const lineH = editor.scrollHeight / Math.max(1, editor.value.split('\n').length);
|
||||||
editor.scrollTop = Math.max(0, (lines.length - 5) * lineH);
|
editor.scrollTop = Math.max(0, (lines.length - 5) * lineH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to Line
|
// -- Go to Line
|
||||||
function openGoto() {
|
function openGoto() {
|
||||||
closeAll();
|
closeAll();
|
||||||
gotoBar.classList.add('open');
|
gotoBar.classList.add('open');
|
||||||
@@ -1275,8 +1262,8 @@
|
|||||||
function execGoto() {
|
function execGoto() {
|
||||||
const n = parseInt(gotoInput.value, 10);
|
const n = parseInt(gotoInput.value, 10);
|
||||||
if (isNaN(n) || n < 1) { showNotif('Enter a valid line number.'); return; }
|
if (isNaN(n) || n < 1) { showNotif('Enter a valid line number.'); return; }
|
||||||
const lines = editor.value.split('\n');
|
const lines = editor.value.split('\n');
|
||||||
const lineN = Math.min(n, lines.length);
|
const lineN = Math.min(n, lines.length);
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
for (let i = 0; i < lineN - 1; i++) pos += lines[i].length + 1;
|
for (let i = 0; i < lineN - 1; i++) pos += lines[i].length + 1;
|
||||||
editor.setSelectionRange(pos, pos);
|
editor.setSelectionRange(pos, pos);
|
||||||
@@ -1288,12 +1275,11 @@
|
|||||||
showNotif(`Jumped to line ${lineN}`);
|
showNotif(`Jumped to line ${lineN}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog Helper
|
// -- Dialog Helper
|
||||||
function showDialog(title, bodyHTML, onReady) {
|
function showDialog(title, bodyHTML, onReady) {
|
||||||
dialogTitle.textContent = title;
|
dialogTitle.textContent = title;
|
||||||
dialogBody.innerHTML = bodyHTML;
|
dialogBody.innerHTML = bodyHTML;
|
||||||
dialogOv.classList.add('open');
|
dialogOv.classList.add('open');
|
||||||
// Run callback after DOM is updated so getElementById finds the elements
|
|
||||||
if (onReady) onReady();
|
if (onReady) onReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1301,14 +1287,14 @@
|
|||||||
if (e.target === dialogOv) closeAll();
|
if (e.target === dialogOv) closeAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
// About Dialog
|
// -- About Dialog
|
||||||
function showAbout() {
|
function showAbout() {
|
||||||
showDialog('About Pascar', `
|
showDialog('About Pascar', `
|
||||||
<div style="text-align:center;padding:8px 0 4px;">
|
<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="font-size:22px;font-weight:bold;margin-bottom:6px;letter-spacing:1px;">Pascar</div>
|
||||||
<div style="margin-bottom:4px;font-size:14px;">MSDOS Editor Browser Edition - Offline Recreation</div>
|
<div style="margin-bottom:4px;font-size:14px;">Browser based offline text editor</div>
|
||||||
<div style="color:#555;font-size:12px;margin-bottom:14px;">Inspired by the classic MS-DOS TUI 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:#333;line-height:1.6;">
|
<div style="font-size:12px;color:#fff;line-height:1.6;">
|
||||||
Runs 100% offline in your browser.<br>
|
Runs 100% offline in your browser.<br>
|
||||||
No server. No data sent anywhere.<br>
|
No server. No data sent anywhere.<br>
|
||||||
Open & save files via your local filesystem.
|
Open & save files via your local filesystem.
|
||||||
@@ -1321,19 +1307,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility
|
// -- Utility
|
||||||
function escHtml(s) {
|
function escHtml(s) {
|
||||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent browser zoom from interfering with Ctrl+0/+/-
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && (e.key === '0' || e.key === '+' || e.key === '=' || e.key === '-')) {
|
if ((e.ctrlKey || e.metaKey) && (e.key === '0' || e.key === '+' || e.key === '=' || e.key === '-')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}, { capture: true });
|
}, { capture: true });
|
||||||
|
|
||||||
// Warn before closing with unsaved changes
|
|
||||||
window.addEventListener('beforeunload', (e) => {
|
window.addEventListener('beforeunload', (e) => {
|
||||||
if (isModified) {
|
if (isModified) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
Reference in New Issue
Block a user