refactor: Modularize UI components and utilities

- 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
This commit is contained in:
Mahmoud-Emad
2025-10-26 15:42:15 +03:00
parent 23a24d42e2
commit 0ed6bcf1f2
34 changed files with 4136 additions and 940 deletions

View File

@@ -1,270 +1,60 @@
/**
* UI Utilities Module
* Toast notifications, context menu, dark mode, file upload dialog
* Toast notifications (kept for backward compatibility)
*
* Other utilities have been moved to separate modules:
* - Context menu: context-menu.js
* - File upload: file-upload.js
* - Dark mode: dark-mode.js
* - Collection selector: collection-selector.js
* - Editor drop handler: editor-drop-handler.js
*/
/**
* Show toast notification
* @param {string} message - The message to display
* @param {string} type - The notification type (info, success, error, warning, danger, primary)
*/
function showNotification(message, type = 'info') {
const container = document.getElementById('toastContainer') || createToastContainer();
const toast = document.createElement('div');
const bgClass = type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'primary';
toast.className = `toast align-items-center text-white bg-${bgClass} border-0`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
const bsToast = new bootstrap.Toast(toast, { delay: Config.TOAST_DURATION });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
/**
* Create the toast container if it doesn't exist
* @returns {HTMLElement} The toast container element
*/
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '9999';
container.style.zIndex = Config.TOAST_Z_INDEX;
document.body.appendChild(container);
return container;
}
/**
* Enhanced Context Menu
*/
function showContextMenu(x, y, target) {
const menu = document.getElementById('contextMenu');
if (!menu) return;
// Store target data
menu.dataset.targetPath = target.path;
menu.dataset.targetIsDir = target.isDir;
// Show/hide menu items based on target type
const items = {
'new-file': target.isDir,
'new-folder': target.isDir,
'upload': target.isDir,
'download': true,
'paste': target.isDir && window.fileTreeActions?.clipboard,
'open': !target.isDir
};
Object.entries(items).forEach(([action, show]) => {
const item = menu.querySelector(`[data-action="${action}"]`);
if (item) {
item.style.display = show ? 'flex' : 'none';
}
});
// Position menu
menu.style.display = 'block';
menu.style.left = x + 'px';
menu.style.top = y + 'px';
// Adjust if off-screen
setTimeout(() => {
const rect = menu.getBoundingClientRect();
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';
}
}, 0);
}
function hideContextMenu() {
const menu = document.getElementById('contextMenu');
if (menu) {
menu.style.display = 'none';
}
}
// Combined click handler for context menu and outside clicks
document.addEventListener('click', async (e) => {
const menuItem = e.target.closest('.context-menu-item');
if (menuItem) {
// Handle context menu item click
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);
}
} else if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
// Hide on outside click
hideContextMenu();
}
});
/**
* File Upload Dialog
*/
function showFileUploadDialog(targetPath, onUpload) {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
if (files.length === 0) return;
for (const file of files) {
try {
await onUpload(targetPath, file);
} catch (error) {
console.error('Upload failed:', error);
}
}
});
input.click();
}
/**
* Dark Mode Manager
*/
class DarkMode {
constructor() {
this.isDark = localStorage.getItem('darkMode') === 'true';
this.apply();
}
toggle() {
this.isDark = !this.isDark;
localStorage.setItem('darkMode', this.isDark);
this.apply();
}
apply() {
if (this.isDark) {
document.body.classList.add('dark-mode');
const btn = document.getElementById('darkModeBtn');
if (btn) btn.textContent = '☀️';
// Update mermaid theme
if (window.mermaid) {
mermaid.initialize({ theme: 'dark' });
}
} else {
document.body.classList.remove('dark-mode');
const btn = document.getElementById('darkModeBtn');
if (btn) btn.textContent = '🌙';
// Update mermaid theme
if (window.mermaid) {
mermaid.initialize({ theme: 'default' });
}
}
}
}
/**
* Collection Selector
*/
class CollectionSelector {
constructor(selectId, webdavClient) {
this.select = document.getElementById(selectId);
this.webdavClient = webdavClient;
this.onChange = null;
}
async load() {
try {
const collections = await this.webdavClient.getCollections();
this.select.innerHTML = '';
collections.forEach(collection => {
const option = document.createElement('option');
option.value = collection;
option.textContent = collection;
this.select.appendChild(option);
});
// Select first collection
if (collections.length > 0) {
this.select.value = collections[0];
this.webdavClient.setCollection(collections[0]);
if (this.onChange) {
this.onChange(collections[0]);
}
}
// Add change listener
this.select.addEventListener('change', () => {
const collection = this.select.value;
this.webdavClient.setCollection(collection);
if (this.onChange) {
this.onChange(collection);
}
});
} catch (error) {
console.error('Failed to load collections:', error);
showNotification('Failed to load collections', 'error');
}
}
}
/**
* Editor Drop Handler
* Handles file drops into the editor
*/
class EditorDropHandler {
constructor(editorElement, onFileDrop) {
this.editorElement = editorElement;
this.onFileDrop = onFileDrop;
this.setupHandlers();
}
setupHandlers() {
this.editorElement.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
this.editorElement.classList.add('drag-over');
});
this.editorElement.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
this.editorElement.classList.remove('drag-over');
});
this.editorElement.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
this.editorElement.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
for (const file of files) {
try {
if (this.onFileDrop) {
await this.onFileDrop(file);
}
} catch (error) {
console.error('Drop failed:', error);
showNotification(`Failed to upload ${file.name}`, 'error');
}
}
});
}
}
// All other UI utilities have been moved to separate modules
// See the module list at the top of this file
// Make showNotification globally available
window.showNotification = showNotification;