fix: save/delete use in-app dialogs instead of native prompt/confirm

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
<dialog> 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.
This commit is contained in:
Michael Pilosov 2026-06-25 21:13:02 +00:00
parent a2e62f4e5e
commit 233ecb97c0
4 changed files with 114 additions and 21 deletions

View File

@ -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 <dialog>. 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();

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess Pressure</title>
<link rel="stylesheet" href="/style.css?v=27">
<link rel="stylesheet" href="/style.css?v=29">
<link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css">
<link rel="manifest" href="/manifest.json">
</head>
@ -121,6 +121,31 @@
</form>
</dialog>
<dialog id="save-dialog">
<form method="dialog">
<h2>Save game</h2>
<label>Name:
<input type="text" id="save-name" autocomplete="off">
</label>
<p id="save-note" class="export-info" style="display:none">A saved game with this name already exists — saving will overwrite it.</p>
<div class="dialog-buttons">
<button type="button" data-ok>Save</button>
<button type="button" data-cancel>Cancel</button>
</div>
</form>
</dialog>
<dialog id="confirm-dialog">
<form method="dialog">
<h2>Please confirm</h2>
<p id="confirm-msg" class="export-info"></p>
<div class="dialog-buttons">
<button type="button" data-ok>OK</button>
<button type="button" data-cancel>Cancel</button>
</div>
</form>
</dialog>
<footer>
<a href="https://github.com/mindthemath/chess-pressure">source code</a>
</footer>
@ -139,6 +164,6 @@
<script src="/chess.min.js"></script>
<script src="/gif.js"></script>
<script src="/engine.js"></script>
<script src="/app.js?v=27"></script>
<script src="/app.js?v=29"></script>
</body>
</html>

View File

@ -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);

View File

@ -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.