/** * Toast UI Editor initialization and management */ /** * Initialize or update the Toast UI Editor for a file * @param {string} content - Content to display * @param {boolean} isMarkdown - Whether content is markdown * @param {string} filePath - Path of the file * @param {string} fileName - Name of the file * @param {FileSystemFileHandle} fileHandle - File handle for saving * @param {number} lastModified - Timestamp of last modification */ function initializeEditor(content, isMarkdown = true, filePath = '', fileName = '', fileHandle = null, lastModified = null) { // Parse front matter let frontMatterData = {}; let markdownBody = content; if (isMarkdown && content) { try { const parsed = parseFrontMatter(content); frontMatterData = parsed.data; markdownBody = parsed.content; } catch (error) { console.error('Failed to parse front matter:', error); } } const contentContainer = document.getElementById('content-container'); if (!contentContainer) { alert('Error: content-container element not found!'); return; } // Hide all file view containers document.querySelectorAll('.file-view-container').forEach(container => { container.style.display = 'none'; }); // Check if file already has an instance if (editorInstances.has(filePath)) { const existingInstance = editorInstances.get(filePath); if (existingInstance.fileViewContainer) { existingInstance.fileViewContainer.style.display = 'flex'; } return existingInstance.editor; } // Create file view container const fileViewContainer = document.createElement('div'); fileViewContainer.className = 'file-view-container flex flex-col h-full'; // Create file header const fileHeader = document.createElement('div'); fileHeader.className = 'file-header flex justify-between items-center px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 font-medium border-b border-gray-200 dark:border-gray-700'; const fileTitle = document.createElement('span'); fileTitle.textContent = fileName || 'No file selected'; fileHeader.appendChild(fileTitle); // Button container for alignment const buttonContainer = document.createElement('div'); buttonContainer.className = 'flex gap-2'; // Determine if this is a scratchpad (no file handle) const isScratchpad = !fileHandle; // Save button (or Save As for scratchpads) const saveButton = document.createElement('button'); saveButton.className = 'btn btn-primary btn-sm'; saveButton.textContent = isScratchpad ? 'Save As...' : 'Save File'; saveButton.disabled = !isScratchpad; // Scratchpads can always save buttonContainer.appendChild(saveButton); // Reload button (only for files, not scratchpads) let reloadButton = null; if (!isScratchpad) { reloadButton = document.createElement('button'); reloadButton.className = 'btn btn-secondary btn-sm'; reloadButton.textContent = 'Reload from Disk'; reloadButton.title = 'Reload file from disk (discards unsaved changes)'; buttonContainer.appendChild(reloadButton); } fileHeader.appendChild(buttonContainer); fileViewContainer.appendChild(fileHeader); // Content area const contentArea = document.createElement('div'); contentArea.className = 'flex flex-col flex-1 overflow-hidden'; // Editor area with TOC const editorArea = document.createElement('div'); editorArea.className = 'flex flex-row flex-1 overflow-hidden'; // TOC pane (markdown only) let tocContainer = null; let frontMatterTextarea = null; if (isMarkdown) { const tocPane = document.createElement('div'); tocPane.className = 'toc-pane bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700'; tocPane.style.width = '325px'; tocPane.style.minWidth = '150px'; // Front matter nav bar (collapsible) const frontMatterNav = document.createElement('div'); frontMatterNav.className = 'front-matter-nav border-b border-gray-200 dark:border-gray-700'; const frontMatterHeader = document.createElement('div'); frontMatterHeader.className = 'front-matter-header px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 font-medium border-b border-gray-200 dark:border-gray-700 cursor-pointer flex items-center gap-2'; const toggleIcon = document.createElement('span'); toggleIcon.textContent = '▼'; toggleIcon.className = 'toggle-icon text-sm'; frontMatterHeader.appendChild(toggleIcon); const headerText = document.createElement('span'); headerText.textContent = 'YAML Front Matter'; frontMatterHeader.appendChild(headerText); frontMatterNav.appendChild(frontMatterHeader); frontMatterTextarea = document.createElement('textarea'); frontMatterTextarea.className = 'front-matter-textarea w-full px-4 py-2 text-sm focus:outline-none resize-none overflow-x-auto'; frontMatterTextarea.placeholder = 'title: Document Title\ndate: 2024-01-01\ntags: [example]'; // Set front matter content if (frontMatterData && Object.keys(frontMatterData).length > 0) { try { let yamlText = ''; for (const [key, value] of Object.entries(frontMatterData)) { if (Array.isArray(value)) { yamlText += `${key}: [${value.map(v => `"${v}"`).join(', ')}]\n`; } else { yamlText += `${key}: ${value}\n`; } } frontMatterTextarea.value = yamlText.trim(); } catch (error) { console.warn('Failed to stringify front matter:', error); frontMatterTextarea.value = ''; } } frontMatterNav.appendChild(frontMatterTextarea); tocPane.appendChild(frontMatterNav); const tocHeader = document.createElement('div'); tocHeader.className = 'toc-header px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 font-medium border-b border-gray-200 dark:border-gray-700'; const tocTitle = document.createElement('span'); tocTitle.textContent = 'Table of Contents'; tocHeader.appendChild(tocTitle); const tocDepthSelector = document.createElement('select'); tocDepthSelector.className = 'toc-depth-selector'; tocDepthSelector.innerHTML = ` `; tocHeader.appendChild(tocDepthSelector); tocPane.appendChild(tocHeader); tocContainer = document.createElement('div'); tocContainer.className = 'toc-container toc-content p-4 h-full overflow-auto'; tocPane.appendChild(tocContainer); // Set up TOC container overflow when front matter is toggled let fmIsCollapsed = false; frontMatterHeader.addEventListener('click', () => { fmIsCollapsed = !fmIsCollapsed; frontMatterNav.classList.toggle('collapsed', fmIsCollapsed); toggleIcon.textContent = fmIsCollapsed ? '▶' : '▼'; }); // Auto-size textarea: no vertical scroll, horizontal scroll for long lines frontMatterTextarea.style.overflowY = 'hidden'; frontMatterTextarea.style.overflowX = 'auto'; const autoResizeFm = () => { frontMatterTextarea.style.height = 'auto'; frontMatterTextarea.style.height = frontMatterTextarea.scrollHeight + 'px'; }; frontMatterTextarea.addEventListener('input', autoResizeFm); // Defer initial resize until element is in the DOM and has layout requestAnimationFrame(() => requestAnimationFrame(autoResizeFm)); editorArea.appendChild(tocPane); // TOC resizer const tocResizer = document.createElement('div'); tocResizer.className = 'pane-resizer bg-gray-200 dark:bg-gray-700 transition-colors relative z-10 w-1 cursor-col-resize hover:bg-blue-500'; tocResizer.setAttribute('data-resizer-for', 'toc-pane'); contentArea.appendChild(tocResizer); makeResizable(tocResizer, tocPane); // TOC depth selector event tocDepthSelector.addEventListener('change', function () { const depth = parseInt(this.value); if (window.updateToc && editorInstance) { const currentContent = editorInstance.getMarkdown(); window.updateToc(currentContent, tocContainer, editorInstance, depth); } }); } // Editor container const editorContainer = document.createElement('div'); editorContainer.className = 'editor-instance flex-1 overflow-hidden'; editorArea.appendChild(editorContainer); contentArea.appendChild(editorArea); fileViewContainer.appendChild(contentArea); contentContainer.appendChild(fileViewContainer); // Check Toast UI availability if (typeof toastui === 'undefined') { alert('Error: Toast UI library not loaded!'); editorContainer.innerHTML = '
Error: Toast UI library not loaded!
'; return; } let editorInstance; try { // Initialize Toast UI Editor const editor = new toastui.Editor({ el: editorContainer, height: '100%', initialEditType: 'markdown', previewStyle: 'vertical', initialValue: markdownBody, toolbarItems: [ ['heading', 'bold', 'italic', 'strike'], ['hr', 'quote'], ['ul', 'ol', 'task', 'indent', 'outdent'], ['table', 'image', 'link'], ['code', 'codeblock'] ] }); editorInstance = editor; if (!isMarkdown) { editorInstance.changeMode('wysiwyg'); } // Generate initial TOC if (isMarkdown && window.updateToc && tocContainer) { try { window.updateToc(markdownBody, tocContainer, editorInstance, tocMaxDepth); } catch (error) { console.error('Error generating TOC:', error); } const debouncedUpdateToc = debounce(() => { const currentContent = editorInstance.getMarkdown(); window.updateToc(currentContent, tocContainer, editorInstance, tocMaxDepth); }, 300); editorInstance.on('change', () => { debouncedUpdateToc(); const instanceData = editorInstances.get(filePath); if (instanceData && !instanceData.isDirty) { instanceData.isDirty = true; updateFileDirtyStatus(filePath, true); updateUnsavedCount(); } saveButton.disabled = false; }); // Scroll listener for TOC highlighting const mdPreview = editorInstance.getEditorElements().mdPreview; if (mdPreview) { let activeTimeout = null; let lastHeader = null; const updateActiveHeader = () => { // Re-query live headings (TOC may have been regenerated) const liveHeaders = mdPreview.querySelectorAll('h1, h2, h3, h4, h5, h6'); const previewRect = mdPreview.getBoundingClientRect(); // Use a threshold slightly below the top so a header touching // the top edge counts as "active" const threshold = previewRect.top + 4; let activeHeader = null; for (const header of liveHeaders) { if (header.getBoundingClientRect().top <= threshold) { activeHeader = header.textContent.trim(); } else { break; } } if (activeHeader !== lastHeader) { lastHeader = activeHeader; setActiveTocItem(tocContainer, activeHeader); } }; const onScroll = () => { cancelAnimationFrame(activeTimeout); activeTimeout = requestAnimationFrame(updateActiveHeader); }; mdPreview.addEventListener('scroll', onScroll); } } else { editorInstance.on('change', () => { const instanceData = editorInstances.get(filePath); if (instanceData && !instanceData.isDirty) { instanceData.isDirty = true; updateFileDirtyStatus(filePath, true); updateUnsavedCount(); } saveButton.disabled = false; }); } // Front matter change listener if (frontMatterTextarea) { frontMatterTextarea.addEventListener('input', () => { const instanceData = editorInstances.get(filePath); if (instanceData && !instanceData.isDirty) { instanceData.isDirty = true; updateFileDirtyStatus(filePath, true); updateUnsavedCount(); } saveButton.disabled = false; }); } // Button event listeners saveButton.addEventListener('click', async () => { if (isScratchpad) { // For scratchpads, use Save As const content = editorInstance.getMarkdown(); const savedHandle = await saveFileAs(content, 'untitled.md'); if (savedHandle && hasFileSystemAccess) { // Check if saved to current directory - add to file tree if (directoryHandle) { try { // Try to get the file from the directory to verify it's there const checkHandle = await directoryHandle.getFileHandle(savedHandle.name); // File is in current directory, add to tree fileTree.entries[savedHandle.name] = { name: savedHandle.name, type: 'file', handle: checkHandle }; renderFileTree(); } catch (e) { // File not in current directory, that's fine } } // Clear scratchpad content after successful save editorInstance.setMarkdown(''); saveButton.disabled = true; const instanceData = editorInstances.get(filePath); if (instanceData) { instanceData.isDirty = false; } } } else { saveFile(filePath); } }); if (reloadButton) { reloadButton.addEventListener('click', async () => { await reloadFileFromDisk(filePath); }); } // Store instance data const instanceData = { editor: editor, fileViewContainer: fileViewContainer, tocContainer: tocContainer, saveButton: saveButton, reloadButton: reloadButton, frontMatterTextarea: frontMatterTextarea, frontMatterData: frontMatterData, fileHandle: fileHandle, lastModified: lastModified, isDirty: false }; editorInstances.set(filePath, instanceData); return editorInstance; } catch (error) { console.error('Error initializing editor:', error); alert(`Error initializing Toast UI Editor: ${error}`); return null; } }