Add hierarchical file picker

This commit is contained in:
“Naeel”
2026-09-05 09:29:03 +03:00
parent 85605fe8c6
commit a965f181d9
11 changed files with 181 additions and 97 deletions
+43 -15
View File
@@ -1,21 +1,49 @@
import { esc } from './esc.js';
import { fs } from './fs.js';
function renderNode(node, depth) {
const isGroup = node.kind !== 'file';
const padding = depth * 4;
const name = esc(node.name);
const action = isGroup
? `<button class="tree-name tree-group tree-${node.kind}" style="padding-left:${padding}ch" data-toggle="${node.id}" `
+ `aria-expanded="${node.expanded}"><span class="tree-chevron">${node.expanded ? '▼' : '▶'}</span>${name}</button>`
: `<span class="tree-name" style="padding-left:${padding}ch">${name}</span>`;
const indent = isGroup ? '' : ` style="padding-left:${padding}ch"`;
const remove = `<button class="remove-btn" type="button" data-remove="${node.id}" aria-label="Удалить ${name}">×</button>`;
const row = `<tr class="tree-row tree-${node.kind}"><td${indent}>${action}</td>`
+ `<td>${node.kind === 'file' ? fs(node.file.size) : ''}</td>`
+ `<td>${node.error ? esc(node.error) : node.kind === 'file' ? 'готов' : ''}</td><td>${remove}</td></tr>`;
if (!isGroup || !node.expanded) return row;
return row + node.children.map((child) => renderNode(child, depth + 1)).join('');
}
export function render(state, elements) {
if (!state.files.length) {
elements.tableBodyEl.innerHTML = '<tr><td colspan="3">Нет выбранных файлов</td></tr>';
} else {
elements.tableBodyEl.innerHTML = state.files.map((file, index) => {
const over = state.overNames.has(file.name);
const status = over ? 'не учитывается (лимит)' : 'готов';
return `<tr id="row-${index}"><td>${esc(file.name)}</td>`
+ `<td>${fs(file.size)}</td><td id="st-${index}">${status}</td></tr>`;
}).join('');
elements.tableBodyEl.innerHTML = state.nodes.length
? state.nodes.map((node) => renderNode(node, 0)).join('')
: '<tr><td colspan="4">Нет выбранных файлов</td></tr>';
const files = [];
const visit = (node) => {
if (node.kind === 'file') files.push(node);
else node.children.forEach(visit);
};
state.nodes.forEach(visit);
elements.countEl.textContent = `${files.length} файлов · ${fs(files.reduce((sum, node) => sum + node.file.size, 0))}`;
}
export function findNode(nodes, id, parent = null) {
for (const node of nodes) {
if (node.id === id) return { node, parent, nodes };
const found = findNode(node.children || [], id, node);
if (found) return found;
}
const included = state.files.filter((file) => !state.overNames.has(file.name));
const overCount = state.files.length - included.length;
const size = included.reduce((total, file) => total + file.size, 0);
elements.countEl.textContent = `${included.length} учитываются`
+ (overCount ? ` + ${overCount} свыше лимита` : '')
+ ` · ${fs(size)}`;
return null;
}
export function flattenFiles(nodes, result = []) {
nodes.forEach((node) => {
if (node.kind === 'file') result.push({ path: node.path, name: node.name, size: node.file.size, file: node.file });
else flattenFiles(node.children, result);
});
return result;
}