- Extract UI components into separate JS files - Centralize configuration values in config.js - Introduce a dedicated logger module - Improve file tree drag-and-drop and undo functionality - Refactor modal handling to a single manager - Add URL routing support for SPA navigation - Implement view mode for read-only access
254 lines
8.5 KiB
JavaScript
254 lines
8.5 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Validate and sanitize filename/folder name
|
|
* Returns { valid: boolean, sanitized: string, message: string }
|
|
* Now uses ValidationUtils from utils.js
|
|
*/
|
|
validateFileName(name, isFolder = false) {
|
|
return ValidationUtils.validateFileName(name, isFolder);
|
|
}
|
|
|
|
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);
|
|
}
|
|
},
|
|
|
|
'new-file': async function (path, isDir) {
|
|
if (!isDir) return;
|
|
|
|
const filename = await window.ModalManager.prompt(
|
|
'Enter filename (lowercase, underscore only):',
|
|
'new_file.md',
|
|
'New File'
|
|
);
|
|
|
|
if (!filename) return;
|
|
|
|
let finalFilename = filename;
|
|
const validation = this.validateFileName(filename, false);
|
|
|
|
if (!validation.valid) {
|
|
showNotification(validation.message, 'warning');
|
|
|
|
// Ask if user wants to use sanitized version
|
|
if (validation.sanitized) {
|
|
const useSanitized = await window.ModalManager.confirm(
|
|
`${filename} → ${validation.sanitized}`,
|
|
'Use sanitized name?',
|
|
false
|
|
);
|
|
if (useSanitized) {
|
|
finalFilename = validation.sanitized;
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const fullPath = `${path}/${finalFilename}`.replace(/\/+/g, '/');
|
|
await this.webdavClient.put(fullPath, '# New File\n\n');
|
|
|
|
// Clear undo history since new file was created
|
|
if (this.fileTree.lastMoveOperation) {
|
|
this.fileTree.lastMoveOperation = null;
|
|
}
|
|
|
|
await this.fileTree.load();
|
|
showNotification(`Created ${finalFilename}`, 'success');
|
|
await this.editor.loadFile(fullPath);
|
|
},
|
|
|
|
'new-folder': async function (path, isDir) {
|
|
if (!isDir) return;
|
|
|
|
const foldername = await window.ModalManager.prompt(
|
|
'Enter folder name (lowercase, underscore only):',
|
|
'new_folder',
|
|
'New Folder'
|
|
);
|
|
|
|
if (!foldername) return;
|
|
|
|
let finalFoldername = foldername;
|
|
const validation = this.validateFileName(foldername, true);
|
|
|
|
if (!validation.valid) {
|
|
showNotification(validation.message, 'warning');
|
|
|
|
if (validation.sanitized) {
|
|
const useSanitized = await window.ModalManager.confirm(
|
|
`${foldername} → ${validation.sanitized}`,
|
|
'Use sanitized name?',
|
|
false
|
|
);
|
|
if (useSanitized) {
|
|
finalFoldername = validation.sanitized;
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const fullPath = `${path}/${finalFoldername}`.replace(/\/+/g, '/');
|
|
await this.webdavClient.mkcol(fullPath);
|
|
|
|
// Clear undo history since new folder was created
|
|
if (this.fileTree.lastMoveOperation) {
|
|
this.fileTree.lastMoveOperation = null;
|
|
}
|
|
|
|
await this.fileTree.load();
|
|
showNotification(`Created folder ${finalFoldername}`, 'success');
|
|
},
|
|
|
|
rename: async function (path, isDir) {
|
|
const oldName = path.split('/').pop();
|
|
const newName = await window.ModalManager.prompt(
|
|
'Rename to:',
|
|
oldName,
|
|
'Rename'
|
|
);
|
|
|
|
if (newName && newName !== oldName) {
|
|
const parentPath = path.substring(0, path.lastIndexOf('/'));
|
|
const newPath = parentPath ? `${parentPath}/${newName}` : newName;
|
|
await this.webdavClient.move(path, newPath);
|
|
|
|
// Clear undo history since manual rename occurred
|
|
if (this.fileTree.lastMoveOperation) {
|
|
this.fileTree.lastMoveOperation = null;
|
|
}
|
|
|
|
await this.fileTree.load();
|
|
showNotification('Renamed', 'success');
|
|
}
|
|
},
|
|
|
|
copy: async function (path, isDir) {
|
|
this.clipboard = { path, operation: 'copy', isDirectory: isDir };
|
|
// No notification for copy - it's a quick operation
|
|
this.updatePasteMenuItem();
|
|
},
|
|
|
|
cut: async function (path, isDir) {
|
|
this.clipboard = { path, operation: 'cut', isDirectory: isDir };
|
|
// No notification for cut - it's a quick operation
|
|
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);
|
|
// No notification for paste - file tree updates show the result
|
|
} else {
|
|
await this.webdavClient.move(this.clipboard.path, destPath);
|
|
this.clipboard = null;
|
|
this.updatePasteMenuItem();
|
|
// No notification for move - file tree updates show the result
|
|
}
|
|
|
|
await this.fileTree.load();
|
|
},
|
|
|
|
delete: async function (path, isDir) {
|
|
const name = path.split('/').pop();
|
|
const type = isDir ? 'folder' : 'file';
|
|
|
|
const confirmed = await window.ModalManager.confirm(
|
|
`Are you sure you want to delete ${name}?`,
|
|
`Delete this ${type}?`,
|
|
true
|
|
);
|
|
|
|
if (!confirmed) return;
|
|
|
|
await this.webdavClient.delete(path);
|
|
|
|
// Clear undo history since manual delete occurred
|
|
if (this.fileTree.lastMoveOperation) {
|
|
this.fileTree.lastMoveOperation = null;
|
|
}
|
|
|
|
await this.fileTree.load();
|
|
showNotification(`Deleted ${name}`, 'success');
|
|
},
|
|
|
|
download: async function (path, isDir) {
|
|
Logger.info(`Downloading ${isDir ? 'folder' : 'file'}: ${path}`);
|
|
|
|
if (isDir) {
|
|
await this.fileTree.downloadFolder(path);
|
|
} else {
|
|
await this.fileTree.downloadFile(path);
|
|
}
|
|
},
|
|
|
|
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();
|
|
}
|
|
};
|
|
|
|
// Old deprecated modal methods removed - all modals now use window.ModalManager
|
|
|
|
updatePasteMenuItem() {
|
|
const pasteItem = document.getElementById('pasteMenuItem');
|
|
if (pasteItem) {
|
|
pasteItem.style.display = this.clipboard ? 'flex' : 'none';
|
|
}
|
|
}
|
|
} |