import { esc } from './esc.js'; import { fs } from './fs.js'; /** * Рендерит один узел дерева и его раскрытых потомков в HTML-строки таблицы. * * @param {{kind: string, id: string, name: string, path: string, file?: File, * children: Array, expanded: boolean, error?: string}} node Узел file/folder/zip. * @param {number} depth Глубина узла; используется для визуального отступа. * @returns {string} HTML одной строки или поддерева строк. * * Имена и пути проходят через esc до вставки в innerHTML. Группа получает * кнопку раскрытия, leaf-файл — размер, статус и data-path для будущего API статусов. */ function renderNode(node, depth) { const isGroup = node.kind !== 'file'; const padding = depth * 4; const name = esc(node.name); const action = isGroup ? `` : `${name}`; const remove = ``; const pathAttribute = node.kind === 'file' ? ` data-path="${esc(node.path)}"` : ''; const row = `${action}` + `${node.kind === 'file' ? fs(node.file.size) : ''}` + `${node.error ? esc(node.error) : node.kind === 'file' ? 'готов' : ''}${remove}`; if (!isGroup || !node.expanded) return row; // Дочерние строки добавляются только для раскрытой группы. return row + node.children.map((child) => renderNode(child, depth + 1)).join(''); } /** * Полностью синхронизирует DOM таблицы и счётчик с текущим state.nodes. * * @param {{nodes: Array}} state Состояние дерева. * @param {{tableBodyEl: HTMLElement, countEl: HTMLElement}} elements DOM-вывод. * @returns {void} * * Обход для счётчика независим от раскрытия: свернутая группа всё равно входит * в количество leaf-файлов и суммарный размер. */ export function render(state, elements) { elements.tableBodyEl.innerHTML = state.nodes.length ? state.nodes.map((node) => renderNode(node, 0)).join('') : 'Нет выбранных файлов'; const files = []; // Собирает все leaf-файлы, включая содержимое свернутых групп. 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))}`; } /** * Ищет узел по уникальному id и возвращает также массив его непосредственного родителя. * * @param {Array} nodes Массив узлов текущего уровня. * @param {string} id Идентификатор, созданный через crypto.randomUUID(). * @returns {{node: object, nodes: Array}|null} Узел и контейнер для удаления либо null. */ export function findNode(nodes, id) { for (const node of nodes) { if (node.id === id) return { node, nodes }; const found = findNode(node.children || [], id); if (found) return found; } return null; } /** * Преобразует иерархическое дерево в плоский список leaf-файлов. * * @param {Array} nodes Узлы любого уровня. * @param {Array} result Внутренний аккумулятор рекурсивного вызова. * @returns {Array<{path: string, name: string, size: number, file: File}>} Файлы для приложения. */ 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; }