# Frontend Script Development Guide This guide covers developing frontend scripts in Trilium Notes. Frontend scripts run in the browser context and can interact with the UI, modify behavior, and create custom functionality. ## Prerequisites - JavaScript/TypeScript knowledge - Understanding of browser APIs and DOM manipulation - Basic knowledge of Trilium's note system - Familiarity with async/await patterns ## Getting Started ### Creating a Frontend Script 1. Create a new code note with type "JS Frontend" 2. Add the `#run=frontendStartup` label to run on startup 3. Write your JavaScript code ```javascript // Basic frontend script api.addButtonToToolbar({ title: 'My Custom Button', icon: 'bx bx-star', action: async () => { await api.showMessage('Hello from custom script!'); } }); ``` ### Script Execution Context Frontend scripts run in the browser with access to: - Trilium's Frontend API (`api` global object) - Browser APIs (DOM, fetch, localStorage, etc.) - jQuery (`$` global) - All loaded libraries ## Frontend API Reference ### Core API Object The `api` object is globally available in all frontend scripts: ```javascript // Access current note const currentNote = api.getActiveContextNote(); // Get note by ID const note = await api.getNote('noteId123'); // Search notes const results = await api.searchForNotes('type:text @label=important'); ``` ### Note Operations #### Reading Notes ```javascript // Get active note const activeNote = api.getActiveContextNote(); console.log('Current note:', activeNote.title); // Get note by ID const note = await api.getNote('noteId123'); // Get note content const content = await note.getContent(); // Get note attributes const attributes = note.getAttributes(); const labels = note.getLabels(); const relations = note.getRelations(); // Get child notes const children = await note.getChildNotes(); // Get parent notes const parents = await note.getParentNotes(); ``` #### Creating Notes ```javascript // Create a simple note const newNote = await api.createNote( parentNoteId, 'New Note Title', 'Note content here' ); // Create note with options const note = await api.createNote( parentNoteId, 'Advanced Note', '
HTML content
', { type: 'text', mime: 'text/html', isProtected: false } ); // Create data note for storing JSON const dataNote = await api.createDataNote( parentNoteId, 'config', { key: 'value', settings: {} } ); ``` #### Modifying Notes ```javascript // Update note title await note.setTitle('New Title'); // Update note content await note.setContent('New content'); // Add label await note.addLabel('status', 'completed'); // Add relation await note.addRelation('relatedTo', targetNoteId); // Remove attribute await note.removeAttribute(attributeId); // Toggle label await note.toggleLabel('archived'); await note.toggleLabel('priority', 'high'); ``` ### UI Interaction #### Showing Messages ```javascript // Simple message await api.showMessage('Operation completed'); // Error message await api.showError('Something went wrong'); // Message with duration await api.showMessage('Saved!', 3000); // Persistent message const toast = await api.showPersistent({ title: 'Processing', message: 'Please wait...', icon: 'loader' }); // Close persistent message toast.close(); ``` #### Dialogs ```javascript // Confirmation dialog const confirmed = await api.showConfirmDialog({ title: 'Delete Note?', message: 'This action cannot be undone.', okButtonLabel: 'Delete', cancelButtonLabel: 'Keep' }); if (confirmed) { // Proceed with deletion } // Prompt dialog const input = await api.showPromptDialog({ title: 'Enter Name', message: 'Please enter a name for the new note:', defaultValue: 'Untitled' }); if (input) { await api.createNote(parentId, input, ''); } ``` ### Custom Commands #### Adding Menu Items ```javascript // Add to note context menu api.addContextMenuItemToNotes({ title: 'Copy Note ID', icon: 'bx bx-copy', handler: async (note) => { await navigator.clipboard.writeText(note.noteId); await api.showMessage('Note ID copied'); } }); // Add to toolbar api.addButtonToToolbar({ title: 'Quick Action', icon: 'bx bx-bolt', shortcut: 'ctrl+shift+q', action: async () => { // Your action here } }); ``` #### Registering Commands ```javascript // Register a global command api.bindGlobalShortcut('ctrl+shift+t', async () => { const note = api.getActiveContextNote(); const timestamp = new Date().toISOString(); await note.addLabel('lastAccessed', timestamp); await api.showMessage('Timestamp added'); }); // Add command palette action api.addCommandPaletteItem({ name: 'Toggle Dark Mode', description: 'Switch between light and dark themes', action: async () => { const currentTheme = await api.getOption('theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; await api.setOption('theme', newTheme); } }); ``` ### Event Handling #### Listening to Events ```javascript // Note switch event api.onNoteChange(async ({ note, previousNote }) => { console.log(`Switched from ${previousNote?.title} to ${note.title}`); // Update custom UI updateCustomPanel(note); }); // Content change event api.onNoteContentChange(async ({ note }) => { console.log(`Content changed for ${note.title}`); // Auto-save to external service await syncToExternalService(note); }); // Attribute change event api.onAttributeChange(async ({ note, attribute }) => { if (attribute.name === 'status' && attribute.value === 'completed') { await note.addLabel('completedDate', new Date().toISOString()); } }); ``` #### Custom Events ```javascript // Trigger custom event api.triggerEvent('myCustomEvent', { data: 'value' }); // Listen to custom event api.onCustomEvent('myCustomEvent', async (data) => { console.log('Custom event received:', data); }); ``` ### Working with Widgets ```javascript // Access widget system const widget = api.getWidget('NoteTreeWidget'); // Refresh widget await widget.refresh(); // Create custom widget container const container = api.createCustomWidget({ title: 'My Widget', position: 'left', render: async () => { return ` `; } }); ``` ## Complete Example: Auto-Formatting Script Here's a comprehensive example that automatically formats notes based on their type: ```javascript /** * Auto-Formatting Script * Automatically formats notes based on their type and content */ class NoteFormatter { constructor() { this.setupEventListeners(); this.registerCommands(); } setupEventListeners() { // Format on note save api.onNoteContentChange(async ({ note }) => { if (await this.shouldAutoFormat(note)) { await this.formatNote(note); } }); // Format when label added api.onAttributeChange(async ({ note, attribute }) => { if (attribute.type === 'label' && attribute.name === 'autoFormat' && attribute.value === 'true') { await this.formatNote(note); } }); } registerCommands() { // Add toolbar button api.addButtonToToolbar({ title: 'Format Note', icon: 'bx bx-text', shortcut: 'ctrl+shift+f', action: async () => { const note = api.getActiveContextNote(); await this.formatNote(note); await api.showMessage('Note formatted'); } }); // Add context menu item api.addContextMenuItemToNotes({ title: 'Auto-Format', icon: 'bx bx-magic', handler: async (note) => { await this.formatNote(note); } }); } async shouldAutoFormat(note) { // Check if note has autoFormat label const labels = note.getLabels(); return labels.some(l => l.name === 'autoFormat' && l.value === 'true'); } async formatNote(note) { const type = note.type; switch (type) { case 'text': await this.formatTextNote(note); break; case 'code': await this.formatCodeNote(note); break; case 'book': await this.formatBookNote(note); break; } } async formatTextNote(note) { let content = await note.getContent(); // Apply formatting rules content = this.addTableOfContents(content); content = this.formatHeadings(content); content = this.formatLists(content); content = this.addMetadata(content, note); await note.setContent(content); } async formatCodeNote(note) { const content = await note.getContent(); const language = note.getLabelValue('language') || 'javascript'; // Add syntax highlighting hints if (!note.hasLabel('language')) { await note.addLabel('language', language); } // Format based on language if (language === 'javascript' || language === 'typescript') { await this.formatJavaScript(note, content); } else if (language === 'python') { await this.formatPython(note, content); } } async formatBookNote(note) { // Organize child notes const children = await note.getChildNotes(); // Sort chapters const chapters = children.filter(n => n.hasLabel('chapter')); chapters.sort((a, b) => { const aNum = parseInt(a.getLabelValue('chapter')) || 999; const bNum = parseInt(b.getLabelValue('chapter')) || 999; return aNum - bNum; }); // Generate table of contents const toc = this.generateBookTOC(chapters); await note.setContent(toc); } addTableOfContents(content) { const $content = $('