import { addFiles, onFilesChange } from './on_files_change.js'; import { onFolderChange } from './on_folder_change.js'; import { findNode, flattenFiles, render } from './render.js'; /** * Удаляет ключи дедупликации для leaf-файлов внутри удаляемого поддерева. * * @param {{fileKeys: Set}} state Состояние дедупликации. * @param {{kind: string, path: string, file?: File, children?: Array}} node Удаляемый узел. * @returns {void} */ function removeFileMeta(state, node) { if (node.kind === 'file') { state.fileKeys.delete(`${node.path}\u0000${node.file.size}`); return; } node.children.forEach((child) => removeFileMeta(state, child)); } /** * Удаляет узел из дерева и синхронно освобождает его dedup-ключи. * * @param {{nodes: Array, fileKeys: Set}} state Состояние таблицы. * @param {string} id Идентификатор узла. * @returns {void} Ничего не делает, если id не найден. */ function removeNode(state, id) { const found = findNode(state.nodes, id); if (!found) return; removeFileMeta(state, found.node); found.nodes.splice(found.nodes.indexOf(found.node), 1); } /** * Создаёт picker и возвращает его публичный API. * * @param {{allowedExt: string[], fileInputEl: HTMLInputElement, * folderInputEl: HTMLInputElement, tableBodyEl: HTMLElement, countEl: HTMLElement}} cfg * Конфигурация и обязательные DOM-элементы интегратора. * @returns {{pickFiles: Function, pickFolder: Function, addFiles: Function, * getFiles: Function, remove: Function, render: Function, clear: Function}} * Управляющий API без прямого доступа к внутреннему state. * * Функция регистрирует DOM-события один раз, хранит дерево и dedup Set внутри * замыкания и сразу рисует пустое состояние. Внешнее приложение получает только * операции выбора, добавления, чтения и удаления. */ export function initUploadTable(cfg) { // Нормализованный набор DOM-ссылок передаётся во все функции рендера. const elements = { fileInputEl: cfg.fileInputEl, folderInputEl: cfg.folderInputEl, tableBodyEl: cfg.tableBodyEl, countEl: cfg.countEl, }; // nodes — дерево; fileKeys — ключи path+NUL+size; busy защищает async change handlers. const state = { nodes: [], fileKeys: new Set(), busy: false }; elements.fileInputEl.addEventListener('change', onFilesChange(state, cfg, elements)); elements.folderInputEl.addEventListener('change', onFolderChange(state, cfg, elements)); elements.tableBodyEl.addEventListener('click', (event) => { // Делегирование событий позволяет обслуживать динамически созданные кнопки. const toggle = event.target.closest('[data-toggle]'); if (toggle) { const found = findNode(state.nodes, toggle.dataset.toggle); if (found) found.node.expanded = !found.node.expanded; render(state, elements, cfg); return; } const remove = event.target.closest('[data-remove]'); if (remove) { removeNode(state, remove.dataset.remove); render(state, elements, cfg); } }); const api = { // Открывают системные диалоги, не обходя браузерные ограничения File API. pickFiles: () => elements.fileInputEl.click(), pickFolder: () => elements.folderInputEl.click(), // Программное добавление использует тот же фильтр и ZIP-парсер, что и input. addFiles: (files) => addFiles(state, cfg, files, elements), // Возвращает только leaf-файлы; группы и внутренний state наружу не выдаются. getFiles: () => flattenFiles(state.nodes), remove: (id) => { removeNode(state, id); render(state, elements, cfg); }, render: () => render(state, elements), clear: () => { // Очистка сбрасывает и дерево, и dedup Set, чтобы повторный выбор был возможен. state.nodes = []; state.fileKeys.clear(); render(state, elements); }, }; api.render(); return api; }