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:
parent
a2e62f4e5e
commit
233ecb97c0
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user