...
This commit is contained in:
@@ -187,6 +187,7 @@ def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
server.start()
|
server.start()
|
||||||
|
server.wait()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\nShutting down...")
|
print("\n\nShutting down...")
|
||||||
server.stop()
|
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 */
|
||||||
.column-resizer {
|
.column-resizer {
|
||||||
width: 4px;
|
width: 1px;
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-color);
|
||||||
cursor: col-resize;
|
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;
|
user-select: none;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
padding: 0 3px; /* Add invisible padding for easier grab */
|
||||||
|
margin: 0 -3px; /* Compensate for padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-resizer:hover {
|
.column-resizer:hover {
|
||||||
background-color: var(--link-color);
|
background-color: var(--link-color);
|
||||||
width: 6px;
|
width: 1px;
|
||||||
margin: 0 -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 {
|
.column-resizer.dragging {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await fileTree.load();
|
await fileTree.load();
|
||||||
|
|
||||||
// Initialize editor
|
// Initialize editor
|
||||||
editor = new MarkdownEditor('editor', 'preview');
|
editor = new MarkdownEditor('editor', 'preview', 'filenameInput');
|
||||||
|
|
||||||
// Setup editor drop handler
|
// Setup editor drop handler
|
||||||
const editorDropHandler = new EditorDropHandler(
|
const editorDropHandler = new EditorDropHandler(
|
||||||
@@ -66,6 +66,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
// Initialize mermaid
|
// Initialize mermaid
|
||||||
mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' });
|
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
|
// Listen for column resize events to refresh editor
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class MarkdownEditor {
|
|||||||
this.currentFile = null;
|
this.currentFile = null;
|
||||||
this.filenameInput.value = '';
|
this.filenameInput.value = '';
|
||||||
this.filenameInput.focus();
|
this.filenameInput.focus();
|
||||||
this.editor.setValue('');
|
this.editor.setValue('# New File\n\nStart typing...\n');
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
|
|
||||||
if (window.showNotification) {
|
if (window.showNotification) {
|
||||||
@@ -192,7 +192,7 @@ class MarkdownEditor {
|
|||||||
*/
|
*/
|
||||||
updatePreview() {
|
updatePreview() {
|
||||||
const markdown = this.editor.getValue();
|
const markdown = this.editor.getValue();
|
||||||
let html = this.marked.parse(markdown);
|
let html = window.marked.parse(markdown);
|
||||||
|
|
||||||
// Process mermaid diagrams
|
// Process mermaid diagrams
|
||||||
html = html.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, (match, code) => {
|
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() {
|
setupEventListeners() {
|
||||||
// Click handler for tree nodes
|
// Click handler for tree nodes
|
||||||
this.container.addEventListener('click', (e) => {
|
this.container.addEventListener('click', (e) => {
|
||||||
|
console.log('Container clicked', e.target);
|
||||||
const node = e.target.closest('.tree-node');
|
const node = e.target.closest('.tree-node');
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
|
console.log('Node found', node);
|
||||||
const path = node.dataset.path;
|
const path = node.dataset.path;
|
||||||
const isDir = node.dataset.isdir === 'true';
|
const isDir = node.dataset.isdir === 'true';
|
||||||
|
|
||||||
// Toggle folder
|
// The toggle is handled inside renderNodes now
|
||||||
if (e.target.closest('.tree-node-toggle')) {
|
|
||||||
this.toggleFolder(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select node
|
// Select node
|
||||||
if (isDir) {
|
if (isDir) {
|
||||||
@@ -69,87 +67,46 @@ class FileTree {
|
|||||||
|
|
||||||
renderNodes(nodes, parentElement, level) {
|
renderNodes(nodes, parentElement, level) {
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
|
const nodeWrapper = document.createElement('div');
|
||||||
|
nodeWrapper.className = 'tree-node-wrapper';
|
||||||
|
|
||||||
|
// Create node element
|
||||||
const nodeElement = this.createNodeElement(node, level);
|
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) {
|
if (node.children && node.children.length > 0) {
|
||||||
const childContainer = document.createElement('div');
|
const childContainer = document.createElement('div');
|
||||||
childContainer.className = 'tree-children';
|
childContainer.className = 'tree-children';
|
||||||
childContainer.style.display = 'none';
|
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);
|
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) {
|
// toggleFolder is no longer needed as the event listener is added in renderNodes.
|
||||||
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 = '▼';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectFile(path) {
|
selectFile(path) {
|
||||||
this.selectedPath = 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) {
|
formatSize(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
@@ -190,11 +173,21 @@ class FileTree {
|
|||||||
return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i];
|
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) {
|
async createFile(parentPath, filename) {
|
||||||
try {
|
try {
|
||||||
const fullPath = parentPath ? `${parentPath}/${filename}` : filename;
|
const fullPath = parentPath ? `${parentPath}/${filename}` : filename;
|
||||||
await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n');
|
await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n');
|
||||||
await this.load();
|
await this.load();
|
||||||
|
this.selectFile(fullPath); // Select the new file
|
||||||
showNotification('File created', 'success');
|
showNotification('File created', 'success');
|
||||||
return fullPath;
|
return fullPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -47,29 +47,26 @@ function showContextMenu(x, y, target) {
|
|||||||
const menu = document.getElementById('contextMenu');
|
const menu = document.getElementById('contextMenu');
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
|
|
||||||
// Store target
|
// Store target data
|
||||||
menu.dataset.targetPath = target.path;
|
menu.dataset.targetPath = target.path;
|
||||||
menu.dataset.targetIsDir = target.isDir;
|
menu.dataset.targetIsDir = target.isDir;
|
||||||
|
|
||||||
// Show/hide menu items based on target type
|
// Show/hide menu items based on target type
|
||||||
const newFileItem = menu.querySelector('[data-action="new-file"]');
|
const items = {
|
||||||
const newFolderItem = menu.querySelector('[data-action="new-folder"]');
|
'new-file': target.isDir,
|
||||||
const uploadItem = menu.querySelector('[data-action="upload"]');
|
'new-folder': target.isDir,
|
||||||
const downloadItem = menu.querySelector('[data-action="download"]');
|
'upload': target.isDir,
|
||||||
|
'download': true,
|
||||||
|
'paste': target.isDir && window.fileTreeActions?.clipboard,
|
||||||
|
'open': !target.isDir
|
||||||
|
};
|
||||||
|
|
||||||
if (target.isDir) {
|
Object.entries(items).forEach(([action, show]) => {
|
||||||
// Folder context menu
|
const item = menu.querySelector(`[data-action="${action}"]`);
|
||||||
if (newFileItem) newFileItem.style.display = 'block';
|
if (item) {
|
||||||
if (newFolderItem) newFolderItem.style.display = 'block';
|
item.style.display = show ? 'flex' : 'none';
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position menu
|
// Position menu
|
||||||
menu.style.display = 'block';
|
menu.style.display = 'block';
|
||||||
@@ -77,13 +74,15 @@ function showContextMenu(x, y, target) {
|
|||||||
menu.style.top = y + 'px';
|
menu.style.top = y + 'px';
|
||||||
|
|
||||||
// Adjust if off-screen
|
// Adjust if off-screen
|
||||||
const rect = menu.getBoundingClientRect();
|
setTimeout(() => {
|
||||||
if (rect.right > window.innerWidth) {
|
const rect = menu.getBoundingClientRect();
|
||||||
menu.style.left = (window.innerWidth - rect.width - 10) + 'px';
|
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';
|
if (rect.bottom > window.innerHeight) {
|
||||||
}
|
menu.style.top = (window.innerHeight - rect.height - 10) + 'px';
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideContextMenu() {
|
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) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!e.target.closest('#contextMenu')) {
|
if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
<!-- Mermaid for diagrams -->
|
<!-- Mermaid for diagrams -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
<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/webdav-client.js"></script>
|
||||||
<script src="/static/js/file-tree.js"></script>
|
<script src="/static/js/file-tree.js"></script>
|
||||||
<script src="/static/js/editor.js"></script>
|
<script src="/static/js/editor.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user