User report: opening an .html file with a '../.archive/' hyperlink in
a new tab works (zddc-server intercepts and serves the right file),
but clicking the same link inside the file previewer does nothing.
Two combined causes:
1. The previewer's iframe was loaded from a blob: URL (built from
the file's bytes). Relative URLs in the iframe resolve relative
to the blob URL — '../.archive/X.html' becomes 'blob:.../.archive/
X.html', which is gibberish. The browser never sends a request to
the server, so the .archive interception never fires.
2. sandbox="" disables every iframe capability including popups,
so even <a target=_blank> is silently swallowed.
Fix per tool:
- archive (table.js): for HTML preview, use file.url (the real
server URL) directly when available; fall back to blob only for
File-System-Access-API mode where there's no server to intercept
anyway. Now relative links in archived HTMLs resolve against the
actual server origin and the .archive interception fires as
designed. Sandbox loosens to allow-same-origin + allow-popups +
allow-popups-to-escape-sandbox so resources within the iframe
load and link clicks (default target / target=_blank / middle-
click) work normally. allow-scripts is intentionally NOT set —
archived HTML still cannot run JS in the popup's origin.
- transmittal (files-preview.js) + classifier (preview.js): same
sandbox loosening for consistency. These tools' files are
typically local (FileSystemAccessAPI), so the file.url branch
doesn't apply — relative URLs that depend on a server still
won't resolve in local mode (intrinsic limitation, no server).
Tested behavior preserved:
- PDFs: unchanged (no sandbox, browser's PDF viewer handles).
- Images / docx / xlsx / tiff / zip / text: unchanged.
- HTML in zddc-server-backed archive: relative '../.archive/' links
now navigate the iframe to the correct target file.
529 lines
19 KiB
JavaScript
529 lines
19 KiB
JavaScript
/**
|
|
* Preview Module
|
|
* Opens file preview in a separate popup window
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
let currentBlobUrl = null;
|
|
let currentFile = null;
|
|
let currentRowIndex = null;
|
|
let previewWindow = null;
|
|
|
|
// Use shared extension lists from window.zddc.preview where possible
|
|
const IMAGE_EXTENSIONS = zddc.preview.IMAGE_EXTENSIONS;
|
|
const TIFF_EXTENSIONS = zddc.preview.TIFF_EXTENSIONS;
|
|
const TEXT_EXTENSIONS = zddc.preview.TEXT_EXTENSIONS;
|
|
const PDF_EXTENSIONS = ['pdf'];
|
|
const ZIP_EXTENSIONS = ['zip'];
|
|
|
|
// Lazily load a script from CDN — delegates to shared cache.
|
|
const loadLibrary = zddc.preview.loadLibrary;
|
|
|
|
/**
|
|
* Initialize preview module
|
|
*/
|
|
function init() {
|
|
// Listen for row focused events from selection module
|
|
document.addEventListener('rowfocused', handleRowFocused);
|
|
|
|
// Set up toggle button to open/close preview window
|
|
const toggleBtn = document.getElementById('togglePreviewBtn');
|
|
if (toggleBtn) {
|
|
toggleBtn.addEventListener('click', () => {
|
|
if (previewWindow && !previewWindow.closed) {
|
|
// Close preview window
|
|
previewWindow.close();
|
|
previewWindow = null;
|
|
toggleBtn.classList.remove('preview-active');
|
|
} else if (currentFile) {
|
|
openPreviewWindow(currentFile);
|
|
toggleBtn.classList.add('preview-active');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle row focused event
|
|
*/
|
|
function handleRowFocused(e) {
|
|
const { rowIndex, file } = e.detail;
|
|
|
|
currentRowIndex = rowIndex;
|
|
|
|
if (file && file !== currentFile) {
|
|
currentFile = file;
|
|
|
|
// Update preview window if open
|
|
if (previewWindow && !previewWindow.closed) {
|
|
openPreviewWindow(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open preview in a separate popup window
|
|
*/
|
|
async function openPreviewWindow(file) {
|
|
if (!file) return;
|
|
|
|
currentFile = file;
|
|
|
|
try {
|
|
const blob = await getFileBlob(file);
|
|
|
|
// Clean up previous blob URL
|
|
if (currentBlobUrl) {
|
|
URL.revokeObjectURL(currentBlobUrl);
|
|
}
|
|
currentBlobUrl = URL.createObjectURL(blob);
|
|
|
|
const fileName = zddc.joinExtension(file.originalFilename, file.extension);
|
|
|
|
// Build preview HTML with toolbar
|
|
const previewHtml = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>${escapeHtml(fileName)} - Preview</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
background: #f5f5f5;
|
|
border-bottom: 1px solid #ddd;
|
|
}
|
|
.toolbar h1 {
|
|
flex: 1;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.btn {
|
|
padding: 0.4rem 0.8rem;
|
|
font-size: 0.85rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
background: white;
|
|
cursor: pointer;
|
|
}
|
|
.btn:hover { background: #e8e8e8; }
|
|
iframe, img {
|
|
flex: 1;
|
|
width: 100%;
|
|
border: none;
|
|
}
|
|
img {
|
|
object-fit: contain;
|
|
background: #f0f0f0;
|
|
}
|
|
pre {
|
|
flex: 1;
|
|
padding: 1rem;
|
|
overflow: auto;
|
|
background: #fafafa;
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
font-size: 0.9rem;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
.unsupported {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #666;
|
|
}
|
|
.unsupported .icon { font-size: 3rem; margin-bottom: 1rem; }
|
|
#previewContent {
|
|
flex: 1;
|
|
overflow: auto;
|
|
}
|
|
.loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: #666;
|
|
font-size: 1.1rem;
|
|
}
|
|
.docx-wrapper { padding: 1rem; }
|
|
.xlsx-table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }
|
|
.xlsx-table th, .xlsx-table td {
|
|
border: 1px solid #ddd;
|
|
padding: 0.35rem 0.5rem;
|
|
text-align: left;
|
|
white-space: nowrap;
|
|
}
|
|
.xlsx-table th { background: #f0f0f0; font-weight: 600; position: sticky; top: 0; }
|
|
.xlsx-table tr:nth-child(even) { background: #fafafa; }
|
|
.xlsx-table tr:hover { background: #f0f7ff; }
|
|
.sheet-tabs { display: flex; gap: 0; border-bottom: 1px solid #ddd; background: #f5f5f5; }
|
|
.sheet-tab {
|
|
padding: 0.4rem 1rem;
|
|
cursor: pointer;
|
|
border: 1px solid transparent;
|
|
border-bottom: none;
|
|
font-size: 0.85rem;
|
|
background: transparent;
|
|
}
|
|
.sheet-tab:hover { background: #e8e8e8; }
|
|
.sheet-tab.active {
|
|
background: white;
|
|
border-color: #ddd;
|
|
border-bottom-color: white;
|
|
margin-bottom: -1px;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="toolbar">
|
|
<h1>${escapeHtml(fileName)}</h1>
|
|
<button class="btn" onclick="downloadFile()">Download</button>
|
|
</div>
|
|
${await getPreviewContent(file, currentBlobUrl)}
|
|
<script>
|
|
var blobUrl = "${currentBlobUrl}";
|
|
var fileName = "${escapeHtml(fileName).replace(/"/g, '\\"')}";
|
|
|
|
function downloadFile() {
|
|
const a = document.createElement('a');
|
|
a.href = blobUrl;
|
|
a.download = fileName;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
<\/script>
|
|
</body>
|
|
</html>`;
|
|
|
|
// Reuse existing window if open, otherwise create new one
|
|
if (previewWindow && !previewWindow.closed) {
|
|
previewWindow.document.open();
|
|
previewWindow.document.write(previewHtml);
|
|
previewWindow.document.close();
|
|
previewWindow.focus();
|
|
} else {
|
|
// Calculate window size
|
|
const width = Math.round(screen.width * 0.6);
|
|
const height = Math.round(screen.height * 0.8);
|
|
const left = Math.round((screen.width - width) / 2);
|
|
const top = Math.round((screen.height - height) / 2);
|
|
|
|
previewWindow = window.open('', 'classifierPreview',
|
|
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`);
|
|
|
|
if (!previewWindow) {
|
|
// Popup blocked - fall back to new tab
|
|
window.open(currentBlobUrl, '_blank');
|
|
return;
|
|
}
|
|
|
|
// Poll for window close — beforeunload is unreliable for popup close buttons
|
|
const closePoll = setInterval(() => {
|
|
if (previewWindow && previewWindow.closed) {
|
|
clearInterval(closePoll);
|
|
previewWindow = null;
|
|
const btn = document.getElementById('togglePreviewBtn');
|
|
if (btn) btn.classList.remove('preview-active');
|
|
}
|
|
}, 500);
|
|
|
|
previewWindow.document.write(previewHtml);
|
|
previewWindow.document.close();
|
|
previewWindow.focus();
|
|
}
|
|
|
|
// For types that need decoding, render content after window is ready
|
|
const ext = (file.extension || '').toLowerCase();
|
|
if (ext === 'docx') {
|
|
await renderDocxInWindow(file);
|
|
} else if (ext === 'xlsx' || ext === 'xls') {
|
|
await renderXlsxInWindow(file);
|
|
} else if (TIFF_EXTENSIONS.includes(ext)) {
|
|
await renderTiffInWindow(file);
|
|
} else if (ZIP_EXTENSIONS.includes(ext)) {
|
|
await renderZipInWindow(file);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error opening preview:', err);
|
|
alert(`Error opening preview: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get preview content HTML based on file type
|
|
*/
|
|
async function getPreviewContent(file, blobUrl) {
|
|
const ext = file.extension.toLowerCase();
|
|
const previewType = getPreviewType(ext);
|
|
|
|
switch (previewType) {
|
|
case 'pdf':
|
|
return `<iframe src="${blobUrl}#view=FitV"></iframe>`;
|
|
case 'html':
|
|
// Render the HTML natively (not as literal text). Sandbox
|
|
// flags allow same-origin resource loads + opening links
|
|
// in real new tabs (target=_blank / middle-click), but
|
|
// NOT allow-scripts — archived HTML cannot run JS.
|
|
return `<iframe src="${blobUrl}" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>`;
|
|
case 'image':
|
|
return `<img src="${blobUrl}" alt="${escapeHtml(file.originalFilename)}" />`;
|
|
case 'text':
|
|
const text = await getFileText(file);
|
|
const maxLength = 100000;
|
|
const displayText = text.length > maxLength
|
|
? text.substring(0, maxLength) + '\n\n... (truncated)'
|
|
: text;
|
|
return `<pre>${escapeHtml(displayText)}</pre>`;
|
|
case 'docx':
|
|
case 'xlsx':
|
|
case 'tiff':
|
|
case 'zip':
|
|
return `<div id="previewContent"><div class="loading">Loading preview...</div></div>`;
|
|
default:
|
|
return `
|
|
<div class="unsupported">
|
|
<div class="icon">📄</div>
|
|
<p>Preview not available for ${ext} files</p>
|
|
<p style="margin-top: 0.5rem;">Click Download to view in external application</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get preview type from extension
|
|
*/
|
|
function getPreviewType(ext) {
|
|
// HTML is technically in TEXT_EXTENSIONS (used for editor
|
|
// syntax-highlighting elsewhere) but for previews we want to
|
|
// RENDER it, not show source. Check before the text branch.
|
|
if (ext === 'html' || ext === 'htm') return 'html';
|
|
if (PDF_EXTENSIONS.includes(ext)) return 'pdf';
|
|
if (TIFF_EXTENSIONS.includes(ext)) return 'tiff';
|
|
if (IMAGE_EXTENSIONS.includes(ext)) return 'image';
|
|
if (TEXT_EXTENSIONS.includes(ext)) return 'text';
|
|
if (ext === 'docx') return 'docx';
|
|
if (ext === 'xlsx' || ext === 'xls') return 'xlsx';
|
|
if (ZIP_EXTENSIONS.includes(ext)) return 'zip';
|
|
return 'none';
|
|
}
|
|
|
|
function getMimeType(ext) {
|
|
return window.app.modules.utils.getMimeType(ext);
|
|
}
|
|
|
|
/**
|
|
* Get file content as blob (handles both real and virtual files)
|
|
*/
|
|
async function getFileBlob(file) {
|
|
if (file.isVirtual) {
|
|
// Get from ZIP cache
|
|
const cached = window.app.modules.scanner.getZipCache(file.zipPath);
|
|
if (!cached) throw new Error('ZIP not found in cache');
|
|
|
|
const zipEntry = cached.zip.file(file.zipEntryPath);
|
|
if (!zipEntry) throw new Error('File not found in ZIP');
|
|
|
|
// Get as arraybuffer and create blob with correct MIME type
|
|
const arrayBuffer = await zipEntry.async('arraybuffer');
|
|
const mimeType = getMimeType(file.extension);
|
|
return new Blob([arrayBuffer], { type: mimeType });
|
|
} else {
|
|
// Get from file handle
|
|
if (!file.handle) {
|
|
throw new Error('File handle not available');
|
|
}
|
|
return await file.handle.getFile();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get file content as text (handles both real and virtual files)
|
|
*/
|
|
async function getFileText(file) {
|
|
if (file.isVirtual) {
|
|
const cached = window.app.modules.scanner.getZipCache(file.zipPath);
|
|
if (!cached) throw new Error('ZIP not found in cache');
|
|
|
|
const zipEntry = cached.zip.file(file.zipEntryPath);
|
|
if (!zipEntry) throw new Error('File not found in ZIP');
|
|
|
|
return await zipEntry.async('string');
|
|
} else {
|
|
if (!file.handle) {
|
|
throw new Error('File handle not available');
|
|
}
|
|
const fileObj = await file.handle.getFile();
|
|
return await fileObj.text();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a DOCX file in the preview window using docx-preview library
|
|
*/
|
|
async function renderDocxInWindow(file) {
|
|
const container = previewWindow.document.getElementById('previewContent');
|
|
if (!container) return;
|
|
|
|
try {
|
|
await loadLibrary('https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js');
|
|
await loadLibrary('https://cdn.jsdelivr.net/npm/docx-preview@latest/dist/docx-preview.min.js');
|
|
|
|
const blob = await getFileBlob(file);
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
|
|
container.innerHTML = '';
|
|
await window.docx.renderAsync(arrayBuffer, container);
|
|
} catch (err) {
|
|
console.error('Error rendering DOCX:', err);
|
|
container.innerHTML = `<div class="loading">Error rendering DOCX: ${err.message}<br>Click Download to view in Word.</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render an XLSX/XLS file in the preview window using SheetJS
|
|
*/
|
|
async function renderXlsxInWindow(file) {
|
|
const container = previewWindow.document.getElementById('previewContent');
|
|
if (!container) return;
|
|
|
|
try {
|
|
await loadLibrary('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js');
|
|
|
|
const blob = await getFileBlob(file);
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
|
|
|
container.innerHTML = '';
|
|
|
|
if (workbook.SheetNames.length > 1) {
|
|
const tabs = previewWindow.document.createElement('div');
|
|
tabs.className = 'sheet-tabs';
|
|
workbook.SheetNames.forEach((name, i) => {
|
|
const tab = previewWindow.document.createElement('button');
|
|
tab.className = 'sheet-tab' + (i === 0 ? ' active' : '');
|
|
tab.textContent = name;
|
|
tab.onclick = () => {
|
|
tabs.querySelectorAll('.sheet-tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
renderSheetInWindow(workbook, name, tableContainer);
|
|
};
|
|
tabs.appendChild(tab);
|
|
});
|
|
container.appendChild(tabs);
|
|
}
|
|
|
|
const tableContainer = previewWindow.document.createElement('div');
|
|
tableContainer.style.flex = '1';
|
|
tableContainer.style.overflow = 'auto';
|
|
container.appendChild(tableContainer);
|
|
|
|
renderSheetInWindow(workbook, workbook.SheetNames[0], tableContainer);
|
|
} catch (err) {
|
|
console.error('Error rendering XLSX:', err);
|
|
container.innerHTML = `<div class="loading">Error rendering spreadsheet: ${err.message}<br>Click Download to view in Excel.</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a single sheet as an HTML table in the preview window
|
|
*/
|
|
function renderSheetInWindow(workbook, sheetName, container) {
|
|
const sheet = workbook.Sheets[sheetName];
|
|
const html = XLSX.utils.sheet_to_html(sheet, { editable: false });
|
|
container.innerHTML = html;
|
|
const table = container.querySelector('table');
|
|
if (table) table.className = 'xlsx-table';
|
|
}
|
|
|
|
/**
|
|
* Render a TIFF file in the preview window using shared zddc.preview.renderTiff
|
|
*/
|
|
async function renderTiffInWindow(file) {
|
|
const container = previewWindow.document.getElementById('previewContent');
|
|
if (!container) return;
|
|
try {
|
|
const blob = await getFileBlob(file);
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
await zddc.preview.renderTiff(previewWindow.document, container, arrayBuffer, {
|
|
fileName: zddc.joinExtension(file.originalFilename, file.extension)
|
|
});
|
|
} catch (err) {
|
|
console.error('Error rendering TIFF:', err);
|
|
container.innerHTML = `<div class="loading">Error rendering TIFF: ${err.message}<br>Click Download to view in another application.</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a ZIP listing in the preview window using shared zddc.preview.renderZipListing
|
|
*/
|
|
async function renderZipInWindow(file) {
|
|
const container = previewWindow.document.getElementById('previewContent');
|
|
if (!container) return;
|
|
try {
|
|
const blob = await getFileBlob(file);
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
await zddc.preview.renderZipListing(previewWindow.document, container, arrayBuffer, {
|
|
fileName: zddc.joinExtension(file.originalFilename, file.extension)
|
|
});
|
|
} catch (err) {
|
|
console.error('Error rendering ZIP listing:', err);
|
|
container.innerHTML = `<div class="loading">Error reading ZIP: ${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape HTML for safe display
|
|
*/
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Download current file
|
|
*/
|
|
async function downloadFile() {
|
|
if (!currentFile) return;
|
|
|
|
try {
|
|
const blob = await getFileBlob(currentFile);
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = zddc.joinExtension(currentFile.originalFilename, currentFile.extension);
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
|
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
} catch (err) {
|
|
console.error('Error downloading file:', err);
|
|
alert('Error downloading file: ' + err.message);
|
|
}
|
|
}
|
|
|
|
// Export module
|
|
window.app.modules.preview = {
|
|
init
|
|
};
|
|
})();
|