# 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 `

Custom Content

`; } }); ``` ## 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 = $('
').html(content); const headings = $content.find('h1, h2, h3'); if (headings.length < 3) return content; let toc = '
\n

Table of Contents

\n\n
\n\n'; return toc + $content.html(); } formatHeadings(content) { const $content = $('
').html(content); // Ensure proper heading hierarchy let lastLevel = 0; $content.find('h1, h2, h3, h4, h5, h6').each((i, heading) => { const $h = $(heading); const level = parseInt(heading.tagName.substring(1)); // Fix heading jumps (e.g., h1 -> h3 becomes h1 -> h2) if (level > lastLevel + 1) { const newTag = `h${lastLevel + 1}`; const $newHeading = $(`<${newTag}>`).html($h.html()); $h.replaceWith($newHeading); } lastLevel = level; }); return $content.html(); } formatLists(content) { const $content = $('
').html(content); // Add classes to lists for styling $content.find('ul').addClass('formatted-list'); $content.find('ol').addClass('formatted-list numbered'); // Add checkboxes to task lists $content.find('li').each((i, li) => { const $li = $(li); const text = $li.text(); if (text.startsWith('[ ] ')) { $li.html(` ${text.substring(4)}`); $li.addClass('task-item'); } else if (text.startsWith('[x] ')) { $li.html(` ${text.substring(4)}`); $li.addClass('task-item completed'); } }); return $content.html(); } addMetadata(content, note) { const metadata = { lastFormatted: new Date().toISOString(), wordCount: content.replace(/<[^>]*>/g, '').split(/\s+/).length, noteId: note.noteId }; const metadataHtml = ` `; return content + metadataHtml; } async formatJavaScript(note, content) { // Add JSDoc comments if missing const lines = content.split('\n'); const formatted = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Detect function declarations if (line.match(/^\s*(async\s+)?function\s+\w+/)) { if (i === 0 || !lines[i-1].includes('*/')) { formatted.push('/**'); formatted.push(' * [Description]'); formatted.push(' */'); } } formatted.push(line); } await note.setContent(formatted.join('\n')); } async formatPython(note, content) { // Add docstrings if missing const lines = content.split('\n'); const formatted = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Detect function definitions if (line.match(/^\s*def\s+\w+/)) { formatted.push(line); if (i + 1 < lines.length && !lines[i + 1].includes('"""')) { formatted.push(' """[Description]"""'); } } else { formatted.push(line); } } await note.setContent(formatted.join('\n')); } generateBookTOC(chapters) { let toc = '

Table of Contents

\n
    \n'; for (const chapter of chapters) { const num = chapter.getLabelValue('chapter'); const title = chapter.title; toc += `
  1. ${num}. ${title}
  2. \n`; } toc += '
'; return toc; } } // Initialize formatter const formatter = new NoteFormatter(); // Add settings UI api.addSettingsTab({ tabId: 'autoFormat', title: 'Auto-Format', render: () => { return `

Auto-Format Settings

Format Rules

`; } }); // Save settings function window.saveFormatSettings = async () => { const settings = { enableAutoFormat: document.getElementById('enableAutoFormat').checked, formatOnSave: document.getElementById('formatOnSave').checked, addTOC: document.getElementById('addTOC').checked, rules: JSON.parse(document.getElementById('formatRules').value) }; await api.setOption('autoFormatSettings', JSON.stringify(settings)); await api.showMessage('Settings saved'); }; console.log('Auto-formatting script loaded'); ``` ## Advanced Techniques ### Working with External APIs ```javascript // Fetch data from external API async function fetchExternalData() { try { const response = await fetch('https://api.example.com/data', { headers: { 'Authorization': `Bearer ${await api.getOption('apiKey')}` } }); const data = await response.json(); // Store in note const dataNote = await api.createDataNote( 'root', 'External Data', data ); await api.showMessage('Data imported successfully'); } catch (error) { await api.showError(`Failed to fetch data: ${error.message}`); } } ``` ### State Management ```javascript // Create a state manager class StateManager { constructor() { this.state = {}; this.subscribers = []; this.loadState(); } async loadState() { const stored = await api.getOption('scriptState'); if (stored) { this.state = JSON.parse(stored); } } async setState(key, value) { this.state[key] = value; await this.saveState(); this.notifySubscribers(key, value); } getState(key) { return this.state[key]; } async saveState() { await api.setOption('scriptState', JSON.stringify(this.state)); } subscribe(callback) { this.subscribers.push(callback); } notifySubscribers(key, value) { this.subscribers.forEach(cb => cb(key, value)); } } const state = new StateManager(); ``` ### Custom UI Components ```javascript // Create custom panel class CustomPanel { constructor() { this.createPanel(); } createPanel() { const $panel = $(`

Custom Panel

`); $('body').append($panel); // Add styles $('