refactor: Modularize UI components and utilities
- Extract UI components into separate JS files - Centralize configuration values in config.js - Introduce a dedicated logger module - Improve file tree drag-and-drop and undo functionality - Refactor modal handling to a single manager - Add URL routing support for SPA navigation - Implement view mode for read-only access
This commit is contained in:
@@ -4,15 +4,21 @@
|
||||
*/
|
||||
|
||||
class MarkdownEditor {
|
||||
constructor(editorId, previewId, filenameInputId) {
|
||||
constructor(editorId, previewId, filenameInputId, readOnly = false) {
|
||||
this.editorElement = document.getElementById(editorId);
|
||||
this.previewElement = document.getElementById(previewId);
|
||||
this.filenameInput = document.getElementById(filenameInputId);
|
||||
this.currentFile = null;
|
||||
this.webdavClient = null;
|
||||
this.macroProcessor = new MacroProcessor(null); // Will be set later
|
||||
|
||||
this.initCodeMirror();
|
||||
this.lastViewedStorageKey = 'lastViewedPage'; // localStorage key for tracking last viewed page
|
||||
this.readOnly = readOnly; // Whether editor is in read-only mode
|
||||
this.editor = null; // Will be initialized later
|
||||
|
||||
// Only initialize CodeMirror if not in read-only mode (view mode)
|
||||
if (!readOnly) {
|
||||
this.initCodeMirror();
|
||||
}
|
||||
this.initMarkdown();
|
||||
this.initMermaid();
|
||||
}
|
||||
@@ -21,22 +27,27 @@ class MarkdownEditor {
|
||||
* Initialize CodeMirror
|
||||
*/
|
||||
initCodeMirror() {
|
||||
// Determine theme based on dark mode
|
||||
const isDarkMode = document.body.classList.contains('dark-mode');
|
||||
const theme = isDarkMode ? 'monokai' : 'default';
|
||||
|
||||
this.editor = CodeMirror(this.editorElement, {
|
||||
mode: 'markdown',
|
||||
theme: 'monokai',
|
||||
theme: theme,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autofocus: true,
|
||||
extraKeys: {
|
||||
autofocus: !this.readOnly, // Don't autofocus in read-only mode
|
||||
readOnly: this.readOnly, // Set read-only mode
|
||||
extraKeys: this.readOnly ? {} : {
|
||||
'Ctrl-S': () => this.save(),
|
||||
'Cmd-S': () => this.save()
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview on change with debouncing
|
||||
this.editor.on('change', this.debounce(() => {
|
||||
this.editor.on('change', TimingUtils.debounce(() => {
|
||||
this.updatePreview();
|
||||
}, 300));
|
||||
}, Config.DEBOUNCE_DELAY));
|
||||
|
||||
// Initial preview render
|
||||
setTimeout(() => {
|
||||
@@ -47,6 +58,27 @@ class MarkdownEditor {
|
||||
this.editor.on('scroll', () => {
|
||||
this.syncScroll();
|
||||
});
|
||||
|
||||
// Listen for dark mode changes
|
||||
this.setupThemeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listener for dark mode changes
|
||||
*/
|
||||
setupThemeListener() {
|
||||
// Watch for dark mode class changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
const isDarkMode = document.body.classList.contains('dark-mode');
|
||||
const newTheme = isDarkMode ? 'monokai' : 'default';
|
||||
this.editor.setOption('theme', newTheme);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, { attributes: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +119,7 @@ class MarkdownEditor {
|
||||
*/
|
||||
setWebDAVClient(client) {
|
||||
this.webdavClient = client;
|
||||
|
||||
|
||||
// Update macro processor with client
|
||||
if (this.macroProcessor) {
|
||||
this.macroProcessor.webdavClient = client;
|
||||
@@ -101,13 +133,23 @@ class MarkdownEditor {
|
||||
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');
|
||||
|
||||
// Update filename input if it exists
|
||||
if (this.filenameInput) {
|
||||
this.filenameInput.value = path;
|
||||
}
|
||||
|
||||
// Update editor if it exists (edit mode)
|
||||
if (this.editor) {
|
||||
this.editor.setValue(content);
|
||||
}
|
||||
|
||||
// Update preview with the loaded content
|
||||
await this.renderPreview(content);
|
||||
|
||||
// Save as last viewed page
|
||||
this.saveLastViewedPage(path);
|
||||
// No notification for successful file load - it's not critical
|
||||
} catch (error) {
|
||||
console.error('Failed to load file:', error);
|
||||
if (window.showNotification) {
|
||||
@@ -116,6 +158,32 @@ class MarkdownEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the last viewed page to localStorage
|
||||
* Stores per collection so different collections can have different last viewed pages
|
||||
*/
|
||||
saveLastViewedPage(path) {
|
||||
if (!this.webdavClient || !this.webdavClient.currentCollection) {
|
||||
return;
|
||||
}
|
||||
const collection = this.webdavClient.currentCollection;
|
||||
const storageKey = `${this.lastViewedStorageKey}:${collection}`;
|
||||
localStorage.setItem(storageKey, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last viewed page from localStorage
|
||||
* Returns null if no page was previously viewed
|
||||
*/
|
||||
getLastViewedPage() {
|
||||
if (!this.webdavClient || !this.webdavClient.currentCollection) {
|
||||
return null;
|
||||
}
|
||||
const collection = this.webdavClient.currentCollection;
|
||||
const storageKey = `${this.lastViewedStorageKey}:${collection}`;
|
||||
return localStorage.getItem(storageKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file
|
||||
*/
|
||||
@@ -133,7 +201,7 @@ class MarkdownEditor {
|
||||
try {
|
||||
await this.webdavClient.put(path, content);
|
||||
this.currentFile = path;
|
||||
|
||||
|
||||
if (window.showNotification) {
|
||||
window.showNotification('✅ Saved', 'success');
|
||||
}
|
||||
@@ -159,10 +227,7 @@ class MarkdownEditor {
|
||||
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');
|
||||
}
|
||||
// No notification needed - UI is self-explanatory
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +239,7 @@ class MarkdownEditor {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await window.ConfirmationManager.confirm(`Are you sure you want to delete ${this.currentFile}?`, 'Delete File');
|
||||
const confirmed = await window.ConfirmationManager.confirm(`Are you sure you want to delete ${this.currentFile}?`, 'Delete File', true);
|
||||
if (confirmed) {
|
||||
try {
|
||||
await this.webdavClient.delete(this.currentFile);
|
||||
@@ -189,10 +254,12 @@ class MarkdownEditor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preview
|
||||
* Render preview from markdown content
|
||||
* Can be called with explicit content (for view mode) or from editor (for edit mode)
|
||||
*/
|
||||
async updatePreview() {
|
||||
const markdown = this.editor.getValue();
|
||||
async renderPreview(markdownContent = null) {
|
||||
// Get markdown content from editor if not provided
|
||||
const markdown = markdownContent !== null ? markdownContent : (this.editor ? this.editor.getValue() : '');
|
||||
const previewDiv = this.previewElement;
|
||||
|
||||
if (!markdown || !markdown.trim()) {
|
||||
@@ -207,24 +274,19 @@ class MarkdownEditor {
|
||||
try {
|
||||
// Step 1: Process macros
|
||||
let processedContent = markdown;
|
||||
|
||||
|
||||
if (this.macroProcessor) {
|
||||
const processingResult = await this.macroProcessor.processMacros(markdown);
|
||||
processedContent = processingResult.content;
|
||||
|
||||
// Log errors if any
|
||||
if (processingResult.errors.length > 0) {
|
||||
console.warn('Macro processing errors:', processingResult.errors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Step 2: 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(processedContent);
|
||||
|
||||
// Replace mermaid code blocks
|
||||
@@ -270,13 +332,25 @@ class MarkdownEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preview (backward compatibility wrapper)
|
||||
* Calls renderPreview with content from editor
|
||||
*/
|
||||
async updatePreview() {
|
||||
if (this.editor) {
|
||||
await this.renderPreview();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync scroll between editor and preview
|
||||
*/
|
||||
syncScroll() {
|
||||
if (!this.editor) return; // Skip if no editor (view mode)
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -289,10 +363,10 @@ class MarkdownEditor {
|
||||
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');
|
||||
}
|
||||
@@ -310,7 +384,7 @@ class MarkdownEditor {
|
||||
getValue() {
|
||||
return this.editor.getValue();
|
||||
}
|
||||
|
||||
|
||||
insertAtCursor(text) {
|
||||
const doc = this.editor.getDoc();
|
||||
const cursor = doc.getCursor();
|
||||
@@ -324,20 +398,7 @@ class MarkdownEditor {
|
||||
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);
|
||||
};
|
||||
}
|
||||
// Debounce function moved to TimingUtils in utils.js
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
|
||||
Reference in New Issue
Block a user