From 5da0deaf942884b56b3217027dee8c98d33e7922 Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Sat, 9 May 2026 14:09:48 +0100 Subject: [PATCH] Enhance selection visibility, add rectangle select, multi drag/drop - Brighter selection highlight with stronger box-shadow, background, and left-border accent - Rectangle/band selection: click-drag on empty space to select multiple entries via intersection test - Drag selected entries to a folder chip to move all, or to trash button to multi-delete - Multi-drag reorder: dragging a selected entry reorders all selected together - Table view selection now highlights individual td cells (box-shadow on tr was invisible) - Escape cancels rectangle selection in progress --- css/style.css | 15 +++++++- js/app.js | 101 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/css/style.css b/css/style.css index 3ade320..73206a5 100644 --- a/css/style.css +++ b/css/style.css @@ -138,7 +138,9 @@ input:focus, select:focus { border-color:var(--accent); } .entry-card, .entry-row, .entry-compact, .table-row-drag { cursor:pointer; user-select:none; } .entry-card.drag-over, .entry-row.drag-over, .entry-compact.drag-over, .table-row-drag.drag-over { border-color:var(--accent)!important; box-shadow:0 0 15px rgba(59,130,246,0.3); } -.entry-card.selected, .entry-row.selected, .entry-compact.selected, .table-row-drag.selected { border-color:var(--accent)!important; box-shadow:0 0 0 2px var(--accent); background:rgba(59,130,246,0.08); } +.entry-card.selected { border-color:var(--accent)!important; box-shadow:0 0 0 2px var(--accent),0 0 18px rgba(59,130,246,0.35); background:rgba(59,130,246,0.18); } +.entry-row.selected, .entry-compact.selected { border-color:var(--accent)!important; box-shadow:0 0 0 2px var(--accent),0 0 18px rgba(59,130,246,0.35); background:rgba(59,130,246,0.14); border-left:3px solid var(--accent); } +.table-row-drag.selected td { background:rgba(59,130,246,0.14)!important; box-shadow:inset 0 0 0 1px var(--accent); border-bottom:1px solid var(--accent); } .entry-card { background:var(--card); border:1px solid var(--border); border-radius:0.8rem; padding:0.9rem 2.8rem 0.9rem 0.9rem; transition:0.2s; position:relative; word-break:break-word; } .entry-card:hover { border-color:rgba(255,255,255,0.15); transform:translateY(-2px); } .card-site { font-weight:700; font-size:0.9rem; color:var(--text); margin-bottom:0.2rem; } @@ -284,4 +286,15 @@ input:focus, select:focus { border-color:var(--accent); } .selected-count { font-weight: 600; color: var(--accent); +} +#rectSelect { + position:fixed; pointer-events:none; z-index:999; + border:1px solid var(--accent); + background:rgba(59,130,246,0.1); + display:none; +} +#trashBtn.drag-over { + 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 diff --git a/js/app.js b/js/app.js index fb0a9d8..53028fd 100644 --- a/js/app.js +++ b/js/app.js @@ -21,6 +21,7 @@ let draggedId = null; let showTrash = false; let selectedIds = new Set(); let lastSelectedId = null; +let rectState = { active: false, startX: 0, startY: 0, el: null, started: false }; // ==================== SOUND ==================== let soundEnabled = localStorage.getItem('soundEnabled') !== 'false'; @@ -222,17 +223,22 @@ function renderFolders() { const entry = entries.find(x => x.id === id); if (!entry) return; const folder = this.dataset.folder; - if (entry.folder === folder) return; - const enc = await encryptPwd(entry.password); - await fetch(API + '/entries/' + id, { - method: 'PUT', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, - body: JSON.stringify({ site: entry.site, username: entry.username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) - }); - entry.folder = folder; + const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id]; + let moved = 0; + for (const sid of ids) { + const e2 = entries.find(x => x.id === sid); + if (!e2 || e2.folder === folder) continue; + const enc = await encryptPwd(e2.password); + const r = await fetch(API + '/entries/' + sid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, + body: JSON.stringify({ site: e2.site, username: e2.username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) + }); + if (r.ok) { e2.folder = folder; moved++; } + } renderFolders(); render(); - playSound('success'); + if (moved) playSound('success'); }); }); } @@ -858,11 +864,26 @@ function setupDrag() { const c = document.getElementById('entriesContainer'); if (!c) return; c.querySelectorAll('[draggable="true"]').forEach(el => { el.ondragstart = function(e) { draggedId = this.dataset.id; this.style.opacity = '0.5'; e.dataTransfer.setData('text/plain', this.dataset.id); e.dataTransfer.effectAllowed = 'move'; }; - el.ondragend = function(e) { this.style.opacity = '1'; draggedId = null; c.querySelectorAll('.drag-over').forEach(x => x.classList.remove('drag-over')); }; + el.ondragend = function(e) { this.style.opacity = '1'; draggedId = null; c.querySelectorAll('.drag-over').forEach(x => x.classList.remove('drag-over')); document.getElementById('trashBtn')?.classList.remove('drag-over'); }; el.ondragover = function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (this.dataset.id !== draggedId) this.classList.add('drag-over'); }; 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 || fromId === toId) return; let fi = -1, ti = -1; for (let i = 0; i < entries.length; i++) { if (entries[i] && entries[i].id === fromId) fi = i; if (entries[i] && entries[i].id === toId) ti = i; } if (fi > -1 && ti > -1 && fi !== ti) { const moved = entries.splice(fi, 1)[0]; entries.splice(ti, 0, moved); order = entries.map(e => e.id); localStorage.setItem('entryOrder', JSON.stringify(order)); render(); } }; + 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 ==================== @@ -1117,7 +1138,62 @@ if (token && curUser) { } ['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); })); -document.getElementById('entriesContainer').addEventListener('click', function(e) { if (e.target === this || (selectedIds.size > 0 && !e.target.closest('.entry-card,.entry-row,.entry-compact,.table-row-drag'))) { 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; + rectState.active = true; + rectState.startX = e.clientX; + rectState.startY = e.clientY; + rectState.started = false; + rectState.el = null; + clearSelection(); +}); +document.addEventListener('mousemove', function(e) { + if (!rectState.active) return; + const dx = e.clientX - rectState.startX; + const dy = e.clientY - rectState.startY; + if (!rectState.started && (dx > 5 || dx < -5 || dy > 5 || dy < -5)) { + rectState.started = true; + rectState.el = document.createElement('div'); + rectState.el.id = 'rectSelect'; + document.body.appendChild(rectState.el); + } + if (rectState.el) { + const x = Math.min(rectState.startX, e.clientX); + const y = Math.min(rectState.startY, e.clientY); + const w = Math.abs(dx); + const h = Math.abs(dy); + rectState.el.style.cssText = `left:${x}px;top:${y}px;width:${w}px;height:${h}px;display:block;position:fixed;pointer-events:none;z-index:999;border:1px solid var(--accent);background:rgba(59,130,246,0.1);`; + } +}); +document.addEventListener('mouseup', function(e) { + if (!rectState.active) return; + rectState.active = false; + if (rectState.el) { + rectState.el.remove(); + rectState.el = null; + if (rectState.started) { + const r = { + left: Math.min(rectState.startX, e.clientX), + top: Math.min(rectState.startY, e.clientY), + right: Math.max(rectState.startX, e.clientX), + bottom: Math.max(rectState.startY, e.clientY) + }; + const container = document.getElementById('entriesContainer'); + if (container) { + container.querySelectorAll('.entry-card,.entry-row,.entry-compact,.table-row-drag').forEach(el => { + const er = el.getBoundingClientRect(); + if (er.left < r.right && er.right > r.left && er.top < r.bottom && er.bottom > r.top) { + const id = parseInt(el.dataset.id); + if (id) selectedIds.add(id); + } + }); + } + if (selectedIds.size > 0) { updateBatchBar(); render(); } + } + } +}); document.addEventListener('keypress', e => { if (e.key === 'Enter') { if (document.getElementById('passwordInput') === document.activeElement) addEntry(); else if (document.getElementById('loginPassword') === document.activeElement) login(); else if (document.getElementById('regPassword') === document.activeElement) register(); } }); document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); }); document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); }); @@ -1128,6 +1204,7 @@ document.addEventListener('keydown', function(e) { // Escape – close any open modal or settings if (e.key === 'Escape') { + if (rectState.active || rectState.el) { rectState.active = false; if (rectState.el) { rectState.el.remove(); rectState.el = null; } clearSelection(); return; } if (selectedIds.size > 0) { clearSelection(); return; } if (!document.getElementById('settingsMenu').classList.contains('hidden')) { document.getElementById('settingsMenu').classList.add('hidden');