From 233ecb97c0f8d5f9f9e82995454167d43003d218 Mon Sep 17 00:00:00 2001 From: Michael Pilosov Date: Thu, 25 Jun 2026 21:13:02 +0000 Subject: [PATCH] fix: save/delete use in-app dialogs instead of native prompt/confirm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browser suppresses native confirm()/prompt() when the page isn't the active tab ('dialog was suppressed because this page is not the active tab'). A suppressed confirm() returns false, so the overwrite-on-save check silently cancelled — no dialog, no overwrite. Replaced the prompt+confirm flow with a real save modal (name input + live 'this overwrites' warning, so Save is the confirmation) and moved delete onto an in-app confirm dialog too. No native popups in these flows now. Bump cache version. --- src/chess_pressure/static/app.js | 94 ++++++++++++++++++++++------ src/chess_pressure/static/index.html | 29 ++++++++- src/chess_pressure/static/style.css | 10 +++ src/chess_pressure/static/sw.js | 2 +- 4 files changed, 114 insertions(+), 21 deletions(-) diff --git a/src/chess_pressure/static/app.js b/src/chess_pressure/static/app.js index ee95471..9318cbd 100644 --- a/src/chess_pressure/static/app.js +++ b/src/chess_pressure/static/app.js @@ -417,37 +417,95 @@ return "My game"; } - function saveCurrentGame() { + function currentSavedName() { + if (!currentSavedId) return null; + const g = getSavedGames().find((x) => x.id === currentSavedId); + return g ? g.name : null; + } + + // Promise-based in-app modal. Native prompt()/confirm() are unreliable here — + // browsers suppress them when the page isn't the active tab — so all save/delete + // prompts go through a real . Resolves to true (OK) or false (cancel/Esc). + function openModal(dlg) { + return new Promise((resolve) => { + const okBtn = dlg.querySelector("[data-ok]"); + const cancelBtn = dlg.querySelector("[data-cancel]"); + let done = false; + const finish = (val) => { + if (done) return; + done = true; + okBtn.removeEventListener("click", onOk); + cancelBtn.removeEventListener("click", onCancel); + dlg.removeEventListener("close", onClose); + dlg.removeEventListener("keydown", onKey); + if (dlg.open) dlg.close(); + resolve(val); + }; + const onOk = () => finish(true); + const onCancel = () => finish(false); + const onClose = () => finish(false); + const onKey = (e) => { + if (e.key === "Enter" && e.target !== cancelBtn) { + e.preventDefault(); + finish(true); + } + }; + okBtn.addEventListener("click", onOk); + cancelBtn.addEventListener("click", onCancel); + dlg.addEventListener("close", onClose); + dlg.addEventListener("keydown", onKey); + dlg.showModal(); + }); + } + + async function saveCurrentGame() { if (!gameData) return; - const name = (prompt("Save game as:", defaultGameName()) || "").trim(); + const dlg = document.getElementById("save-dialog"); + const input = document.getElementById("save-name"); + const note = document.getElementById("save-note"); + input.value = currentSavedName() || defaultGameName(); + + // Live "this overwrites" warning, so clicking Save IS the confirmation. + const refreshNote = () => { + const key = input.value.trim().toLowerCase(); + const exists = key && getSavedGames().some((g) => g.name.toLowerCase() === key); + note.style.display = exists ? "block" : "none"; + }; + input.addEventListener("input", refreshNote); + refreshNote(); + setTimeout(() => { input.focus(); input.select(); }, 0); + + const ok = await openModal(dlg); + input.removeEventListener("input", refreshNote); + if (!ok) return; + + const name = input.value.trim(); if (!name) return; - const pgn = ChessPressure.toPgn(gameData.headers, gameData.moves); - let saved = getSavedGames(); - // Overwrite an existing game with the same name (case-insensitive) instead of - // creating a duplicate; also collapses any pre-existing duplicates. + // Replace any same-named game (case-insensitive); also collapses old duplicates. const key = name.toLowerCase(); - const exists = saved.some((g) => g.name.toLowerCase() === key); - if (exists) { - if (!confirm(`A game named "${name}" already exists. Overwrite it?`)) return; - saved = saved.filter((g) => g.name.toLowerCase() !== key); - } - + const saved = getSavedGames().filter((g) => g.name.toLowerCase() !== key); const id = "g" + Date.now().toString(36); - saved.push({ id, name, pgn, savedAt: new Date().toISOString() }); + saved.push({ + id, + name, + pgn: ChessPressure.toPgn(gameData.headers, gameData.moves), + savedAt: new Date().toISOString(), + }); setSavedGames(saved); currentSavedId = id; populateGameSelect(); updateSavedControls(); } - function deleteSavedGame() { + async function deleteSavedGame() { if (!currentSavedId) return; - const saved = getSavedGames(); - const g = saved.find((x) => x.id === currentSavedId); + const g = getSavedGames().find((x) => x.id === currentSavedId); if (!g) return; - if (!confirm(`Delete saved game "${g.name}"?`)) return; - setSavedGames(saved.filter((x) => x.id !== currentSavedId)); + document.getElementById("confirm-msg").textContent = `Delete saved game "${g.name}"?`; + const ok = await openModal(document.getElementById("confirm-dialog")); + if (!ok) return; + setSavedGames(getSavedGames().filter((x) => x.id !== currentSavedId)); currentSavedId = null; populateGameSelect(); updateSavedControls(); diff --git a/src/chess_pressure/static/index.html b/src/chess_pressure/static/index.html index e8bbdbe..da52253 100644 --- a/src/chess_pressure/static/index.html +++ b/src/chess_pressure/static/index.html @@ -4,7 +4,7 @@ Chess Pressure - + @@ -121,6 +121,31 @@ + +
+

Save game

+ + +
+ + +
+
+
+ + +
+

Please confirm

+

+
+ + +
+
+
+ @@ -139,6 +164,6 @@ - + diff --git a/src/chess_pressure/static/style.css b/src/chess_pressure/static/style.css index 457fea3..ca8411f 100644 --- a/src/chess_pressure/static/style.css +++ b/src/chess_pressure/static/style.css @@ -325,6 +325,16 @@ dialog select { padding: 0.3rem; font-size: 0.85rem; } +dialog input[type="text"] { + width: 100%; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.4rem; + font-size: 0.9rem; + margin-top: 0.3rem; +} .export-info { font-size: 0.8rem; color: var(--fg2); diff --git a/src/chess_pressure/static/sw.js b/src/chess_pressure/static/sw.js index 8eecf68..a8f8b96 100644 --- a/src/chess_pressure/static/sw.js +++ b/src/chess_pressure/static/sw.js @@ -7,7 +7,7 @@ * * Bump CACHE on every deploy so clients pick up new assets. */ -const CACHE = "chess-pressure-v27"; +const CACHE = "chess-pressure-v29"; // Same-origin app shell. Cached by canonical path; the fetch handler matches // with ignoreSearch so cache-busting query strings (?v=NN) still hit.