From 02e5868d66c1390a7e2db473819afbe2097b39e4 Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Sat, 9 May 2026 18:37:57 +0100 Subject: [PATCH] Fix batch confirm: modal overlay + positioned beside button Replace showCenterConfirm with showBatchConfirm(btn,message,callback) that creates a transparent modal overlay (z-index 9999) to block background clicks, positions the confirm dialog beside the trigger button. Updated all callers (batchDelete, batchPermanentDelete, permanentDelete, emptyTrash) to pass the clicked button. Added CSS for batch-confirm-overlay backdrop. --- css/style.css | 6 ++++-- js/app.js | 38 ++++++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/css/style.css b/css/style.css index cb24455..12926cc 100644 --- a/css/style.css +++ b/css/style.css @@ -215,7 +215,7 @@ input:focus, select:focus { border-color:var(--accent); } /* Trash */ .trash-badge { background:var(--danger); color:#fff; padding:0.15rem 0.5rem; border-radius:1rem; font-size:0.65rem; margin-left:0.3rem; } .trash-info { font-size:0.7rem; color:var(--text2); margin-top:0.2rem; } -.restore-btn { background:var(--success); color:#fff; border:none; padding:0.2rem 0.6rem; border-radius:1.5rem; cursor:pointer; font-size:0.7rem; } +.restore-btn { background:none; color:#fff; border:none; padding:0.2rem 0.6rem; border-radius:1.5rem; cursor:pointer; font-size:0.7rem; } .restore-btn:hover { filter:brightness(1.2); } .empty-trash-btn { background:var(--danger); color:#fff; border:none; padding:0.3rem 0.8rem; border-radius:1.5rem; cursor:pointer; font-size:0.75rem; } .empty-trash-btn:hover { filter:brightness(1.2); } @@ -331,4 +331,6 @@ input:focus, select:focus { border-color:var(--accent); } border-color:var(--danger)!important; box-shadow:0 0 15px rgba(239,68,68,0.4); background:rgba(239,68,68,0.15); -} \ No newline at end of file +} +/* Batch confirm modal backdrop */ +.batch-confirm-overlay { background:rgba(0,0,0,0.3); } \ No newline at end of file diff --git a/js/app.js b/js/app.js index 5d2b37c..e240198 100644 --- a/js/app.js +++ b/js/app.js @@ -305,10 +305,13 @@ async function permanentDelete(id, silent) { try { const r = await fetch(API + '/entries/' + id + '?permanent=1', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); if (!silent) { toast('🗑️ Permanently deleted'); await loadEntries(); playSound('error'); } } } catch (e) { if (!silent) toast('⚠️ Error', 'error'); } }; if (silent) { await doDelete(); return; } - showCenterConfirm('Permanently delete?', doDelete); + const btn = document.querySelector('.delete-btn[data-id="' + id + '"]'); + if (btn) showBatchConfirm(btn, 'Permanently delete?', doDelete); + else showBatchConfirm(document.body, 'Permanently delete?', doDelete); } async function emptyTrash() { - showCenterConfirm('Delete ALL trashed entries?', async () => { + const btn = document.querySelector('.empty-trash-btn'); + showBatchConfirm(btn || document.body, 'Delete ALL trashed entries?', async () => { try { const r = await fetch(API + '/entries/trash/empty', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { toast('🗑️ Trash emptied'); await loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); } }); } @@ -941,35 +944,40 @@ function showConfirm(btn, message, callback) { }, 10); } function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.password : ''; } -function showCenterConfirm(message, callback) { - const existing = document.querySelector('.custom-confirm'); +function showBatchConfirm(btn, message, callback) { + const existing = document.querySelector('.batch-confirm-overlay'); if (existing) existing.remove(); + const overlay = document.createElement('div'); + overlay.className = 'batch-confirm-overlay'; + overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;background:transparent;'; const confirm = document.createElement('div'); confirm.className = 'custom-confirm show'; - confirm.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:10000;'; + confirm.style.cssText = 'position:fixed;background:var(--bg2);border:1px solid var(--accent);border-radius:0.8rem;padding:0.7rem 1rem;z-index:10000;box-shadow:0 10px 30px rgba(0,0,0,0.5);font-size:0.8rem;color:var(--text);white-space:nowrap;'; confirm.innerHTML = '
' + message + '
' + '
' + '' + '' + '
'; - document.body.appendChild(confirm); + const rect = btn.getBoundingClientRect(); + confirm.style.top = (rect.top - 60) + 'px'; + let leftPos = rect.left - 20; + if (leftPos < 10) leftPos = 10; + confirm.style.left = leftPos + 'px'; + overlay.appendChild(confirm); + document.body.appendChild(overlay); const yesBtn = confirm.querySelector('.confirm-yes'); const noBtn = confirm.querySelector('.confirm-no'); - const cleanup = () => { confirm.remove(); document.removeEventListener('keydown', keyHandler); }; + const cleanup = () => { overlay.remove(); document.removeEventListener('keydown', keyHandler); }; const keyHandler = (e) => { if (e.key === 'Enter' || e.key === 'y' || e.key === 'Y') { e.preventDefault(); cleanup(); callback(); playSound('delete'); } else if (e.key === 'Escape' || e.key === 'n' || e.key === 'N') { e.preventDefault(); cleanup(); } }; yesBtn.onclick = () => { cleanup(); callback(); playSound('delete'); }; noBtn.onclick = () => cleanup(); + overlay.onclick = (e) => { if (e.target === overlay) cleanup(); }; confirm.tabIndex = 0; confirm.focus(); document.addEventListener('keydown', keyHandler); - setTimeout(() => { - document.addEventListener('click', function closeConfirm(e) { - if (!confirm.contains(e.target)) { cleanup(); document.removeEventListener('click', closeConfirm); } - }); - }, 10); } function attachEvents() { document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); if (showTrash) { permanentDelete(this.dataset.id); } else { showConfirm(this, 'Delete this entry?', id => delEntry(id)); } }); @@ -1101,7 +1109,8 @@ function hideBatchBar() { async function batchDelete() { const count = selectedIds.size; - showCenterConfirm('Move ' + count + ' entries to trash?', async () => { + const btn = document.querySelector('#batchBar .btn-danger'); + showBatchConfirm(btn || document.body, 'Move ' + count + ' entries to trash?', async () => { for (const id of selectedIds) await delEntry(id, true); toast('📦 Moved ' + count + ' entries to trash'); playSound('delete'); @@ -1112,7 +1121,8 @@ async function batchDelete() { async function batchPermanentDelete() { const count = selectedIds.size; - showCenterConfirm('Permanently delete ' + count + ' entries?', async () => { + const btn = document.querySelector('#batchBar .btn-danger'); + showBatchConfirm(btn || document.body, 'Permanently delete ' + count + ' entries?', async () => { for (const id of selectedIds) await permanentDelete(id, true); toast('🗑️ Permanently deleted ' + count + ' entries'); playSound('error');