refactor: remove the dead legacy entry modal
openEntryModal had zero callers — the slideover editor replaced it — but the whole cluster still shipped: the #entryModal markup, saveEntry (wired to the form submit), closeEntryModal, populateFolderSelect, updateEntryStrength, and the entire tag-chip subsystem (editingTags, renderChips, addTag, the suggest dropdown) plus their init wirings and the Esc-chain branch. saveEntry was a latent hazard: still bound to #entryForm submit, it did a PARTIAL PUT (no kind / totp_secret / custom_fields / template) — the exact silent-data-loss bug moveEntryToFolder documents. Unreachable today, but one stray call away from wiping fields. Removed JS + HTML + the orphaned .chip-suggestion CSS. The generator is shared with the slideover — kept; its dead 'entry' target branch collapsed to slideover-only. 322 lines gone, tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -368,27 +368,6 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.chip button:hover { opacity: 1; background: rgba(0,0,0,0.1); }
|
.chip button:hover { opacity: 1; background: rgba(0,0,0,0.1); }
|
||||||
.chip button svg { width: 10px; height: 10px; }
|
.chip button svg { width: 10px; height: 10px; }
|
||||||
|
|
||||||
/* Tag suggestions dropdown */
|
|
||||||
.chip-suggestions {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 50;
|
|
||||||
background: var(--bg-elev);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
padding: 4px;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
|
||||||
.chip-suggestion {
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.chip-suggestion:hover, .chip-suggestion.is-active { background: var(--bg-elev-2); color: var(--text); }
|
|
||||||
.chip-suggestion-empty { padding: 6px 10px; color: var(--text-faint); font-size: 12px; }
|
|
||||||
|
|
||||||
/* Settings rows */
|
/* Settings rows */
|
||||||
.setting-row {
|
.setting-row {
|
||||||
|
|||||||
-54
@@ -1013,60 +1013,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
|
||||||
<!-- MODAL: New / Edit Entry -->
|
|
||||||
<!-- ============================================================ -->
|
|
||||||
<div id="entryModal" class="modal is-hidden" role="dialog" aria-modal="true">
|
|
||||||
<div class="modal-backdrop" data-close></div>
|
|
||||||
<div class="modal-panel">
|
|
||||||
<header class="modal-header">
|
|
||||||
<h3 id="entryModalTitle">New entry</h3>
|
|
||||||
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
|
|
||||||
</header>
|
|
||||||
<form id="entryForm" class="modal-body" autocomplete="off">
|
|
||||||
<input type="hidden" id="entryId">
|
|
||||||
<label class="field">
|
|
||||||
<span>Display name <small style="color:var(--text-faint);font-weight:400">— shown on cards, also used for autofill matching</small></span>
|
|
||||||
<input id="entryTitle" type="text" placeholder="GitHub (work account)">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Website / App <small style="color:var(--text-faint);font-weight:400">— hostname or brand, used for autofill matching</small></span>
|
|
||||||
<input id="entrySite" type="text" required placeholder="github.com">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Username / Email</span>
|
|
||||||
<input id="entryUsername" type="text" placeholder="me@example.com">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Password</span>
|
|
||||||
<div class="input-with-action">
|
|
||||||
<input id="entryPassword" type="password" required placeholder="••••••••">
|
|
||||||
<button type="button" class="icon-btn" id="entryPwToggle"><svg><use href="#i-eye"/></svg></button>
|
|
||||||
<button type="button" class="icon-btn" id="entryPwGen" title="Generate"><svg><use href="#i-dice"/></svg></button>
|
|
||||||
</div>
|
|
||||||
<div class="strength-bar" id="entryStrengthBar"></div>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Folder</span>
|
|
||||||
<select id="entryFolder"></select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Tags <span class="hint">enter to add</span></span>
|
|
||||||
<div class="chip-input" id="entryTagsInput">
|
|
||||||
<input id="entryTagsField" type="text" placeholder="add a tag…" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
<input type="hidden" id="entryTags">
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<footer class="modal-footer">
|
|
||||||
<button class="btn btn-ghost" data-close>Cancel</button>
|
|
||||||
<button class="btn btn-primary" id="entrySaveBtn">
|
|
||||||
<svg><use href="#i-check"/></svg> Save
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- MODAL: Password Generator -->
|
<!-- MODAL: Password Generator -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
|
|||||||
@@ -5026,210 +5026,6 @@ async function requestCloseSlideOver() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// TAG CHIP INPUT
|
|
||||||
// ============================================================
|
|
||||||
// Local mutable list of tags currently in the entry modal. Synced to the
|
|
||||||
// hidden #entryTags field on every change so saveEntry can read it.
|
|
||||||
|
|
||||||
let editingTags = [];
|
|
||||||
let chipSuggestEl = null;
|
|
||||||
let chipSuggestActive = -1;
|
|
||||||
|
|
||||||
function syncTagsHidden() {
|
|
||||||
$('#entryTags').value = editingTags.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderChips() {
|
|
||||||
const container = $('#entryTagsInput');
|
|
||||||
// Wipe existing chips but keep the input element
|
|
||||||
$$('.chip', container).forEach(c => c.remove());
|
|
||||||
const input = $('#entryTagsField');
|
|
||||||
editingTags.forEach((t, i) => {
|
|
||||||
const chip = el('span', { class: 'chip' });
|
|
||||||
chip.appendChild(el('span', null, t));
|
|
||||||
const x = el('button', {
|
|
||||||
type: 'button',
|
|
||||||
on: { click: ev => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
editingTags.splice(i, 1);
|
|
||||||
renderChips();
|
|
||||||
syncTagsHidden();
|
|
||||||
} },
|
|
||||||
});
|
|
||||||
x.appendChild(icon('i-x'));
|
|
||||||
chip.appendChild(x);
|
|
||||||
container.insertBefore(chip, input);
|
|
||||||
});
|
|
||||||
syncTagsHidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setEditingTags(arr) {
|
|
||||||
editingTags = (arr || []).filter(Boolean).map(t => t.trim()).filter(Boolean);
|
|
||||||
renderChips();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTag(raw) {
|
|
||||||
const t = (raw || '').trim().replace(/,/g, '');
|
|
||||||
if (!t) return;
|
|
||||||
if (editingTags.includes(t)) return;
|
|
||||||
editingTags.push(t);
|
|
||||||
renderChips();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeChipSuggest() {
|
|
||||||
if (chipSuggestEl) { chipSuggestEl.remove(); chipSuggestEl = null; }
|
|
||||||
chipSuggestActive = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openChipSuggest() {
|
|
||||||
closeChipSuggest();
|
|
||||||
const field = $('#entryTagsField');
|
|
||||||
const q = field.value.trim().toLowerCase();
|
|
||||||
const existing = allTags();
|
|
||||||
const candidates = existing
|
|
||||||
.filter(t => !editingTags.includes(t))
|
|
||||||
.filter(t => !q || t.toLowerCase().includes(q))
|
|
||||||
.slice(0, 8);
|
|
||||||
if (!q && candidates.length === 0) return;
|
|
||||||
|
|
||||||
chipSuggestEl = el('div', { class: 'chip-suggestions' });
|
|
||||||
if (candidates.length === 0) {
|
|
||||||
chipSuggestEl.appendChild(el('div', { class: 'chip-suggestion-empty' }, 'Press Enter to create "' + q + '"'));
|
|
||||||
} else {
|
|
||||||
candidates.forEach((t, i) => {
|
|
||||||
const item = el('div', {
|
|
||||||
class: 'chip-suggestion' + (i === 0 ? ' is-active' : ''),
|
|
||||||
on: { mousedown: ev => { ev.preventDefault(); addTag(t); field.value = ''; closeChipSuggest(); } },
|
|
||||||
}, t);
|
|
||||||
chipSuggestEl.appendChild(item);
|
|
||||||
});
|
|
||||||
chipSuggestActive = 0;
|
|
||||||
}
|
|
||||||
// Position under the chip input
|
|
||||||
const rect = $('#entryTagsInput').getBoundingClientRect();
|
|
||||||
chipSuggestEl.style.position = 'fixed';
|
|
||||||
chipSuggestEl.style.top = (rect.bottom + 4) + 'px';
|
|
||||||
chipSuggestEl.style.left = rect.left + 'px';
|
|
||||||
chipSuggestEl.style.width = Math.max(160, rect.width / 2) + 'px';
|
|
||||||
document.body.appendChild(chipSuggestEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveChipSuggest(dir) {
|
|
||||||
if (!chipSuggestEl) return;
|
|
||||||
const items = $$('.chip-suggestion', chipSuggestEl);
|
|
||||||
if (items.length === 0) return;
|
|
||||||
items.forEach(it => it.classList.remove('is-active'));
|
|
||||||
chipSuggestActive = (chipSuggestActive + dir + items.length) % items.length;
|
|
||||||
items[chipSuggestActive].classList.add('is-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectActiveSuggestion() {
|
|
||||||
if (!chipSuggestEl || chipSuggestActive < 0) return false;
|
|
||||||
const items = $$('.chip-suggestion', chipSuggestEl);
|
|
||||||
if (items[chipSuggestActive]) {
|
|
||||||
addTag(items[chipSuggestActive].textContent);
|
|
||||||
$('#entryTagsField').value = '';
|
|
||||||
closeChipSuggest();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// ENTRY MODAL (new / edit)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function openEntryModal(entry) {
|
|
||||||
populateFolderSelect();
|
|
||||||
if (entry) {
|
|
||||||
$('#entryModalTitle').textContent = 'Edit entry';
|
|
||||||
$('#entryId').value = entry.id;
|
|
||||||
$('#entryTitle').value = entry.title || '';
|
|
||||||
$('#entrySite').value = entry.site;
|
|
||||||
$('#entryUsername').value = entry.username || '';
|
|
||||||
$('#entryFolder').value = entry.folder || 'All';
|
|
||||||
setEditingTags(parseTags(entry.tags));
|
|
||||||
$('#entryPassword').value = await decryptPwd(entry.encrypted_password, entry.iv);
|
|
||||||
if ($('#entryPassword').value === '[ERROR]') $('#entryPassword').value = '';
|
|
||||||
} else {
|
|
||||||
$('#entryModalTitle').textContent = 'New entry';
|
|
||||||
$('#entryId').value = '';
|
|
||||||
$('#entryForm').reset();
|
|
||||||
$('#entryFolder').value = state.view.startsWith('folder:') ? state.view.slice(7) : 'All';
|
|
||||||
setEditingTags([]);
|
|
||||||
}
|
|
||||||
$('#entryTagsField').value = '';
|
|
||||||
closeChipSuggest();
|
|
||||||
updateEntryStrength();
|
|
||||||
$('#entryModal').classList.remove('is-hidden');
|
|
||||||
$('#entrySite').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeEntryModal() {
|
|
||||||
$('#entryModal').classList.add('is-hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateFolderSelect() {
|
|
||||||
const sel = $('#entryFolder');
|
|
||||||
sel.innerHTML = '';
|
|
||||||
state.folders.forEach(f => sel.appendChild(el('option', { value: f.name }, folderLabel(f.name))));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveEntry(e) {
|
|
||||||
e && e.preventDefault();
|
|
||||||
// Flush any pending text in the chip input as a final tag
|
|
||||||
const pending = $('#entryTagsField').value.trim();
|
|
||||||
if (pending) { addTag(pending); $('#entryTagsField').value = ''; }
|
|
||||||
const id = $('#entryId').value;
|
|
||||||
const title = $('#entryTitle').value.trim();
|
|
||||||
const site = $('#entrySite').value.trim();
|
|
||||||
const user = $('#entryUsername').value.trim();
|
|
||||||
const pwd = $('#entryPassword').value;
|
|
||||||
const fold = $('#entryFolder').value;
|
|
||||||
const tags = editingTags.join(',');
|
|
||||||
if (!site || !pwd) return toast('Site and password required', 'error');
|
|
||||||
|
|
||||||
const enc = await encryptPwd(pwd);
|
|
||||||
const body = JSON.stringify(await withEncryptedMeta({
|
|
||||||
site, title, username: user, encrypted_password: enc.encrypted, iv: enc.iv,
|
|
||||||
folder: fold, tags,
|
|
||||||
}));
|
|
||||||
try {
|
|
||||||
let savedId = id ? parseInt(id) : null;
|
|
||||||
if (id) {
|
|
||||||
await api('/entries/' + id, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
toast('Updated');
|
|
||||||
} else {
|
|
||||||
const r = await api('/entries', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
if (r && typeof r.id === 'number') savedId = r.id;
|
|
||||||
toast('Saved');
|
|
||||||
}
|
|
||||||
closeEntryModal();
|
|
||||||
await loadEntries();
|
|
||||||
if (typeof healthCache !== 'undefined') healthCache = null;
|
|
||||||
render();
|
|
||||||
if (savedId) flashEntry(savedId);
|
|
||||||
// Fire-and-forget favicon fetch for the saved entry. Updates the
|
|
||||||
// card in place when it arrives. No-op when feature is off or
|
|
||||||
// the entry already has a cached icon.
|
|
||||||
if (savedId && state.faviconsEnabled) {
|
|
||||||
const saved = state.entries.find(e => e.id === savedId);
|
|
||||||
if (saved) ensureEntryFavicon(saved); // honours the toggle
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
toast(err.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function restoreEntry(id) {
|
async function restoreEntry(id) {
|
||||||
try {
|
try {
|
||||||
await api('/entries/' + id + '/restore', { method: 'POST', headers: authHeaders() });
|
await api('/entries/' + id + '/restore', { method: 'POST', headers: authHeaders() });
|
||||||
@@ -5802,12 +5598,12 @@ function genPassword() {
|
|||||||
$('#genPreview').textContent = p;
|
$('#genPreview').textContent = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
// genTarget: 'entry' (insert into entry form) or 'standalone' (just copy/dismiss)
|
// genTarget: 'slideover' (insert into the editor) or 'standalone' (just copy)
|
||||||
let genTarget = 'entry';
|
let genTarget = 'slideover';
|
||||||
|
|
||||||
function openGen(target) {
|
function openGen(target) {
|
||||||
genTarget = target || 'entry';
|
genTarget = target || 'slideover';
|
||||||
// Show "Use" when targeting an editable field (entry modal or slide-over)
|
// Show "Use" when targeting the editor; hide it in standalone (copy-only).
|
||||||
$('#genUse').style.display = (genTarget === 'standalone') ? 'none' : '';
|
$('#genUse').style.display = (genTarget === 'standalone') ? 'none' : '';
|
||||||
$('#genModal').classList.remove('is-hidden');
|
$('#genModal').classList.remove('is-hidden');
|
||||||
genPassword();
|
genPassword();
|
||||||
@@ -5832,11 +5628,6 @@ function updateRegStrength() {
|
|||||||
const p = $('#regPassword').value;
|
const p = $('#regPassword').value;
|
||||||
$('#regStrengthBar').style.setProperty('--strength', computeStrength(p) + '%');
|
$('#regStrengthBar').style.setProperty('--strength', computeStrength(p) + '%');
|
||||||
}
|
}
|
||||||
function updateEntryStrength() {
|
|
||||||
const p = $('#entryPassword').value;
|
|
||||||
$('#entryStrengthBar').style.setProperty('--strength', computeStrength(p) + '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// COMMAND PALETTE
|
// COMMAND PALETTE
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -7187,7 +6978,6 @@ async function init() {
|
|||||||
$('#loginForm').addEventListener('submit', doLogin);
|
$('#loginForm').addEventListener('submit', doLogin);
|
||||||
$('#registerForm').addEventListener('submit', doRegister);
|
$('#registerForm').addEventListener('submit', doRegister);
|
||||||
$('#regPassword').addEventListener('input', updateRegStrength);
|
$('#regPassword').addEventListener('input', updateRegStrength);
|
||||||
$('#entryPassword').addEventListener('input', updateEntryStrength);
|
|
||||||
|
|
||||||
// Top-bar
|
// Top-bar
|
||||||
function applyViewMode() {
|
function applyViewMode() {
|
||||||
@@ -7446,14 +7236,6 @@ async function init() {
|
|||||||
closeSettings();
|
closeSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Entry modal
|
|
||||||
$('#entryForm').addEventListener('submit', saveEntry);
|
|
||||||
$('#entrySaveBtn').addEventListener('click', saveEntry);
|
|
||||||
$$('#entryModal [data-close]').forEach(b => b.addEventListener('click', closeEntryModal));
|
|
||||||
$('#entryPwToggle').addEventListener('click', () => {
|
|
||||||
const input = $('#entryPassword');
|
|
||||||
input.type = input.type === 'password' ? 'text' : 'password';
|
|
||||||
});
|
|
||||||
$('#loginPwToggle').addEventListener('click', () => {
|
$('#loginPwToggle').addEventListener('click', () => {
|
||||||
const input = $('#loginPassword');
|
const input = $('#loginPassword');
|
||||||
input.type = input.type === 'password' ? 'text' : 'password';
|
input.type = input.type === 'password' ? 'text' : 'password';
|
||||||
@@ -7467,30 +7249,6 @@ async function init() {
|
|||||||
const p = document.getElementById('loginPassword');
|
const p = document.getElementById('loginPassword');
|
||||||
if (p) p.focus();
|
if (p) p.focus();
|
||||||
});
|
});
|
||||||
$('#entryPwGen').addEventListener('click', openGen);
|
|
||||||
|
|
||||||
// Chip input (tags)
|
|
||||||
$('#entryTagsInput').addEventListener('click', () => $('#entryTagsField').focus());
|
|
||||||
$('#entryTagsField').addEventListener('keydown', e => {
|
|
||||||
const field = e.target;
|
|
||||||
if (e.key === 'Enter' || e.key === ',') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!selectActiveSuggestion()) {
|
|
||||||
addTag(field.value);
|
|
||||||
field.value = '';
|
|
||||||
closeChipSuggest();
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Backspace' && !field.value && editingTags.length) {
|
|
||||||
editingTags.pop();
|
|
||||||
renderChips();
|
|
||||||
} else if (e.key === 'ArrowDown') { e.preventDefault(); moveChipSuggest(+1); }
|
|
||||||
else if (e.key === 'ArrowUp') { e.preventDefault(); moveChipSuggest(-1); }
|
|
||||||
else if (e.key === 'Escape') { closeChipSuggest(); }
|
|
||||||
});
|
|
||||||
$('#entryTagsField').addEventListener('input', openChipSuggest);
|
|
||||||
$('#entryTagsField').addEventListener('focus', openChipSuggest);
|
|
||||||
$('#entryTagsField').addEventListener('blur', () => setTimeout(closeChipSuggest, 150));
|
|
||||||
|
|
||||||
// Generator
|
// Generator
|
||||||
$('#genLen').addEventListener('input', genPassword);
|
$('#genLen').addEventListener('input', genPassword);
|
||||||
$$('#genModal input[type=checkbox]').forEach(c => c.addEventListener('change', genPassword));
|
$$('#genModal input[type=checkbox]').forEach(c => c.addEventListener('change', genPassword));
|
||||||
@@ -7507,12 +7265,11 @@ async function init() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#genUse').addEventListener('click', () => {
|
$('#genUse').addEventListener('click', () => {
|
||||||
|
// Only the slideover editor consumes a generated password now (the
|
||||||
|
// 'standalone' target hides #genUse). The old entry-modal target is gone.
|
||||||
if (genTarget === 'slideover') {
|
if (genTarget === 'slideover') {
|
||||||
const soPw = $('#soPassword');
|
const soPw = $('#soPassword');
|
||||||
if (soPw) { soPw.value = genCurrent; soDirtyCheck(); }
|
if (soPw) { soPw.value = genCurrent; soDirtyCheck(); }
|
||||||
} else {
|
|
||||||
$('#entryPassword').value = genCurrent;
|
|
||||||
updateEntryStrength();
|
|
||||||
}
|
}
|
||||||
closeGen();
|
closeGen();
|
||||||
});
|
});
|
||||||
@@ -8085,10 +7842,6 @@ async function init() {
|
|||||||
closeHistoryModal();
|
closeHistoryModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!$('#entryModal').classList.contains('is-hidden')) {
|
|
||||||
closeEntryModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!$('#genModal').classList.contains('is-hidden')) {
|
if (!$('#genModal').classList.contains('is-hidden')) {
|
||||||
closeGen();
|
closeGen();
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user