/** * Paper Module JavaScript * AI Writing & Notes Application */ (function() { 'use strict'; // DOM Elements const elements = { editor: document.getElementById('editor-content'), title: document.getElementById('paper-title'), slashMenu: document.getElementById('slash-menu'), aiPanel: document.getElementById('ai-panel'), sidebar: document.getElementById('paper-sidebar'), wordCount: document.getElementById('word-count'), charCount: document.getElementById('char-count'), saveStatus: document.getElementById('save-status') }; // State let slashPosition = null; let autoSaveTimer = null; /** * Initialize the Paper module */ function init() { if (!elements.editor) return; setupEditorEvents(); setupToolbarCommands(); setupSlashMenu(); setupAIPanel(); setupSidebar(); setupModals(); setupKeyboardShortcuts(); updateWordCount(); } /** * Setup editor input and keydown events */ function setupEditorEvents() { elements.editor.addEventListener('input', function() { updateWordCount(); scheduleAutoSave(); checkSlashCommand(); }); elements.editor.addEventListener('keydown', function(e) { // Handle slash menu navigation if (elements.slashMenu && !elements.slashMenu.classList.contains('hidden')) { if (e.key === 'Escape') { hideSlashMenu(); e.preventDefault(); } else if (e.key === 'Enter') { const selected = elements.slashMenu.querySelector('.slash-item.selected') || elements.slashMenu.querySelector('.slash-item'); if (selected) { executeSlashCommand(selected.dataset.cmd); e.preventDefault(); } } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { navigateSlashMenu(e.key === 'ArrowDown' ? 1 : -1); e.preventDefault(); } } }); } /** * Check for slash command trigger */ function checkSlashCommand() { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const text = range.startContainer.textContent || ''; const cursorPos = range.startOffset; // Check for slash command if (text[cursorPos - 1] === '/') { showSlashMenu(range); } else if (elements.slashMenu && !elements.slashMenu.classList.contains('hidden')) { // Filter slash menu based on input after / const slashIndex = text.lastIndexOf('/'); if (slashIndex >= 0 && cursorPos > slashIndex) { const filter = text.substring(slashIndex + 1, cursorPos).toLowerCase(); filterSlashMenu(filter); } } } /** * Setup keyboard shortcuts */ function setupKeyboardShortcuts() { document.addEventListener('keydown', function(e) { if (e.ctrlKey || e.metaKey) { switch(e.key.toLowerCase()) { case 'b': e.preventDefault(); document.execCommand('bold'); break; case 'i': e.preventDefault(); document.execCommand('italic'); break; case 'u': e.preventDefault(); document.execCommand('underline'); break; case 's': e.preventDefault(); saveDocument(); break; case 'k': e.preventDefault(); insertLink(); break; case '/': e.preventDefault(); toggleFocusMode(); break; } } // Escape to close panels if (e.key === 'Escape') { hideSlashMenu(); elements.aiPanel?.classList.add('hidden'); closeModals(); } }); } /** * Setup toolbar command buttons */ function setupToolbarCommands() { document.querySelectorAll('[data-cmd]').forEach(btn => { btn.addEventListener('click', function() { const cmd = this.dataset.cmd; executeToolbarCommand(cmd); elements.editor?.focus(); }); }); // Heading select const headingSelect = document.getElementById('heading-select'); if (headingSelect) { headingSelect.addEventListener('change', function() { const value = this.value; document.execCommand('formatBlock', false, value === 'p' ? 'p' : value); elements.editor?.focus(); }); } // Text color picker const colorPicker = document.getElementById('text-color'); if (colorPicker) { colorPicker.addEventListener('change', function() { document.execCommand('foreColor', false, this.value); elements.editor?.focus(); }); } } /** * Execute toolbar command */ function executeToolbarCommand(cmd) { switch(cmd) { case 'bold': document.execCommand('bold'); break; case 'italic': document.execCommand('italic'); break; case 'underline': document.execCommand('underline'); break; case 'strikethrough': document.execCommand('strikeThrough'); break; case 'highlight': document.execCommand('hiliteColor', false, '#ffff00'); break; case 'alignLeft': document.execCommand('justifyLeft'); break; case 'alignCenter': document.execCommand('justifyCenter'); break; case 'alignRight': document.execCommand('justifyRight'); break; case 'bulletList': document.execCommand('insertUnorderedList'); break; case 'numberedList': document.execCommand('insertOrderedList'); break; case 'todoList': insertTodo(); break; case 'link': insertLink(); break; case 'image': insertImage(); break; case 'table': insertTable(); break; case 'code': document.execCommand('formatBlock', false, 'pre'); break; case 'quote': document.execCommand('formatBlock', false, 'blockquote'); break; case 'undo': document.execCommand('undo'); break; case 'redo': document.execCommand('redo'); break; } } /** * Show slash command menu */ function showSlashMenu(range) { if (!elements.slashMenu || !elements.editor) return; const rect = range.getBoundingClientRect(); const editorRect = elements.editor.getBoundingClientRect(); elements.slashMenu.style.top = (rect.bottom - editorRect.top + elements.editor.scrollTop + 8) + 'px'; elements.slashMenu.style.left = (rect.left - editorRect.left) + 'px'; elements.slashMenu.classList.remove('hidden'); slashPosition = range.startOffset; // Reset filter filterSlashMenu(''); } /** * Hide slash command menu */ function hideSlashMenu() { if (elements.slashMenu) { elements.slashMenu.classList.add('hidden'); } slashPosition = null; } /** * Filter slash menu items */ function filterSlashMenu(filter) { if (!elements.slashMenu) return; const items = elements.slashMenu.querySelectorAll('.slash-item'); let firstVisible = null; items.forEach(item => { const label = item.querySelector('.slash-label')?.textContent.toLowerCase() || ''; const matches = label.includes(filter); item.style.display = matches ? 'flex' : 'none'; if (matches && !firstVisible) firstVisible = item; }); // Select first visible items.forEach(item => item.classList.remove('selected')); if (firstVisible) firstVisible.classList.add('selected'); } /** * Navigate slash menu with arrow keys */ function navigateSlashMenu(direction) { if (!elements.slashMenu) return; const items = Array.from(elements.slashMenu.querySelectorAll('.slash-item')) .filter(i => i.style.display !== 'none'); const current = items.findIndex(i => i.classList.contains('selected')); items.forEach(i => i.classList.remove('selected')); let next = current + direction; if (next < 0) next = items.length - 1; if (next >= items.length) next = 0; items[next]?.classList.add('selected'); items[next]?.scrollIntoView({ block: 'nearest' }); } /** * Execute slash command */ function executeSlashCommand(cmd) { hideSlashMenu(); // Remove the slash character and any filter text const selection = window.getSelection(); if (selection.rangeCount) { const range = selection.getRangeAt(0); const text = range.startContainer.textContent || ''; const slashIndex = text.lastIndexOf('/'); if (slashIndex >= 0) { range.startContainer.textContent = text.substring(0, slashIndex) + text.substring(range.startOffset); range.setStart(range.startContainer, slashIndex); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } // Execute command switch(cmd) { case 'h1': document.execCommand('formatBlock', false, 'h1'); break; case 'h2': document.execCommand('formatBlock', false, 'h2'); break; case 'h3': document.execCommand('formatBlock', false, 'h3'); break; case 'bullet': document.execCommand('insertUnorderedList'); break; case 'number': document.execCommand('insertOrderedList'); break; case 'todo': insertTodo(); break; case 'quote': document.execCommand('formatBlock', false, 'blockquote'); break; case 'code': document.execCommand('formatBlock', false, 'pre'); break; case 'divider': document.execCommand('insertHTML', false, '