...
This commit is contained in:
		| @@ -187,6 +187,7 @@ def main(): | ||||
|      | ||||
|     try: | ||||
|         server.start() | ||||
|         server.wait() | ||||
|     except KeyboardInterrupt: | ||||
|         print("\n\nShutting down...") | ||||
|         server.stop() | ||||
|   | ||||
| @@ -158,3 +158,51 @@ body.dark-mode .context-menu { | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| /* Modal Dialogs */ | ||||
| .modal { | ||||
|     z-index: 10000; | ||||
| } | ||||
|  | ||||
| .modal-backdrop { | ||||
|     z-index: 9999; | ||||
|     background-color: rgba(0, 0, 0, 0.5); | ||||
| } | ||||
|  | ||||
| body.dark-mode .modal-content { | ||||
|     background-color: var(--bg-secondary); | ||||
|     color: var(--text-primary); | ||||
|     border: 1px solid var(--border-color); | ||||
| } | ||||
|  | ||||
| body.dark-mode .modal-header { | ||||
|     border-bottom-color: var(--border-color); | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .modal-footer { | ||||
|     border-top-color: var(--border-color); | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| .modal-header.border-danger { | ||||
|     border-bottom: 2px solid var(--danger-color) !important; | ||||
| } | ||||
|  | ||||
| .modal-open { | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| /* Input in modal */ | ||||
| .modal-body input.form-control { | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     border-color: var(--border-color); | ||||
| } | ||||
|  | ||||
| .modal-body input.form-control:focus { | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     border-color: var(--link-color); | ||||
|     box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); | ||||
| } | ||||
| @@ -11,18 +11,25 @@ html, body { | ||||
|  | ||||
| /* Column Resizer */ | ||||
| .column-resizer { | ||||
|     width: 4px; | ||||
|     width: 1px; | ||||
|     background-color: var(--border-color); | ||||
|     cursor: col-resize; | ||||
|     transition: background-color 0.2s ease; | ||||
|     transition: background-color 0.2s ease, width 0.2s ease, box-shadow 0.2s ease; | ||||
|     user-select: none; | ||||
|     flex-shrink: 0; | ||||
|     padding: 0 3px;  /* Add invisible padding for easier grab */ | ||||
|     margin: 0 -3px;  /* Compensate for padding */ | ||||
| } | ||||
|  | ||||
| .column-resizer:hover { | ||||
|     background-color: var(--link-color); | ||||
|     width: 6px; | ||||
|     margin: 0 -1px; | ||||
|     width: 1px; | ||||
|     box-shadow: 0 0 6px rgba(13, 110, 253, 0.3);  /* Visual feedback instead of width change */ | ||||
| } | ||||
|  | ||||
| .column-resizer.dragging { | ||||
|     background-color: var(--link-color); | ||||
|     box-shadow: 0 0 8px rgba(13, 110, 253, 0.5); | ||||
| } | ||||
|  | ||||
| .column-resizer.dragging { | ||||
|   | ||||
| @@ -38,7 +38,7 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|     await fileTree.load(); | ||||
|      | ||||
|     // Initialize editor | ||||
|     editor = new MarkdownEditor('editor', 'preview'); | ||||
|     editor = new MarkdownEditor('editor', 'preview', 'filenameInput'); | ||||
|      | ||||
|     // Setup editor drop handler | ||||
|     const editorDropHandler = new EditorDropHandler( | ||||
| @@ -66,6 +66,9 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|      | ||||
|     // Initialize mermaid | ||||
|     mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' }); | ||||
|  | ||||
|     // Initialize file tree actions manager | ||||
|     window.fileTreeActions = new FileTreeActions(webdavClient, fileTree, editor); | ||||
| }); | ||||
|  | ||||
| // Listen for column resize events to refresh editor | ||||
|   | ||||
| @@ -143,7 +143,7 @@ class MarkdownEditor { | ||||
|         this.currentFile = null; | ||||
|         this.filenameInput.value = ''; | ||||
|         this.filenameInput.focus(); | ||||
|         this.editor.setValue(''); | ||||
|         this.editor.setValue('# New File\n\nStart typing...\n'); | ||||
|         this.updatePreview(); | ||||
|  | ||||
|         if (window.showNotification) { | ||||
| @@ -192,7 +192,7 @@ class MarkdownEditor { | ||||
|      */ | ||||
|     updatePreview() { | ||||
|         const markdown = this.editor.getValue(); | ||||
|         let html = this.marked.parse(markdown); | ||||
|         let html = window.marked.parse(markdown); | ||||
|  | ||||
|         // Process mermaid diagrams | ||||
|         html = html.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, (match, code) => { | ||||
|   | ||||
							
								
								
									
										276
									
								
								static/js/file-tree-actions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								static/js/file-tree-actions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,276 @@ | ||||
| /** | ||||
|  * File Tree Actions Manager | ||||
|  * Centralized handling of all tree operations | ||||
|  */ | ||||
|  | ||||
| class FileTreeActions { | ||||
|     constructor(webdavClient, fileTree, editor) { | ||||
|         this.webdavClient = webdavClient; | ||||
|         this.fileTree = fileTree; | ||||
|         this.editor = editor; | ||||
|         this.clipboard = null; | ||||
|     } | ||||
|  | ||||
|     async execute(action, targetPath, isDirectory) { | ||||
|         const handler = this.actions[action]; | ||||
|         if (!handler) { | ||||
|             console.error(`Unknown action: ${action}`); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             await handler.call(this, targetPath, isDirectory); | ||||
|         } catch (error) { | ||||
|             console.error(`Action failed: ${action}`, error); | ||||
|             showNotification(`Failed to ${action}`, 'error'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     actions = { | ||||
|         open: async function(path, isDir) { | ||||
|             if (!isDir) { | ||||
|                 await this.editor.loadFile(path); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         newFile: async function(path, isDir) { | ||||
|             if (!isDir) return; | ||||
|              | ||||
|             const filename = await this.showInputDialog('Enter filename:', 'new-file.md'); | ||||
|             if (filename) { | ||||
|                 const fullPath = `${path}/${filename}`.replace(/\/+/g, '/'); | ||||
|                 await this.webdavClient.put(fullPath, '# New File\n\n'); | ||||
|                 await this.fileTree.load(); | ||||
|                 showNotification(`Created ${filename}`, 'success'); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         newFolder: async function(path, isDir) { | ||||
|             if (!isDir) return; | ||||
|              | ||||
|             const foldername = await this.showInputDialog('Enter folder name:', 'new-folder'); | ||||
|             if (foldername) { | ||||
|                 const fullPath = `${path}/${foldername}`.replace(/\/+/g, '/'); | ||||
|                 await this.webdavClient.mkcol(fullPath); | ||||
|                 await this.fileTree.load(); | ||||
|                 showNotification(`Created folder ${foldername}`, 'success'); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         rename: async function(path, isDir) { | ||||
|             const oldName = path.split('/').pop(); | ||||
|             const newName = await this.showInputDialog('Rename to:', oldName); | ||||
|              | ||||
|             if (newName && newName !== oldName) { | ||||
|                 const parentPath = path.substring(0, path.lastIndexOf('/')); | ||||
|                 const newPath = parentPath ? `${parentPath}/${newName}` : newName; | ||||
|                  | ||||
|                 await this.webdavClient.move(path, newPath); | ||||
|                 await this.fileTree.load(); | ||||
|                 showNotification('Renamed', 'success'); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         copy: async function(path, isDir) { | ||||
|             this.clipboard = { path, operation: 'copy', isDirectory: isDir }; | ||||
|             showNotification(`Copied: ${path.split('/').pop()}`, 'info'); | ||||
|             this.updatePasteMenuItem(); | ||||
|         }, | ||||
|  | ||||
|         cut: async function(path, isDir) { | ||||
|             this.clipboard = { path, operation: 'cut', isDirectory: isDir }; | ||||
|             showNotification(`Cut: ${path.split('/').pop()}`, 'warning'); | ||||
|             this.updatePasteMenuItem(); | ||||
|         }, | ||||
|  | ||||
|         paste: async function(targetPath, isDir) { | ||||
|             if (!this.clipboard || !isDir) return; | ||||
|              | ||||
|             const itemName = this.clipboard.path.split('/').pop(); | ||||
|             const destPath = `${targetPath}/${itemName}`.replace(/\/+/g, '/'); | ||||
|              | ||||
|             if (this.clipboard.operation === 'copy') { | ||||
|                 await this.webdavClient.copy(this.clipboard.path, destPath); | ||||
|                 showNotification('Copied', 'success'); | ||||
|             } else { | ||||
|                 await this.webdavClient.move(this.clipboard.path, destPath); | ||||
|                 this.clipboard = null; | ||||
|                 this.updatePasteMenuItem(); | ||||
|                 showNotification('Moved', 'success'); | ||||
|             } | ||||
|              | ||||
|             await this.fileTree.load(); | ||||
|         }, | ||||
|  | ||||
|         delete: async function(path, isDir) { | ||||
|             const name = path.split('/').pop(); | ||||
|             const type = isDir ? 'folder' : 'file'; | ||||
|              | ||||
|             if (!await this.showConfirmDialog(`Delete this ${type}?`, `${name}`)) { | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             await this.webdavClient.delete(path); | ||||
|             await this.fileTree.load(); | ||||
|             showNotification(`Deleted ${name}`, 'success'); | ||||
|         }, | ||||
|  | ||||
|         download: async function(path, isDir) { | ||||
|             showNotification('Downloading...', 'info'); | ||||
|             // Implementation here | ||||
|         }, | ||||
|  | ||||
|         upload: async function(path, isDir) { | ||||
|             if (!isDir) return; | ||||
|              | ||||
|             const input = document.createElement('input'); | ||||
|             input.type = 'file'; | ||||
|             input.multiple = true; | ||||
|              | ||||
|             input.onchange = async (e) => { | ||||
|                 const files = Array.from(e.target.files); | ||||
|                 for (const file of files) { | ||||
|                     const fullPath = `${path}/${file.name}`.replace(/\/+/g, '/'); | ||||
|                     const content = await file.arrayBuffer(); | ||||
|                     await this.webdavClient.putBinary(fullPath, content); | ||||
|                     showNotification(`Uploaded ${file.name}`, 'success'); | ||||
|                 } | ||||
|                 await this.fileTree.load(); | ||||
|             }; | ||||
|              | ||||
|             input.click(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Modern dialog implementations | ||||
|     async showInputDialog(title, placeholder = '') { | ||||
|         return new Promise((resolve) => { | ||||
|             const dialog = this.createInputDialog(title, placeholder); | ||||
|             const input = dialog.querySelector('input'); | ||||
|             const confirmBtn = dialog.querySelector('.btn-primary'); | ||||
|             const cancelBtn = dialog.querySelector('.btn-secondary'); | ||||
|              | ||||
|             const cleanup = () => { | ||||
|                 dialog.remove(); | ||||
|                 document.body.classList.remove('modal-open'); | ||||
|             }; | ||||
|              | ||||
|             confirmBtn.onclick = () => { | ||||
|                 resolve(input.value.trim()); | ||||
|                 cleanup(); | ||||
|             }; | ||||
|              | ||||
|             cancelBtn.onclick = () => { | ||||
|                 resolve(null); | ||||
|                 cleanup(); | ||||
|             }; | ||||
|              | ||||
|             input.onkeypress = (e) => { | ||||
|                 if (e.key === 'Enter') confirmBtn.click(); | ||||
|                 if (e.key === 'Escape') cancelBtn.click(); | ||||
|             }; | ||||
|              | ||||
|             document.body.appendChild(dialog); | ||||
|             document.body.classList.add('modal-open'); | ||||
|             input.focus(); | ||||
|             input.select(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async showConfirmDialog(title, message = '') { | ||||
|         return new Promise((resolve) => { | ||||
|             const dialog = this.createConfirmDialog(title, message); | ||||
|             const confirmBtn = dialog.querySelector('.btn-danger'); | ||||
|             const cancelBtn = dialog.querySelector('.btn-secondary'); | ||||
|              | ||||
|             const cleanup = () => { | ||||
|                 dialog.remove(); | ||||
|                 document.body.classList.remove('modal-open'); | ||||
|             }; | ||||
|              | ||||
|             confirmBtn.onclick = () => { | ||||
|                 resolve(true); | ||||
|                 cleanup(); | ||||
|             }; | ||||
|              | ||||
|             cancelBtn.onclick = () => { | ||||
|                 resolve(false); | ||||
|                 cleanup(); | ||||
|             }; | ||||
|              | ||||
|             document.body.appendChild(dialog); | ||||
|             document.body.classList.add('modal-open'); | ||||
|             confirmBtn.focus(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     createInputDialog(title, placeholder) { | ||||
|         const backdrop = document.createElement('div'); | ||||
|         backdrop.className = 'modal-backdrop fade show'; | ||||
|          | ||||
|         const dialog = document.createElement('div'); | ||||
|         dialog.className = 'modal fade show d-block'; | ||||
|         dialog.setAttribute('tabindex', '-1'); | ||||
|         dialog.style.display = 'block'; | ||||
|          | ||||
|         dialog.innerHTML = ` | ||||
|             <div class="modal-dialog modal-dialog-centered"> | ||||
|                 <div class="modal-content"> | ||||
|                     <div class="modal-header"> | ||||
|                         <h5 class="modal-title">${title}</h5> | ||||
|                         <button type="button" class="btn-close"></button> | ||||
|                     </div> | ||||
|                     <div class="modal-body"> | ||||
|                         <input type="text" class="form-control" placeholder="${placeholder}" autofocus> | ||||
|                     </div> | ||||
|                     <div class="modal-footer"> | ||||
|                         <button type="button" class="btn btn-secondary">Cancel</button> | ||||
|                         <button type="button" class="btn btn-primary">OK</button> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         `; | ||||
|          | ||||
|         document.body.appendChild(backdrop); | ||||
|         return dialog; | ||||
|     } | ||||
|  | ||||
|     createConfirmDialog(title, message) { | ||||
|         const backdrop = document.createElement('div'); | ||||
|         backdrop.className = 'modal-backdrop fade show'; | ||||
|          | ||||
|         const dialog = document.createElement('div'); | ||||
|         dialog.className = 'modal fade show d-block'; | ||||
|         dialog.setAttribute('tabindex', '-1'); | ||||
|         dialog.style.display = 'block'; | ||||
|          | ||||
|         dialog.innerHTML = ` | ||||
|             <div class="modal-dialog modal-dialog-centered"> | ||||
|                 <div class="modal-content"> | ||||
|                     <div class="modal-header border-danger"> | ||||
|                         <h5 class="modal-title text-danger">${title}</h5> | ||||
|                         <button type="button" class="btn-close"></button> | ||||
|                     </div> | ||||
|                     <div class="modal-body"> | ||||
|                         <p>${message}</p> | ||||
|                         <p class="text-danger small">This action cannot be undone.</p> | ||||
|                     </div> | ||||
|                     <div class="modal-footer"> | ||||
|                         <button type="button" class="btn btn-secondary">Cancel</button> | ||||
|                         <button type="button" class="btn btn-danger">Delete</button> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         `; | ||||
|          | ||||
|         document.body.appendChild(backdrop); | ||||
|         return dialog; | ||||
|     } | ||||
|  | ||||
|     updatePasteMenuItem() { | ||||
|         const pasteItem = document.getElementById('pasteMenuItem'); | ||||
|         if (pasteItem) { | ||||
|             pasteItem.style.display = this.clipboard ? 'flex' : 'none'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -18,17 +18,15 @@ class FileTree { | ||||
|     setupEventListeners() { | ||||
|         // Click handler for tree nodes | ||||
|         this.container.addEventListener('click', (e) => { | ||||
|             console.log('Container clicked', e.target); | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             console.log('Node found', node); | ||||
|             const path = node.dataset.path; | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|              | ||||
|             // Toggle folder | ||||
|             if (e.target.closest('.tree-node-toggle')) { | ||||
|                 this.toggleFolder(node); | ||||
|                 return; | ||||
|             } | ||||
|             // The toggle is handled inside renderNodes now | ||||
|              | ||||
|             // Select node | ||||
|             if (isDir) { | ||||
| @@ -69,87 +67,46 @@ class FileTree { | ||||
|      | ||||
|     renderNodes(nodes, parentElement, level) { | ||||
|         nodes.forEach(node => { | ||||
|             const nodeWrapper = document.createElement('div'); | ||||
|             nodeWrapper.className = 'tree-node-wrapper'; | ||||
|  | ||||
|             // Create node element | ||||
|             const nodeElement = this.createNodeElement(node, level); | ||||
|             parentElement.appendChild(nodeElement); | ||||
|              | ||||
|             nodeWrapper.appendChild(nodeElement); | ||||
|  | ||||
|             // Create children container ONLY if has children | ||||
|             if (node.children && node.children.length > 0) { | ||||
|                 const childContainer = document.createElement('div'); | ||||
|                 childContainer.className = 'tree-children'; | ||||
|                 childContainer.style.display = 'none'; | ||||
|                 nodeElement.appendChild(childContainer); | ||||
|                 childContainer.dataset.parent = node.path; | ||||
|                 childContainer.style.marginLeft = `${(level + 1) * 12}px`; | ||||
|  | ||||
|                 // Recursively render children | ||||
|                 this.renderNodes(node.children, childContainer, level + 1); | ||||
|                 nodeWrapper.appendChild(childContainer); | ||||
|  | ||||
|                 // Make toggle functional | ||||
|                 const toggle = nodeElement.querySelector('.tree-node-toggle'); | ||||
|                 if (toggle) { | ||||
|                     toggle.addEventListener('click', (e) => { | ||||
|                         console.log('Toggle clicked', e.target); | ||||
|                         e.stopPropagation(); | ||||
|                         const isHidden = childContainer.style.display === 'none'; | ||||
|                         console.log('Is hidden?', isHidden); | ||||
|                         childContainer.style.display = isHidden ? 'block' : 'none'; | ||||
|                         toggle.innerHTML = isHidden ? '▼' : '▶'; | ||||
|                         toggle.classList.toggle('expanded'); | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             parentElement.appendChild(nodeWrapper); | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     createNodeElement(node, level) { | ||||
|         const nodeWrapper = document.createElement('div'); | ||||
|         nodeWrapper.className = 'tree-node-wrapper'; | ||||
|         nodeWrapper.style.marginLeft = `${level * 12}px`; | ||||
|          | ||||
|         const div = document.createElement('div'); | ||||
|         div.className = 'tree-node'; | ||||
|         div.dataset.path = node.path; | ||||
|         div.dataset.isdir = node.isDirectory; | ||||
|          | ||||
|         // Toggle arrow for folders | ||||
|         if (node.isDirectory) { | ||||
|             const toggle = document.createElement('span'); | ||||
|             toggle.className = 'tree-node-toggle'; | ||||
|             toggle.innerHTML = '▶'; | ||||
|             div.appendChild(toggle); | ||||
|         } else { | ||||
|             const spacer = document.createElement('span'); | ||||
|             spacer.style.width = '16px'; | ||||
|             spacer.style.display = 'inline-block'; | ||||
|             div.appendChild(spacer); | ||||
|         } | ||||
|          | ||||
|         // Icon | ||||
|         const icon = document.createElement('i'); | ||||
|         icon.className = `bi ${node.isDirectory ? 'bi-folder-fill' : 'bi-file-earmark-text'} tree-node-icon`; | ||||
|         div.appendChild(icon); | ||||
|          | ||||
|         // Content wrapper | ||||
|         const contentWrapper = document.createElement('div'); | ||||
|         contentWrapper.className = 'tree-node-content'; | ||||
|          | ||||
|         // Name | ||||
|         const name = document.createElement('span'); | ||||
|         name.className = 'tree-node-name'; | ||||
|         name.textContent = node.name; | ||||
|         name.title = node.name; // Tooltip on hover | ||||
|         contentWrapper.appendChild(name); | ||||
|          | ||||
|         // Size for files | ||||
|         if (!node.isDirectory && node.size) { | ||||
|             const size = document.createElement('span'); | ||||
|             size.className = 'file-size-badge'; | ||||
|             size.textContent = this.formatSize(node.size); | ||||
|             contentWrapper.appendChild(size); | ||||
|         } | ||||
|          | ||||
|         div.appendChild(contentWrapper); | ||||
|         nodeWrapper.appendChild(div); | ||||
|          | ||||
|         return nodeWrapper; | ||||
|     } | ||||
|      | ||||
|     toggleFolder(nodeElement) { | ||||
|         const childContainer = nodeElement.querySelector('.tree-children'); | ||||
|         if (!childContainer) return; | ||||
|          | ||||
|         const toggle = nodeElement.querySelector('.tree-node-toggle'); | ||||
|         const isExpanded = childContainer.style.display !== 'none'; | ||||
|          | ||||
|         if (isExpanded) { | ||||
|             childContainer.style.display = 'none'; | ||||
|             toggle.innerHTML = '▶'; | ||||
|         } else { | ||||
|             childContainer.style.display = 'block'; | ||||
|             toggle.innerHTML = '▼'; | ||||
|         } | ||||
|     } | ||||
|     // toggleFolder is no longer needed as the event listener is added in renderNodes. | ||||
|      | ||||
|     selectFile(path) { | ||||
|         this.selectedPath = path; | ||||
| @@ -181,6 +138,32 @@ class FileTree { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     createNodeElement(node, level) { | ||||
|         const nodeElement = document.createElement('div'); | ||||
|         nodeElement.className = 'tree-node'; | ||||
|         nodeElement.dataset.path = node.path; | ||||
|         nodeElement.dataset.isdir = node.isDirectory; | ||||
|         nodeElement.style.paddingLeft = `${level * 12}px`; | ||||
|  | ||||
|         const icon = document.createElement('span'); | ||||
|         icon.className = 'tree-node-icon'; | ||||
|         if (node.isDirectory) { | ||||
|             icon.innerHTML = '▶'; // Collapsed by default | ||||
|             icon.classList.add('tree-node-toggle'); | ||||
|         } else { | ||||
|             icon.innerHTML = '●'; // File icon | ||||
|         } | ||||
|  | ||||
|         const title = document.createElement('span'); | ||||
|         title.className = 'tree-node-title'; | ||||
|         title.textContent = node.name; | ||||
|  | ||||
|         nodeElement.appendChild(icon); | ||||
|         nodeElement.appendChild(title); | ||||
|  | ||||
|         return nodeElement; | ||||
|     } | ||||
|      | ||||
|     formatSize(bytes) { | ||||
|         if (bytes === 0) return '0 B'; | ||||
| @@ -190,11 +173,21 @@ class FileTree { | ||||
|         return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i]; | ||||
|     } | ||||
|      | ||||
|     newFile() { | ||||
|         this.selectedPath = null; | ||||
|         this.updateSelection(); | ||||
|         // Potentially clear editor via callback | ||||
|         if (this.onFileSelect) { | ||||
|             this.onFileSelect({ path: null, isDirectory: false }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async createFile(parentPath, filename) { | ||||
|         try { | ||||
|             const fullPath = parentPath ? `${parentPath}/${filename}` : filename; | ||||
|             await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n'); | ||||
|             await this.load(); | ||||
|             this.selectFile(fullPath); // Select the new file | ||||
|             showNotification('File created', 'success'); | ||||
|             return fullPath; | ||||
|         } catch (error) { | ||||
|   | ||||
| @@ -47,29 +47,26 @@ function showContextMenu(x, y, target) { | ||||
|     const menu = document.getElementById('contextMenu'); | ||||
|     if (!menu) return; | ||||
|      | ||||
|     // Store target | ||||
|     // Store target data | ||||
|     menu.dataset.targetPath = target.path; | ||||
|     menu.dataset.targetIsDir = target.isDir; | ||||
|      | ||||
|     // Show/hide menu items based on target type | ||||
|     const newFileItem = menu.querySelector('[data-action="new-file"]'); | ||||
|     const newFolderItem = menu.querySelector('[data-action="new-folder"]'); | ||||
|     const uploadItem = menu.querySelector('[data-action="upload"]'); | ||||
|     const downloadItem = menu.querySelector('[data-action="download"]'); | ||||
|     const items = { | ||||
|         'new-file': target.isDir, | ||||
|         'new-folder': target.isDir, | ||||
|         'upload': target.isDir, | ||||
|         'download': true, | ||||
|         'paste': target.isDir && window.fileTreeActions?.clipboard, | ||||
|         'open': !target.isDir | ||||
|     }; | ||||
|      | ||||
|     if (target.isDir) { | ||||
|         // Folder context menu | ||||
|         if (newFileItem) newFileItem.style.display = 'block'; | ||||
|         if (newFolderItem) newFolderItem.style.display = 'block'; | ||||
|         if (uploadItem) uploadItem.style.display = 'block'; | ||||
|         if (downloadItem) downloadItem.style.display = 'block'; | ||||
|     } else { | ||||
|         // File context menu | ||||
|         if (newFileItem) newFileItem.style.display = 'none'; | ||||
|         if (newFolderItem) newFolderItem.style.display = 'none'; | ||||
|         if (uploadItem) uploadItem.style.display = 'none'; | ||||
|         if (downloadItem) downloadItem.style.display = 'block'; | ||||
|     } | ||||
|     Object.entries(items).forEach(([action, show]) => { | ||||
|         const item = menu.querySelector(`[data-action="${action}"]`); | ||||
|         if (item) { | ||||
|             item.style.display = show ? 'flex' : 'none'; | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     // Position menu | ||||
|     menu.style.display = 'block'; | ||||
| @@ -77,13 +74,15 @@ function showContextMenu(x, y, target) { | ||||
|     menu.style.top = y + 'px'; | ||||
|      | ||||
|     // Adjust if off-screen | ||||
|     const rect = menu.getBoundingClientRect(); | ||||
|     if (rect.right > window.innerWidth) { | ||||
|         menu.style.left = (window.innerWidth - rect.width - 10) + 'px'; | ||||
|     } | ||||
|     if (rect.bottom > window.innerHeight) { | ||||
|         menu.style.top = (window.innerHeight - rect.height - 10) + 'px'; | ||||
|     } | ||||
|     setTimeout(() => { | ||||
|         const rect = menu.getBoundingClientRect(); | ||||
|         if (rect.right > window.innerWidth) { | ||||
|             menu.style.left = (window.innerWidth - rect.width - 10) + 'px'; | ||||
|         } | ||||
|         if (rect.bottom > window.innerHeight) { | ||||
|             menu.style.top = (window.innerHeight - rect.height - 10) + 'px'; | ||||
|         } | ||||
|     }, 0); | ||||
| } | ||||
|  | ||||
| function hideContextMenu() { | ||||
| @@ -93,9 +92,29 @@ function hideContextMenu() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Hide context menu on click outside | ||||
| // Context menu item click handler | ||||
| 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); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // Hide on outside click | ||||
| document.addEventListener('click', (e) => { | ||||
|     if (!e.target.closest('#contextMenu')) { | ||||
|     if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) { | ||||
|         hideContextMenu(); | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -158,7 +158,7 @@ | ||||
|     <!-- Mermaid for diagrams --> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> | ||||
|  | ||||
|     <!-- Modular JavaScript --> | ||||
|     <script src="/static/js/file-tree-actions.js"></script> | ||||
|     <script src="/static/js/webdav-client.js"></script> | ||||
|     <script src="/static/js/file-tree.js"></script> | ||||
|     <script src="/static/js/editor.js"></script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user