271 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			271 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * UI Utilities Module
 | 
						|
 * Toast notifications, context menu, dark mode, file upload dialog
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Show toast notification
 | 
						|
 */
 | 
						|
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 });
 | 
						|
    bsToast.show();
 | 
						|
    
 | 
						|
    toast.addEventListener('hidden.bs.toast', () => {
 | 
						|
        toast.remove();
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
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';
 | 
						|
    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');
 | 
						|
                }
 | 
						|
            }
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 |