// Table management functionality (function() { 'use strict'; // FileBlobCache, processedLinks, preview state, and utilities const fileBlobCache = new Map(); const processedLinks = new WeakSet(); let fileLinkHandlersAttached = false; let filePreviewWindow = null; const PREVIEW_EXTENSIONS = ['pdf', 'docx', 'xlsx', 'xls']; const loadedLibraries = new Map(); let resizing = null; // Currently-previewing file (visual highlight in the file table). Survives // re-renders via applyPreviewHighlight, which is called at the tail of // updateFileTable. Cleared when the preview popup is closed. let currentPreviewFileId = null; let previewWindowWatcher = null; function setCurrentPreviewFile(fileId) { currentPreviewFileId = fileId; applyPreviewHighlight(); } function applyPreviewHighlight() { const tbody = document.getElementById('filesTableBody'); if (!tbody) return; // Clear any prior highlight first. tbody.querySelectorAll('tr.is-previewing').forEach(el => el.classList.remove('is-previewing')); tbody.querySelectorAll('.revision-file.is-previewing').forEach(el => el.classList.remove('is-previewing')); if (!currentPreviewFileId) return; const checkbox = tbody.querySelector(`input[type="checkbox"][data-file-id="${cssEscape(currentPreviewFileId)}"]`); if (!checkbox) return; const wrapper = checkbox.closest('.revision-file'); if (wrapper) wrapper.classList.add('is-previewing'); const row = checkbox.closest('tr'); if (row) row.classList.add('is-previewing'); } function cssEscape(s) { if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(s); return String(s).replace(/[^a-zA-Z0-9_-]/g, c => '\\' + c); } // Watch the preview popup; clear the highlight when the user closes it so // the table doesn't keep advertising a preview that's no longer on screen. function watchPreviewWindow() { if (previewWindowWatcher) { clearInterval(previewWindowWatcher); previewWindowWatcher = null; } if (!filePreviewWindow) return; previewWindowWatcher = setInterval(() => { if (!filePreviewWindow || filePreviewWindow.closed) { clearInterval(previewWindowWatcher); previewWindowWatcher = null; if (currentPreviewFileId) setCurrentPreviewFile(null); } }, 500); } /** * Get or create a blob URL for a file. * - Local files: reads via File System Access API, caches the blob URL. * - HTTP files: fetches the remote URL, caches the blob URL. * Returns a Promise resolving to a blob: URL. */ async function getFileBlobUrl(file) { if (fileBlobCache.has(file.id)) { return fileBlobCache.get(file.id); } let blob; if (file.handle) { // Local file via File System Access API const f = await file.handle.getFile(); blob = f; } else if (file.url) { // HTTP file — fetch and convert to blob const resp = await fetch(file.url); if (!resp.ok) throw new Error('HTTP ' + resp.status + ' fetching ' + file.url); blob = await resp.blob(); } else { throw new Error('File has neither a handle nor a URL'); } const url = URL.createObjectURL(blob); fileBlobCache.set(file.id, url); return url; } /** * Clean up blob URLs for files no longer displayed */ function cleanupUnusedBlobUrls() { const displayedFileIds = new Set(window.app.filteredFiles.map(f => f.id)); for (const [fileId, url] of fileBlobCache.entries()) { if (!displayedFileIds.has(fileId)) { URL.revokeObjectURL(url); fileBlobCache.delete(fileId); } } } /** * Revoke all blob URLs and clear cache */ function cleanupAllBlobUrls() { for (const url of fileBlobCache.values()) { URL.revokeObjectURL(url); } fileBlobCache.clear(); } // Update file table function updateFileTable() { const tbody = document.getElementById('filesTableBody'); if (window.app.filteredFiles.length === 0) { tbody.innerHTML = ` No files found matching the current filters. `; cleanupUnusedBlobUrls(); // Clean up all blob URLs return; } // Group and sort files const grouped = window.app.modules.parser.groupFilesByTrackingNumber(window.app.filteredFiles); const sorted = window.app.modules.parser.sortGroupedFiles(grouped); // Build table rows const rows = []; sorted.forEach(group => { rows.push(createFileGroupRow(group)); }); tbody.innerHTML = rows.join(''); // Re-apply the preview highlight after every re-render so a file that // was being previewed when filters changed still shows as previewing if // it's still in the visible set. applyPreviewHighlight(); // Clean up blob URLs for files no longer visible cleanupUnusedBlobUrls(); } // Create row for a file group function createFileGroupRow(group) { // Generate one per revision; last row gets class group-last for border const lastIndex = group.sortedRevisions.length - 1; return group.sortedRevisions.map((revision, i) => { const titleClass = revision.hasModifier ? 'revision-title-modifier' : 'revision-title-base'; const titleHtml = `
${window.app.modules.app.escapeHtml(revision.title)}
`; const revisionHtml = createRevisionHtml(group.trackingNumber, revision); const lastClass = i === lastIndex ? ' group-last' : ''; // First row includes trackingNumber cell with rowspan if (i === 0) { return ` ${window.app.modules.app.escapeHtml(group.trackingNumber)} ${titleHtml} ${revisionHtml} `; } // Subsequent rows omit trackingNumber cell return ` ${titleHtml} ${revisionHtml} `; }).join(''); } // Create HTML for a revision function createRevisionHtml(trackingNumber, revision) { const filesHtml = revision.files.map(file => createFileHtml(file) ).join(' '); return `
${window.app.modules.app.escapeHtml(revision.revision)} (${window.app.modules.app.escapeHtml(revision.status)}) ${filesHtml}
`; } // Create HTML for a file function createFileHtml(file) { const checked = window.app.selectedFiles.has(file.id) ? 'checked' : ''; const fullPath = file.path || file.folderPath + '/' + file.name; // Handle files with path errors (Windows 260-char limit) if (file.hasPathError) { const errorTitle = `⚠️ Cannot access: Microsoft Windows path length limit (260 chars)\n\nPath: ${fullPath}\n\nUse 'subst' to map archive to a drive letter, or shorten folder names.`; return ` ⚠️ ${window.app.modules.app.escapeHtml(file.extension.toUpperCase())} ${file.size != null ? `${window.app.modules.export.formatFileSize(file.size)}` : ''} `; } return ` ${window.app.modules.app.escapeHtml(file.extension.toUpperCase())} ${file.size != null ? `${window.app.modules.export.formatFileSize(file.size)}` : ''} `; } // Toggle file selection function toggleFileSelection(fileId) { if (window.app.selectedFiles.has(fileId)) { window.app.selectedFiles.delete(fileId); } else { window.app.selectedFiles.add(fileId); } window.app.modules.app.updateStatusBar(); updateSelectAllVisibleCheckbox(); } // Toggle selection of all visible files based on checkbox state function toggleSelectAllVisible(selectAll) { window.app.filteredFiles.forEach(file => { if (selectAll) { window.app.selectedFiles.add(file.id); } else { window.app.selectedFiles.delete(file.id); } }); updateFileTable(); window.app.modules.app.updateStatusBar(); updateSelectAllVisibleCheckbox(); } // Update the select all visible checkbox to reflect current state function updateSelectAllVisibleCheckbox() { const checkbox = document.getElementById('selectAllVisibleCheckbox'); if (!checkbox) return; const visibleCount = window.app.filteredFiles.length; if (visibleCount === 0) { checkbox.checked = false; checkbox.indeterminate = false; return; } const selectedVisibleCount = window.app.filteredFiles.filter(f => window.app.selectedFiles.has(f.id) ).length; if (selectedVisibleCount === 0) { checkbox.checked = false; checkbox.indeterminate = false; } else if (selectedVisibleCount === visibleCount) { checkbox.checked = true; checkbox.indeterminate = false; } else { checkbox.checked = false; checkbox.indeterminate = true; } } /** * Memory-efficient blob URL management * * fileBlobCache: Maps file IDs to blob URLs for reuse * processedLinks: WeakSet tracks DOM elements that already have blob URLs * - Automatically garbage collected when DOM elements are removed * - Prevents redundant async operations on mouseover */ /** * Lazily load a script from CDN. Returns a promise that resolves when loaded. * Caches the promise so subsequent calls return immediately. */ function loadLibrary(url) { if (loadedLibraries.has(url)) return loadedLibraries.get(url); const promise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = url; script.onload = resolve; script.onerror = () => reject(new Error(`Failed to load library: ${url}`)); document.head.appendChild(script); }); loadedLibraries.set(url, promise); return promise; } /** * Check if file preview mode is enabled */ function isFilePreviewEnabled() { const toggle = document.getElementById('filePreviewToggle'); return toggle && toggle.checked; } /** * Show file preview in a separate popup window * Supports PDF (iframe), DOCX (docx-preview), XLSX/XLS (SheetJS) */ async function showFilePreview(file) { const ext = file.extension.toLowerCase(); try { const url = await getFileBlobUrl(file); // Mirror the parent window's theme in the popup const parentTheme = document.documentElement.getAttribute('data-theme') || ''; const themeAttr = parentTheme ? ` data-theme="${parentTheme}"` : ''; // Base HTML shell for the preview window const previewHtml = ` ${window.app.modules.app.escapeHtml(file.name)} - Preview

${window.app.modules.app.escapeHtml(file.name)}

${ext === 'pdf' ? '' : '
Loading preview...
'}