From a97d05412efe310c232326609826c10933fc66b9 Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Sat, 9 May 2026 14:34:20 +0100 Subject: [PATCH] Fix duplicate trash drop listener and rectangle selection bugs - Move trash button drop listener out of setupDrag() to an IIFE so it only registers once (was accumulating listeners on every render() call, causing multiple toast messages on single drop). - Fix rectangle selection mousedown handler: use optional chaining on closest() for safety with text nodes; avoid calling render() inside mousedown (which would detach the event target's children mid-handler) and instead just remove .selected class directly from the DOM. --- js/app.js | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/js/app.js b/js/app.js index 53028fd..597954b 100644 --- a/js/app.js +++ b/js/app.js @@ -869,21 +869,6 @@ function setupDrag() { el.ondragleave = function(e) { this.classList.remove('drag-over'); }; el.ondrop = function(e) { e.preventDefault(); e.stopPropagation(); this.classList.remove('drag-over'); const fromId = parseInt(e.dataTransfer.getData('text/plain')); const toId = parseInt(this.dataset.id); if (!fromId || !toId) return; const ids = selectedIds.has(fromId) && selectedIds.size > 1 ? [...selectedIds] : [fromId]; if (ids.length === 1 && ids[0] === toId) return; const base = order.length > 0 ? order : entries.filter(e => e).map(e => e.id); const filtered = base.filter(id => !ids.includes(id)); const idx = filtered.indexOf(toId); idx > -1 ? filtered.splice(idx, 0, ...ids) : filtered.push(...ids); order = filtered; localStorage.setItem('entryOrder', JSON.stringify(order)); const map = new Map(entries.filter(e => e).map(e => [e.id, e])); entries = order.map(id => map.get(id)).filter(e => e); entries.sort((a, b) => (b.favorite || 0) - (a.favorite || 0)); render(); }; }); - // Trash button as drop target for multi-delete - const trashBtn = document.getElementById('trashBtn'); - if (trashBtn) { - trashBtn.addEventListener('dragover', e => { if (!showTrash) { e.preventDefault(); trashBtn.classList.add('drag-over'); } }); - trashBtn.addEventListener('dragleave', () => trashBtn.classList.remove('drag-over')); - trashBtn.addEventListener('drop', async function(e) { - e.preventDefault(); - this.classList.remove('drag-over'); - const id = parseInt(e.dataTransfer.getData('text/plain')); - if (!id) return; - const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id]; - for (const sid of ids) await delEntry(sid); - clearSelection(); - }); - } } // ==================== EDIT ==================== @@ -1138,16 +1123,38 @@ if (token && curUser) { } ['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); })); +// Trash button as drop target (set up once, outside setupDrag to avoid duplicates) +(function() { + const trashBtn = document.getElementById('trashBtn'); + if (trashBtn) { + trashBtn.addEventListener('dragover', e => { if (!showTrash) { e.preventDefault(); trashBtn.classList.add('drag-over'); } }); + trashBtn.addEventListener('dragleave', () => trashBtn.classList.remove('drag-over')); + trashBtn.addEventListener('drop', async function(e) { + e.preventDefault(); + this.classList.remove('drag-over'); + const id = parseInt(e.dataTransfer.getData('text/plain')); + if (!id) return; + const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id]; + for (const sid of ids) await delEntry(sid); + clearSelection(); + }); + } +})(); + // Rectangle selection document.getElementById('entriesContainer').addEventListener('mousedown', function(e) { if (rectState.active || e.button !== 0) return; - if (e.target.closest('[draggable="true"]') || e.target.closest('.action-btns')) return; + if (e.target?.closest?.('[draggable="true"]') || e.target?.closest?.('.action-btns')) return; rectState.active = true; rectState.startX = e.clientX; rectState.startY = e.clientY; rectState.started = false; rectState.el = null; - clearSelection(); + // Clear selection without full re-render to avoid detaching event target + selectedIds.clear(); + lastSelectedId = null; + hideBatchBar(); + document.getElementById('entriesContainer')?.querySelectorAll('.selected').forEach(el => el.classList.remove('selected')); }); document.addEventListener('mousemove', function(e) { if (!rectState.active) return;