...
This commit is contained in:
		| @@ -1,58 +0,0 @@ | ||||
| # Welcome to Markdown Editor | ||||
|  | ||||
| This is a **WebDAV-based** markdown editor with modular architecture. | ||||
|  | ||||
| ```mermaid | ||||
| %%{init: {'theme':'dark'}}%% | ||||
| graph TD | ||||
|  | ||||
|     %% User side | ||||
|     H1[Human A] --> PA1[Personal Agent A] | ||||
|     H2[Human B] --> PA2[Personal Agent B] | ||||
|  | ||||
|     %% Local mail nodes | ||||
|     PA1 --> M1[MyMail Node A] | ||||
|     PA2 --> M2[MyMail Node B] | ||||
|  | ||||
|     %% Proxy coordination layer | ||||
|     M1 --> Proxy1A[Proxy Agent L1] | ||||
|     Proxy1A --> Proxy2A[Proxy Agent L2] | ||||
|     Proxy2A --> Proxy2B[Proxy Agent L2] | ||||
|     Proxy2B --> Proxy1B[Proxy Agent L1] | ||||
|     Proxy1B --> M2 | ||||
|  | ||||
|     %% Blockchain anchoring | ||||
|     M1 --> Chain[Dynamic Blockchain] | ||||
|     M2 --> Chain | ||||
| ``` | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - ✅ Standards-compliant WebDAV backend | ||||
| - ✅ Multiple document collections | ||||
| - ✅ Modular JavaScript/CSS | ||||
| - ✅ Live preview | ||||
| - ✅ Syntax highlighting | ||||
| - ✅ Mermaid diagrams | ||||
| - ✅ Dark mode | ||||
|  | ||||
| ## WebDAV Methods | ||||
|  | ||||
| This editor uses standard WebDAV methods: | ||||
|  | ||||
| - `PROPFIND` - List files | ||||
| - `GET` - Read files | ||||
| - `PUT` - Create/update files | ||||
| - `DELETE` - Delete files | ||||
| - `COPY` - Copy files | ||||
| - `MOVE` - Move/rename files | ||||
| - `MKCOL` - Create directories | ||||
|  | ||||
| ## Try It Out | ||||
|  | ||||
| 1. Create a new file | ||||
| 2. Edit markdown | ||||
| 3. See live preview | ||||
| 4. Save with WebDAV PUT | ||||
|  | ||||
| Enjoy! | ||||
| @@ -1,9 +0,0 @@ | ||||
|  | ||||
| # test | ||||
|  | ||||
| - 1 | ||||
| - 2 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -83,6 +83,11 @@ class MarkdownEditorApp: | ||||
|         if path.startswith('/static/'): | ||||
|             return self.handle_static(environ, start_response) | ||||
|          | ||||
|         # Health check | ||||
|         if path == '/health' and method == 'GET': | ||||
|             start_response('200 OK', [('Content-Type', 'text/plain')]) | ||||
|             return [b'OK'] | ||||
|              | ||||
|         # API for collections | ||||
|         if path == '/fs/' and method == 'GET': | ||||
|             return self.handle_collections_list(environ, start_response) | ||||
|   | ||||
							
								
								
									
										3
									
								
								static/css/modal.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								static/css/modal.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| .modal-header .btn-close { | ||||
|     filter: var(--bs-btn-close-white-filter); | ||||
| } | ||||
							
								
								
									
										117
									
								
								static/js/app.js
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								static/js/app.js
									
									
									
									
									
								
							| @@ -102,6 +102,12 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|             fileTree.selectNode(path); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     window.eventBus.on('file-deleted', async () => { | ||||
|         if (fileTree) { | ||||
|             await fileTree.load(); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| // Listen for column resize events to refresh editor | ||||
| @@ -131,117 +137,12 @@ function setupContextMenuHandlers() { | ||||
|          | ||||
|         hideContextMenu(); | ||||
|          | ||||
|         await handleContextAction(action, targetPath, isDir); | ||||
|         await window.fileTreeActions.execute(action, targetPath, isDir); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function handleContextAction(action, targetPath, isDir) { | ||||
|     switch (action) { | ||||
|         case 'open': | ||||
|             if (!isDir) { | ||||
|                 await editor.loadFile(targetPath); | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'new-file': | ||||
|             if (isDir) { | ||||
|                 const filename = prompt('Enter filename:'); | ||||
|                 if (filename) { | ||||
|                     await fileTree.createFile(targetPath, filename); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'new-folder': | ||||
|             if (isDir) { | ||||
|                 const foldername = prompt('Enter folder name:'); | ||||
|                 if (foldername) { | ||||
|                     await fileTree.createFolder(targetPath, foldername); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'upload': | ||||
|             if (isDir) { | ||||
|                 showFileUploadDialog(targetPath, async (path, file) => { | ||||
|                     await fileTree.uploadFile(path, file); | ||||
|                 }); | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'download': | ||||
|             if (isDir) { | ||||
|                 await fileTree.downloadFolder(targetPath); | ||||
|             } else { | ||||
|                 await fileTree.downloadFile(targetPath); | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'rename': | ||||
|             const newName = prompt('Enter new name:', targetPath.split('/').pop()); | ||||
|             if (newName) { | ||||
|                 const parentPath = targetPath.split('/').slice(0, -1).join('/'); | ||||
|                 const newPath = parentPath ? `${parentPath}/${newName}` : newName; | ||||
|                 try { | ||||
|                     await webdavClient.move(targetPath, newPath); | ||||
|                     await fileTree.load(); | ||||
|                     showNotification('Renamed', 'success'); | ||||
|                 } catch (error) { | ||||
|                     console.error('Failed to rename:', error); | ||||
|                     showNotification('Failed to rename', 'error'); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'copy': | ||||
|             clipboard = { path: targetPath, operation: 'copy' }; | ||||
|             showNotification('Copied to clipboard', 'info'); | ||||
|             updatePasteVisibility(); | ||||
|             break; | ||||
|              | ||||
|         case 'cut': | ||||
|             clipboard = { path: targetPath, operation: 'cut' }; | ||||
|             showNotification('Cut to clipboard', 'info'); | ||||
|             updatePasteVisibility(); | ||||
|             break; | ||||
|              | ||||
|         case 'paste': | ||||
|             if (clipboard && isDir) { | ||||
|                 const filename = clipboard.path.split('/').pop(); | ||||
|                 const destPath = `${targetPath}/${filename}`; | ||||
|                  | ||||
|                 try { | ||||
|                     if (clipboard.operation === 'copy') { | ||||
|                         await webdavClient.copy(clipboard.path, destPath); | ||||
|                         showNotification('Copied', 'success'); | ||||
|                     } else { | ||||
|                         await webdavClient.move(clipboard.path, destPath); | ||||
|                         showNotification('Moved', 'success'); | ||||
|                         clipboard = null; | ||||
|                         updatePasteVisibility(); | ||||
|                     } | ||||
|                     await fileTree.load(); | ||||
|                 } catch (error) { | ||||
|                     console.error('Failed to paste:', error); | ||||
|                     showNotification('Failed to paste', 'error'); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'delete': | ||||
|             if (confirm(`Delete ${targetPath}?`)) { | ||||
|                 try { | ||||
|                     await webdavClient.delete(targetPath); | ||||
|                     await fileTree.load(); | ||||
|                     showNotification('Deleted', 'success'); | ||||
|                 } catch (error) { | ||||
|                     console.error('Failed to delete:', error); | ||||
|                     showNotification('Failed to delete', 'error'); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|     } | ||||
| } | ||||
| // 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'); | ||||
|   | ||||
							
								
								
									
										68
									
								
								static/js/confirmation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								static/js/confirmation.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| /** | ||||
|  * Confirmation Modal Manager | ||||
|  * Handles showing and hiding a Bootstrap modal for confirmations and prompts. | ||||
|  */ | ||||
| class Confirmation { | ||||
|     constructor(modalId) { | ||||
|         this.modalElement = document.getElementById(modalId); | ||||
|         this.modal = new bootstrap.Modal(this.modalElement); | ||||
|         this.messageElement = this.modalElement.querySelector('#confirmationMessage'); | ||||
|         this.inputElement = this.modalElement.querySelector('#confirmationInput'); | ||||
|         this.confirmButton = this.modalElement.querySelector('#confirmButton'); | ||||
|         this.titleElement = this.modalElement.querySelector('.modal-title'); | ||||
|         this.currentResolver = null; | ||||
|     } | ||||
|  | ||||
|     _show(message, title, showInput = false, defaultValue = '') { | ||||
|         return new Promise((resolve) => { | ||||
|             this.currentResolver = resolve; | ||||
|             this.titleElement.textContent = title; | ||||
|             this.messageElement.textContent = message; | ||||
|  | ||||
|             if (showInput) { | ||||
|                 this.inputElement.style.display = 'block'; | ||||
|                 this.inputElement.value = defaultValue; | ||||
|                 this.inputElement.focus(); | ||||
|             } else { | ||||
|                 this.inputElement.style.display = 'none'; | ||||
|             } | ||||
|  | ||||
|             this.confirmButton.onclick = () => this._handleConfirm(showInput); | ||||
|             this.modalElement.addEventListener('hidden.bs.modal', () => this._handleCancel(), { once: true }); | ||||
|              | ||||
|             this.modal.show(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _handleConfirm(isPrompt) { | ||||
|         if (this.currentResolver) { | ||||
|             const value = isPrompt ? this.inputElement.value : true; | ||||
|             this.currentResolver(value); | ||||
|             this._cleanup(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     _handleCancel() { | ||||
|         if (this.currentResolver) { | ||||
|             this.currentResolver(null); // Resolve with null for cancellation | ||||
|             this._cleanup(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     _cleanup() { | ||||
|         this.confirmButton.onclick = null; | ||||
|         this.modal.hide(); | ||||
|         this.currentResolver = null; | ||||
|     } | ||||
|  | ||||
|     confirm(message, title = 'Confirmation') { | ||||
|         return this._show(message, title, false); | ||||
|     } | ||||
|  | ||||
|     prompt(message, defaultValue = '', title = 'Prompt') { | ||||
|         return this._show(message, title, true, defaultValue); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Make it globally available | ||||
| window.ConfirmationManager = new Confirmation('confirmationModal'); | ||||
| @@ -164,32 +164,19 @@ class MarkdownEditor { | ||||
|      */ | ||||
|     async deleteFile() { | ||||
|         if (!this.currentFile) { | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('No file selected', 'warning'); | ||||
|             } | ||||
|             window.showNotification('No file selected', 'warning'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!confirm(`Delete ${this.currentFile}?`)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             await this.webdavClient.delete(this.currentFile); | ||||
|              | ||||
|             if (window.showNotification) { | ||||
|         const confirmed = await window.ConfirmationManager.confirm(`Are you sure you want to delete ${this.currentFile}?`, 'Delete File'); | ||||
|         if (confirmed) { | ||||
|             try { | ||||
|                 await this.webdavClient.delete(this.currentFile); | ||||
|                 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) { | ||||
|                 this.newFile(); | ||||
|                 window.eventBus.dispatch('file-deleted'); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to delete file:', error); | ||||
|                 window.showNotification('Failed to delete file', 'danger'); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -152,6 +152,7 @@ class FileTreeActions { | ||||
|              | ||||
|             const cleanup = () => { | ||||
|                 dialog.remove(); | ||||
|                 document.querySelector('.modal-backdrop').remove(); | ||||
|                 document.body.classList.remove('modal-open'); | ||||
|             }; | ||||
|              | ||||
| @@ -221,7 +222,7 @@ class FileTreeActions { | ||||
|                         <button type="button" class="btn-close"></button> | ||||
|                     </div> | ||||
|                     <div class="modal-body"> | ||||
|                         <input type="text" class="form-control" placeholder="${placeholder}" autofocus> | ||||
|                         <input type="text" class="form-control" value="${placeholder}" autofocus> | ||||
|                     </div> | ||||
|                     <div class="modal-footer"> | ||||
|                         <button type="button" class="btn btn-secondary">Cancel</button> | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
|     <link rel="stylesheet" href="/static/css/file-tree.css"> | ||||
|     <link rel="stylesheet" href="/static/css/editor.css"> | ||||
|     <link rel="stylesheet" href="/static/css/components.css"> | ||||
|     <link rel="stylesheet" href="/static/css/modal.css"> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
| @@ -124,6 +125,26 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Confirmation Modal --> | ||||
|     <div class="modal fade" id="confirmationModal" tabindex="-1" aria-labelledby="confirmationModalLabel" aria-hidden="true"> | ||||
|         <div class="modal-dialog"> | ||||
|             <div class="modal-content"> | ||||
|                 <div class="modal-header"> | ||||
|                     <h5 class="modal-title" id="confirmationModalLabel">Confirmation</h5> | ||||
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <p id="confirmationMessage"></p> | ||||
|                     <input type="text" id="confirmationInput" class="form-control" style="display: none;"> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | ||||
|                     <button type="button" class="btn btn-primary" id="confirmButton">OK</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Bootstrap JS --> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> | ||||
|  | ||||
| @@ -161,6 +182,7 @@ | ||||
|     <script src="/static/js/file-tree.js" defer></script> | ||||
|     <script src="/static/js/editor.js" defer></script> | ||||
|     <script src="/static/js/ui-utils.js" defer></script> | ||||
|     <script src="/static/js/confirmation.js" defer></script> | ||||
|     <script src="/static/js/file-tree-actions.js" defer></script> | ||||
|     <script src="/static/js/column-resizer.js" defer></script> | ||||
|     <script src="/static/js/app.js" defer></script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user