/** * file-upload-queue.js * * Renders visual file queues for: * 1. #tfe-files-input — multi-file upload with drag-to-reorder (SortableJS) * and per-file label inputs. Injects hidden file_labels[] / file_orders[]. * 2. input[data-preview] — single-file previews (couverture, note_intention, etc.) * 3. #existing-files-sortable — edit-mode sortable list * * Exposes window.XamxamInitFileUploads() so HTMX fragments can re-bind * after swap without a global event listener. */ window.XamxamInitFileUploads = function () { console.log('[file-upload-queue] XamxamInitFileUploads called'); var ICON = { pdf: '\uD83D\uDCC4', video: '\uD83C\uDFAC', audio: '\uD83D\uDD0A', zip: '\uD83D\uDDDC\uFE0F', vtt: '\uD83D\uDCAC', image: '\uD83D\uDDBC\uFE0F', other: '\uD83D\uDCCE' }; function iconFor(file) { var t = file.type || '', n = file.name.toLowerCase(); if (/^image\//.test(t)) return ICON.image; if (t === 'application/pdf' || /\.pdf$/.test(n)) return ICON.pdf; if (/^video\//.test(t) || /\.(mp4|webm|mov|ogv)$/.test(n)) return ICON.video; if (/^audio\//.test(t) || /\.(mp3|ogg|oga|wav|flac|aac|m4a)$/.test(n)) return ICON.audio; if (/\.(zip|tar|gz|tgz)$/.test(n)) return ICON.zip; if (/\.vtt$/.test(n)) return ICON.vtt; return ICON.other; } function humanSize(b) { return b >= 1073741824 ? (b / 1073741824).toFixed(2) + ' GB' : b >= 1048576 ? (b / 1048576).toFixed(2) + ' MB' : b >= 1024 ? (b / 1024).toFixed(1) + ' KB' : b + ' B'; } function esc(s) { return s.replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); } // ── 1. TFE multi-file queue ──────────────────────────────────────────── var picker = document.getElementById('tfe-files-input'); var queue = document.getElementById('tfe-file-queue'); var empty = document.getElementById('tfe-file-queue-empty'); var sortHint = document.getElementById('tfe-file-queue-sort-hint'); if (picker && queue) { console.log('[file-upload-queue] init TFE queue picker=', picker, 'multiple=', picker.multiple); var fileArray = []; if (typeof Sortable !== 'undefined') { Sortable.create(queue, { animation: 150, handle: '.fq-drag-handle', ghostClass: 'fq-ghost', onEnd: function () { var items = queue.querySelectorAll('.fq-item'); var newArr = Array.prototype.map.call(items, function (li) { return fileArray[parseInt(li.getAttribute('data-idx'), 10)]; }); fileArray = newArr; renderQueue(); } }); } picker.onchange = function () { console.log('[file-upload-queue] onchange fired, files count:', picker.files.length, 'names:', Array.from(picker.files).map(function(f){return f.name})); fileArray = fileArray.concat(Array.from(picker.files)); console.log('[file-upload-queue] fileArray after concat, length:', fileArray.length); picker.value = ''; renderQueue(); }; function renderQueue() { queue.innerHTML = ''; if (!fileArray.length) { empty.style.display = ''; if (sortHint) sortHint.style.display = 'none'; injectHiddenFields([]); return; } empty.style.display = 'none'; if (sortHint) sortHint.style.display = ''; fileArray.forEach(function (file, idx) { var li = document.createElement('li'); li.className = 'fq-item'; li.setAttribute('data-idx', idx); li.innerHTML = '\u2820' + '' + iconFor(file) + '' + '' + esc(file.name) + '' + '' + humanSize(file.size) + '' + ''; li.querySelector('.fq-remove').onclick = (function (i) { return function () { fileArray.splice(i, 1); renderQueue(); }; })(idx); queue.appendChild(li); }); injectHiddenFields(Array.from(queue.querySelectorAll('.fq-item'))); } function injectHiddenFields(items) { var form = picker.closest('form'); if (!form) return; form.querySelectorAll('.fq-hidden-label, .fq-hidden-order').forEach(function (el) { el.remove(); }); items.forEach(function (li, sortedIdx) { var label = li.querySelector('.fq-label'); var lInp = document.createElement('input'); lInp.type = 'hidden'; lInp.name = 'file_labels[]'; lInp.value = label ? label.value : ''; lInp.className = 'fq-hidden-label'; form.appendChild(lInp); var oInp = document.createElement('input'); oInp.type = 'hidden'; oInp.name = 'file_orders[]'; oInp.value = sortedIdx + 1; oInp.className = 'fq-hidden-order'; form.appendChild(oInp); }); } // On submit, refresh hidden fields from current queue state var form = picker.closest('form'); if (form) form.addEventListener('submit', function () { injectHiddenFields(Array.from(queue.querySelectorAll('.fq-item'))); }); } // ── 2. Single-file previews (data-preview attribute) ──────────────────── document.querySelectorAll('input[type="file"][data-preview]').forEach(function (input) { if (input.id === 'tfe-files-input') return; console.log('[file-upload-queue] binding preview for', input.id, 'multiple=', input.multiple); var container = document.getElementById(input.getAttribute('data-preview')); if (!container) return; input.onchange = function () { container.innerHTML = ''; Array.from(input.files).forEach(function (file) { var item = document.createElement('div'); item.className = 'fp-item'; if (/^image\//.test(file.type)) { var img = document.createElement('img'); img.className = 'fp-thumb'; img.alt = file.name; var reader = new FileReader(); reader.onload = function (e) { img.src = e.target.result; }; reader.readAsDataURL(file); item.appendChild(img); } else { var ic = document.createElement('span'); ic.className = 'fp-icon'; ic.textContent = iconFor(file); item.appendChild(ic); } var meta = document.createElement('span'); meta.className = 'fp-meta'; meta.innerHTML = '' + esc(file.name) + '' + humanSize(file.size) + ''; item.appendChild(meta); container.appendChild(item); }); }; }); // ── 3. Existing-files sortable (edit mode) ────────────────────────────── var sortList = document.getElementById('existing-files-sortable'); if (sortList && typeof Sortable !== 'undefined') { Sortable.create(sortList, { animation: 150, handle: '.admin-file-drag-handle', ghostClass: 'fq-ghost', onEnd: function () { sortList.querySelectorAll('input[name="file_sort_order[]"]').forEach(function (el) { el.remove(); }); sortList.querySelectorAll('.admin-file-list-item[data-file-id]').forEach(function (li) { var inp = document.createElement('input'); inp.type = 'hidden'; inp.name = 'file_sort_order[]'; inp.value = li.getAttribute('data-file-id'); li.prepend(inp); }); } }); } }; // Bootstrap on page load if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', window.XamxamInitFileUploads); else window.XamxamInitFileUploads();