// ========================================== // Chat Switchboard — NoteEditor Component // ========================================== // v0.25.0: Component wrapper for notes list + editor. // Hydrates the server-rendered note-editor.html partial. // Can run standalone (in editor tabbed pane) or coexist with // the existing notes.js panel system (on chat/notes surfaces). // // Usage: // const notes = NoteEditor.create({ // id: 'main', // matches template prefix // onOpenGraph: () => { ... }, // }); // notes.bind(); // await notes.loadNotes(); // notes.destroy(); // // The existing notes.js + PanelRegistry integration continues to // work on the chat surface. This component is for NEW mount points // (editor pane, future notes-studio layout). // // Exports: window.NoteEditor (function() { 'use strict'; const NoteEditor = { ...createComponentRegistry('NoteEditor'), create(opts) { const pfx = opts.id || 'main'; const instance = componentMixin({ id: pfx, rootEl: document.getElementById(pfx + 'NoteEditor'), listViewEl: document.getElementById(pfx + 'NotesListView'), editorViewEl: document.getElementById(pfx + 'NotesEditorView'), graphViewEl: document.getElementById(pfx + 'NotesGraphView'), listEl: document.getElementById(pfx + 'NotesList'), searchEl: document.getElementById(pfx + 'NotesSearchInput'), folderFilterEl: document.getElementById(pfx + 'NotesFolderFilter'), sortEl: document.getElementById(pfx + 'NotesSortSelect'), titleEl: document.getElementById(pfx + 'NoteEditorTitle'), folderEl: document.getElementById(pfx + 'NoteEditorFolder'), tagsEl: document.getElementById(pfx + 'NoteEditorTags'), contentContainerEl: document.getElementById(pfx + 'NoteEditorContentContainer'), readTitleEl: document.getElementById(pfx + 'NoteReadTitle'), readMetaEl: document.getElementById(pfx + 'NoteReadMeta'), readContentEl: document.getElementById(pfx + 'NoteReadContent'), editModeEl: document.getElementById(pfx + 'NoteEditMode'), readModeEl: document.getElementById(pfx + 'NoteReadMode'), backlinksEl: document.getElementById(pfx + 'NoteBacklinks'), backlinksListEl: document.getElementById(pfx + 'NoteBacklinksList'), backlinksCountEl: document.getElementById(pfx + 'NoteBacklinksCount'), onOpenGraph: opts.onOpenGraph || null, _editingNoteId: null, _currentNote: null, _sort: 'updated_desc', _cmEditor: null, // ── View Switching ─────────────────── showList() { if (this.listViewEl) this.listViewEl.style.display = ''; if (this.editorViewEl) this.editorViewEl.style.display = 'none'; if (this.graphViewEl) this.graphViewEl.style.display = 'none'; this._destroyCmEditor(); }, showEditor() { if (this.listViewEl) this.listViewEl.style.display = 'none'; if (this.editorViewEl) this.editorViewEl.style.display = ''; if (this.graphViewEl) this.graphViewEl.style.display = 'none'; }, showGraph() { if (this.listViewEl) this.listViewEl.style.display = 'none'; if (this.editorViewEl) this.editorViewEl.style.display = 'none'; if (this.graphViewEl) this.graphViewEl.style.display = ''; }, // ── Note List ─────────────────────── async loadNotes(folder, searchQuery) { if (!this.listEl) return; this.listEl.innerHTML = '
Loading…
'; try { let notes; if (searchQuery) { const resp = await API.searchNotes(searchQuery); notes = resp.data || []; } else { const folderVal = folder || this.folderFilterEl?.value || ''; const resp = await API.listNotes(100, 0, folderVal, '', this._sort); notes = resp.data || []; } if (notes.length === 0) { this.listEl.innerHTML = '
' + (searchQuery ? 'No results found' : 'No notes yet') + '
'; return; } this.listEl.innerHTML = ''; const self = this; notes.forEach(note => { const item = document.createElement('div'); item.className = 'note-item'; item.dataset.noteId = note.id; const title = note.title || 'Untitled'; const preview = (note.content || '').slice(0, 80).replace(/\n/g, ' '); const date = note.updated_at ? new Date(note.updated_at).toLocaleDateString() : ''; item.innerHTML = '
' + '
' + esc(title) + '
' + '
' + esc(preview) + '
' + '
' + esc(date) + '
' + '
'; item.addEventListener('click', () => self.openNote(note.id)); self.listEl.appendChild(item); }); } catch (e) { this.listEl.innerHTML = '
Failed to load: ' + esc(e.message) + '
'; } }, async loadFolders() { if (!this.folderFilterEl) return; try { const resp = await API.listNoteFolders(); const folders = resp.data || resp || []; // Keep "All folders" option, rebuild the rest this.folderFilterEl.innerHTML = ''; folders.forEach(f => { const opt = document.createElement('option'); opt.value = f; opt.textContent = f; this.folderFilterEl.appendChild(opt); }); } catch (_) { /* ignore */ } }, // ── Note CRUD ─────────────────────── async openNote(noteId) { this.showEditor(); if (noteId) { this._editingNoteId = noteId; try { this._currentNote = await API.getNote(noteId); this._populateFields(this._currentNote); this._showReadMode(); } catch (e) { if (typeof UI !== 'undefined') UI.toast('Failed to load note: ' + e.message, 'error'); this.showList(); } } else { this._editingNoteId = null; this._currentNote = null; this._clearFields(); this._showEditMode(); if (this.titleEl) this.titleEl.focus(); } }, async saveNote() { const title = this.titleEl?.value?.trim() || ''; const content = this._getContent(); const folder = this.folderEl?.value?.trim() || ''; const tagsStr = this.tagsEl?.value || ''; const tags = tagsStr.split(',').map(t => t.trim()).filter(Boolean); if (!title && !content) { if (typeof UI !== 'undefined') UI.toast('Note is empty', 'warning'); return; } try { if (this._editingNoteId) { await API.updateNote(this._editingNoteId, { title, content, folder_path: folder, tags }); } else { const resp = await API.createNote(title, content, folder, tags); this._editingNoteId = resp.id || resp.data?.id; } this._currentNote = { ...this._currentNote, title, content, folder_path: folder, tags }; this._showReadMode(); if (typeof UI !== 'undefined') UI.toast('Note saved', 'success'); } catch (e) { if (typeof UI !== 'undefined') UI.toast('Save failed: ' + e.message, 'error'); } }, async deleteNote() { if (!this._editingNoteId) return; const ok = typeof showConfirm === 'function' ? await showConfirm('Delete this note?') : window.confirm('Delete this note?'); if (!ok) return; try { await API.deleteNote(this._editingNoteId); if (typeof UI !== 'undefined') UI.toast('Note deleted', 'success'); this._editingNoteId = null; this._currentNote = null; this.showList(); this.loadNotes(); } catch (e) { if (typeof UI !== 'undefined') UI.toast('Delete failed: ' + e.message, 'error'); } }, // ── Read / Edit / Preview Modes ───── _showReadMode() { if (this.editModeEl) this.editModeEl.style.display = 'none'; if (this.readModeEl) this.readModeEl.style.display = ''; const n = this._currentNote; if (!n) return; if (this.readTitleEl) this.readTitleEl.textContent = n.title || 'Untitled'; if (this.readMetaEl) { const parts = []; if (n.folder_path) parts.push('📁 ' + n.folder_path); if (n.tags?.length) parts.push('🏷 ' + n.tags.join(', ')); if (n.updated_at) parts.push(new Date(n.updated_at).toLocaleString()); this.readMetaEl.textContent = parts.join(' · '); } if (this.readContentEl) { if (typeof marked !== 'undefined') { const raw = typeof marked.parse === 'function' ? marked.parse(n.content || '') : marked(n.content || ''); this.readContentEl.innerHTML = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw) : raw; } else { this.readContentEl.textContent = n.content || ''; } } // Show edit/delete buttons for read mode this._setBtn('NoteEditBtn', true); this._setBtn('NotePreviewBtn', false); this._setBtn('NoteCancelEditBtn', false); this._setBtn('NoteDeleteBtn', true); }, _showEditMode() { if (this.editModeEl) this.editModeEl.style.display = ''; if (this.readModeEl) this.readModeEl.style.display = 'none'; this._setBtn('NoteEditBtn', false); this._setBtn('NotePreviewBtn', true); this._setBtn('NoteCancelEditBtn', !!this._currentNote); this._setBtn('NoteDeleteBtn', !!this._editingNoteId); }, _showPreview() { // Toggle between edit and read-mode preview if (this.readModeEl?.style.display === 'none') { // Show preview of current edits const tempNote = { title: this.titleEl?.value || '', content: this._getContent(), folder_path: this.folderEl?.value || '', tags: (this.tagsEl?.value || '').split(',').map(t => t.trim()).filter(Boolean), }; const saved = this._currentNote; this._currentNote = tempNote; this._showReadMode(); this._currentNote = saved; // Swap buttons for preview-of-edits state this._setBtn('NoteEditBtn', true); this._setBtn('NotePreviewBtn', false); } else { this._showEditMode(); } }, // ── CM6 Editor Management ─────────── _getContent() { if (this._cmEditor) return this._cmEditor.getValue(); const ta = this.contentContainerEl?.querySelector('textarea'); return ta ? ta.value : ''; }, _setContent(text) { if (this._cmEditor) { this._cmEditor.setValue(text); return; } // Lazy-init CM6 if (this.contentContainerEl && window.CM?.noteEditor) { this.contentContainerEl.innerHTML = ''; this._cmEditor = CM.noteEditor(this.contentContainerEl, { value: text, darkMode: document.documentElement.getAttribute('data-theme') !== 'light', onLink: (title) => this._navigateToLink(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 []; } }, }); return; } // Textarea fallback const ta = this.contentContainerEl?.querySelector('textarea'); if (ta) ta.value = text; }, _destroyCmEditor() { if (this._cmEditor?.destroy) this._cmEditor.destroy(); this._cmEditor = null; }, async _navigateToLink(title) { try { const resp = await API.searchNotes(title); const notes = resp.data || []; const match = notes.find(n => n.title?.toLowerCase() === title.toLowerCase()); if (match) { this.openNote(match.id); } else { // Create new note with this title this._editingNoteId = null; this._currentNote = null; this._clearFields(); if (this.titleEl) this.titleEl.value = title; this._showEditMode(); } } catch (_) {} }, // ── Field Helpers ─────────────────── _populateFields(note) { if (this.titleEl) this.titleEl.value = note.title || ''; if (this.folderEl) this.folderEl.value = note.folder_path || ''; if (this.tagsEl) this.tagsEl.value = (note.tags || []).join(', '); this._setContent(note.content || ''); }, _clearFields() { if (this.titleEl) this.titleEl.value = ''; if (this.folderEl) this.folderEl.value = ''; if (this.tagsEl) this.tagsEl.value = ''; this._setContent(''); }, _setBtn(suffix, show) { const el = document.getElementById(pfx + suffix); if (el) el.style.display = show ? '' : 'none'; }, // ── Event Wiring ──────────────────── bind() { const $ = (id) => document.getElementById(pfx + id); const self = this; // Toolbar this._on($('NotesNewBtn'), 'click', () => self.openNote(null)); this._on($('NotesTodayBtn'), 'click', () => self._openDailyNote()); this._on($('NotesGraphBtn'), 'click', () => { if (self.onOpenGraph) self.onOpenGraph(); }); // Editor header this._on($('NotesBackBtn'), 'click', () => { self.showList(); self.loadNotes(); self.loadFolders(); }); this._on($('NoteSaveBtn'), 'click', () => self.saveNote()); this._on($('NoteDeleteBtn'), 'click', () => self.deleteNote()); this._on($('NoteEditBtn'), 'click', () => self._showEditMode()); this._on($('NotePreviewBtn'), 'click', () => self._showPreview()); this._on($('NoteCancelEditBtn'), 'click', () => { if (self._currentNote) { self._populateFields(self._currentNote); self._showReadMode(); } else self.showList(); }); // Filters this._on(this.folderFilterEl, 'change', () => { if (self.searchEl) self.searchEl.value = ''; self.loadNotes(self.folderFilterEl.value); }); this._on(this.sortEl, 'change', () => { self._sort = self.sortEl.value; self.loadNotes(); }); // Search with debounce let timer; this._on(this.searchEl, 'input', () => { clearTimeout(timer); const q = self.searchEl.value.trim(); timer = setTimeout(() => { if (q.length >= 2) { if (self.folderFilterEl) self.folderFilterEl.value = ''; self.loadNotes(null, q); } else if (q.length === 0) { self.loadNotes(); } }, 300); }); }, // ── Daily Note ────────────────────── async _openDailyNote() { const today = new Date().toISOString().slice(0, 10); try { const resp = await API.searchNotes(today); const notes = resp.data || []; const daily = notes.find(n => n.title === today || n.title?.startsWith(today)); if (daily) { this.openNote(daily.id); } else { this._editingNoteId = null; this._currentNote = null; this._clearFields(); if (this.titleEl) this.titleEl.value = today; this._setContent('# ' + today + '\n\n'); this.showEditor(); this._showEditMode(); } } catch (e) { if (typeof UI !== 'undefined') UI.toast('Failed to open daily note: ' + e.message, 'error'); } }, _cleanup() { this._destroyCmEditor(); this._editingNoteId = null; this._currentNote = null; }, }, NoteEditor); NoteEditor._register(pfx, instance); return instance; }, }; // ── Exports ───────────────────────────────── sb.ns('NoteEditor', NoteEditor); })();