This commit is contained in:
2025-10-26 08:42:43 +04:00
parent 5c9e07eee0
commit 98a529a3cc
10 changed files with 118 additions and 198 deletions

View File

@@ -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!

View File

@@ -1,9 +0,0 @@
# test
- 1
- 2

View File

@@ -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
View File

@@ -0,0 +1,3 @@
.modal-header .btn-close {
filter: var(--bs-btn-close-white-filter);
}

View File

@@ -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
View 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');

View File

@@ -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');
}
}

View File

@@ -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>

View File

@@ -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>