Files
markdown_editor/static/js/editor.js
2025-10-26 10:52:27 +04:00

325 lines
9.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;
// Initialize macro processor AFTER webdavClient is set
this.macroProcessor = 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;
// NOW initialize macro processor
this.macroProcessor = new MacroProcessor(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
*/
async updatePreview() {
const markdown = this.editor.getValue();
const previewDiv = this.previewElement;
if (!markdown || !markdown.trim()) {
previewDiv.innerHTML = `<div class="text-muted">Start typing...</div>`;
return;
}
try {
// Step 1: Process macros
console.log('[Editor] Processing macros...');
let processedContent = markdown;
if (this.macroProcessor) {
const result = await this.macroProcessor.processMacros(markdown);
processedContent = result.content;
if (result.errors.length > 0) {
console.warn('[Editor] Macro errors:', result.errors);
}
}
// Step 2: Parse markdown
console.log('[Editor] Parsing markdown...');
let html = this.marked.parse(processedContent);
// Step 3: Handle mermaid
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;
// Step 4: Syntax highlighting
const codeBlocks = previewDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => {
const lang = Array.from(block.classList)
.find(cls => cls.startsWith('language-'));
if (lang && lang !== 'language-mermaid' && window.Prism) {
window.Prism.highlightElement(block);
}
});
// Step 5: Render mermaid
if (window.mermaid) {
await window.mermaid.run();
}
} catch (error) {
console.error('[Editor] Preview error:', error);
previewDiv.innerHTML = `<div class="alert alert-danger">Error: ${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 = `![${file.name}](${imageUrl})`;
// 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;