Navigation pane에서 폴더를 클릭하면, 해당 폴더가 펼쳐지면서 서브 폴더 목록을 보여주기
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// 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; // callback(folderPath)
|
||||
let _onSelect = null;
|
||||
let _selectedPath = null;
|
||||
|
||||
const ICON_CLOSED = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.67 1.67 2 2.5 2H5.38l1.5 1.5H11.5C12.33 3.5 13 4.17 13 5v5.5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V3.5Z" fill="#7c6af7" fill-opacity=".75"/><path d="M1 5.5h12v5C13 11.33 12.33 12 11.5 12h-9C1.67 12 1 11.33 1 10.5V5.5Z" fill="#9180ff" fill-opacity=".9"/></svg>`;
|
||||
@@ -43,9 +44,9 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
|
||||
|
||||
const chevron = document.createElement('span');
|
||||
chevron.className = 'tree-chevron';
|
||||
chevron.innerHTML = hasChildren
|
||||
? `<svg width="10" height="10" viewBox="0 0 10 10"><path d="M3 2l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
||||
: '';
|
||||
if (hasChildren) {
|
||||
chevron.innerHTML = `<svg width="10" height="10" viewBox="0 0 10 10"><path d="M3 2l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
||||
}
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'tree-icon';
|
||||
@@ -67,15 +68,32 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) {
|
||||
wrap.appendChild(children);
|
||||
}
|
||||
|
||||
row.addEventListener('click', (e) => {
|
||||
// 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) toggleNode(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'));
|
||||
@@ -84,20 +102,11 @@ function selectNode(nodeEl) {
|
||||
if (_onSelect) _onSelect(path);
|
||||
}
|
||||
|
||||
async function toggleNode(nodeEl) {
|
||||
const expanded = nodeEl.dataset.expanded === 'true';
|
||||
if (expanded) {
|
||||
collapseNode(nodeEl);
|
||||
} else {
|
||||
await expandNode(nodeEl, null);
|
||||
}
|
||||
}
|
||||
|
||||
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 = ICON_CLOSED;
|
||||
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';
|
||||
}
|
||||
@@ -106,41 +115,46 @@ 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 = nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN;
|
||||
row.querySelector('.tree-icon').innerHTML = nodeIcon(nodeEl);
|
||||
|
||||
const childrenEl = nodeEl.querySelector(':scope > .tree-children');
|
||||
if (!childrenEl) return;
|
||||
childrenEl.style.display = '';
|
||||
|
||||
// Load children if not yet loaded
|
||||
// Lazy-load children on first expansion
|
||||
if (!childrenEl.dataset.loaded) {
|
||||
childrenEl.dataset.loaded = 'true';
|
||||
const dirs = await window.api.listDirectories(nodeEl.dataset.path);
|
||||
dirs.forEach(dir => {
|
||||
const child = createNode(dir);
|
||||
childrenEl.appendChild(child);
|
||||
});
|
||||
dirs.forEach(dir => childrenEl.appendChild(createNode(dir)));
|
||||
}
|
||||
|
||||
// Auto-expand path to target
|
||||
// Auto-expand toward the target path
|
||||
if (autoExpandPath && autoExpandPath !== nodeEl.dataset.path) {
|
||||
const childNodes = childrenEl.querySelectorAll(':scope > .tree-node');
|
||||
for (const child of childNodes) {
|
||||
if (autoExpandPath === child.dataset.path || autoExpandPath.startsWith(child.dataset.path + '/')) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function setSelectedPath(folderPath) {
|
||||
// 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) node.querySelector(':scope > .tree-row').classList.add('active');
|
||||
if (!node) return;
|
||||
node.querySelector(':scope > .tree-row').classList.add('active');
|
||||
if (node.dataset.hasChildren === 'true' && node.dataset.expanded !== 'true') {
|
||||
await expandNode(node, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ async function loadFolder(folder) {
|
||||
state.currentFolder = folder;
|
||||
dirPath.textContent = folder;
|
||||
await window.api.setLastFolder(folder);
|
||||
fileExplorer.setSelectedPath(folder);
|
||||
await fileExplorer.setSelectedPath(folder);
|
||||
|
||||
showLoading('이미지 목록 불러오는 중...');
|
||||
const list = await window.api.listImages(folder);
|
||||
|
||||
@@ -450,9 +450,12 @@ footer span { font-size: 11px; color: var(--muted); }
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0; color: var(--muted);
|
||||
transition: transform .15s;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.tree-chevron:hover { background: rgba(255,255,255,.08); color: var(--text); }
|
||||
.tree-chevron.open { transform: rotate(90deg); }
|
||||
.tree-chevron svg { display: block; }
|
||||
.tree-chevron svg { display: block; pointer-events: none; }
|
||||
|
||||
.tree-icon {
|
||||
width: 16px; height: 16px;
|
||||
|
||||
Reference in New Issue
Block a user