ZDDC/transmittal/js/files-render.js
2026-06-11 13:32:31 -05:00

585 lines
25 KiB
JavaScript

(function (app) {
'use strict';
var dom = app.dom;
var util = app.util;
var filesModule = app.modules.files;
// Build ZDDC filename from file row data — delegates to shared zddc.formatFilename()
function buildZddcFileName(fileData, droppedExt) {
var ext = (fileData.extension || droppedExt || '').toLowerCase().replace(/^\.+/, '');
return zddc.formatFilename({
trackingNumber: fileData.trackingNumber || '',
revision: fileData.revision || '',
status: fileData.status || '',
title: fileData.title || '',
extension: ext,
});
}
function setupRowDropTargets() {
if (app.state.mode !== 'edit') { return; }
var rows = document.querySelectorAll('table tbody tr:not(.self-entry)');
rows.forEach(function (row) {
row.addEventListener('dragover', function (e) {
if (app.state.mode !== 'edit') { return; }
e.preventDefault();
e.stopPropagation();
row.classList.add('ring-2', 'ring-blue-400', 'bg-blue-50');
});
row.addEventListener('dragleave', function () {
row.classList.remove('ring-2', 'ring-blue-400', 'bg-blue-50');
});
row.addEventListener('drop', async function (e) {
row.classList.remove('ring-2', 'ring-blue-400', 'bg-blue-50');
if (app.state.mode !== 'edit') { return; }
var droppedFile = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
if (!droppedFile) { return; }
e.preventDefault();
e.stopPropagation();
var setStatus = app.modules.data && app.modules.data.setStatus;
// Prompt for directory if none selected
if (!app.data.selectedDirHandle) {
try {
app.data.selectedDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
app.data.selectedDirName = app.data.selectedDirHandle.name || '';
if (filesModule.updateDirectoryIndicator) {
filesModule.updateDirectoryIndicator();
}
} catch (pickErr) {
if (setStatus) { setStatus('A directory is required to copy files', 'error'); }
return;
}
}
var indexCell = row.querySelector('td[data-index]');
if (!indexCell) { return; }
var fileIndex = Number(indexCell.dataset.index);
var fileData = app.data.files[fileIndex];
if (!fileData) { return; }
var droppedExt = zddc.splitExtension(droppedFile.name).extension;
var zddcName = buildZddcFileName(fileData, droppedExt);
if (!zddcName) { zddcName = droppedFile.name; }
var hashCell = row.querySelector('td:last-child');
try {
if (hashCell) { hashCell.textContent = 'copying\u2026'; }
var dirHandle = app.data.selectedDirHandle;
var newHandle = await dirHandle.getFileHandle(zddcName, { create: true });
var writable = await newHandle.createWritable();
await writable.write(droppedFile);
await writable.close();
if (hashCell) { hashCell.textContent = 'hashing\u2026'; }
var written = await newHandle.getFile();
var hash = await util.hashFile(written);
fileData.fileHandle = newHandle;
fileData.path = zddcName;
fileData.name = zddcName;
fileData.size = written.size;
fileData.fileSize = written.size;
fileData.sha256 = hash;
if (!fileData.extension) { fileData.extension = droppedExt; }
filesModule.updateFilesInJson(app.data.files);
filesModule.render();
app.state.apply();
app.markDirty();
if (setStatus) { setStatus('Copied \u2192 ' + zddcName, 'success'); }
} catch (err) {
console.error('[transmittal] row file drop failed', err);
if (hashCell) { hashCell.textContent = 'error'; }
if (setStatus) { setStatus('Drop failed: ' + (err.message || err), 'error'); }
}
});
});
}
filesModule.clearTable = function clearTable() {
var tbody = document.querySelector('table tbody');
if (tbody) { tbody.innerHTML = ''; }
};
filesModule.renderSingleRow = function renderSingleRow(file, index) {
var tbody = document.querySelector('table tbody');
if (!tbody) { return null; }
var row = document.createElement('tr');
var numCell = document.createElement('td');
numCell.className = 'px-2 py-1 align-top text-center text-gray-400';
numCell.textContent = String(index + 1);
row.appendChild(numCell);
var trackingCell = document.createElement('td');
trackingCell.className = 'px-2 py-1 align-top whitespace-nowrap font-mono';
var link = document.createElement('a');
link.textContent = file.trackingNumber || '';
var relativePath = file.path || file.name || '';
if (relativePath) {
link.href = encodeURI(relativePath);
link.dataset.relativePath = relativePath;
} else {
link.href = '#';
}
link.title = relativePath || (file.name || '');
link.className = 'text-blue-600 hover:underline';
var ext = (file.extension || zddc.splitExtension(relativePath).extension);
if (app.constants.viewableExts.indexOf(ext) !== -1) {
link.target = '_blank';
link.rel = 'noopener';
} else {
var filename = (relativePath.split('/').pop() || file.name || 'download');
link.setAttribute('download', filename);
}
trackingCell.appendChild(link);
trackingCell.contentEditable = 'false';
trackingCell.dataset.index = String(index);
trackingCell.dataset.field = 'trackingNumber';
row.appendChild(trackingCell);
var titleCell = document.createElement('td');
titleCell.className = 'px-2 py-1 align-top whitespace-normal break-words w-full';
titleCell.textContent = file.title || '';
titleCell.contentEditable = (app.state.mode === 'edit').toString();
titleCell.setAttribute('role', 'textbox');
titleCell.setAttribute('aria-multiline', 'false');
titleCell.setAttribute('tabindex', '0');
titleCell.dataset.index = String(index);
titleCell.dataset.field = 'title';
row.appendChild(titleCell);
var revisionCell = document.createElement('td');
revisionCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
revisionCell.textContent = file.revision || '';
revisionCell.contentEditable = (app.state.mode === 'edit').toString();
revisionCell.setAttribute('role', 'textbox');
revisionCell.setAttribute('aria-multiline', 'false');
revisionCell.setAttribute('tabindex', '0');
revisionCell.dataset.index = String(index);
revisionCell.dataset.field = 'revision';
row.appendChild(revisionCell);
var statusCell = document.createElement('td');
statusCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
statusCell.textContent = file.status || '';
statusCell.contentEditable = (app.state.mode === 'edit').toString();
statusCell.setAttribute('role', 'textbox');
statusCell.setAttribute('aria-multiline', 'false');
statusCell.setAttribute('tabindex', '0');
statusCell.dataset.index = String(index);
statusCell.dataset.field = 'status';
row.appendChild(statusCell);
var extCell = document.createElement('td');
extCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
extCell.textContent = (file.extension || '').toLowerCase();
extCell.contentEditable = 'false';
row.appendChild(extCell);
var sizeCell = document.createElement('td');
sizeCell.className = 'px-2 py-1 align-top whitespace-nowrap text-right font-mono';
var fileSizeValue = (file.fileSize != null ? file.fileSize : file.size);
sizeCell.textContent = util.formatFileSize(fileSizeValue);
sizeCell.contentEditable = 'false';
row.appendChild(sizeCell);
var hashCell = document.createElement('td');
hashCell.className = 'px-2 py-1 align-top font-mono text-[9px] whitespace-normal break-all leading-snug';
if (file.sha256) {
hashCell.textContent = util.formatShortFileHash(file.sha256);
} else {
var prog = document.createElement('div');
prog.className = 'hash-progress';
var bar = document.createElement('div');
bar.className = 'hash-progress-bar';
var fill = document.createElement('div');
fill.className = 'hash-progress-fill';
bar.appendChild(fill);
prog.appendChild(bar);
hashCell.appendChild(prog);
}
row.appendChild(hashCell);
tbody.appendChild(row);
return hashCell;
};
function renderSelfRow(tbody, rowNum) {
var self = filesModule.buildSelfEntry ? filesModule.buildSelfEntry() : null;
if (!self) { return; }
var row = document.createElement('tr');
row.className = 'self-entry';
var numCell = document.createElement('td');
numCell.className = 'px-2 py-1 align-top text-center text-gray-400';
numCell.textContent = String(rowNum);
row.appendChild(numCell);
var trackingCell = document.createElement('td');
trackingCell.className = 'px-2 py-1 align-top whitespace-nowrap font-mono text-gray-500';
var selfPath = self.path || self.name || '';
if (selfPath) {
var link = document.createElement('a');
link.href = encodeURI(selfPath);
link.textContent = self.trackingNumber || '';
link.className = 'text-gray-500 hover:underline';
link.target = '_blank';
link.rel = 'noopener';
trackingCell.appendChild(link);
} else {
trackingCell.textContent = self.trackingNumber || '';
}
row.appendChild(trackingCell);
var titleCell = document.createElement('td');
titleCell.className = 'px-2 py-1 align-top whitespace-normal break-words w-full text-gray-500';
titleCell.textContent = self.title || '';
row.appendChild(titleCell);
var revisionCell = document.createElement('td');
revisionCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono text-gray-500';
revisionCell.textContent = self.revision || '';
row.appendChild(revisionCell);
var statusCell = document.createElement('td');
statusCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono text-gray-500';
statusCell.textContent = self.status || '';
row.appendChild(statusCell);
var extCell = document.createElement('td');
extCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono text-gray-500';
extCell.textContent = 'html';
row.appendChild(extCell);
var sizeCell = document.createElement('td');
sizeCell.className = 'px-2 py-1 align-top whitespace-nowrap text-right font-mono text-gray-400';
sizeCell.textContent = '\u2014';
row.appendChild(sizeCell);
var hashCell = document.createElement('td');
hashCell.className = 'px-2 py-1 align-top font-mono text-[9px] whitespace-nowrap text-gray-400 italic';
hashCell.textContent = 'see above';
row.appendChild(hashCell);
tbody.appendChild(row);
}
filesModule.render = function render() {
var tbody = document.querySelector('table tbody');
if (!tbody) {
return;
}
tbody.innerHTML = '';
filesModule.sortFilesInPlace(app.data.files);
var filters = app.modules.filters ? app.modules.filters.getActiveFilters() : {};
var filtered = [];
(app.data.files || []).forEach(function (file, originalIndex) {
if (!app.modules.filters || app.modules.filters.fileMatchesFilters(file, filters)) {
filtered.push({ file: file, index: originalIndex });
}
});
// Row 0: transmittal self-entry (always pinned first)
renderSelfRow(tbody, 0);
var rowNum = 0;
filtered.forEach(function (entry) {
var file = entry.file;
var index = entry.index;
var row = document.createElement('tr');
rowNum++;
var numCell = document.createElement('td');
numCell.className = 'px-2 py-1 align-top text-center text-gray-400 whitespace-nowrap';
if (app.state.mode === 'edit') {
var delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'row-delete-btn';
delBtn.textContent = '\u00d7';
delBtn.title = 'Remove this file';
delBtn.dataset.fileIndex = String(index);
numCell.appendChild(delBtn);
var numSpan = document.createElement('span');
numSpan.textContent = String(rowNum);
numCell.appendChild(numSpan);
} else {
numCell.textContent = String(rowNum);
}
row.appendChild(numCell);
var trackingCell = document.createElement('td');
trackingCell.className = 'px-2 py-1 align-top whitespace-nowrap font-mono';
var link = document.createElement('a');
link.textContent = file.trackingNumber || '';
var relativePath = file.path || file.name || '';
if (relativePath) {
link.href = encodeURI(relativePath);
link.dataset.relativePath = relativePath;
} else {
link.href = '#';
}
link.title = relativePath || (file.name || '');
link.className = 'text-blue-600 hover:underline';
var ext = (file.extension || zddc.splitExtension(relativePath).extension);
if (app.constants.viewableExts.indexOf(ext) !== -1) {
link.target = '_blank';
link.rel = 'noopener';
} else {
var filename = (relativePath.split('/').pop() || file.name || 'download');
link.setAttribute('download', filename);
}
trackingCell.appendChild(link);
trackingCell.contentEditable = 'false';
trackingCell.dataset.index = String(index);
trackingCell.dataset.field = 'trackingNumber';
row.appendChild(trackingCell);
var titleCell = document.createElement('td');
titleCell.className = 'px-2 py-1 align-top whitespace-normal break-words w-full';
titleCell.textContent = file.title || '';
titleCell.contentEditable = (app.state.mode === 'edit').toString();
titleCell.setAttribute('role', 'textbox');
titleCell.setAttribute('aria-multiline', 'false');
titleCell.setAttribute('tabindex', '0');
titleCell.dataset.index = String(index);
titleCell.dataset.field = 'title';
row.appendChild(titleCell);
var revisionCell = document.createElement('td');
revisionCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
revisionCell.textContent = file.revision || '';
revisionCell.contentEditable = (app.state.mode === 'edit').toString();
revisionCell.setAttribute('role', 'textbox');
revisionCell.setAttribute('aria-multiline', 'false');
revisionCell.setAttribute('tabindex', '0');
revisionCell.dataset.index = String(index);
revisionCell.dataset.field = 'revision';
row.appendChild(revisionCell);
var statusCell = document.createElement('td');
statusCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
statusCell.textContent = file.status || '';
statusCell.contentEditable = (app.state.mode === 'edit').toString();
statusCell.setAttribute('role', 'textbox');
statusCell.setAttribute('aria-multiline', 'false');
statusCell.setAttribute('tabindex', '0');
statusCell.dataset.index = String(index);
statusCell.dataset.field = 'status';
row.appendChild(statusCell);
var extCell = document.createElement('td');
extCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
extCell.textContent = (file.extension || '').toLowerCase();
extCell.contentEditable = 'false';
row.appendChild(extCell);
var sizeCell = document.createElement('td');
sizeCell.className = 'px-2 py-1 align-top whitespace-nowrap text-right font-mono';
var isUnmatched = !file.fileHandle && !file.sha256;
if (isUnmatched) {
sizeCell.textContent = '\u2014';
sizeCell.classList.add('text-gray-400');
} else {
var fileSizeValue = (file.fileSize != null ? file.fileSize : file.size);
sizeCell.textContent = util.formatFileSize(fileSizeValue);
}
sizeCell.contentEditable = 'false';
row.appendChild(sizeCell);
var hashCell = document.createElement('td');
hashCell.className = 'px-2 py-1 align-top font-mono text-[9px] whitespace-normal break-all leading-snug';
if (isUnmatched) {
hashCell.textContent = 'pending';
hashCell.classList.add('italic', 'text-gray-400');
} else {
hashCell.textContent = util.formatShortFileHash(file.sha256 || '');
}
row.appendChild(hashCell);
if (file._verifyResult) {
row.classList.add('verify-' + file._verifyResult);
}
tbody.appendChild(row);
});
if (!filtered.length) {
var placeholderRow = document.createElement('tr');
for (var i = 0; i < 8; i += 1) {
var cell = document.createElement('td');
cell.className = 'px-2 py-1 align-top';
placeholderRow.appendChild(cell);
}
tbody.appendChild(placeholderRow);
}
if (app.modules.filters && typeof app.modules.filters.refreshPlaceholders === 'function') {
app.modules.filters.refreshPlaceholders();
}
setupRowDropTargets();
};
// Undo state for row deletion
var _lastDeleted = null;
var _undoTimer = null;
function deleteFileRow(fileIndex) {
var file = app.data.files[fileIndex];
if (!file) { return; }
var setStatus = app.modules.data && app.modules.data.setStatus;
var label = (file.trackingNumber || '') + (file.title ? ' - ' + file.title : '');
// Store for undo
_lastDeleted = { file: file, index: fileIndex };
if (_undoTimer) { clearTimeout(_undoTimer); }
_undoTimer = setTimeout(function () { _lastDeleted = null; }, 10000);
app.data.files.splice(fileIndex, 1);
filesModule.updateFilesInJson(app.data.files);
filesModule.render();
app.state.apply();
app.markDirty();
if (setStatus) {
setStatus('Removed ' + (label || 'row') + ' — click here to undo', 'success');
// Attach one-time undo click handler to status bar
var statusEl = document.querySelector('#data-status');
if (statusEl) {
statusEl.style.cursor = 'pointer';
var cleanup = function () {
statusEl.removeEventListener('click', handler);
statusEl.style.cursor = '';
};
var handler = function () {
cleanup();
if (!_lastDeleted) { return; }
var d = _lastDeleted;
_lastDeleted = null;
if (_undoTimer) { clearTimeout(_undoTimer); _undoTimer = null; }
var idx = Math.min(d.index, app.data.files.length);
app.data.files.splice(idx, 0, d.file);
filesModule.updateFilesInJson(app.data.files);
filesModule.render();
app.state.apply();
app.markDirty();
if (setStatus) { setStatus('Restored ' + (label || 'row'), 'success'); }
};
// Clear cursor when undo expires
if (_undoTimer) { clearTimeout(_undoTimer); }
_undoTimer = setTimeout(function () { _lastDeleted = null; cleanup(); }, 10000);
statusEl.addEventListener('click', handler);
}
}
}
filesModule.setupTableEditing = function setupTableEditing() {
var tbody = document.querySelector('table tbody');
if (!tbody) {
return;
}
// Delegated handler for row delete buttons
tbody.addEventListener('click', function (event) {
var delBtn = event.target.closest('.row-delete-btn');
if (delBtn && delBtn.dataset.fileIndex !== undefined) {
event.preventDefault();
event.stopPropagation();
deleteFileRow(Number(delBtn.dataset.fileIndex));
return;
}
});
// Click delegation for file preview
// Edit mode: always preview (relative paths don't work until HTML is saved)
// View mode: preview only when checkbox is checked and file source is loaded
tbody.addEventListener('click', function (event) {
var link = event.target.closest('a');
if (!link) {
return;
}
var cell = link.closest('td');
if (!cell || !cell.dataset.index) {
return;
}
var file = app.data.files[Number(cell.dataset.index)];
if (!file || !filesModule.hasFileSource || !filesModule.hasFileSource(file)) {
return;
}
var usePreview = (app.state.mode === 'edit') ||
(filesModule.isPreviewEnabled && filesModule.isPreviewEnabled()) ||
!!(file.fileHandle);
if (usePreview) {
event.preventDefault();
event.stopPropagation();
filesModule.showFilePreview(file);
}
});
tbody.addEventListener('input', function (event) {
var target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
var index = target.dataset.index;
var field = target.dataset.field;
if (index === undefined || !field) {
return;
}
var entry = app.data.files[Number(index)];
if (!entry) {
return;
}
entry[field] = (target.textContent || '').replace(/\r?\n/g, ' ').trim();
filesModule.sortFilesInPlace(app.data.files);
filesModule.updateFilesInJson(app.data.files);
filesModule.render();
app.markDirty();
if (app.modules.liveDigest && app.modules.liveDigest.schedule) {
app.modules.liveDigest.schedule();
}
});
tbody.addEventListener('keydown', function (event) {
var target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (target.isContentEditable && event.key === 'Enter') {
event.preventDefault();
}
});
tbody.addEventListener('paste', function (event) {
var target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (!target.isContentEditable) {
return;
}
event.preventDefault();
var text = (event.clipboardData || window.clipboardData).getData('text');
var sanitized = (text || '').replace(/\r?\n/g, ' ');
try {
document.execCommand('insertText', false, sanitized);
} catch (err) {
target.textContent = (target.textContent || '') + sanitized;
}
});
};
filesModule.handleClear = function handleClear() {
if (app.state.mode !== 'edit') {
return;
}
if (filesModule.cleanupBlobUrls) {
filesModule.cleanupBlobUrls();
}
app.data.files = [];
app.data.selectedDirHandle = null;
filesModule.updateDirectoryIndicator('');
filesModule.updateFilesInJson([]);
filesModule.render();
app.state.apply();
app.markDirty();
};
})(window.transmittalApp);