292 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			292 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * File Tree Component
 | 
						|
 * Manages the hierarchical file tree display and interactions
 | 
						|
 */
 | 
						|
 | 
						|
class FileTree {
 | 
						|
    constructor(containerId, webdavClient) {
 | 
						|
        this.container = document.getElementById(containerId);
 | 
						|
        this.webdavClient = webdavClient;
 | 
						|
        this.tree = [];
 | 
						|
        this.selectedPath = null;
 | 
						|
        this.onFileSelect = null;
 | 
						|
        this.onFolderSelect = null;
 | 
						|
        
 | 
						|
        this.setupEventListeners();
 | 
						|
    }
 | 
						|
    
 | 
						|
    setupEventListeners() {
 | 
						|
        // Click handler for tree nodes
 | 
						|
        this.container.addEventListener('click', (e) => {
 | 
						|
            console.log('Container clicked', e.target);
 | 
						|
            const node = e.target.closest('.tree-node');
 | 
						|
            if (!node) return;
 | 
						|
            
 | 
						|
            console.log('Node found', node);
 | 
						|
            const path = node.dataset.path;
 | 
						|
            const isDir = node.dataset.isdir === 'true';
 | 
						|
            
 | 
						|
            // The toggle is handled inside renderNodes now
 | 
						|
            
 | 
						|
            // Select node
 | 
						|
            if (isDir) {
 | 
						|
                this.selectFolder(path);
 | 
						|
            } else {
 | 
						|
                this.selectFile(path);
 | 
						|
            }
 | 
						|
        });
 | 
						|
        
 | 
						|
        // Context menu
 | 
						|
        this.container.addEventListener('contextmenu', (e) => {
 | 
						|
            const node = e.target.closest('.tree-node');
 | 
						|
            e.preventDefault();
 | 
						|
 | 
						|
            if (node) {
 | 
						|
                // Clicked on a node
 | 
						|
                const path = node.dataset.path;
 | 
						|
                const isDir = node.dataset.isdir === 'true';
 | 
						|
                window.showContextMenu(e.clientX, e.clientY, { path, isDir });
 | 
						|
            } else if (e.target === this.container) {
 | 
						|
                // Clicked on the empty space in the file tree container
 | 
						|
                window.showContextMenu(e.clientX, e.clientY, { path: '', isDir: true });
 | 
						|
            }
 | 
						|
        });
 | 
						|
    }
 | 
						|
    
 | 
						|
    async load() {
 | 
						|
        try {
 | 
						|
            const items = await this.webdavClient.propfind('', 'infinity');
 | 
						|
            this.tree = this.webdavClient.buildTree(items);
 | 
						|
            this.render();
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to load file tree:', error);
 | 
						|
            showNotification('Failed to load files', 'error');
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    render() {
 | 
						|
        this.container.innerHTML = '';
 | 
						|
        this.renderNodes(this.tree, this.container, 0);
 | 
						|
    }
 | 
						|
    
 | 
						|
    renderNodes(nodes, parentElement, level) {
 | 
						|
        nodes.forEach(node => {
 | 
						|
            const nodeWrapper = document.createElement('div');
 | 
						|
            nodeWrapper.className = 'tree-node-wrapper';
 | 
						|
 | 
						|
            // Create node element
 | 
						|
            const nodeElement = this.createNodeElement(node, level);
 | 
						|
            nodeWrapper.appendChild(nodeElement);
 | 
						|
 | 
						|
            // Create children container ONLY if has children
 | 
						|
            if (node.children && node.children.length > 0) {
 | 
						|
                const childContainer = document.createElement('div');
 | 
						|
                childContainer.className = 'tree-children';
 | 
						|
                childContainer.style.display = 'none';
 | 
						|
                childContainer.dataset.parent = node.path;
 | 
						|
                childContainer.style.marginLeft = `${(level + 1) * 12}px`;
 | 
						|
 | 
						|
                // Recursively render children
 | 
						|
                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);
 | 
						|
        });
 | 
						|
    }
 | 
						|
    
 | 
						|
    
 | 
						|
    // toggleFolder is no longer needed as the event listener is added in renderNodes.
 | 
						|
    
 | 
						|
    selectFile(path) {
 | 
						|
        this.selectedPath = path;
 | 
						|
        this.updateSelection();
 | 
						|
        if (this.onFileSelect) {
 | 
						|
            this.onFileSelect({ path, isDirectory: false });
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    selectFolder(path) {
 | 
						|
        this.selectedPath = path;
 | 
						|
        this.updateSelection();
 | 
						|
        if (this.onFolderSelect) {
 | 
						|
            this.onFolderSelect({ path, isDirectory: true });
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    updateSelection() {
 | 
						|
        // Remove previous selection
 | 
						|
        this.container.querySelectorAll('.tree-node').forEach(node => {
 | 
						|
            node.classList.remove('selected');
 | 
						|
        });
 | 
						|
        
 | 
						|
        // Add selection to current
 | 
						|
        if (this.selectedPath) {
 | 
						|
            const node = this.container.querySelector(`[data-path="${this.selectedPath}"]`);
 | 
						|
            if (node) {
 | 
						|
                node.classList.add('selected');
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    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) {
 | 
						|
        if (bytes === 0) return '0 B';
 | 
						|
        const k = 1024;
 | 
						|
        const sizes = ['B', 'KB', 'MB', 'GB'];
 | 
						|
        const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
						|
        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) {
 | 
						|
        try {
 | 
						|
            const fullPath = parentPath ? `${parentPath}/${filename}` : filename;
 | 
						|
            await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n');
 | 
						|
            await this.load();
 | 
						|
            this.selectFile(fullPath); // Select the new file
 | 
						|
            showNotification('File created', 'success');
 | 
						|
            return fullPath;
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to create file:', error);
 | 
						|
            showNotification('Failed to create file', 'error');
 | 
						|
            throw error;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    async createFolder(parentPath, foldername) {
 | 
						|
        try {
 | 
						|
            const fullPath = parentPath ? `${parentPath}/${foldername}` : foldername;
 | 
						|
            await this.webdavClient.mkcol(fullPath);
 | 
						|
            await this.load();
 | 
						|
            showNotification('Folder created', 'success');
 | 
						|
            return fullPath;
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to create folder:', error);
 | 
						|
            showNotification('Failed to create folder', 'error');
 | 
						|
            throw error;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    async uploadFile(parentPath, file) {
 | 
						|
        try {
 | 
						|
            const fullPath = parentPath ? `${parentPath}/${file.name}` : file.name;
 | 
						|
            const content = await file.arrayBuffer();
 | 
						|
            await this.webdavClient.putBinary(fullPath, content);
 | 
						|
            await this.load();
 | 
						|
            showNotification(`Uploaded ${file.name}`, 'success');
 | 
						|
            return fullPath;
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to upload file:', error);
 | 
						|
            showNotification('Failed to upload file', 'error');
 | 
						|
            throw error;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    async downloadFile(path) {
 | 
						|
        try {
 | 
						|
            const content = await this.webdavClient.get(path);
 | 
						|
            const filename = path.split('/').pop();
 | 
						|
            this.triggerDownload(content, filename);
 | 
						|
            showNotification('Downloaded', 'success');
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to download file:', error);
 | 
						|
            showNotification('Failed to download file', 'error');
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    async downloadFolder(path) {
 | 
						|
        try {
 | 
						|
            showNotification('Creating zip...', 'info');
 | 
						|
            // Get all files in folder
 | 
						|
            const items = await this.webdavClient.propfind(path, 'infinity');
 | 
						|
            const files = items.filter(item => !item.isDirectory);
 | 
						|
            
 | 
						|
            // Use JSZip to create zip file
 | 
						|
            const JSZip = window.JSZip;
 | 
						|
            if (!JSZip) {
 | 
						|
                throw new Error('JSZip not loaded');
 | 
						|
            }
 | 
						|
            
 | 
						|
            const zip = new JSZip();
 | 
						|
            const folder = zip.folder(path.split('/').pop() || 'download');
 | 
						|
            
 | 
						|
            // Add all files to zip
 | 
						|
            for (const file of files) {
 | 
						|
                const content = await this.webdavClient.get(file.path);
 | 
						|
                const relativePath = file.path.replace(path + '/', '');
 | 
						|
                folder.file(relativePath, content);
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Generate zip
 | 
						|
            const zipBlob = await zip.generateAsync({ type: 'blob' });
 | 
						|
            const zipFilename = `${path.split('/').pop() || 'download'}.zip`;
 | 
						|
            this.triggerDownload(zipBlob, zipFilename);
 | 
						|
            showNotification('Downloaded', 'success');
 | 
						|
        } catch (error) {
 | 
						|
            console.error('Failed to download folder:', error);
 | 
						|
            showNotification('Failed to download folder', 'error');
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    triggerDownload(content, filename) {
 | 
						|
        const blob = content instanceof Blob ? content : new Blob([content]);
 | 
						|
        const url = URL.createObjectURL(blob);
 | 
						|
        const a = document.createElement('a');
 | 
						|
        a.href = url;
 | 
						|
        a.download = filename;
 | 
						|
        document.body.appendChild(a);
 | 
						|
        a.click();
 | 
						|
        document.body.removeChild(a);
 | 
						|
        URL.revokeObjectURL(url);
 | 
						|
    }
 | 
						|
}
 | 
						|
 |