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
This commit is contained in:
2026-05-09 14:09:48 +01:00
parent eac02ed17c
commit 5da0deaf94
2 changed files with 103 additions and 13 deletions
+14 -1
View File
@@ -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, .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.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 { 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); } .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; } .card-site { font-weight:700; font-size:0.9rem; color:var(--text); margin-bottom:0.2rem; }
@@ -285,3 +287,14 @@ input:focus, select:focus { border-color:var(--accent); }
font-weight: 600; font-weight: 600;
color: var(--accent); 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);
}
+86 -9
View File
@@ -21,6 +21,7 @@ let draggedId = null;
let showTrash = false; let showTrash = false;
let selectedIds = new Set(); let selectedIds = new Set();
let lastSelectedId = null; let lastSelectedId = null;
let rectState = { active: false, startX: 0, startY: 0, el: null, started: false };
// ==================== SOUND ==================== // ==================== SOUND ====================
let soundEnabled = localStorage.getItem('soundEnabled') !== 'false'; let soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
@@ -222,17 +223,22 @@ function renderFolders() {
const entry = entries.find(x => x.id === id); const entry = entries.find(x => x.id === id);
if (!entry) return; if (!entry) return;
const folder = this.dataset.folder; const folder = this.dataset.folder;
if (entry.folder === folder) return; const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id];
const enc = await encryptPwd(entry.password); let moved = 0;
await fetch(API + '/entries/' + id, { 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', method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, 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 }) body: JSON.stringify({ site: e2.site, username: e2.username, encrypted_password: enc.encrypted, iv: enc.iv, folder })
}); });
entry.folder = folder; if (r.ok) { e2.folder = folder; moved++; }
}
renderFolders(); renderFolders();
render(); render();
playSound('success'); if (moved) playSound('success');
}); });
}); });
} }
@@ -858,11 +864,26 @@ function setupDrag() {
const c = document.getElementById('entriesContainer'); if (!c) return; const c = document.getElementById('entriesContainer'); if (!c) return;
c.querySelectorAll('[draggable="true"]').forEach(el => { 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.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.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.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 ==================== // ==================== EDIT ====================
@@ -1117,7 +1138,62 @@ if (token && curUser) {
} }
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); })); ['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.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('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); });
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); }); 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 // Escape close any open modal or settings
if (e.key === 'Escape') { 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 (selectedIds.size > 0) { clearSelection(); return; }
if (!document.getElementById('settingsMenu').classList.contains('hidden')) { if (!document.getElementById('settingsMenu').classList.contains('hidden')) {
document.getElementById('settingsMenu').classList.add('hidden'); document.getElementById('settingsMenu').classList.add('hidden');