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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user