...
This commit is contained in:
103
static/js/app.js
103
static/js/app.js
@@ -12,6 +12,23 @@ let collectionSelector;
|
||||
let clipboard = null;
|
||||
let currentFilePath = null;
|
||||
|
||||
// Simple event bus
|
||||
const eventBus = {
|
||||
listeners: {},
|
||||
on(event, callback) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(callback);
|
||||
},
|
||||
dispatch(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
};
|
||||
window.eventBus = eventBus;
|
||||
|
||||
// Initialize application
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize WebDAV client
|
||||
@@ -26,7 +43,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize file tree
|
||||
fileTree = new FileTree('fileTree', webdavClient);
|
||||
fileTree.onFileSelect = async (item) => {
|
||||
await loadFile(item.path);
|
||||
await editor.loadFile(item.path);
|
||||
};
|
||||
|
||||
// Initialize collection selector
|
||||
@@ -39,6 +56,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Initialize editor
|
||||
editor = new MarkdownEditor('editor', 'preview', 'filenameInput');
|
||||
editor.setWebDAVClient(webdavClient);
|
||||
|
||||
// Add test content to verify preview works
|
||||
setTimeout(() => {
|
||||
if (!editor.editor.getValue()) {
|
||||
editor.editor.setValue('# Welcome to Markdown Editor\n\nStart typing to see preview...\n');
|
||||
editor.updatePreview();
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// Setup editor drop handler
|
||||
const editorDropHandler = new EditorDropHandler(
|
||||
@@ -50,15 +76,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Setup button handlers
|
||||
document.getElementById('newBtn').addEventListener('click', () => {
|
||||
newFile();
|
||||
editor.newFile();
|
||||
});
|
||||
|
||||
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||
await saveFile();
|
||||
await editor.save();
|
||||
});
|
||||
|
||||
document.getElementById('deleteBtn').addEventListener('click', async () => {
|
||||
await deleteCurrentFile();
|
||||
await editor.deleteFile();
|
||||
});
|
||||
|
||||
// Setup context menu handlers
|
||||
@@ -69,6 +95,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Initialize file tree actions manager
|
||||
window.fileTreeActions = new FileTreeActions(webdavClient, fileTree, editor);
|
||||
// Listen for file-saved event to reload file tree
|
||||
window.eventBus.on('file-saved', async (path) => {
|
||||
if (fileTree) {
|
||||
await fileTree.load();
|
||||
fileTree.selectNode(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for column resize events to refresh editor
|
||||
@@ -81,66 +114,6 @@ window.addEventListener('column-resize', () => {
|
||||
/**
|
||||
* File Operations
|
||||
*/
|
||||
async function loadFile(path) {
|
||||
try {
|
||||
const content = await webdavClient.get(path);
|
||||
editor.setValue(content);
|
||||
document.getElementById('filenameInput').value = path;
|
||||
currentFilePath = path;
|
||||
showNotification('File loaded', 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to load file:', error);
|
||||
showNotification('Failed to load file', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function newFile() {
|
||||
editor.setValue('# New File\n\nStart typing...\n');
|
||||
document.getElementById('filenameInput').value = '';
|
||||
document.getElementById('filenameInput').focus();
|
||||
currentFilePath = null;
|
||||
showNotification('New file', 'info');
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
const filename = document.getElementById('filenameInput').value.trim();
|
||||
if (!filename) {
|
||||
showNotification('Please enter a filename', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = editor.getValue();
|
||||
await webdavClient.put(filename, content);
|
||||
currentFilePath = filename;
|
||||
await fileTree.load();
|
||||
showNotification('Saved', 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to save file:', error);
|
||||
showNotification('Failed to save file', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCurrentFile() {
|
||||
if (!currentFilePath) {
|
||||
showNotification('No file selected', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Delete ${currentFilePath}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await webdavClient.delete(currentFilePath);
|
||||
await fileTree.load();
|
||||
newFile();
|
||||
showNotification('Deleted', 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete file:', error);
|
||||
showNotification('Failed to delete file', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context Menu Handlers
|
||||
@@ -166,7 +139,7 @@ async function handleContextAction(action, targetPath, isDir) {
|
||||
switch (action) {
|
||||
case 'open':
|
||||
if (!isDir) {
|
||||
await loadFile(targetPath);
|
||||
await editor.loadFile(targetPath);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -32,10 +32,15 @@ class MarkdownEditor {
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview on change
|
||||
this.editor.on('change', () => {
|
||||
// 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', () => {
|
||||
@@ -47,17 +52,21 @@ class MarkdownEditor {
|
||||
* 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);
|
||||
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;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error('Marked library not found.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,10 +132,9 @@ class MarkdownEditor {
|
||||
window.showNotification('✅ Saved', 'success');
|
||||
}
|
||||
|
||||
// Trigger file tree reload
|
||||
if (window.fileTree) {
|
||||
await window.fileTree.load();
|
||||
window.fileTree.selectNode(path);
|
||||
// Dispatch event to reload file tree
|
||||
if (window.eventBus) {
|
||||
window.eventBus.dispatch('file-saved', path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save file:', error);
|
||||
@@ -192,24 +200,66 @@ class MarkdownEditor {
|
||||
*/
|
||||
updatePreview() {
|
||||
const markdown = this.editor.getValue();
|
||||
let html = window.marked.parse(markdown);
|
||||
const previewDiv = this.previewElement;
|
||||
|
||||
// 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'));
|
||||
if (!markdown || !markdown.trim()) {
|
||||
previewDiv.innerHTML = `
|
||||
<div class="text-muted text-center mt-5">
|
||||
<p>Start typing to see preview...</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight code blocks
|
||||
if (window.Prism) {
|
||||
window.Prism.highlightAllUnder(this.previewElement);
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +316,21 @@ class MarkdownEditor {
|
||||
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
|
||||
|
||||
@@ -92,29 +92,24 @@ function hideContextMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu item click handler
|
||||
// Combined click handler for context menu and outside clicks
|
||||
document.addEventListener('click', async (e) => {
|
||||
const menuItem = e.target.closest('.context-menu-item');
|
||||
if (!menuItem) {
|
||||
hideContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const action = menuItem.dataset.action;
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const targetPath = menu.dataset.targetPath;
|
||||
const isDir = menu.dataset.targetIsDir === 'true';
|
||||
|
||||
hideContextMenu();
|
||||
|
||||
if (window.fileTreeActions) {
|
||||
await window.fileTreeActions.execute(action, targetPath, isDir);
|
||||
}
|
||||
});
|
||||
if (menuItem) {
|
||||
// Handle context menu item click
|
||||
const action = menuItem.dataset.action;
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const targetPath = menu.dataset.targetPath;
|
||||
const isDir = menu.dataset.targetIsDir === 'true';
|
||||
|
||||
// Hide on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
|
||||
hideContextMenu();
|
||||
|
||||
if (window.fileTreeActions) {
|
||||
await window.fileTreeActions.execute(action, targetPath, isDir);
|
||||
}
|
||||
} else if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
|
||||
// Hide on outside click
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user