/** * initAttachmentZone — wire up drag-and-drop, clipboard paste, and file-picker * for a form that already has a multipart file input. * * Uses a DataTransfer object as a mutable file store so that * fileInput.files always reflects the full set of chosen files and * native form submission works unchanged. */ function initAttachmentZone({ formEl, fileInput, dropZone, fileListEl, pasteTarget }) { const dt = new DataTransfer(); let dragCounter = 0; // ── Helpers ────────────────────────────────────────────────────────── function fileKey(f) { return f.name + '|' + f.size + '|' + f.lastModified; } function currentKeys() { const keys = new Set(); for (let i = 0; i < dt.items.length; i++) { keys.add(fileKey(dt.files[i])); } return keys; } function addFiles(files) { const keys = currentKeys(); for (const f of files) { if (!keys.has(fileKey(f))) { dt.items.add(f); keys.add(fileKey(f)); } } sync(); } function removeFile(index) { dt.items.remove(index); sync(); } function sync() { fileInput.files = dt.files; renderList(); } // ── File list rendering ───────────────────────────────────────────── function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } function renderList() { fileListEl.innerHTML = ''; if (dt.files.length === 0) { fileListEl.classList.add('hidden'); return; } fileListEl.classList.remove('hidden'); for (let i = 0; i < dt.files.length; i++) { const f = dt.files[i]; const row = document.createElement('div'); row.className = 'flex items-center justify-between rounded-md bg-gray-50 px-3 py-1.5 text-sm'; const info = document.createElement('span'); info.className = 'text-gray-700 truncate'; info.textContent = f.name; const size = document.createElement('span'); size.className = 'text-gray-400 ml-2 whitespace-nowrap'; size.textContent = formatSize(f.size); const left = document.createElement('div'); left.className = 'flex items-center min-w-0'; left.appendChild(info); left.appendChild(size); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ml-2 text-gray-400 hover:text-red-500 text-xs font-bold'; btn.textContent = '\u00d7'; btn.setAttribute('aria-label', 'Remove ' + f.name); btn.addEventListener('click', function () { removeFile(i); }); row.appendChild(left); row.appendChild(btn); fileListEl.appendChild(row); } } // ── Drag-and-drop ─────────────────────────────────────────────────── function highlight() { dropZone.classList.add('ring-blue-400', 'bg-blue-50', 'border-blue-400'); } function unhighlight() { dropZone.classList.remove('ring-blue-400', 'bg-blue-50', 'border-blue-400'); } dropZone.addEventListener('dragenter', function (e) { e.preventDefault(); dragCounter++; highlight(); }); dropZone.addEventListener('dragover', function (e) { e.preventDefault(); }); dropZone.addEventListener('dragleave', function (e) { e.preventDefault(); dragCounter--; if (dragCounter <= 0) { dragCounter = 0; unhighlight(); } }); dropZone.addEventListener('drop', function (e) { e.preventDefault(); dragCounter = 0; unhighlight(); if (e.dataTransfer && e.dataTransfer.files.length) { addFiles(e.dataTransfer.files); } }); // ── File picker (additive) ────────────────────────────────────────── fileInput.addEventListener('change', function () { addFiles(fileInput.files); }); // ── Clipboard paste ───────────────────────────────────────────────── function pasteTimestamp() { const d = new Date(); const pad = function (n) { return n < 10 ? '0' + n : '' + n; }; return d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()); } if (pasteTarget) { pasteTarget.addEventListener('paste', function (e) { var items = e.clipboardData && e.clipboardData.items; if (!items) return; var files = []; for (var i = 0; i < items.length; i++) { if (items[i].kind === 'file') { var f = items[i].getAsFile(); if (!f) continue; // Rename generic pasted names like "image.png" if (/^image\.\w+$/.test(f.name)) { var ext = f.name.split('.').pop(); var renamed = new File([f], 'paste-' + pasteTimestamp() + '.' + ext, { type: f.type }); files.push(renamed); } else { files.push(f); } } } if (files.length > 0) { addFiles(files); // Don't prevent default — let text pastes through } }); } }