...
This commit is contained in:
273
static/js/editor.js
Normal file
273
static/js/editor.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
Reference in New Issue
Block a user