274 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			274 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * Editor Module
 | |
|  * Handles CodeMirror editor and markdown preview
 | |
|  */
 | |
| 
 | |
| class MarkdownEditor {
 | |
|     constructor(editorId, previewId, filenameInputId) {
 | |
|         this.editorElement = document.getElementById(editorId);
 | |
|         this.previewElement = document.getElementById(previewId);
 | |
|         this.filenameInput = document.getElementById(filenameInputId);
 | |
|         this.currentFile = null;
 | |
|         this.webdavClient = null;
 | |
|         
 | |
|         this.initCodeMirror();
 | |
|         this.initMarkdown();
 | |
|         this.initMermaid();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize CodeMirror
 | |
|      */
 | |
|     initCodeMirror() {
 | |
|         this.editor = CodeMirror(this.editorElement, {
 | |
|             mode: 'markdown',
 | |
|             theme: 'monokai',
 | |
|             lineNumbers: true,
 | |
|             lineWrapping: true,
 | |
|             autofocus: true,
 | |
|             extraKeys: {
 | |
|                 'Ctrl-S': () => this.save(),
 | |
|                 'Cmd-S': () => this.save()
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         // Update preview on change
 | |
|         this.editor.on('change', () => {
 | |
|             this.updatePreview();
 | |
|         });
 | |
| 
 | |
|         // Sync scroll
 | |
|         this.editor.on('scroll', () => {
 | |
|             this.syncScroll();
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize markdown parser
 | |
|      */
 | |
|     initMarkdown() {
 | |
|         this.marked = window.marked;
 | |
|         this.marked.setOptions({
 | |
|             breaks: true,
 | |
|             gfm: true,
 | |
|             highlight: (code, lang) => {
 | |
|                 if (lang && window.Prism.languages[lang]) {
 | |
|                     return window.Prism.highlight(code, window.Prism.languages[lang], lang);
 | |
|                 }
 | |
|                 return code;
 | |
|             }
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize Mermaid
 | |
|      */
 | |
|     initMermaid() {
 | |
|         if (window.mermaid) {
 | |
|             window.mermaid.initialize({
 | |
|                 startOnLoad: false,
 | |
|                 theme: document.body.classList.contains('dark-mode') ? 'dark' : 'default'
 | |
|             });
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set WebDAV client
 | |
|      */
 | |
|     setWebDAVClient(client) {
 | |
|         this.webdavClient = client;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Load file
 | |
|      */
 | |
|     async loadFile(path) {
 | |
|         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');
 | |
|             }
 | |
|         } catch (error) {
 | |
|             console.error('Failed to load file:', error);
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Failed to load file', 'danger');
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Save file
 | |
|      */
 | |
|     async save() {
 | |
|         const path = this.filenameInput.value.trim();
 | |
|         if (!path) {
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Please enter a filename', 'warning');
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const content = this.editor.getValue();
 | |
| 
 | |
|         try {
 | |
|             await this.webdavClient.put(path, content);
 | |
|             this.currentFile = path;
 | |
|             
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('✅ Saved', 'success');
 | |
|             }
 | |
| 
 | |
|             // Trigger file tree reload
 | |
|             if (window.fileTree) {
 | |
|                 await window.fileTree.load();
 | |
|                 window.fileTree.selectNode(path);
 | |
|             }
 | |
|         } catch (error) {
 | |
|             console.error('Failed to save file:', error);
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Failed to save file', 'danger');
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create new file
 | |
|      */
 | |
|     newFile() {
 | |
|         this.currentFile = null;
 | |
|         this.filenameInput.value = '';
 | |
|         this.filenameInput.focus();
 | |
|         this.editor.setValue('');
 | |
|         this.updatePreview();
 | |
| 
 | |
|         if (window.showNotification) {
 | |
|             window.showNotification('Enter filename and start typing', 'info');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Delete current file
 | |
|      */
 | |
|     async deleteFile() {
 | |
|         if (!this.currentFile) {
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('No file selected', 'warning');
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (!confirm(`Delete ${this.currentFile}?`)) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|             await this.webdavClient.delete(this.currentFile);
 | |
|             
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification(`Deleted ${this.currentFile}`, 'success');
 | |
|             }
 | |
| 
 | |
|             this.newFile();
 | |
| 
 | |
|             // Trigger file tree reload
 | |
|             if (window.fileTree) {
 | |
|                 await window.fileTree.load();
 | |
|             }
 | |
|         } catch (error) {
 | |
|             console.error('Failed to delete file:', error);
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Failed to delete file', 'danger');
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Update preview
 | |
|      */
 | |
|     updatePreview() {
 | |
|         const markdown = this.editor.getValue();
 | |
|         let html = this.marked.parse(markdown);
 | |
| 
 | |
|         // Process mermaid diagrams
 | |
|         html = html.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, (match, code) => {
 | |
|             const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
 | |
|             return `<div class="mermaid" id="${id}">${code}</div>`;
 | |
|         });
 | |
| 
 | |
|         this.previewElement.innerHTML = html;
 | |
| 
 | |
|         // Render mermaid diagrams
 | |
|         if (window.mermaid) {
 | |
|             window.mermaid.init(undefined, this.previewElement.querySelectorAll('.mermaid'));
 | |
|         }
 | |
| 
 | |
|         // Highlight code blocks
 | |
|         if (window.Prism) {
 | |
|             window.Prism.highlightAllUnder(this.previewElement);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sync scroll between editor and preview
 | |
|      */
 | |
|     syncScroll() {
 | |
|         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;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Handle image upload
 | |
|      */
 | |
|     async uploadImage(file) {
 | |
|         try {
 | |
|             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');
 | |
|             }
 | |
|         } catch (error) {
 | |
|             console.error('Failed to upload image:', error);
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Failed to upload image', 'danger');
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get editor content
 | |
|      */
 | |
|     getValue() {
 | |
|         return this.editor.getValue();
 | |
|     }
 | |
|     
 | |
|     insertAtCursor(text) {
 | |
|         const doc = this.editor.getDoc();
 | |
|         const cursor = doc.getCursor();
 | |
|         doc.replaceRange(text, cursor);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set editor content
 | |
|      */
 | |
|     setValue(content) {
 | |
|         this.editor.setValue(content);
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Export for use in other modules
 | |
| window.MarkdownEditor = MarkdownEditor;
 | |
| 
 |