import { esc } from './esc.js'; import { fs } from './fs.js'; const DEFAULT_RENDER_CONFIG = { labels: { statusReady: 'готов', remove: 'Удалить', empty: 'Нет выбранных файлов', columns: { path: 'Путь', size: 'Размер', status: 'Статус' }, count: (count, bytes) => `${count} файлов · ${fs(bytes)}`, }, layout: { columns: ['path', 'size', 'status'] }, }; function renderConfig(cfg = {}) { return { labels: { ...DEFAULT_RENDER_CONFIG.labels, ...(cfg.labels || {}), columns: { ...DEFAULT_RENDER_CONFIG.labels.columns, ...(cfg.labels?.columns || {}), }, }, layout: { ...DEFAULT_RENDER_CONFIG.layout, ...(cfg.layout || {}) }, }; } /** * Рендерит один узел дерева и его раскрытых потомков в 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, cfg) { 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 cells = { path: action, size: node.kind === 'file' ? fs(node.file.size) : '', status: node.error ? esc(node.error) : node.kind === 'file' ? esc(cfg.labels.statusReady) : '', }; const columns = cfg.layout.columns?.length ? cfg.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); const row = `` + columns.map((column) => `${cells[column] || ''}`).join('') + `${remove}`; if (!isGroup || !node.expanded) return row; // Дочерние строки добавляются только для раскрытой группы. return row + node.children.map((child) => renderNode(child, depth + 1, cfg)).join(''); } /** * Полностью синхронизирует DOM таблицы и счётчик с текущим state.nodes. * * @param {{nodes: Array}} state Состояние дерева. * @param {{tableBodyEl: HTMLElement, countEl: HTMLElement}} elements DOM-вывод. * @returns {void} * * Обход для счётчика независим от раскрытия: свернутая группа всё равно входит * в количество leaf-файлов и суммарный размер. */ export function render(state, elements, cfg) { const normalized = renderConfig(cfg); const columns = normalized.layout.columns?.length ? normalized.layout.columns : Object.keys(DEFAULT_RENDER_CONFIG.labels.columns); elements.tableBodyEl.innerHTML = state.nodes.length ? state.nodes.map((node) => renderNode(node, 0, normalized)).join('') : `${esc(normalized.labels.empty)}`; const files = []; // Собирает все leaf-файлы, включая содержимое свернутых групп. const visit = (node) => { if (node.kind === 'file') files.push(node); else node.children.forEach(visit); }; state.nodes.forEach(visit); const totalBytes = files.reduce((sum, node) => sum + node.file.size, 0); elements.countEl.textContent = normalized.labels.count(files.length, totalBytes); } /** * Ищет узел по уникальному 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; }