refactor: Modularize UI components and utilities
- Extract UI components into separate JS files - Centralize configuration values in config.js - Introduce a dedicated logger module - Improve file tree drag-and-drop and undo functionality - Refactor modal handling to a single manager - Add URL routing support for SPA navigation - Implement view mode for read-only access
This commit is contained in:
		| @@ -4,15 +4,21 @@ | ||||
|  */ | ||||
|  | ||||
| class MarkdownEditor { | ||||
|     constructor(editorId, previewId, filenameInputId) { | ||||
|     constructor(editorId, previewId, filenameInputId, readOnly = false) { | ||||
|         this.editorElement = document.getElementById(editorId); | ||||
|         this.previewElement = document.getElementById(previewId); | ||||
|         this.filenameInput = document.getElementById(filenameInputId); | ||||
|         this.currentFile = null; | ||||
|         this.webdavClient = null; | ||||
|         this.macroProcessor = new MacroProcessor(null); // Will be set later | ||||
|          | ||||
|         this.initCodeMirror(); | ||||
|         this.lastViewedStorageKey = 'lastViewedPage'; // localStorage key for tracking last viewed page | ||||
|         this.readOnly = readOnly; // Whether editor is in read-only mode | ||||
|         this.editor = null; // Will be initialized later | ||||
|  | ||||
|         // Only initialize CodeMirror if not in read-only mode (view mode) | ||||
|         if (!readOnly) { | ||||
|             this.initCodeMirror(); | ||||
|         } | ||||
|         this.initMarkdown(); | ||||
|         this.initMermaid(); | ||||
|     } | ||||
| @@ -21,22 +27,27 @@ class MarkdownEditor { | ||||
|      * Initialize CodeMirror | ||||
|      */ | ||||
|     initCodeMirror() { | ||||
|         // Determine theme based on dark mode | ||||
|         const isDarkMode = document.body.classList.contains('dark-mode'); | ||||
|         const theme = isDarkMode ? 'monokai' : 'default'; | ||||
|  | ||||
|         this.editor = CodeMirror(this.editorElement, { | ||||
|             mode: 'markdown', | ||||
|             theme: 'monokai', | ||||
|             theme: theme, | ||||
|             lineNumbers: true, | ||||
|             lineWrapping: true, | ||||
|             autofocus: true, | ||||
|             extraKeys: { | ||||
|             autofocus: !this.readOnly, // Don't autofocus in read-only mode | ||||
|             readOnly: this.readOnly, // Set read-only mode | ||||
|             extraKeys: this.readOnly ? {} : { | ||||
|                 'Ctrl-S': () => this.save(), | ||||
|                 'Cmd-S': () => this.save() | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Update preview on change with debouncing | ||||
|         this.editor.on('change', this.debounce(() => { | ||||
|         this.editor.on('change', TimingUtils.debounce(() => { | ||||
|             this.updatePreview(); | ||||
|         }, 300)); | ||||
|         }, Config.DEBOUNCE_DELAY)); | ||||
|  | ||||
|         // Initial preview render | ||||
|         setTimeout(() => { | ||||
| @@ -47,6 +58,27 @@ class MarkdownEditor { | ||||
|         this.editor.on('scroll', () => { | ||||
|             this.syncScroll(); | ||||
|         }); | ||||
|  | ||||
|         // Listen for dark mode changes | ||||
|         this.setupThemeListener(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Setup listener for dark mode changes | ||||
|      */ | ||||
|     setupThemeListener() { | ||||
|         // Watch for dark mode class changes | ||||
|         const observer = new MutationObserver((mutations) => { | ||||
|             mutations.forEach((mutation) => { | ||||
|                 if (mutation.attributeName === 'class') { | ||||
|                     const isDarkMode = document.body.classList.contains('dark-mode'); | ||||
|                     const newTheme = isDarkMode ? 'monokai' : 'default'; | ||||
|                     this.editor.setOption('theme', newTheme); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         observer.observe(document.body, { attributes: true }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -87,7 +119,7 @@ class MarkdownEditor { | ||||
|      */ | ||||
|     setWebDAVClient(client) { | ||||
|         this.webdavClient = client; | ||||
|          | ||||
|  | ||||
|         // Update macro processor with client | ||||
|         if (this.macroProcessor) { | ||||
|             this.macroProcessor.webdavClient = client; | ||||
| @@ -101,13 +133,23 @@ class MarkdownEditor { | ||||
|         try { | ||||
|             const content = await this.webdavClient.get(path); | ||||
|             this.currentFile = path; | ||||
|             this.filenameInput.value = path; | ||||
|             this.editor.setValue(content); | ||||
|             this.updatePreview(); | ||||
|              | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification(`Loaded ${path}`, 'info'); | ||||
|  | ||||
|             // Update filename input if it exists | ||||
|             if (this.filenameInput) { | ||||
|                 this.filenameInput.value = path; | ||||
|             } | ||||
|  | ||||
|             // Update editor if it exists (edit mode) | ||||
|             if (this.editor) { | ||||
|                 this.editor.setValue(content); | ||||
|             } | ||||
|  | ||||
|             // Update preview with the loaded content | ||||
|             await this.renderPreview(content); | ||||
|  | ||||
|             // Save as last viewed page | ||||
|             this.saveLastViewedPage(path); | ||||
|             // No notification for successful file load - it's not critical | ||||
|         } catch (error) { | ||||
|             console.error('Failed to load file:', error); | ||||
|             if (window.showNotification) { | ||||
| @@ -116,6 +158,32 @@ class MarkdownEditor { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save the last viewed page to localStorage | ||||
|      * Stores per collection so different collections can have different last viewed pages | ||||
|      */ | ||||
|     saveLastViewedPage(path) { | ||||
|         if (!this.webdavClient || !this.webdavClient.currentCollection) { | ||||
|             return; | ||||
|         } | ||||
|         const collection = this.webdavClient.currentCollection; | ||||
|         const storageKey = `${this.lastViewedStorageKey}:${collection}`; | ||||
|         localStorage.setItem(storageKey, path); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the last viewed page from localStorage | ||||
|      * Returns null if no page was previously viewed | ||||
|      */ | ||||
|     getLastViewedPage() { | ||||
|         if (!this.webdavClient || !this.webdavClient.currentCollection) { | ||||
|             return null; | ||||
|         } | ||||
|         const collection = this.webdavClient.currentCollection; | ||||
|         const storageKey = `${this.lastViewedStorageKey}:${collection}`; | ||||
|         return localStorage.getItem(storageKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save file | ||||
|      */ | ||||
| @@ -133,7 +201,7 @@ class MarkdownEditor { | ||||
|         try { | ||||
|             await this.webdavClient.put(path, content); | ||||
|             this.currentFile = path; | ||||
|              | ||||
|  | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('✅ Saved', 'success'); | ||||
|             } | ||||
| @@ -159,10 +227,7 @@ class MarkdownEditor { | ||||
|         this.filenameInput.focus(); | ||||
|         this.editor.setValue('# New File\n\nStart typing...\n'); | ||||
|         this.updatePreview(); | ||||
|  | ||||
|         if (window.showNotification) { | ||||
|             window.showNotification('Enter filename and start typing', 'info'); | ||||
|         } | ||||
|         // No notification needed - UI is self-explanatory | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -174,7 +239,7 @@ class MarkdownEditor { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const confirmed = await window.ConfirmationManager.confirm(`Are you sure you want to delete ${this.currentFile}?`, 'Delete File'); | ||||
|         const confirmed = await window.ConfirmationManager.confirm(`Are you sure you want to delete ${this.currentFile}?`, 'Delete File', true); | ||||
|         if (confirmed) { | ||||
|             try { | ||||
|                 await this.webdavClient.delete(this.currentFile); | ||||
| @@ -189,10 +254,12 @@ class MarkdownEditor { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update preview | ||||
|      * Render preview from markdown content | ||||
|      * Can be called with explicit content (for view mode) or from editor (for edit mode) | ||||
|      */ | ||||
|     async updatePreview() { | ||||
|         const markdown = this.editor.getValue(); | ||||
|     async renderPreview(markdownContent = null) { | ||||
|         // Get markdown content from editor if not provided | ||||
|         const markdown = markdownContent !== null ? markdownContent : (this.editor ? this.editor.getValue() : ''); | ||||
|         const previewDiv = this.previewElement; | ||||
|  | ||||
|         if (!markdown || !markdown.trim()) { | ||||
| @@ -207,24 +274,19 @@ class MarkdownEditor { | ||||
|         try { | ||||
|             // Step 1: Process macros | ||||
|             let processedContent = markdown; | ||||
|              | ||||
|  | ||||
|             if (this.macroProcessor) { | ||||
|                 const processingResult = await this.macroProcessor.processMacros(markdown); | ||||
|                 processedContent = processingResult.content; | ||||
|                  | ||||
|                 // Log errors if any | ||||
|                 if (processingResult.errors.length > 0) { | ||||
|                     console.warn('Macro processing errors:', processingResult.errors); | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|  | ||||
|             // Step 2: Parse markdown to HTML | ||||
|             if (!this.marked) { | ||||
|                 console.error("Markdown parser (marked) not initialized."); | ||||
|                 previewDiv.innerHTML = `<div class="alert alert-danger">Preview engine not loaded.</div>`; | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|  | ||||
|             let html = this.marked.parse(processedContent); | ||||
|  | ||||
|             // Replace mermaid code blocks | ||||
| @@ -270,13 +332,25 @@ class MarkdownEditor { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update preview (backward compatibility wrapper) | ||||
|      * Calls renderPreview with content from editor | ||||
|      */ | ||||
|     async updatePreview() { | ||||
|         if (this.editor) { | ||||
|             await this.renderPreview(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sync scroll between editor and preview | ||||
|      */ | ||||
|     syncScroll() { | ||||
|         if (!this.editor) return; // Skip if no editor (view mode) | ||||
|  | ||||
|         const scrollInfo = this.editor.getScrollInfo(); | ||||
|         const scrollPercent = scrollInfo.top / (scrollInfo.height - scrollInfo.clientHeight); | ||||
|          | ||||
|  | ||||
|         const previewHeight = this.previewElement.scrollHeight - this.previewElement.clientHeight; | ||||
|         this.previewElement.scrollTop = previewHeight * scrollPercent; | ||||
|     } | ||||
| @@ -289,10 +363,10 @@ class MarkdownEditor { | ||||
|             const filename = await this.webdavClient.uploadImage(file); | ||||
|             const imageUrl = `/fs/${this.webdavClient.currentCollection}/images/${filename}`; | ||||
|             const markdown = ``; | ||||
|              | ||||
|  | ||||
|             // Insert at cursor | ||||
|             this.editor.replaceSelection(markdown); | ||||
|              | ||||
|  | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('Image uploaded', 'success'); | ||||
|             } | ||||
| @@ -310,7 +384,7 @@ class MarkdownEditor { | ||||
|     getValue() { | ||||
|         return this.editor.getValue(); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     insertAtCursor(text) { | ||||
|         const doc = this.editor.getDoc(); | ||||
|         const cursor = doc.getCursor(); | ||||
| @@ -324,20 +398,7 @@ class MarkdownEditor { | ||||
|         this.editor.setValue(content); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Debounce function | ||||
|      */ | ||||
|     debounce(func, wait) { | ||||
|         let timeout; | ||||
|         return function executedFunction(...args) { | ||||
|             const later = () => { | ||||
|                 clearTimeout(timeout); | ||||
|                 func(...args); | ||||
|             }; | ||||
|             clearTimeout(timeout); | ||||
|             timeout = setTimeout(later, wait); | ||||
|         }; | ||||
|     } | ||||
|     // Debounce function moved to TimingUtils in utils.js | ||||
| } | ||||
|  | ||||
| // Export for use in other modules | ||||
|   | ||||
		Reference in New Issue
	
	Block a user