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 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 */
|
||||
.setting-row {
|
||||
|
||||
-54
@@ -1013,60 +1013,6 @@
|
||||
|
||||
</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 -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
@@ -5026,210 +5026,6 @@ async function requestCloseSlideOver() {
|
||||
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) {
|
||||
try {
|
||||
await api('/entries/' + id + '/restore', { method: 'POST', headers: authHeaders() });
|
||||
@@ -5802,12 +5598,12 @@ function genPassword() {
|
||||
$('#genPreview').textContent = p;
|
||||
}
|
||||
|
||||
// genTarget: 'entry' (insert into entry form) or 'standalone' (just copy/dismiss)
|
||||
let genTarget = 'entry';
|
||||
// genTarget: 'slideover' (insert into the editor) or 'standalone' (just copy)
|
||||
let genTarget = 'slideover';
|
||||
|
||||
function openGen(target) {
|
||||
genTarget = target || 'entry';
|
||||
// Show "Use" when targeting an editable field (entry modal or slide-over)
|
||||
genTarget = target || 'slideover';
|
||||
// Show "Use" when targeting the editor; hide it in standalone (copy-only).
|
||||
$('#genUse').style.display = (genTarget === 'standalone') ? 'none' : '';
|
||||
$('#genModal').classList.remove('is-hidden');
|
||||
genPassword();
|
||||
@@ -5832,11 +5628,6 @@ function updateRegStrength() {
|
||||
const p = $('#regPassword').value;
|
||||
$('#regStrengthBar').style.setProperty('--strength', computeStrength(p) + '%');
|
||||
}
|
||||
function updateEntryStrength() {
|
||||
const p = $('#entryPassword').value;
|
||||
$('#entryStrengthBar').style.setProperty('--strength', computeStrength(p) + '%');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// COMMAND PALETTE
|
||||
// ============================================================
|
||||
@@ -7187,7 +6978,6 @@ async function init() {
|
||||
$('#loginForm').addEventListener('submit', doLogin);
|
||||
$('#registerForm').addEventListener('submit', doRegister);
|
||||
$('#regPassword').addEventListener('input', updateRegStrength);
|
||||
$('#entryPassword').addEventListener('input', updateEntryStrength);
|
||||
|
||||
// Top-bar
|
||||
function applyViewMode() {
|
||||
@@ -7446,14 +7236,6 @@ async function init() {
|
||||
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', () => {
|
||||
const input = $('#loginPassword');
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
@@ -7467,30 +7249,6 @@ async function init() {
|
||||
const p = document.getElementById('loginPassword');
|
||||
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
|
||||
$('#genLen').addEventListener('input', genPassword);
|
||||
$$('#genModal input[type=checkbox]').forEach(c => c.addEventListener('change', genPassword));
|
||||
@@ -7507,12 +7265,11 @@ async function init() {
|
||||
}
|
||||
});
|
||||
$('#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') {
|
||||
const soPw = $('#soPassword');
|
||||
if (soPw) { soPw.value = genCurrent; soDirtyCheck(); }
|
||||
} else {
|
||||
$('#entryPassword').value = genCurrent;
|
||||
updateEntryStrength();
|
||||
}
|
||||
closeGen();
|
||||
});
|
||||
@@ -8085,10 +7842,6 @@ async function init() {
|
||||
closeHistoryModal();
|
||||
return;
|
||||
}
|
||||
if (!$('#entryModal').classList.contains('is-hidden')) {
|
||||
closeEntryModal();
|
||||
return;
|
||||
}
|
||||
if (!$('#genModal').classList.contains('is-hidden')) {
|
||||
closeGen();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user