/** * 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();