326 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			326 lines
		
	
	
		
			9.5 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 with debouncing
 | 
						|
        this.editor.on('change', this.debounce(() => {
 | 
						|
            this.updatePreview();
 | 
						|
        }, 300));
 | 
						|
 | 
						|
        // Initial preview render
 | 
						|
        setTimeout(() => {
 | 
						|
            this.updatePreview();
 | 
						|
        }, 100);
 | 
						|
 | 
						|
        // Sync scroll
 | 
						|
        this.editor.on('scroll', () => {
 | 
						|
            this.syncScroll();
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Initialize markdown parser
 | 
						|
     */
 | 
						|
    initMarkdown() {
 | 
						|
        if (window.marked) {
 | 
						|
            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;
 | 
						|
                }
 | 
						|
            });
 | 
						|
        } else {
 | 
						|
            console.error('Marked library not found.');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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');
 | 
						|
            }
 | 
						|
 | 
						|
            // Dispatch event to reload file tree
 | 
						|
            if (window.eventBus) {
 | 
						|
                window.eventBus.dispatch('file-saved', 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('# New File\n\nStart typing...\n');
 | 
						|
        this.updatePreview();
 | 
						|
 | 
						|
        if (window.showNotification) {
 | 
						|
            window.showNotification('Enter filename and start typing', 'info');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Delete current file
 | 
						|
     */
 | 
						|
    async deleteFile() {
 | 
						|
        if (!this.currentFile) {
 | 
						|
            window.showNotification('No file selected', 'warning');
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        const confirmed = await window.ConfirmationManager.confirm(`Are you sure you want to delete ${this.currentFile}?`, 'Delete File');
 | 
						|
        if (confirmed) {
 | 
						|
            try {
 | 
						|
                await this.webdavClient.delete(this.currentFile);
 | 
						|
                window.showNotification(`Deleted ${this.currentFile}`, 'success');
 | 
						|
                this.newFile();
 | 
						|
                window.eventBus.dispatch('file-deleted');
 | 
						|
            } catch (error) {
 | 
						|
                console.error('Failed to delete file:', error);
 | 
						|
                window.showNotification('Failed to delete file', 'danger');
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Update preview
 | 
						|
     */
 | 
						|
    updatePreview() {
 | 
						|
        const markdown = this.editor.getValue();
 | 
						|
        const previewDiv = this.previewElement;
 | 
						|
 | 
						|
        if (!markdown || !markdown.trim()) {
 | 
						|
            previewDiv.innerHTML = `
 | 
						|
                <div class="text-muted text-center mt-5">
 | 
						|
                    <p>Start typing to see preview...</p>
 | 
						|
                </div>
 | 
						|
            `;
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
            // 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(markdown);
 | 
						|
 | 
						|
            // Replace mermaid code blocks with div containers
 | 
						|
            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.trim()}</div>`;
 | 
						|
                }
 | 
						|
            );
 | 
						|
 | 
						|
            previewDiv.innerHTML = html;
 | 
						|
 | 
						|
            // Apply syntax highlighting to code blocks
 | 
						|
            const codeBlocks = previewDiv.querySelectorAll('pre code');
 | 
						|
            codeBlocks.forEach(block => {
 | 
						|
                const languageClass = Array.from(block.classList)
 | 
						|
                    .find(cls => cls.startsWith('language-'));
 | 
						|
                if (languageClass && languageClass !== 'language-mermaid') {
 | 
						|
                    if (window.Prism) {
 | 
						|
                        window.Prism.highlightElement(block);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            });
 | 
						|
 | 
						|
            // Render mermaid diagrams
 | 
						|
            const mermaidElements = previewDiv.querySelectorAll('.mermaid');
 | 
						|
            if (mermaidElements.length > 0 && window.mermaid) {
 | 
						|
                try {
 | 
						|
                    window.mermaid.contentLoaded();
 | 
						|
                } catch (error) {
 | 
						|
                    console.warn('Mermaid rendering error:', error);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Preview rendering error:', error);
 | 
						|
            previewDiv.innerHTML = `
 | 
						|
                <div class="alert alert-danger" role="alert">
 | 
						|
                    <strong>Error rendering preview:</strong><br>
 | 
						|
                    ${error.message}
 | 
						|
                </div>
 | 
						|
            `;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Debounce function
 | 
						|
     */
 | 
						|
    debounce(func, wait) {
 | 
						|
        let timeout;
 | 
						|
        return function executedFunction(...args) {
 | 
						|
            const later = () => {
 | 
						|
                clearTimeout(timeout);
 | 
						|
                func(...args);
 | 
						|
            };
 | 
						|
            clearTimeout(timeout);
 | 
						|
            timeout = setTimeout(later, wait);
 | 
						|
        };
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Export for use in other modules
 | 
						|
window.MarkdownEditor = MarkdownEditor;
 | 
						|
 |