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