`;
}
function _wireGoalForm(form, container) {
if (!container) return;
// _wireHashtag is a closure inside _buildForm : out of scope here. Inline
// the same behavior (type "#foo " in the description → tag added to the
// form's label input) so editing a goal note doesn't ReferenceError.
const desc = container.querySelector('.note-form-goal-desc');
const labelInput = form?.querySelector('.note-form-label');
if (desc && labelInput) {
const tagRe = /(^|\s)#([A-Za-z0-9][\w-]*)\s$/;
desc.addEventListener('input', () => {
const m = tagRe.exec(desc.value);
if (!m) return;
const tag = m[2];
// Same dedup-after-stripping fix as the plain note hashtag handler.
const existing = labelInput.value.trim().split(/\s+/).filter(Boolean);
const stripped = existing.map(t => t.replace(/^#+/, ''));
if (!stripped.includes(tag)) {
existing.push('#' + tag);
labelInput.value = existing.join(' ');
labelInput.classList.add('flash-once');
setTimeout(() => labelInput.classList.remove('flash-once'), 600);
}
const cut = desc.value.length - m[0].length + m[1].length;
desc.value = desc.value.slice(0, cut);
});
}
// Always wire the checklist. The previous gate on a `note-form-goal-fresh`
// class was dead code : nothing ever set that class, so the editor never
// hooked up add/drag/Tab.
_wireChecklist(container);
}
function _buildChecklistHtml(items) {
let html = '
';
for (const item of items) {
const indent = Math.min(item.indent || 0, 3);
html += `
⋮⋮
`;
}
// `type="button"` matters on mobile : without it some browsers treat
// bare
`;
return html;
}
function _wireRow(row, container) {
row.querySelector('.note-cl-rm')?.addEventListener('click', () => row.remove());
row.querySelector('.note-cl-dot')?.addEventListener('click', () => {
const wasDone = row.classList.contains('done');
row.classList.toggle('done');
// The custom dot has no native input/change event. Notify the form so
// mobile edit mode switches its action from Archive back to Update.
container.dispatchEvent(new Event('input', { bubbles: true }));
const becameDone = !wasDone; // we just flipped it
const dot = row.querySelector('.note-cl-dot');
const dRect = (dot || row).getBoundingClientRect();
// Small confetti burst on each fresh check so the user gets a
// "well done" beat per item, not just the grand-finale on all-done.
if (becameDone) {
spawnConfetti(dRect.left + dRect.width / 2, dRect.top + dRect.height / 2, 16);
}
// Bigger burst when the whole list is now done.
const rows = [...container.querySelectorAll('.note-cl-row')];
const hasText = rows.some(r => (r.querySelector('.note-cl-text')?.value || '').trim().length > 0);
if (hasText && rows.every(r => r.classList.contains('done') || !(r.querySelector('.note-cl-text')?.value || '').trim())) {
spawnConfetti(dRect.left + dRect.width / 2, dRect.top + dRect.height / 2, 60);
}
});
const txt = row.querySelector('.note-cl-text');
txt?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); container.querySelector('.note-cl-add')?.click(); }
else if (e.key === 'Tab') {
e.preventDefault();
const cur = parseInt(row.dataset.indent || '0');
const next = e.shiftKey ? Math.max(0, cur - 1) : Math.min(3, cur + 1);
row.dataset.indent = String(next);
row.style.paddingLeft = (next * 16) + 'px';
} else if (e.key === 'Backspace' && txt.value === '') {
e.preventDefault();
const prev = row.previousElementSibling;
row.remove();
if (prev && prev.classList.contains('note-cl-row')) prev.querySelector('.note-cl-text')?.focus();
}
});
// Drag handlers
row.addEventListener('dragstart', (e) => {
row.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', row.dataset.itemId); } catch {}
});
row.addEventListener('dragend', () => {
row.classList.remove('dragging');
container.querySelectorAll('.drop-before, .drop-after').forEach(el => el.classList.remove('drop-before', 'drop-after'));
});
}
function _wireChecklist(container) {
if (!container) return;
// Delegate the + Add click off the container so re-renders + mobile
// touch quirks don't leave the button dead. The previous direct
// `addEventListener` on the button silently broke on mobile when
// _wireChecklist ran more than once (or before the button was in DOM).
if (!container._addDelegated) {
container._addDelegated = true;
container.addEventListener('click', (ev) => {
const addBtn = ev.target.closest('.note-cl-add');
if (!addBtn || !container.contains(addBtn)) return;
ev.preventDefault();
ev.stopPropagation();
const inputs = container.querySelector('.note-checklist-inputs');
if (!inputs) return;
const row = document.createElement('div');
row.className = 'note-cl-row';
row.draggable = true;
row.dataset.itemId = _uid();
row.dataset.indent = '0';
row.innerHTML = `⋮⋮×`;
inputs.insertBefore(row, addBtn);
row.querySelector('.note-cl-text').focus();
_wireRow(row, container);
});
}
container.querySelectorAll('.note-cl-row').forEach(row => _wireRow(row, container));
// Drag-over handler on the inputs container
const inputs = container.querySelector('.note-checklist-inputs');
if (inputs) {
inputs.addEventListener('dragover', (e) => {
e.preventDefault();
const dragging = inputs.querySelector('.note-cl-row.dragging');
if (!dragging) return;
inputs.querySelectorAll('.drop-before, .drop-after').forEach(el => el.classList.remove('drop-before', 'drop-after'));
const rows = [...inputs.querySelectorAll('.note-cl-row:not(.dragging)')];
const after = rows.find(r => {
const box = r.getBoundingClientRect();
return e.clientY < box.top + box.height / 2;
});
if (after) {
after.classList.add('drop-before');
inputs.insertBefore(dragging, after);
} else if (rows.length) {
rows[rows.length - 1].classList.add('drop-after');
inputs.insertBefore(dragging, container.querySelector('.note-cl-add'));
}
});
inputs.addEventListener('dragleave', (e) => {
if (!inputs.contains(e.relatedTarget)) {
inputs.querySelectorAll('.drop-before, .drop-after').forEach(el => el.classList.remove('drop-before', 'drop-after'));
}
});
}
}
function _collectItems(form) {
const items = [];
form.querySelectorAll('.note-cl-row').forEach(row => {
const text = row.querySelector('.note-cl-text')?.value?.trim();
if (text) items.push({
id: row.dataset.itemId || _uid(),
text,
done: row.classList.contains('done'),
indent: parseInt(row.dataset.indent || '0'),
});
});
return items;
}
return { _buildGoalHtml, _wireGoalForm, _buildChecklistHtml, _wireRow, _wireChecklist, _collectItems };
}