// Directory tree for the sidebar file-explorer pane. // Lazy-loads children when a node is expanded. Tracks one selected path. // Row click = select + always expand. Chevron click = toggle collapse/expand. let _onSelect = null; let _selectedPath = null; const ICON_CLOSED = ``; const ICON_OPEN = ``; const ICON_HOME = ``; export function init(container, onSelect) { _onSelect = onSelect; container.innerHTML = ''; container.classList.add('tree-root'); } export async function loadTree(rootPath, selectedPath) { _selectedPath = selectedPath; const container = document.querySelector('.file-explorer'); if (!container) return; container.innerHTML = ''; const rootNode = createNode({ name: rootPath.split('/').pop() || rootPath, path: rootPath, hasChildren: true, isRoot: true, }); container.appendChild(rootNode); await expandNode(rootNode, selectedPath); } function createNode({ name, path: nodePath, hasChildren, isRoot = false }) { const wrap = document.createElement('div'); wrap.className = 'tree-node'; wrap.dataset.path = nodePath; wrap.dataset.expanded = 'false'; wrap.dataset.hasChildren = hasChildren ? 'true' : 'false'; wrap.dataset.isRoot = isRoot ? 'true' : 'false'; const row = document.createElement('div'); row.className = 'tree-row'; const chevron = document.createElement('span'); chevron.className = 'tree-chevron'; if (hasChildren) { chevron.innerHTML = ``; } const icon = document.createElement('span'); icon.className = 'tree-icon'; icon.innerHTML = isRoot ? ICON_HOME : ICON_CLOSED; const label = document.createElement('span'); label.className = 'tree-label'; label.textContent = name; label.title = nodePath; row.appendChild(chevron); row.appendChild(icon); row.appendChild(label); wrap.appendChild(row); if (hasChildren) { const children = document.createElement('div'); children.className = 'tree-children'; wrap.appendChild(children); } // Chevron click: toggle collapse/expand without changing selection chevron.addEventListener('click', (e) => { e.stopPropagation(); if (!hasChildren) return; if (wrap.dataset.expanded === 'true') { collapseNode(wrap); } else { expandNode(wrap, null); } }); // Row click: select + always expand (never collapse) row.addEventListener('click', () => { selectNode(wrap); if (hasChildren && wrap.dataset.expanded !== 'true') { expandNode(wrap, null); } }); return wrap; } function nodeIcon(nodeEl) { return nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN; } function selectNode(nodeEl) { const path = nodeEl.dataset.path; document.querySelectorAll('.tree-row.active').forEach(r => r.classList.remove('active')); nodeEl.querySelector(':scope > .tree-row').classList.add('active'); _selectedPath = path; if (_onSelect) _onSelect(path); } function collapseNode(nodeEl) { nodeEl.dataset.expanded = 'false'; const row = nodeEl.querySelector(':scope > .tree-row'); row.querySelector('.tree-chevron').classList.remove('open'); row.querySelector('.tree-icon').innerHTML = nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_CLOSED; const children = nodeEl.querySelector(':scope > .tree-children'); if (children) children.style.display = 'none'; } async function expandNode(nodeEl, autoExpandPath) { nodeEl.dataset.expanded = 'true'; const row = nodeEl.querySelector(':scope > .tree-row'); row.querySelector('.tree-chevron').classList.add('open'); row.querySelector('.tree-icon').innerHTML = nodeIcon(nodeEl); const childrenEl = nodeEl.querySelector(':scope > .tree-children'); if (!childrenEl) return; childrenEl.style.display = ''; // Lazy-load children on first expansion if (!childrenEl.dataset.loaded) { childrenEl.dataset.loaded = 'true'; const dirs = await window.api.listDirectories(nodeEl.dataset.path); if (dirs === null) { const err = document.createElement('div'); err.className = 'tree-access-error'; err.textContent = '접근 불가'; childrenEl.appendChild(err); } else { dirs.forEach(dir => childrenEl.appendChild(createNode(dir))); } } // Auto-expand toward the target path if (autoExpandPath && autoExpandPath !== nodeEl.dataset.path) { const childNodes = childrenEl.querySelectorAll(':scope > .tree-node'); for (const child of childNodes) { const cp = child.dataset.path; if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/')) { await expandNode(child, autoExpandPath); break; } } } // Reached the target — select it if (autoExpandPath === nodeEl.dataset.path) { selectNode(nodeEl); } } // Mark a path as selected and ensure the node is expanded to show its children. // Called from renderer when a folder is loaded externally (e.g. "폴더 열기" dialog). export async function setSelectedPath(folderPath) { _selectedPath = folderPath; document.querySelectorAll('.tree-row.active').forEach(r => r.classList.remove('active')); const node = document.querySelector(`.tree-node[data-path="${CSS.escape(folderPath)}"]`); if (!node) return; node.querySelector(':scope > .tree-row').classList.add('active'); if (node.dataset.hasChildren === 'true' && node.dataset.expanded !== 'true') { await expandNode(node, null); } }