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.
This commit is contained in:
2026-05-09 18:37:57 +01:00
parent 131bb91cd2
commit 02e5868d66
2 changed files with 28 additions and 16 deletions
+3 -1
View File
@@ -215,7 +215,7 @@ input:focus, select:focus { border-color:var(--accent); }
/* Trash */ /* 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-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; } .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); } .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 { 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); } .empty-trash-btn:hover { filter:brightness(1.2); }
@@ -332,3 +332,5 @@ input:focus, select:focus { border-color:var(--accent); }
box-shadow:0 0 15px rgba(239,68,68,0.4); box-shadow:0 0 15px rgba(239,68,68,0.4);
background:rgba(239,68,68,0.15); background:rgba(239,68,68,0.15);
} }
/* Batch confirm modal backdrop */
.batch-confirm-overlay { background:rgba(0,0,0,0.3); }
+24 -14
View File
@@ -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'); } 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; } 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() { 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'); } 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); }, 10);
} }
function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.password : ''; } function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.password : ''; }
function showCenterConfirm(message, callback) { function showBatchConfirm(btn, message, callback) {
const existing = document.querySelector('.custom-confirm'); const existing = document.querySelector('.batch-confirm-overlay');
if (existing) existing.remove(); 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'); const confirm = document.createElement('div');
confirm.className = 'custom-confirm show'; 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 = confirm.innerHTML =
'<div class="confirm-text">' + message + '</div>' + '<div class="confirm-text">' + message + '</div>' +
'<div class="confirm-btns">' + '<div class="confirm-btns">' +
'<button class="confirm-yes">Yes</button>' + '<button class="confirm-yes">Yes</button>' +
'<button class="confirm-no">No</button>' + '<button class="confirm-no">No</button>' +
'</div>'; '</div>';
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 yesBtn = confirm.querySelector('.confirm-yes');
const noBtn = confirm.querySelector('.confirm-no'); 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) => { const keyHandler = (e) => {
if (e.key === 'Enter' || e.key === 'y' || e.key === 'Y') { e.preventDefault(); cleanup(); callback(); playSound('delete'); } 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(); } else if (e.key === 'Escape' || e.key === 'n' || e.key === 'N') { e.preventDefault(); cleanup(); }
}; };
yesBtn.onclick = () => { cleanup(); callback(); playSound('delete'); }; yesBtn.onclick = () => { cleanup(); callback(); playSound('delete'); };
noBtn.onclick = () => cleanup(); noBtn.onclick = () => cleanup();
overlay.onclick = (e) => { if (e.target === overlay) cleanup(); };
confirm.tabIndex = 0; confirm.focus(); confirm.tabIndex = 0; confirm.focus();
document.addEventListener('keydown', keyHandler); document.addEventListener('keydown', keyHandler);
setTimeout(() => {
document.addEventListener('click', function closeConfirm(e) {
if (!confirm.contains(e.target)) { cleanup(); document.removeEventListener('click', closeConfirm); }
});
}, 10);
} }
function attachEvents() { 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)); } }); 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() { async function batchDelete() {
const count = selectedIds.size; 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); for (const id of selectedIds) await delEntry(id, true);
toast('📦 Moved ' + count + ' entries to trash'); toast('📦 Moved ' + count + ' entries to trash');
playSound('delete'); playSound('delete');
@@ -1112,7 +1121,8 @@ async function batchDelete() {
async function batchPermanentDelete() { async function batchPermanentDelete() {
const count = selectedIds.size; 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); for (const id of selectedIds) await permanentDelete(id, true);
toast('🗑️ Permanently deleted ' + count + ' entries'); toast('🗑️ Permanently deleted ' + count + ' entries');
playSound('error'); playSound('error');