// ==========================================
// Chat Switchboard – NotePanel Component
// ==========================================
// v0.30.1: Mountable, self-contained notes panel instances.
// Follows the ChatPane pattern: createComponentRegistry + componentMixin.
// NotePanel.primary is the backward-compat bridge (singleton used by notes.js shim).
//
// Exports: window.NotePanel
const NotePanel = {
...createComponentRegistry('NotePanel'),
create(opts) {
const id = opts.id || 'notes-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
const container = opts.container;
if (!container) {
console.error('[NotePanel] container is required');
return null;
}
const instance = componentMixin({
id,
container,
projectId: opts.projectId || null,
// ── State (was module-level in notes.js) ──
_editingNoteId: null,
_notesSelectMode: false,
_selectedNoteIds: new Set(),
_notesSort: 'updated_desc',
_noteEditor: null,
_currentNote: null,
_searchTimer: null,
_page: 1,
_pageSize: 50,
_hasMore: false,
// ── DOM query helper ──
$(sel) { return this.container.querySelector(sel); },
$$(sel) { return this.container.querySelectorAll(sel); },
// ── Notes List ──
async loadNotesList(folder, searchQuery, append) {
const list = this.$('#notesList');
if (!list) return;
if (!append) {
list.innerHTML = '
Loading\u2026
';
this._page = 1;
}
try {
let notes, isSearch = false;
if (searchQuery) {
isSearch = true;
const data = await API.searchNotes(searchQuery);
notes = data.data || [];
this._hasMore = false;
} else {
const folderVal = folder || this.$('#notesFolderFilter')?.value || '';
const offset = (this._page - 1) * this._pageSize;
const data = await API.listNotes(this._pageSize, offset, folderVal, '', this._notesSort);
notes = data.data || [];
this._hasMore = notes.length >= this._pageSize;
}
if (!append) list.innerHTML = '';
if (notes.length === 0 && !append) {
const folderVal = this.$('#notesFolderFilter')?.value || '';
list.innerHTML = `${isSearch ? 'No results found' : (folderVal ? 'No notes in this folder' : 'No notes yet. Create one or ask the AI to save a note.')}
`;
return;
}
// Remove old "Load more" button if present
list.querySelector('.notes-load-more')?.remove();
list.insertAdjacentHTML('beforeend', notes.map(n => this._noteListItem(n, isSearch)).join(''));
// Pagination: "Load more" button
if (this._hasMore) {
const btn = document.createElement('button');
btn.className = 'btn-small notes-load-more';
btn.textContent = 'Load more\u2026';
btn.style.cssText = 'display:block;margin:8px auto;';
btn.addEventListener('click', () => {
this._page++;
this.loadNotesList(null, null, true);
});
list.appendChild(btn);
}
} catch (e) {
if (!append) list.innerHTML = `Failed to load: ${esc(e.message)}
`;
}
},
_noteListItem(note, isSearch) {
const tags = (note.tags || []).map(t => `${esc(t)}`).join('');
const folder = note.folder_path && note.folder_path !== '/' ? `${esc(note.folder_path)}` : '';
const preview = isSearch && note.headline
? `${this._highlightHeadline(note.headline)}
`
: `${esc((note.preview || note.content || '').slice(0, 120))}
`;
const time = _relativeTime(note.updated_at);
const checked = this._selectedNoteIds.has(note.id) ? 'checked' : '';
return `
${preview}
${folder}${tags}
`;
},
_highlightHeadline(headline) {
const escaped = esc(headline);
return escaped.replace(/\*\*(.+?)\*\*/g, '$1');
},
// ── Multi-select ──
_enterSelectMode() {
this._notesSelectMode = true;
this._selectedNoteIds.clear();
const bar = this.$('#notesSelectionBar');
if (bar) bar.style.display = '';
this.$$('.note-select-col').forEach(el => el.style.display = '');
this._updateSelectedCount();
},
_exitSelectMode() {
this._notesSelectMode = false;
this._selectedNoteIds.clear();
const bar = this.$('#notesSelectionBar');
if (bar) bar.style.display = 'none';
this.$$('.note-select-col').forEach(el => el.style.display = 'none');
this.$$('.note-item.selected').forEach(el => el.classList.remove('selected'));
this.$$('.note-checkbox').forEach(cb => cb.checked = false);
const sa = this.$('#notesSelectAll');
if (sa) sa.checked = false;
},
_toggleNoteSelect(noteId, forceState) {
if (!this._notesSelectMode) {
this._enterSelectMode();
}
const has = this._selectedNoteIds.has(noteId);
const newState = forceState !== undefined ? forceState : !has;
if (newState) this._selectedNoteIds.add(noteId);
else this._selectedNoteIds.delete(noteId);
const item = this.container.querySelector(`.note-item[data-note-id="${noteId}"]`);
if (item) {
item.classList.toggle('selected', newState);
const cb = item.querySelector('.note-checkbox');
if (cb) cb.checked = newState;
}
this._updateSelectedCount();
},
_toggleSelectAll(checked) {
this.$$('.note-checkbox').forEach(cb => {
const cbId = cb.dataset.id;
if (checked) this._selectedNoteIds.add(cbId);
else this._selectedNoteIds.delete(cbId);
cb.checked = checked;
cb.closest('.note-item')?.classList.toggle('selected', checked);
});
this._updateSelectedCount();
},
_updateSelectedCount() {
const el = this.$('#notesSelectedCount');
if (el) el.textContent = this._selectedNoteIds.size;
},
async _bulkDeleteSelected() {
if (this._selectedNoteIds.size === 0) return;
const count = this._selectedNoteIds.size;
if (!await showConfirm(`Delete ${count} note${count > 1 ? 's' : ''}?`)) return;
try {
const resp = await API.bulkDeleteNotes([...this._selectedNoteIds]);
UI.toast(`Deleted ${resp.deleted} note${resp.deleted !== 1 ? 's' : ''}`, 'success');
this._exitSelectMode();
await this.loadNotesList();
await this.loadNoteFolders();
} catch (e) { UI.toast(e.message, 'error'); }
},
// ── Folders ──
async loadNoteFolders() {
const sel = this.$('#notesFolderFilter');
if (!sel) return;
try {
const data = await API.listNoteFolders();
const folders = data.folders || [];
sel.innerHTML = '';
folders.forEach(f => {
sel.innerHTML += ``;
});
} catch (e) { /* non-critical */ }
},
// ── View switching ──
showNotesList() {
const lv = this.$('#notesListView');
const ev = this.$('#notesEditorView');
const gv = this.$('#notesGraphView');
if (lv) lv.style.display = '';
if (ev) ev.style.display = 'none';
if (gv) gv.style.display = 'none';
this._editingNoteId = null;
this._destroyNoteEditor();
const list = this.$('#notesList');
if (list) list.innerHTML = 'Loading\u2026
';
},
// ── Note Editor ──
copyNoteContent() {
if (!this._currentNote) return;
const text = `# ${this._currentNote.title || ''}\n\n${this._currentNote.content || ''}`;
navigator.clipboard.writeText(text).then(() => UI.toast('Copied', 'success'));
},
async openNoteEditor(noteId) {
const lv = this.$('#notesListView');
const ev = this.$('#notesEditorView');
if (lv) lv.style.display = 'none';
if (ev) ev.style.display = '';
if (noteId) {
this._editingNoteId = noteId;
try {
this._currentNote = await API.getNote(noteId);
this._populateEditFields(this._currentNote);
this._showNoteReadMode();
} catch (e) {
UI.toast('Failed to load note: ' + e.message, 'error');
this.showNotesList();
}
} else {
this._editingNoteId = null;
this._currentNote = null;
this._clearEditFields();
this._showNoteEditMode();
this.$('#noteEditorTitle')?.focus();
}
},
_populateEditFields(note) {
const t = this.$('#noteEditorTitle');
const f = this.$('#noteEditorFolder');
const g = this.$('#noteEditorTags');
if (t) t.value = note.title || '';
if (f) f.value = note.folder_path || '';
if (g) g.value = (note.tags || []).join(', ');
this._setNoteEditorContent(note.content || '');
},
_clearEditFields() {
const t = this.$('#noteEditorTitle');
const f = this.$('#noteEditorFolder');
const g = this.$('#noteEditorTags');
if (t) t.value = '';
if (f) f.value = '';
if (g) g.value = '';
this._setNoteEditorContent('');
},
_getNoteEditorContent() {
if (this._noteEditor) return this._noteEditor.getValue();
const ta = this.container.querySelector('#noteEditorContentContainer textarea');
return ta ? ta.value : '';
},
_setNoteEditorContent(text) {
if (this._noteEditor) {
this._noteEditor.setValue(text);
return;
}
const container = this.$('#noteEditorContentContainer');
if (!container) return;
if (window.CM?.noteEditor) {
container.innerHTML = '';
this._noteEditor = CM.noteEditor(container, {
value: text,
darkMode: document.documentElement.getAttribute('data-theme') !== 'light',
onChange: null,
onLink: (title) => {
this._navigateToLinkedNote(title);
},
linkCompleter: async (query) => {
if (!query || query.length < 1) return [];
try {
const resp = await API.searchNoteTitles(query, 8);
return (resp.data || []).map(n => ({ label: n.title, id: n.id }));
} catch { return []; }
},
});
} else {
if (!container.querySelector('textarea')) {
container.innerHTML = '';
}
const ta = container.querySelector('textarea');
if (ta) ta.value = text;
}
},
async _navigateToLinkedNote(title) {
try {
const resp = await API.searchNoteTitles(title, 1);
const match = (resp.data || []).find(n => n.title.toLowerCase() === title.toLowerCase());
if (match) {
await this.openNoteEditor(match.id);
} else {
if (await showConfirm(`Note "${title}" not found. Create it?`)) {
const created = await API.createNote(title, `# ${title}\n\n`, '/', []);
await this.openNoteEditor(created.id);
}
}
} catch (e) { UI.toast('Failed to navigate: ' + e.message, 'error'); }
},
_destroyNoteEditor() {
if (this._noteEditor) {
this._noteEditor.destroy();
this._noteEditor = null;
}
},
_showNoteReadMode() {
if (!this._currentNote) return;
const n = this._currentNote;
const titleEl = this.$('#noteReadTitle');
if (titleEl) titleEl.textContent = n.title || '';
const parts = [];
if (n.folder_path && n.folder_path !== '/') parts.push(`${esc(n.folder_path)}`);
(n.tags || []).forEach(t => parts.push(`${esc(t)}`));
const metaEl = this.$('#noteReadMeta');
if (metaEl) metaEl.innerHTML = parts.join(' ');
const noteReadEl = this.$('#noteReadContent');
if (noteReadEl) {
noteReadEl.innerHTML = formatMessage(n.content || '');
if (typeof runExtensionPostRender === 'function') runExtensionPostRender(noteReadEl);
this._renderWikilinksInReadMode(noteReadEl);
}
const delBtn = this.$('#noteDeleteBtn2');
if (delBtn) delBtn.style.display = this._editingNoteId ? '' : 'none';
const rm = this.$('#noteReadMode');
const em = this.$('#noteEditMode');
const eb = this.$('#noteEditBtn');
const pb = this.$('#notePreviewBtn');
if (rm) rm.style.display = '';
if (em) em.style.display = 'none';
if (eb) eb.style.display = '';
if (pb) pb.style.display = 'none';
if (this._editingNoteId) this._loadBacklinks(this._editingNoteId);
this._updateDailyNav(this._currentNote);
},
_showNoteEditMode() {
const rm = this.$('#noteReadMode');
const em = this.$('#noteEditMode');
const db = this.$('#noteDeleteBtn');
const cb = this.$('#noteCancelEditBtn');
const eb = this.$('#noteEditBtn');
const pb = this.$('#notePreviewBtn');
if (rm) rm.style.display = 'none';
if (em) em.style.display = '';
if (db) db.style.display = this._editingNoteId ? '' : 'none';
if (cb) cb.style.display = this._editingNoteId ? '' : 'none';
if (eb) eb.style.display = 'none';
if (pb) pb.style.display = '';
},
_showNotePreview() {
const title = this.$('#noteEditorTitle')?.value.trim() || '';
const content = this._getNoteEditorContent();
const folderVal = this.$('#noteEditorFolder')?.value.trim() || '';
const tagsStr = this.$('#noteEditorTags')?.value.trim() || '';
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
const titleEl = this.$('#noteReadTitle');
if (titleEl) titleEl.textContent = title || 'Untitled';
const parts = [];
if (folderVal) parts.push(`${esc(folderVal)}`);
tags.forEach(t => parts.push(`${esc(t)}`));
const metaEl = this.$('#noteReadMeta');
if (metaEl) metaEl.innerHTML = parts.join(' ');
const readEl = this.$('#noteReadContent');
if (readEl) {
readEl.innerHTML = formatMessage(content || '');
if (typeof runExtensionPostRender === 'function') runExtensionPostRender(readEl);
this._renderWikilinksInReadMode(readEl);
}
const rm = this.$('#noteReadMode');
const em = this.$('#noteEditMode');
const eb = this.$('#noteEditBtn');
const pb = this.$('#notePreviewBtn');
if (rm) rm.style.display = '';
if (em) em.style.display = 'none';
if (eb) eb.style.display = '';
if (pb) pb.style.display = 'none';
const delBtn = this.$('#noteDeleteBtn2');
if (delBtn) delBtn.style.display = 'none';
},
// ── CRUD ──
async saveNote() {
const title = this.$('#noteEditorTitle')?.value.trim() || '';
const content = this._getNoteEditorContent();
const folder = this.$('#noteEditorFolder')?.value.trim() || '';
const tagsStr = this.$('#noteEditorTags')?.value.trim() || '';
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
if (!title) { UI.toast('Title is required', 'warning'); return; }
if (!content) { UI.toast('Content is required', 'warning'); return; }
try {
if (this._editingNoteId) {
const updated = await API.updateNote(this._editingNoteId, { title, content, folder_path: folder, tags, mode: 'replace' });
this._currentNote = updated;
UI.toast('Note updated', 'success');
this._showNoteReadMode();
} else {
const created = await API.createNote(title, content, folder, tags);
this._editingNoteId = created.id;
this._currentNote = created;
UI.toast('Note created', 'success');
this._showNoteReadMode();
}
if (typeof invalidateNoteGraph === 'function') invalidateNoteGraph();
} catch (e) { UI.toast(e.message, 'error'); }
},
async deleteNote() {
if (!this._editingNoteId) return;
if (!await showConfirm('Delete this note?')) return;
try {
await API.deleteNote(this._editingNoteId);
UI.toast('Note deleted', 'success');
if (typeof invalidateNoteGraph === 'function') invalidateNoteGraph();
this.showNotesList();
await this.loadNotesList();
await this.loadNoteFolders();
} catch (e) { UI.toast(e.message, 'error'); }
},
// ── Backlinks ──
async _loadBacklinks(noteId) {
const container = this.$('#noteBacklinks');
const listEl = this.$('#noteBacklinksList');
const countEl = this.$('#noteBacklinksCount');
if (!container || !listEl || !countEl) return;
try {
const resp = await API.getNoteBacklinks(noteId);
const links = resp.data || [];
countEl.textContent = links.length;
container.style.display = links.length > 0 ? '' : 'none';
listEl.innerHTML = links.map(n => `
${esc(n.title)}
${esc(n.folder_path)}
`).join('');
} catch {
container.style.display = 'none';
}
},
toggleBacklinks() {
const list = this.$('#noteBacklinksList');
if (list) list.style.display = list.style.display === 'none' ? '' : 'none';
},
// ── Wikilink Read-Mode Rendering ──
_renderWikilinksInReadMode(containerEl) {
if (!containerEl) return;
const html = containerEl.innerHTML;
let transclusionId = 0;
const rendered = html.replace(
/(!?)\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g,
(match, bang, title, display) => {
const titleTrimmed = title.trim();
const displayText = display ? display.trim() : titleTrimmed;
if (bang === '!') {
const tid = `transclusion-${++transclusionId}`;
return ``;
}
return `${esc(displayText)}`;
}
);
containerEl.innerHTML = rendered;
this._resolveTransclusions(containerEl);
},
async _resolveTransclusions(containerEl) {
const embeds = containerEl.querySelectorAll('.transclusion-embed');
if (!embeds.length) return;
const cache = {};
for (const el of embeds) {
const title = el.dataset.title;
const contentEl = el.querySelector('.transclusion-content');
if (!title || !contentEl) continue;
try {
let note = cache[title.toLowerCase()];
if (!note) {
const resp = await API.searchNoteTitles(title, 1);
const match = (resp.data || []).find(n => n.title.toLowerCase() === title.toLowerCase());
if (!match) {
contentEl.innerHTML = 'Note not found';
continue;
}
note = await API.getNote(match.id);
cache[title.toLowerCase()] = note;
}
const safeContent = (note.content || '').replace(/!\[\[[^\]]+\]\]/g, (m) => {
const inner = m.match(/!\[\[([^\]|]+)/)?.[1]?.trim() || '';
return `[Embedded: ${inner}]`;
});
contentEl.innerHTML = formatMessage(safeContent);
if (typeof runExtensionPostRender === 'function') runExtensionPostRender(contentEl);
} catch {
contentEl.innerHTML = 'Failed to load';
}
}
},
// ── Daily Notes ──
async openDailyNote() {
const today = new Date().toISOString().slice(0, 10);
const title = `Daily \u2014 ${today}`;
const folder = '/daily/';
try {
const resp = await API.searchNoteTitles(title, 1);
const existing = (resp.data || []).find(
n => n.title.toLowerCase() === title.toLowerCase()
);
if (existing) {
await this.openNoteEditor(existing.id);
} else {
const content = `# ${today}\n\n## Tasks\n\n- [ ] \n\n## Notes\n\n`;
const created = await API.createNote(title, content, folder, ['daily']);
await this.openNoteEditor(created.id);
}
} catch (e) { UI.toast('Failed to open daily note: ' + e.message, 'error'); }
},
_isDailyNote(note) {
return note && /^Daily \u2014 \d{4}-\d{2}-\d{2}$/.test(note.title);
},
_updateDailyNav(note) {
let nav = this.$('#noteDailyNav');
if (!this._isDailyNote(note)) {
if (nav) nav.style.display = 'none';
return;
}
if (!nav) {
const toolbar = this.container.querySelector('.notes-editor-toolbar');
if (!toolbar) return;
nav = document.createElement('div');
nav.id = 'noteDailyNav';
nav.className = 'note-daily-nav';
nav.innerHTML = `
`;
toolbar.after(nav);
nav.querySelector('#dailyPrevBtn')?.addEventListener('click', () => this._navigateDaily(-1));
nav.querySelector('#dailyNextBtn')?.addEventListener('click', () => this._navigateDaily(1));
}
nav.style.display = '';
const dateStr = note.title.replace('Daily \u2014 ', '');
const label = nav.querySelector('#dailyDateLabel');
if (label) label.textContent = dateStr;
const today = new Date().toISOString().slice(0, 10);
const nextBtn = nav.querySelector('#dailyNextBtn');
if (nextBtn) nextBtn.disabled = dateStr >= today;
},
async _navigateDaily(offset) {
if (!this._currentNote || !this._isDailyNote(this._currentNote)) return;
const dateStr = this._currentNote.title.replace('Daily \u2014 ', '');
const d = new Date(dateStr + 'T12:00:00');
d.setDate(d.getDate() + offset);
const newDateStr = d.toISOString().slice(0, 10);
const title = `Daily \u2014 ${newDateStr}`;
try {
const resp = await API.searchNoteTitles(title, 1);
const existing = (resp.data || []).find(
n => n.title.toLowerCase() === title.toLowerCase()
);
if (existing) {
await this.openNoteEditor(existing.id);
} else {
const content = `# ${newDateStr}\n\n## Tasks\n\n- [ ] \n\n## Notes\n\n`;
const created = await API.createNote(title, content, '/daily/', ['daily']);
await this.openNoteEditor(created.id);
}
} catch (e) { UI.toast('Failed to navigate: ' + e.message, 'error'); }
},
// ── Save-to-Note (from chat messages) ──
async saveMessageToNote(msgIndex) {
const chat = App.chats?.find(c => c.id === App.activeId);
const msg = chat?.messages?.[msgIndex];
if (!msg) return;
const sel = window.getSelection();
let content = msg.content;
content = content.replace(/<(?:thinking|think)>[\s\S]*?<\/(?:thinking|think)>/gi, '').trim();
const msgId = msg.id || '';
const msgEl = msgId ? document.querySelector(`.message[data-msg-id="${msgId}"] .msg-text`) : null;
if (sel && sel.rangeCount > 0 && msgEl?.contains(sel.anchorNode)) {
const selected = sel.toString().trim();
if (selected) content = selected;
}
const firstLine = content.split('\n')[0].replace(/^#+\s*/, '').trim();
const defaultTitle = firstLine.slice(0, 60) || 'Chat excerpt';
this._showSaveToNoteModal({
content,
sourceChannelId: App.activeId || '',
sourceMessageId: msg.id || '',
defaultTitle,
});
},
_showSaveToNoteModal(opts) {
document.getElementById('saveToNoteModal')?.remove();
const modal = document.createElement('div');
modal.id = 'saveToNoteModal';
modal.className = 'modal-overlay active';
modal.innerHTML = `
`;
document.body.appendChild(modal);
let selectedAppendNote = null;
modal.querySelectorAll('input[name="saveNoteMode"]').forEach(radio => {
radio.addEventListener('change', (e) => {
const isAppend = e.target.value === 'append';
document.getElementById('saveNoteCreateFields').style.display = isAppend ? 'none' : '';
document.getElementById('saveNoteAppendFields').style.display = isAppend ? '' : 'none';
document.getElementById('saveNoteConfirmBtn').textContent = isAppend ? 'Append' : 'Create';
selectedAppendNote = null;
document.getElementById('saveNoteSearchResults').innerHTML = '';
});
});
let _searchTimer;
document.getElementById('saveNoteSearch')?.addEventListener('input', (e) => {
clearTimeout(_searchTimer);
const q = e.target.value.trim();
selectedAppendNote = null;
_searchTimer = setTimeout(async () => {
const resultsEl = document.getElementById('saveNoteSearchResults');
if (q.length < 2) { resultsEl.innerHTML = ''; return; }
try {
const resp = await API.searchNoteTitles(q, 8);
const notes = resp.data || [];
resultsEl.innerHTML = notes.map(n =>
`${esc(n.title)}
`
).join('') || 'No results
';
resultsEl.querySelectorAll('.save-note-result').forEach(el => {
el.addEventListener('click', () => {
resultsEl.querySelectorAll('.save-note-result').forEach(r => r.classList.remove('selected'));
el.classList.add('selected');
selectedAppendNote = { id: el.dataset.id, title: el.dataset.title };
document.getElementById('saveNoteSearch').value = el.dataset.title;
});
});
} catch { resultsEl.innerHTML = 'Search failed
'; }
}, 250);
});
document.getElementById('saveNoteCancelBtn').addEventListener('click', () => modal.remove());
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
document.getElementById('saveNoteConfirmBtn').addEventListener('click', async () => {
const isAppend = modal.querySelector('input[name="saveNoteMode"]:checked')?.value === 'append';
try {
if (isAppend) {
if (!selectedAppendNote) { UI.toast('Select a note to append to', 'warning'); return; }
await API.updateNote(selectedAppendNote.id, {
content: '\n\n---\n\n' + opts.content,
mode: 'append',
});
UI.toast(`Appended to "${selectedAppendNote.title}"`, 'success');
} else {
const title = document.getElementById('saveNoteTitle').value.trim();
if (!title) { UI.toast('Title is required', 'warning'); return; }
const folder = document.getElementById('saveNoteFolder').value.trim();
const tagsStr = document.getElementById('saveNoteTags').value.trim();
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
await API.createNote(title, opts.content, folder, tags, opts.sourceChannelId, opts.sourceMessageId);
UI.toast('Note created', 'success');
}
if (typeof invalidateNoteGraph === 'function') invalidateNoteGraph();
modal.remove();
} catch (e) { UI.toast(e.message, 'error'); }
});
document.getElementById('saveNoteTitle')?.focus();
document.getElementById('saveNoteTitle')?.select();
},
// ── Compound onclick wrappers (for data-action dispatch) ──
_toggleNoteSelectCb(el, noteId) {
this._toggleNoteSelect(noteId, el.checked);
},
_noteItemClick(noteId) {
if (this._notesSelectMode) {
this._toggleNoteSelect(noteId);
} else {
this.openNoteEditor(noteId);
}
},
// ── Lifecycle ──
_cleanup() {
this._destroyNoteEditor();
clearTimeout(this._searchTimer);
},
}, NotePanel);
NotePanel._register(id, instance);
return instance;
},
};
// ── Self-mount: build DOM + create + wire in one call ──
// Canonical entry point for all consumers (notes.js sidebar, editor package, sw.notes()).
// Uses the same IDs that NotePanel.create() expects — $() is container-scoped so no collisions.
NotePanel.mount = function (container, opts) {
const _opts = opts || {};
const el = document.createElement('div');
el.className = 'note-panel-root';
el.innerHTML =
'' +
'' +
'
' +
'' +
'' +
'' +
'' +
'
' +
'
' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'
' +
'' +
'0 selected' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' +
'';
container.appendChild(el);
// Create NotePanel instance — $() is container-scoped, so the IDs above "just work"
const np = this.create({ container: el, projectId: _opts.projectId || null });
// ── Wire all events (same as notes.js _registerNotesPanel) ──
const q = (sel) => el.querySelector(sel);
q('#notesNewBtn').addEventListener('click', () => np.openNoteEditor(null));
q('#notesTodayBtn').addEventListener('click', () => np.openDailyNote());
q('#notesGraphBtn').addEventListener('click', () => {
np.showGraph();
if (typeof openNoteGraph === 'function') openNoteGraph();
if (_opts.onOpenGraph) _opts.onOpenGraph();
});
q('#notesGraphBackBtn').addEventListener('click', () => {
np.showNotesList();
if (typeof closeNoteGraph === 'function') closeNoteGraph();
});
q('#notesSelectModeBtn').addEventListener('click', () => {
if (np._notesSelectMode) np._exitSelectMode();
else np._enterSelectMode();
});
q('#notesBackBtn').addEventListener('click', () => { np.showNotesList(); np.loadNotesList(); np.loadNoteFolders(); });
q('#noteSaveBtn').addEventListener('click', () => np.saveNote());
q('#noteDeleteBtn').addEventListener('click', () => np.deleteNote());
q('#noteDeleteBtn2').addEventListener('click', () => np.deleteNote());
q('#noteEditBtn').addEventListener('click', () => np._showNoteEditMode());
q('#notePreviewBtn').addEventListener('click', () => np._showNotePreview());
q('#noteCancelEditBtn').addEventListener('click', () => {
if (np._currentNote) { np._populateEditFields(np._currentNote); np._showNoteReadMode(); }
else np.showNotesList();
});
q('#notesFolderFilter').addEventListener('change', (e) => {
np.$('#notesSearchInput').value = '';
np._exitSelectMode();
np.loadNotesList(e.target.value);
});
q('#notesSortSelect').addEventListener('change', (e) => {
np._notesSort = e.target.value;
np._exitSelectMode();
np.loadNotesList();
});
q('#notesSelectAll').addEventListener('change', (e) => np._toggleSelectAll(e.target.checked));
q('#notesDeleteSelectedBtn').addEventListener('click', () => np._bulkDeleteSelected());
q('#notesCancelSelectBtn').addEventListener('click', () => np._exitSelectMode());
let _searchTimer;
q('#notesSearchInput').addEventListener('input', (e) => {
clearTimeout(_searchTimer);
const query = e.target.value.trim();
_searchTimer = setTimeout(() => {
np._exitSelectMode();
if (query.length >= 2) {
np.$('#notesFolderFilter').value = '';
np.loadNotesList(null, query);
} else if (query.length === 0) {
np.loadNotesList();
}
}, 300);
});
// Add showGraph view switcher
np.showGraph = function () {
const lv = np.$('#notesListView');
const ev = np.$('#notesEditorView');
const gv = np.$('#notesGraphView');
if (lv) lv.style.display = 'none';
if (ev) ev.style.display = 'none';
if (gv) gv.style.display = '';
};
return np;
};
// ── Exports ─────────────────────────────────
sb.ns('NotePanel', NotePanel);