- 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
		
			
				
	
	
		
			517 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			517 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * Main Application
 | |
|  * Coordinates all modules and handles user interactions
 | |
|  */
 | |
| 
 | |
| // Global state
 | |
| let webdavClient;
 | |
| let fileTree;
 | |
| let editor;
 | |
| let darkMode;
 | |
| let collectionSelector;
 | |
| let clipboard = null;
 | |
| let currentFilePath = null;
 | |
| 
 | |
| // Event bus is now loaded from event-bus.js module
 | |
| // No need to define it here - it's available as window.eventBus
 | |
| 
 | |
| /**
 | |
|  * Auto-load page in view mode
 | |
|  * Tries to load the last viewed page, falls back to first file if none saved
 | |
|  */
 | |
| async function autoLoadPageInViewMode() {
 | |
|     if (!editor || !fileTree) return;
 | |
| 
 | |
|     try {
 | |
|         // Try to get last viewed page
 | |
|         let pageToLoad = editor.getLastViewedPage();
 | |
| 
 | |
|         // If no last viewed page, get the first markdown file
 | |
|         if (!pageToLoad) {
 | |
|             pageToLoad = fileTree.getFirstMarkdownFile();
 | |
|         }
 | |
| 
 | |
|         // If we found a page to load, load it
 | |
|         if (pageToLoad) {
 | |
|             await editor.loadFile(pageToLoad);
 | |
|             // Highlight the file in the tree and expand parent directories
 | |
|             fileTree.selectAndExpandPath(pageToLoad);
 | |
|         } else {
 | |
|             // No files found, show empty state message
 | |
|             editor.previewElement.innerHTML = `
 | |
|                 <div class="text-muted text-center mt-5">
 | |
|                     <p>No content available</p>
 | |
|                 </div>
 | |
|             `;
 | |
|         }
 | |
|     } catch (error) {
 | |
|         console.error('Failed to auto-load page in view mode:', error);
 | |
|         editor.previewElement.innerHTML = `
 | |
|             <div class="alert alert-danger">
 | |
|                 <p>Failed to load content</p>
 | |
|             </div>
 | |
|         `;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Show directory preview with list of files
 | |
|  * @param {string} dirPath - The directory path
 | |
|  */
 | |
| async function showDirectoryPreview(dirPath) {
 | |
|     if (!editor || !fileTree || !webdavClient) return;
 | |
| 
 | |
|     try {
 | |
|         const dirName = dirPath.split('/').pop() || dirPath;
 | |
|         const files = fileTree.getDirectoryFiles(dirPath);
 | |
| 
 | |
|         // Start building the preview HTML
 | |
|         let html = `<div class="directory-preview">`;
 | |
|         html += `<h2>${dirName}</h2>`;
 | |
| 
 | |
|         if (files.length === 0) {
 | |
|             html += `<p>This directory is empty</p>`;
 | |
|         } else {
 | |
|             html += `<div class="directory-files">`;
 | |
| 
 | |
|             // Create cards for each file
 | |
|             for (const file of files) {
 | |
|                 const fileName = file.name;
 | |
|                 let fileDescription = '';
 | |
| 
 | |
|                 // Try to get file description from markdown files
 | |
|                 if (file.name.endsWith('.md')) {
 | |
|                     try {
 | |
|                         const content = await webdavClient.get(file.path);
 | |
|                         // Extract first heading or first line as description
 | |
|                         const lines = content.split('\n');
 | |
|                         for (const line of lines) {
 | |
|                             if (line.trim().startsWith('#')) {
 | |
|                                 fileDescription = line.replace(/^#+\s*/, '').trim();
 | |
|                                 break;
 | |
|                             } else if (line.trim() && !line.startsWith('---')) {
 | |
|                                 fileDescription = line.trim().substring(0, 100);
 | |
|                                 break;
 | |
|                             }
 | |
|                         }
 | |
|                     } catch (error) {
 | |
|                         console.error('Failed to read file description:', error);
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 html += `
 | |
|                     <div class="file-card" data-path="${file.path}">
 | |
|                         <div class="file-card-header">
 | |
|                             <i class="bi bi-file-earmark-text"></i>
 | |
|                             <span class="file-card-name">${fileName}</span>
 | |
|                         </div>
 | |
|                         ${fileDescription ? `<div class="file-card-description">${fileDescription}</div>` : ''}
 | |
|                     </div>
 | |
|                 `;
 | |
|             }
 | |
| 
 | |
|             html += `</div>`;
 | |
|         }
 | |
| 
 | |
|         html += `</div>`;
 | |
| 
 | |
|         // Set the preview content
 | |
|         editor.previewElement.innerHTML = html;
 | |
| 
 | |
|         // Add click handlers to file cards
 | |
|         editor.previewElement.querySelectorAll('.file-card').forEach(card => {
 | |
|             card.addEventListener('click', async () => {
 | |
|                 const filePath = card.dataset.path;
 | |
|                 await editor.loadFile(filePath);
 | |
|                 fileTree.selectAndExpandPath(filePath);
 | |
|             });
 | |
|         });
 | |
|     } catch (error) {
 | |
|         console.error('Failed to show directory preview:', error);
 | |
|         editor.previewElement.innerHTML = `
 | |
|             <div class="alert alert-danger">
 | |
|                 <p>Failed to load directory preview</p>
 | |
|             </div>
 | |
|         `;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Parse URL to extract collection and file path
 | |
|  * URL format: /<collection>/<file_path> or /<collection>/<dir>/<file>
 | |
|  * @returns {Object} {collection, filePath} or {collection, null} if only collection
 | |
|  */
 | |
| function parseURLPath() {
 | |
|     const pathname = window.location.pathname;
 | |
|     const parts = pathname.split('/').filter(p => p); // Remove empty parts
 | |
| 
 | |
|     if (parts.length === 0) {
 | |
|         return { collection: null, filePath: null };
 | |
|     }
 | |
| 
 | |
|     const collection = parts[0];
 | |
|     const filePath = parts.length > 1 ? parts.slice(1).join('/') : null;
 | |
| 
 | |
|     return { collection, filePath };
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Update URL based on current collection and file
 | |
|  * @param {string} collection - The collection name
 | |
|  * @param {string} filePath - The file path (optional)
 | |
|  * @param {boolean} isEditMode - Whether in edit mode
 | |
|  */
 | |
| function updateURL(collection, filePath, isEditMode) {
 | |
|     let url = `/${collection}`;
 | |
|     if (filePath) {
 | |
|         url += `/${filePath}`;
 | |
|     }
 | |
|     if (isEditMode) {
 | |
|         url += '?edit=true';
 | |
|     }
 | |
| 
 | |
|     // Use pushState to update URL without reloading
 | |
|     window.history.pushState({ collection, filePath }, '', url);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Load file from URL path
 | |
|  * Assumes the collection is already set and file tree is loaded
 | |
|  * @param {string} collection - The collection name (for validation)
 | |
|  * @param {string} filePath - The file path
 | |
|  */
 | |
| async function loadFileFromURL(collection, filePath) {
 | |
|     console.log('[loadFileFromURL] Called with:', { collection, filePath });
 | |
| 
 | |
|     if (!fileTree || !editor || !collectionSelector) {
 | |
|         console.error('[loadFileFromURL] Missing dependencies:', { fileTree: !!fileTree, editor: !!editor, collectionSelector: !!collectionSelector });
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         // Verify we're on the right collection
 | |
|         const currentCollection = collectionSelector.getCurrentCollection();
 | |
|         if (currentCollection !== collection) {
 | |
|             console.error(`[loadFileFromURL] Collection mismatch: expected ${collection}, got ${currentCollection}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // Load the file or directory
 | |
|         if (filePath) {
 | |
|             // Check if the path is a directory or a file
 | |
|             const node = fileTree.findNode(filePath);
 | |
|             console.log('[loadFileFromURL] Found node:', node);
 | |
| 
 | |
|             if (node && node.isDirectory) {
 | |
|                 // It's a directory, show directory preview
 | |
|                 console.log('[loadFileFromURL] Loading directory preview');
 | |
|                 await showDirectoryPreview(filePath);
 | |
|                 fileTree.selectAndExpandPath(filePath);
 | |
|             } else if (node) {
 | |
|                 // It's a file, load it
 | |
|                 console.log('[loadFileFromURL] Loading file');
 | |
|                 await editor.loadFile(filePath);
 | |
|                 fileTree.selectAndExpandPath(filePath);
 | |
|             } else {
 | |
|                 console.error(`[loadFileFromURL] Path not found in file tree: ${filePath}`);
 | |
|             }
 | |
|         }
 | |
|     } catch (error) {
 | |
|         console.error('[loadFileFromURL] Failed to load file from URL:', error);
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handle browser back/forward navigation
 | |
|  */
 | |
| function setupPopStateListener() {
 | |
|     window.addEventListener('popstate', async (event) => {
 | |
|         const { collection, filePath } = parseURLPath();
 | |
|         if (collection) {
 | |
|             // Ensure the collection is set
 | |
|             const currentCollection = collectionSelector.getCurrentCollection();
 | |
|             if (currentCollection !== collection) {
 | |
|                 await collectionSelector.setCollection(collection);
 | |
|                 await fileTree.load();
 | |
|             }
 | |
| 
 | |
|             // Load the file/directory
 | |
|             await loadFileFromURL(collection, filePath);
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| // Initialize application
 | |
| document.addEventListener('DOMContentLoaded', async () => {
 | |
|     // Determine view mode from URL parameter
 | |
|     const urlParams = new URLSearchParams(window.location.search);
 | |
|     const isEditMode = urlParams.get('edit') === 'true';
 | |
| 
 | |
|     // Set view mode class on body
 | |
|     if (isEditMode) {
 | |
|         document.body.classList.add('edit-mode');
 | |
|         document.body.classList.remove('view-mode');
 | |
|     } else {
 | |
|         document.body.classList.add('view-mode');
 | |
|         document.body.classList.remove('edit-mode');
 | |
|     }
 | |
| 
 | |
|     // Initialize WebDAV client
 | |
|     webdavClient = new WebDAVClient('/fs/');
 | |
| 
 | |
|     // Initialize dark mode
 | |
|     darkMode = new DarkMode();
 | |
|     document.getElementById('darkModeBtn').addEventListener('click', () => {
 | |
|         darkMode.toggle();
 | |
|     });
 | |
| 
 | |
|     // Initialize collection selector (always needed)
 | |
|     collectionSelector = new CollectionSelector('collectionSelect', webdavClient);
 | |
|     await collectionSelector.load();
 | |
| 
 | |
|     // Setup URL routing
 | |
|     setupPopStateListener();
 | |
| 
 | |
|     // Initialize editor (always needed for preview)
 | |
|     // In view mode, editor is read-only
 | |
|     editor = new MarkdownEditor('editor', 'preview', 'filenameInput', !isEditMode);
 | |
|     editor.setWebDAVClient(webdavClient);
 | |
| 
 | |
|     // Initialize file tree (needed in both modes)
 | |
|     fileTree = new FileTree('fileTree', webdavClient);
 | |
|     fileTree.onFileSelect = async (item) => {
 | |
|         try {
 | |
|             await editor.loadFile(item.path);
 | |
|             // Highlight the file in the tree and expand parent directories
 | |
|             fileTree.selectAndExpandPath(item.path);
 | |
|             // Update URL to reflect current file
 | |
|             const currentCollection = collectionSelector.getCurrentCollection();
 | |
|             updateURL(currentCollection, item.path, isEditMode);
 | |
|         } catch (error) {
 | |
|             Logger.error('Failed to select file:', error);
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Failed to load file', 'error');
 | |
|             }
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     fileTree.onFolderSelect = async (item) => {
 | |
|         try {
 | |
|             // Show directory preview
 | |
|             await showDirectoryPreview(item.path);
 | |
|             // Highlight the directory in the tree and expand parent directories
 | |
|             fileTree.selectAndExpandPath(item.path);
 | |
|             // Update URL to reflect current directory
 | |
|             const currentCollection = collectionSelector.getCurrentCollection();
 | |
|             updateURL(currentCollection, item.path, isEditMode);
 | |
|         } catch (error) {
 | |
|             Logger.error('Failed to select folder:', error);
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Failed to load folder', 'error');
 | |
|             }
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     collectionSelector.onChange = async (collection) => {
 | |
|         try {
 | |
|             await fileTree.load();
 | |
|             // In view mode, auto-load last viewed page when collection changes
 | |
|             if (!isEditMode) {
 | |
|                 await autoLoadPageInViewMode();
 | |
|             }
 | |
|         } catch (error) {
 | |
|             Logger.error('Failed to change collection:', error);
 | |
|             if (window.showNotification) {
 | |
|                 window.showNotification('Failed to change collection', 'error');
 | |
|             }
 | |
|         }
 | |
|     };
 | |
|     await fileTree.load();
 | |
| 
 | |
|     // Parse URL to load file if specified
 | |
|     const { collection: urlCollection, filePath: urlFilePath } = parseURLPath();
 | |
|     console.log('[URL PARSE]', { urlCollection, urlFilePath });
 | |
| 
 | |
|     if (urlCollection && urlFilePath) {
 | |
|         console.log('[URL LOAD] Loading from URL:', urlCollection, urlFilePath);
 | |
| 
 | |
|         // First ensure the collection is set
 | |
|         const currentCollection = collectionSelector.getCurrentCollection();
 | |
|         if (currentCollection !== urlCollection) {
 | |
|             console.log('[URL LOAD] Switching collection from', currentCollection, 'to', urlCollection);
 | |
|             await collectionSelector.setCollection(urlCollection);
 | |
|             await fileTree.load();
 | |
|         }
 | |
| 
 | |
|         // Now load the file from URL
 | |
|         console.log('[URL LOAD] Calling loadFileFromURL');
 | |
|         await loadFileFromURL(urlCollection, urlFilePath);
 | |
|     } else if (!isEditMode) {
 | |
|         // In view mode, auto-load last viewed page if no URL file specified
 | |
|         await autoLoadPageInViewMode();
 | |
|     }
 | |
| 
 | |
|     // Initialize file tree and editor-specific features only in edit mode
 | |
|     if (isEditMode) {
 | |
|         // 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(
 | |
|             document.querySelector('.editor-container'),
 | |
|             async (file) => {
 | |
|                 try {
 | |
|                     await handleEditorFileDrop(file);
 | |
|                 } catch (error) {
 | |
|                     Logger.error('Failed to handle file drop:', error);
 | |
|                 }
 | |
|             }
 | |
|         );
 | |
| 
 | |
|         // Setup button handlers
 | |
|         document.getElementById('newBtn').addEventListener('click', () => {
 | |
|             editor.newFile();
 | |
|         });
 | |
| 
 | |
|         document.getElementById('saveBtn').addEventListener('click', async () => {
 | |
|             try {
 | |
|                 await editor.save();
 | |
|             } catch (error) {
 | |
|                 Logger.error('Failed to save file:', error);
 | |
|                 if (window.showNotification) {
 | |
|                     window.showNotification('Failed to save file', 'error');
 | |
|                 }
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         document.getElementById('deleteBtn').addEventListener('click', async () => {
 | |
|             try {
 | |
|                 await editor.deleteFile();
 | |
|             } catch (error) {
 | |
|                 Logger.error('Failed to delete file:', error);
 | |
|                 if (window.showNotification) {
 | |
|                     window.showNotification('Failed to delete file', 'error');
 | |
|                 }
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         // Setup context menu handlers
 | |
|         setupContextMenuHandlers();
 | |
| 
 | |
|         // Initialize file tree actions manager
 | |
|         window.fileTreeActions = new FileTreeActions(webdavClient, fileTree, editor);
 | |
|     } else {
 | |
|         // In view mode, hide editor buttons
 | |
|         document.getElementById('newBtn').style.display = 'none';
 | |
|         document.getElementById('saveBtn').style.display = 'none';
 | |
|         document.getElementById('deleteBtn').style.display = 'none';
 | |
| 
 | |
|         // Auto-load last viewed page or first file
 | |
|         await autoLoadPageInViewMode();
 | |
|     }
 | |
| 
 | |
|     // Initialize mermaid (always needed)
 | |
|     mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' });
 | |
|     // Listen for file-saved event to reload file tree
 | |
|     window.eventBus.on('file-saved', async (path) => {
 | |
|         try {
 | |
|             if (fileTree) {
 | |
|                 await fileTree.load();
 | |
|                 fileTree.selectNode(path);
 | |
|             }
 | |
|         } catch (error) {
 | |
|             Logger.error('Failed to reload file tree after save:', error);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     window.eventBus.on('file-deleted', async () => {
 | |
|         try {
 | |
|             if (fileTree) {
 | |
|                 await fileTree.load();
 | |
|             }
 | |
|         } catch (error) {
 | |
|             Logger.error('Failed to reload file tree after delete:', error);
 | |
|         }
 | |
|     });
 | |
| });
 | |
| 
 | |
| // Listen for column resize events to refresh editor
 | |
| window.addEventListener('column-resize', () => {
 | |
|     if (editor && editor.editor) {
 | |
|         editor.editor.refresh();
 | |
|     }
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * File Operations
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * Context Menu Handlers
 | |
|  */
 | |
| function setupContextMenuHandlers() {
 | |
|     const menu = document.getElementById('contextMenu');
 | |
| 
 | |
|     menu.addEventListener('click', async (e) => {
 | |
|         const item = e.target.closest('.context-menu-item');
 | |
|         if (!item) return;
 | |
| 
 | |
|         const action = item.dataset.action;
 | |
|         const targetPath = menu.dataset.targetPath;
 | |
|         const isDir = menu.dataset.targetIsDir === 'true';
 | |
| 
 | |
|         hideContextMenu();
 | |
| 
 | |
|         await window.fileTreeActions.execute(action, targetPath, isDir);
 | |
|     });
 | |
| }
 | |
| 
 | |
| // All context actions are now handled by FileTreeActions, so this function is no longer needed.
 | |
| // async function handleContextAction(action, targetPath, isDir) { ... }
 | |
| 
 | |
| function updatePasteVisibility() {
 | |
|     const pasteItem = document.getElementById('pasteMenuItem');
 | |
|     if (pasteItem) {
 | |
|         pasteItem.style.display = clipboard ? 'block' : 'none';
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Editor File Drop Handler
 | |
|  */
 | |
| async function handleEditorFileDrop(file) {
 | |
|     try {
 | |
|         // Get current file's directory
 | |
|         let targetDir = '';
 | |
|         if (currentFilePath) {
 | |
|             const parts = currentFilePath.split('/');
 | |
|             parts.pop(); // Remove filename
 | |
|             targetDir = parts.join('/');
 | |
|         }
 | |
| 
 | |
|         // Upload file
 | |
|         const uploadedPath = await fileTree.uploadFile(targetDir, file);
 | |
| 
 | |
|         // Insert markdown link at cursor
 | |
|         const isImage = file.type.startsWith('image/');
 | |
|         const link = isImage
 | |
|             ? ``
 | |
|             : `[${file.name}](/${webdavClient.currentCollection}/${uploadedPath})`;
 | |
| 
 | |
|         editor.insertAtCursor(link);
 | |
|         showNotification(`Uploaded and inserted link`, 'success');
 | |
|     } catch (error) {
 | |
|         console.error('Failed to handle file drop:', error);
 | |
|         showNotification('Failed to upload file', 'error');
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Make showContextMenu global
 | |
| window.showContextMenu = showContextMenu;
 | |
| 
 |