Replace batch mode toggle with click/Shift/Ctrl selection

- Remove separate 'select mode' toggle and checkbox UI
- Click any entry to select it (blue highlight border)
- Shift+click for range selection between two entries
- Ctrl/Cmd+click to toggle individual entries
- Click entry background or press Escape to clear selection
- Batch action bar appears automatically when items are selected
- Selected entries get accent-color border and highlight
- Single delEntry() cleans up selectedIds
- Shortcut help updated with selection tips
This commit is contained in:
2026-05-09 13:48:22 +01:00
parent 367b85e991
commit eac02ed17c
3 changed files with 42 additions and 46 deletions
+2 -1
View File
@@ -136,8 +136,9 @@ input:focus, select:focus { border-color:var(--accent); }
#entriesContainer.table-view th { text-align:left; padding:0.4rem 0.6rem; color:var(--text2); font-size:0.75rem; border-bottom:1px solid var(--border); } #entriesContainer.table-view th { text-align:left; padding:0.4rem 0.6rem; color:var(--text2); font-size:0.75rem; border-bottom:1px solid var(--border); }
#entriesContainer.table-view td { padding:0.4rem 0.6rem; font-size:0.8rem; border-bottom:1px solid rgba(255,255,255,0.03); } #entriesContainer.table-view td { padding:0.4rem 0.6rem; font-size:0.8rem; border-bottom:1px solid rgba(255,255,255,0.03); }
.entry-card, .entry-row, .entry-compact, .table-row-drag { cursor:grab; 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 { 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; }
-1
View File
@@ -155,7 +155,6 @@
</div> </div>
<div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap"> <div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap">
<button class="btn btn-outline btn-sm" id="trashBtn" onclick="toggleTrash()" title="Trash">🗑️</button> <button class="btn btn-outline btn-sm" id="trashBtn" onclick="toggleTrash()" title="Trash">🗑️</button>
<button class="btn btn-outline btn-sm" id="selectBtn" onclick="toggleSelectMode()" title="Batch select">☐ Select</button>
<button class="btn btn-outline btn-sm" onclick="showShortcutsHelp()" title="Shortcuts">⌨️</button> <button class="btn btn-outline btn-sm" onclick="showShortcutsHelp()" title="Shortcuts">⌨️</button>
<div class="view-toggle" id="viewToggle"> <div class="view-toggle" id="viewToggle">
<button class="view-btn active" data-view="grid">🟫 Grid</button> <button class="view-btn active" data-view="grid">🟫 Grid</button>
+40 -44
View File
@@ -19,8 +19,8 @@ let searchQuery = '';
let idleT, warnT, countT; let idleT, warnT, countT;
let draggedId = null; let draggedId = null;
let showTrash = false; let showTrash = false;
let selectMode = false;
let selectedIds = new Set(); let selectedIds = new Set();
let lastSelectedId = null;
// ==================== SOUND ==================== // ==================== SOUND ====================
let soundEnabled = localStorage.getItem('soundEnabled') !== 'false'; let soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
@@ -657,7 +657,8 @@ function render() {
if (view === 'table') { if (view === 'table') {
let h = '<table><thead><tr><th></th><th>Site</th>' + (showMail ? '<th>User</th>' : '') + '<th>Password</th>' + (!showTrash ? '<th>Folder</th>' : '<th>Deleted</th>') + '<th>Actions</th></tr></thead><tbody>'; let h = '<table><thead><tr><th></th><th>Site</th>' + (showMail ? '<th>User</th>' : '') + '<th>Password</th>' + (!showTrash ? '<th>Folder</th>' : '<th>Deleted</th>') + '<th>Actions</th></tr></thead><tbody>';
filtered.forEach(e => { filtered.forEach(e => {
h += '<tr class="table-row-drag' + (e.favorite ? ' favorite' : '') + '" draggable="true" data-id="' + e.id + '">' + const sel = selectedIds.has(e.id);
h += '<tr class="table-row-drag' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">' +
'<td>' + (e.favorite ? '⭐' : '') + '</td>' + '<td>' + (e.favorite ? '⭐' : '') + '</td>' +
'<td>🌐 ' + highlightText(e.site, searchQuery) + '</td>' + '<td>🌐 ' + highlightText(e.site, searchQuery) + '</td>' +
(showMail ? '<td>👤 ' + highlightText(e.username, searchQuery) + '</td>' : '') + (showMail ? '<td>👤 ' + highlightText(e.username, searchQuery) + '</td>' : '') +
@@ -665,7 +666,6 @@ function render() {
if (!showTrash) { if (!showTrash) {
h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' + h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
'<td class="actions-cell">' + '<td class="actions-cell">' +
(selectMode ? `<input type="checkbox" class="select-checkbox" style="position:static;margin-right:8px;" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">` : '') +
'<button class="star-btn" data-id="' + e.id + '">' + (e.favorite ? '⭐' : '☆') + '</button> ' + '<button class="star-btn" data-id="' + e.id + '">' + (e.favorite ? '⭐' : '☆') + '</button> ' +
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' + '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' +
'<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' + '<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' +
@@ -674,7 +674,6 @@ function render() {
} else { } else {
h += '<td><span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></td>' + h += '<td><span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></td>' +
'<td class="actions-cell">' + '<td class="actions-cell">' +
(selectMode ? `<input type="checkbox" class="select-checkbox" style="position:static;margin-right:8px;" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">` : '') +
'<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button> ' + '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button> ' +
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' + '<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
'</td>'; '</td>';
@@ -695,10 +694,8 @@ function render() {
} }
function gridC(e) { function gridC(e) {
let html = '<div class="entry-card' + (e.favorite ? ' favorite' : '') + '" draggable="' + (!selectMode) + '" data-id="' + e.id + '">'; const sel = selectedIds.has(e.id);
if (selectMode) { let html = '<div class="entry-card' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">';
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
}
html += '<div class="action-btns">'; html += '<div class="action-btns">';
if (showTrash) { if (showTrash) {
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>'; html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
@@ -722,10 +719,8 @@ function gridC(e) {
return html; return html;
} }
function listC(e) { function listC(e) {
let html = '<div class="entry-row' + (e.favorite ? ' favorite' : '') + '" draggable="' + (!selectMode) + '" data-id="' + e.id + '">'; const sel = selectedIds.has(e.id);
if (selectMode) { let html = '<div class="entry-row' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">';
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
}
html += '<div class="action-btns">'; html += '<div class="action-btns">';
if (showTrash) { if (showTrash) {
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>'; html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
@@ -749,10 +744,8 @@ function listC(e) {
return html; return html;
} }
function compC(e) { function compC(e) {
let html = '<div class="entry-compact' + (e.favorite ? ' favorite' : '') + '" draggable="' + (!selectMode) + '" data-id="' + e.id + '">'; const sel = selectedIds.has(e.id);
if (selectMode) { let html = '<div class="entry-compact' + (e.favorite ? ' favorite' : '') + (sel ? ' selected' : '') + '" draggable="true" data-id="' + e.id + '" onclick="toggleSelectEntry(' + e.id + ', event)">';
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
}
html += '<div class="action-btns">'; html += '<div class="action-btns">';
if (showTrash) { if (showTrash) {
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>'; html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
@@ -911,37 +904,42 @@ async function saveEdit() {
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); } else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
} catch (e) { toast('⚠️ Error', 'error'); } } catch (e) { toast('⚠️ Error', 'error'); }
} }
//======================== batch operations ========================== //======================== batch selection ==========================
function toggleSelectMode() { function toggleSelectEntry(id, e) {
selectMode = !selectMode; if (e?.shiftKey && lastSelectedId !== null) {
document.getElementById('selectBtn').textContent = selectMode ? '☑ Deselect' : '☐ Select'; const ids = getFilteredEntries().map(x => x.id);
document.getElementById('selectBtn').classList.toggle('active', selectMode); const i1 = ids.indexOf(lastSelectedId);
if (!selectMode) { const i2 = ids.indexOf(id);
selectedIds.clear(); if (i1 > -1 && i2 > -1) {
hideBatchBar(); const start = Math.min(i1, i2), end = Math.max(i1, i2);
for (let i = start; i <= end; i++) selectedIds.add(ids[i]);
}
} else if (e?.ctrlKey || e?.metaKey) {
if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id);
} else {
if (selectedIds.size === 1 && selectedIds.has(id)) { selectedIds.clear(); }
else { selectedIds.clear(); selectedIds.add(id); }
} }
lastSelectedId = id;
updateBatchBar();
render(); render();
} }
function toggleSelectEntry(id) { function clearSelection() {
if (selectedIds.has(id)) { selectedIds.clear();
selectedIds.delete(id); lastSelectedId = null;
} else { hideBatchBar();
selectedIds.add(id); render();
}
updateBatchBar();
} }
function updateBatchBar() { function updateBatchBar() {
const existing = document.getElementById('batchBar'); const existing = document.getElementById('batchBar');
if (existing) existing.remove(); if (existing) existing.remove();
if (selectedIds.size === 0) return; if (selectedIds.size === 0) return;
const bar = document.createElement('div'); const bar = document.createElement('div');
bar.id = 'batchBar'; bar.id = 'batchBar';
bar.className = 'batch-actions'; bar.className = 'batch-actions';
bar.innerHTML = `<span class="selected-count">${selectedIds.size} selected</span>`; bar.innerHTML = `<span class="selected-count">${selectedIds.size} selected</span>`;
if (showTrash) { if (showTrash) {
bar.innerHTML += ` bar.innerHTML += `
<button class="btn btn-sm restore-btn" onclick="batchRestore()">↩️ Restore All</button> <button class="btn btn-sm restore-btn" onclick="batchRestore()">↩️ Restore All</button>
@@ -956,7 +954,7 @@ function updateBatchBar() {
<button class="btn btn-sm btn-danger" onclick="batchDelete()">Delete</button> <button class="btn btn-sm btn-danger" onclick="batchDelete()">Delete</button>
`; `;
} }
bar.innerHTML += `<button class="btn btn-sm btn-outline" onclick="toggleSelectMode()">Cancel</button>`; bar.innerHTML += `<button class="btn btn-sm btn-outline" onclick="clearSelection()">Clear</button>`;
document.body.appendChild(bar); document.body.appendChild(bar);
} }
@@ -968,21 +966,18 @@ function hideBatchBar() {
async function batchDelete() { async function batchDelete() {
if (!confirm(`Move ${selectedIds.size} entries to trash?`)) return; if (!confirm(`Move ${selectedIds.size} entries to trash?`)) return;
for (const id of selectedIds) await delEntry(id); for (const id of selectedIds) await delEntry(id);
selectedIds.clear(); clearSelection();
hideBatchBar();
} }
async function batchPermanentDelete() { async function batchPermanentDelete() {
if (!confirm(`Permanently delete ${selectedIds.size} entries?`)) return; if (!confirm(`Permanently delete ${selectedIds.size} entries?`)) return;
for (const id of selectedIds) await permanentDelete(id); for (const id of selectedIds) await permanentDelete(id);
selectedIds.clear(); clearSelection();
hideBatchBar();
} }
async function batchRestore() { async function batchRestore() {
for (const id of selectedIds) await restoreEntry(id); for (const id of selectedIds) await restoreEntry(id);
selectedIds.clear(); clearSelection();
hideBatchBar();
} }
async function batchMove() { async function batchMove() {
@@ -999,8 +994,7 @@ async function batchMove() {
}); });
} }
} }
selectedIds.clear(); clearSelection();
hideBatchBar();
loadEntries(); loadEntries();
} }
// ==================== ADD / DELETE ==================== // ==================== ADD / DELETE ====================
@@ -1035,7 +1029,7 @@ async function addEntry() {
async function delEntry(id) { async function delEntry(id) {
try { try {
const r = await fetch(API + '/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); const r = await fetch(API + '/entries/' + id, { 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)); toast('📦 Moved to trash'); await loadEntries(); playSound('delete'); } if (r.ok) { selectedIds.delete(id); order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); toast('📦 Moved to trash'); await loadEntries(); playSound('delete'); }
} catch (e) { toast('Error', 'error'); } } catch (e) { toast('Error', 'error'); }
} }
@@ -1090,7 +1084,7 @@ function highlightText(text, query) {
function showShortcutsHelp() { function showShortcutsHelp() {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay show'; overlay.className = 'custom-modal-overlay show';
overlay.innerHTML = `<div class="custom-modal" style="min-width:360px"><h3>⌨️ Keyboard Shortcuts</h3><div style="display:grid;grid-template-columns:auto 1fr;gap:0.5rem 1.2rem;font-size:0.85rem;margin-bottom:1rem"><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+N</span><span>New entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+F</span><span>Search</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+T</span><span>Toggle trash</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+L</span><span>Lock vault</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+S</span><span>Save entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Esc</span><span>Close modal / settings</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">?</span><span>Show this help</span></div><div class="modal-actions"><button class="btn btn-sm" onclick="this.closest('.custom-modal-overlay').remove()">Got it</button></div></div>`; overlay.innerHTML = `<div class="custom-modal" style="min-width:380px"><h3>⌨️ Keyboard Shortcuts</h3><div style="display:grid;grid-template-columns:auto 1fr;gap:0.5rem 1.2rem;font-size:0.85rem;margin-bottom:1rem"><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+N</span><span>New entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+F</span><span>Search</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+T</span><span>Toggle trash</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+L</span><span>Lock vault</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Ctrl+S</span><span>Save entry</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">Esc</span><span>Close modal / deselect</span><span style="background:var(--input);padding:0.15rem 0.6rem;border-radius:0.4rem;font-family:monospace;text-align:center">?</span><span>Show this help</span></div><p style="color:var(--text2);font-size:0.8rem;margin-bottom:0.8rem">💡 Click any entry to select, Shift+click for range, Ctrl+click to toggle</p><div class="modal-actions"><button class="btn btn-sm" onclick="this.closest('.custom-modal-overlay').remove()">Got it</button></div></div>`;
document.body.appendChild(overlay); document.body.appendChild(overlay);
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
} }
@@ -1123,6 +1117,7 @@ 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(); } });
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(); });
@@ -1133,6 +1128,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 (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');
return; return;