/** Quick-add expansion, draft restoration and opening inline editors. */
export function createQuickAdd({
_applyDraftToNote,
_applyMasonry,
_buildForm,
app,
openPanel,
state,
uiModule
}) {
function _renderQuickAdd(body) {
const wrap = document.createElement('div');
wrap.className = 'notes-quick-add';
// 2-pill Note/Todo toggle mirrors the full form's type-seg (minus Draw :
// drawing happens in the expanded form). The pill that's active steers
// both the placeholder and the type the form opens in.
wrap.innerHTML = `
`;
body.appendChild(wrap);
const input = wrap.querySelector('.notes-quick-input');
const seg = wrap.querySelector('.notes-quick-type-seg');
let currentType = 'todo';
const setType = (t) => {
if (t !== 'note' && t !== 'todo') return;
currentType = t;
seg.classList.toggle('is-todo', t === 'todo');
seg.classList.toggle('is-note', t === 'note');
seg.querySelectorAll('.notes-quick-type-pill').forEach(p => {
const on = p.dataset.type === t;
p.classList.toggle('active', on);
p.setAttribute('aria-pressed', on ? 'true' : 'false');
});
input.placeholder = t === 'note' ? 'Add a note…' : 'Add a to-do…';
};
seg.querySelectorAll('.notes-quick-type-pill').forEach(p => {
p.addEventListener('click', (e) => {
e.stopPropagation();
setType(p.dataset.type);
});
});
// Click input or type → expand to full form
const expandToForm = (initialType = 'note', initialText = '') => {
app.setEditingId('__new__');
const form = _buildForm({ note_type: initialType });
form.classList.add('note-form-new');
if (initialText) {
const titleEl = form.querySelector('.note-form-title');
if (titleEl) titleEl.value = initialText;
}
const mobileGrid = body.closest('.notes-pane')?.classList.contains('notes-view-grid')
&& window.matchMedia('(max-width: 768px)').matches;
if (mobileGrid) {
form.style.gridColumn = '1 / -1';
form.style.gridRowEnd = 'span 64';
}
wrap.replaceWith(form);
_applyMasonry(body);
requestAnimationFrame(() => _applyMasonry(body));
const titleEl = form.querySelector('.note-form-title');
if (titleEl) {
titleEl.focus();
// Move caret to end
titleEl.setSelectionRange(titleEl.value.length, titleEl.value.length);
}
};
// Expand only on real intent: a click directly on the input, or actual
// typing. Focus alone : including focus stolen from a missed nearby
// click : no longer creates an empty form.
input.addEventListener('click', () => expandToForm(currentType, input.value));
input.addEventListener('input', () => expandToForm(currentType, input.value));
wrap.querySelector('[data-action="photo"]').addEventListener('click', (e) => {
e.stopPropagation();
expandToForm(currentType);
// Trigger photo input on the new form
setTimeout(() => document.querySelector('.note-form-photo-btn')?.click(), 50);
});
}
function _createNote(type = 'todo') {
const body = document.querySelector('#notes-pane .notes-pane-body');
if (!body || state.editingId === '__new__') return;
app.setEditingId('__new__');
// Restore an unsaved new-note draft if one survived a prior close/loss.
const { note: _n, restored } = _applyDraftToNote({ note_type: type }, '__new__');
const form = _buildForm(_n);
form.classList.add('note-form-new');
body.prepend(form);
const initialField = type === 'note'
? form.querySelector('.note-form-content')
: form.querySelector('.note-cl-text, .note-form-title');
initialField?.focus();
if (restored) uiModule.showToast('Restored unsaved note');
}
function newNote() {
if (!state.open) openPanel();
requestAnimationFrame(() => _createNote('note'));
}
function _editNote(id) {
const note = state.notes.find(n => n.id === id);
if (!note) return;
app.setEditingId(id);
const card = document.querySelector(`.note-card[data-note-id="${id}"]`);
if (!card) return;
// Restore an unsaved draft (from a prior connection loss / close) over
// the saved note so the user picks up where they left off.
const { note: _n, restored } = _applyDraftToNote(note, id);
const form = _buildForm(_n);
card.replaceWith(form);
if (restored) uiModule.showToast('Restored unsaved changes');
// Pinned notes live in the first masonry column : the edit form has
// column-span:all, which can leave the form rendered above the fold or
// visually buried under neighboring pinned cards. Bring it into view
// (and onto a higher stacking context) so editing a pinned note always
// pops to the top.
form.style.position = 'relative';
form.style.zIndex = '5';
// Grid view: the form keeps the CSS default `grid-row-end: span 16` (64px)
// after replaceWith, which is much shorter than the actual form. Recompute
// masonry so the form gets the correct row span and cards below stop
// overlapping it. ResizeObserver inside _applyMasonry keeps it in sync as
// the user types / adds checklist items.
const _body = form.closest('.notes-pane-body');
if (_body) {
_applyMasonry(_body);
requestAnimationFrame(() => _applyMasonry(_body));
}
requestAnimationFrame(() => {
try { form.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
catch { form.scrollIntoView(); }
});
// Pick the most useful field to focus. On phones especially, the user
// taps Edit to type : landing in the title when there's already a title
// (and likely a body to extend) loses momentum. Prefer the body textarea
// for plain notes, the first checklist item for todos, fall back to title.
const _focusBest = () => {
if (note.note_type === 'note' || !note.note_type) {
const ta = form.querySelector('.note-form-content');
if (ta) { ta.focus(); try { ta.setSelectionRange(ta.value.length, ta.value.length); } catch {} return; }
}
if (note.note_type === 'todo' || note.note_type === 'goal' || note.note_type === 'checklist') {
// Last non-empty checklist row, or the first row if all empty.
const rows = form.querySelectorAll('.note-cl-row .note-cl-text');
let target = null;
for (const inp of rows) { if ((inp.value || '').trim()) target = inp; }
target = target || rows[0];
if (target) { target.focus(); try { target.setSelectionRange(target.value.length, target.value.length); } catch {} return; }
}
const titleEl = form.querySelector('.note-form-title');
if (titleEl) titleEl.focus();
};
_focusBest();
}
return { _renderQuickAdd, _createNote, newNote, _editNote };
}