/** * EntryForm — create/edit credential form. */ import { Component } from './component.js' import { addEntry, updateEntry, getEntryById, getGroups } from '../lib/storage/db.js' import { encrypt, decrypt } from '../lib/crypto/crypto.js' import { createEntry, updateEntry as updateEntryModel, validateEntry, isTrashGroup } from '../lib/models/schema.js' import { generatePassword } from '../lib/crypto/crypto.js' import { app } from '../lib/stores/app.js' import { search } from '../lib/stores/search.js' import { autofocus } from '../lib/autofocus.js' export class EntryForm extends Component { /** @param {{ entryId: string|null, onSave: Function, onCancel: Function }} props */ constructor(container, props = {}) { super(container) this.entryId = props.entryId || null this.onSave = props.onSave || (() => {}) this.onCancel = props.onCancel || (() => {}) this.title = '' this.username = '' this.password = '' this.url = '' this.notes = '' this.groupId = '' this.passwordVisible = false this.groups = [] this.loading = true this.error = '' this.saving = false this.isEdit = false this.formErrors = [] } mount() { super.mount() this.#loadForm() return this } render() { this.el = this.ce('div', { className: 'entry-form' }) this.#renderContent() return this.el } #renderContent() { this.el.innerHTML = '' if (this.loading) { this.el.appendChild(this.ce('div', { className: 'loading', textContent: 'Loading...' })) return } if (this.error && !this.isEdit) { this.el.appendChild(this.ce('div', { className: 'error-banner', textContent: this.error })) return } const form = this.ce('form', { className: 'ef-form-card', id: 'entry-form' }) // Validation errors if (this.formErrors.length > 0) { const errDiv = this.ce('div', { className: 'validation-errors' }) for (const err of this.formErrors) { errDiv.appendChild(this.ce('div', { className: 'validation-error', textContent: `⚠ ${err}` })) } form.appendChild(errDiv) } // Title form.appendChild(this.ce('div', { className: 'form-group' }, this.ce('label', { htmlFor: 'title', textContent: 'Title *' }), this.ce('input', { id: 'title', type: 'text', placeholder: 'e.g. GitHub, Gmail', value: this.title }), )) // Username form.appendChild(this.ce('div', { className: 'form-group' }, this.ce('label', { htmlFor: 'username', textContent: 'Username / Email' }), this.ce('input', { id: 'username', type: 'text', placeholder: 'username or email', value: this.username }), )) // Password const pwdGroup = this.ce('div', { className: 'form-group' }, this.ce('label', { htmlFor: 'password', textContent: 'Password *' }), this.ce('div', { className: 'password-input-group' }, this.ce('input', { id: 'password', type: this.passwordVisible ? 'text' : 'password', placeholder: 'Password', value: this.password, }), this.ce('button', { type: 'button', className: 'btn btn-ghost btn-sm', id: 'toggle-pwd', textContent: this.passwordVisible ? '🙈' : '👁', title: 'Toggle visibility' }), this.ce('button', { type: 'button', className: 'btn btn-ghost btn-sm', id: 'generate-pwd', textContent: '🎲', title: 'Generate password' }), ), ) form.appendChild(pwdGroup) // URL form.appendChild(this.ce('div', { className: 'form-group' }, this.ce('label', { htmlFor: 'url', textContent: 'URL' }), this.ce('input', { id: 'url', type: 'url', placeholder: 'https://example.com', value: this.url }), )) // Group const select = this.ce('select', { id: 'group' }) const defaultOpt = this.ce('option', { value: '' }, this.text('No group')) select.appendChild(defaultOpt) for (const group of this.groups) { if (isTrashGroup(group.id)) continue const opt = this.ce('option', { value: group.id }, this.text(group.name)) if (group.id === this.groupId) opt.selected = true select.appendChild(opt) } form.appendChild(this.ce('div', { className: 'form-group' }, this.ce('label', { htmlFor: 'group', textContent: 'Group' }), select, )) // Notes form.appendChild(this.ce('div', { className: 'form-group' }, this.ce('label', { htmlFor: 'notes', textContent: 'Notes' }), this.ce('textarea', { id: 'notes', placeholder: 'Any additional notes...' }), )) // Actions form.appendChild(this.ce('div', { className: 'ef-form-actions' }, this.ce('button', { type: 'submit', className: 'btn btn-primary', disabled: this.saving, textContent: this.saving ? 'Saving...' : (this.isEdit ? '💾 Update' : '➕ Create'), }), this.ce('button', { type: 'button', className: 'btn btn-ghost', id: 'cancel-btn', textContent: 'Cancel' }), )) this.el.appendChild(form) // Wire up form events const formEl = this.q('#entry-form') if (formEl) this.on(formEl, 'submit', this.#handleSubmit) const cancelBtn = this.q('#cancel-btn') if (cancelBtn) this.on(cancelBtn, 'click', () => this.onCancel()) const togglePwd = this.q('#toggle-pwd') if (togglePwd) { this.on(togglePwd, 'click', () => { this.passwordVisible = !this.passwordVisible const pwdInput = this.q('#password') if (pwdInput) pwdInput.type = this.passwordVisible ? 'text' : 'password' togglePwd.textContent = this.passwordVisible ? '🙈' : '👁' }) } const generatePwd = this.q('#generate-pwd') if (generatePwd) { this.on(generatePwd, 'click', () => { this.password = generatePassword({ length: 16 }) const pwdInput = this.q('#password') if (pwdInput) pwdInput.value = this.password }) } // Wire input changes const titleInput = this.q('#title') if (titleInput) this.on(titleInput, 'input', (e) => { this.title = e.target.value }) const usernameInput = this.q('#username') if (usernameInput) this.on(usernameInput, 'input', (e) => { this.username = e.target.value }) const pwdInput = this.q('#password') if (pwdInput) this.on(pwdInput, 'input', (e) => { this.password = e.target.value }) const urlInput = this.q('#url') if (urlInput) this.on(urlInput, 'input', (e) => { this.url = e.target.value }) const notesInput = this.q('#notes') if (notesInput) this.on(notesInput, 'input', (e) => { this.notes = e.target.value }) const groupSelect = this.q('#group') if (groupSelect) this.on(groupSelect, 'change', (e) => { this.groupId = e.target.value }) // Autofocus title on new entries if (!this.isEdit && titleInput) autofocus(titleInput, true) } async #loadForm() { this.loading = true try { this.groups = await getGroups() if (this.entryId) { this.isEdit = true const entry = await getEntryById(this.entryId) if (entry) { this.title = entry.title this.username = entry.username this.password = await decrypt(entry.encryptedPassword, app.encryptionKey) this.url = entry.url || '' this.notes = entry.notes || '' this.groupId = entry.groupId || '' } else { this.error = 'Entry not found' } } else { const active = search.activeGroupId this.groupId = (active !== 'all' && active !== 'trash') ? active : '' } } catch (e) { this.error = 'Failed to load form: ' + e.message } this.loading = false this.#renderContent() } #handleSubmit = async (e) => { e.preventDefault() this.formErrors = [] this.error = '' this.saving = true // Read current values from inputs const titleInput = this.q('#title') if (titleInput) this.title = titleInput.value const usernameInput = this.q('#username') if (usernameInput) this.username = usernameInput.value const pwdInput = this.q('#password') if (pwdInput) this.password = pwdInput.value const urlInput = this.q('#url') if (urlInput) this.url = urlInput.value const notesInput = this.q('#notes') if (notesInput) this.notes = notesInput.value const groupSelect = this.q('#group') if (groupSelect) this.groupId = groupSelect.value try { const validation = validateEntry({ title: this.title, username: this.username, encryptedPassword: this.password }) if (!validation.valid) { this.formErrors = validation.errors this.saving = false this.#renderContent() return } const encryptedPassword = await encrypt(this.password, app.encryptionKey) if (this.isEdit) { const existing = await getEntryById(this.entryId) const updated = updateEntryModel(existing, { title: this.title, username: this.username, encryptedPassword, url: this.url, notes: this.notes, groupId: this.groupId, }) await updateEntry(updated) } else { const entry = createEntry({ title: this.title, username: this.username, encryptedPassword, url: this.url, notes: this.notes, groupId: this.groupId, }) await addEntry(entry) } this.onSave() } catch (e) { this.error = 'Failed to save: ' + e.message } this.saving = false this.#renderContent() } }