mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
fix modal backdrop-click guard, re-entrancy, and monotonic clock
- Track window focus via performance.now() instead of suppressing all backdrop clicks for 300ms after the modal appears. This fixes over-suppression (Ctrl+W close-sheet now dismisses on immediate click) while keeping protection for genuine stray refocus-clicks (native dialog closing, tray window refocus). - Serialize concurrent askModal calls via a promise chain so a second call queues instead of clobbering the first's handlers (was: hang on concurrent modal show). - Use monotonic performance.now() throughout to avoid clock-jump edge cases with Date.now().
This commit is contained in:
parent
087ee44f12
commit
68d7492dba
1 changed files with 18 additions and 6 deletions
24
src/main.ts
24
src/main.ts
|
|
@ -37,6 +37,7 @@ let lastResults: LineResult[] = [];
|
|||
let selectedRange: [number, number] | null = null;
|
||||
let ratesFetchedAt = 0;
|
||||
let liveRates: Record<string, number> = {};
|
||||
let lastWindowFocusAt = 0;
|
||||
|
||||
function uid(): string {
|
||||
return Math.random().toString(36).slice(2, 10);
|
||||
|
|
@ -663,8 +664,18 @@ function renderRatesInfo(): void {
|
|||
|
||||
// ---------- modal
|
||||
|
||||
/** two-button modal; resolves "a" | "b" | null (click outside = cancel) */
|
||||
let modalChain: Promise<unknown> = Promise.resolve();
|
||||
|
||||
/** two-button modal; resolves "a" | "b" | null (click outside = cancel).
|
||||
calls serialize on the shared #modal singleton — a second call waits for
|
||||
the first to resolve rather than clobbering its handlers */
|
||||
function askModal(msg: string, aLabel: string, bLabel: string): Promise<"a" | "b" | null> {
|
||||
const run = modalChain.then(() => showModal(msg, aLabel, bLabel));
|
||||
modalChain = run.catch(() => {});
|
||||
return run;
|
||||
}
|
||||
|
||||
function showModal(msg: string, aLabel: string, bLabel: string): Promise<"a" | "b" | null> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = $("#modal");
|
||||
const a = $("#modal-a");
|
||||
|
|
@ -674,7 +685,6 @@ function askModal(msg: string, aLabel: string, bLabel: string): Promise<"a" | "b
|
|||
b.textContent = bLabel;
|
||||
a.classList.add("primary");
|
||||
modal.classList.remove("hidden");
|
||||
const shownAt = Date.now();
|
||||
const done = (res: "a" | "b" | null) => {
|
||||
modal.classList.add("hidden");
|
||||
a.onclick = b.onclick = modal.onclick = null;
|
||||
|
|
@ -683,10 +693,10 @@ function askModal(msg: string, aLabel: string, bLabel: string): Promise<"a" | "b
|
|||
a.onclick = () => done("a");
|
||||
b.onclick = () => done("b");
|
||||
modal.onclick = (e) => {
|
||||
// a click that focuses the window (e.g. after an unattended auto-check
|
||||
// shows this modal) lands on the backdrop too — ignore it right after
|
||||
// showing so it can't dismiss a prompt the user hasn't actually seen yet
|
||||
if (e.target === modal && Date.now() - shownAt > 300) done(null);
|
||||
// a click that just refocused the window (native dialog closing, or
|
||||
// bringing a backgrounded tray window to front) lands on the backdrop
|
||||
// too — ignore a backdrop dismiss that arrives right after refocus
|
||||
if (e.target === modal && performance.now() - lastWindowFocusAt > 300) done(null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -884,6 +894,8 @@ async function initUI(): Promise<void> {
|
|||
window.addEventListener("mouseup", onUp);
|
||||
});
|
||||
|
||||
window.addEventListener("focus", () => { lastWindowFocusAt = performance.now(); });
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "n") {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue