/** * file-upload-queue.js * * Client-side file upload queues for TFE, Video, Audio, and Annexe files. * Replaces the old HTMX+PHP session-backed queue system. * * Queues: * tfe — main thesis files (multi-format) * video — video files (non-PeerTube path) * audio — audio files (non-PeerTube path) * annexe — annex files * * Architecture: * 1. Intercept 'change' on all .tfe-file-picker inputs. * 2. Validate MIME/extension/size client-side. * 3. Store File objects in window.__xamxamQueues. * 4. Render queue UI with SortableJS drag-to-reorder. * 5. On form submit: inject all queued files into FormData and POST normally. * * The queue containers are rendered server-side by fichiers-fragment.php * as empty
Aucun fichier sélectionné.
'; container.innerHTML = html; // Wire Sortable if (window.Sortable && files.length > 1) { var list = container.querySelector("ul.tfe-file-queue"); if (list) { Sortable.create(list, { handle: ".fq-drag-handle", animation: 150, onEnd: function () { reorderFromDOM(queueType); }, }); } } // Mark form as dirty markDirty(); } function reorderFromDOM(queueType) { var container = document.getElementById(getQueueContainerId(queueType)); if (!container) return; var ids = Array.from(container.querySelectorAll("[data-fq-id]")).map(function (li) { return li.dataset.fqId; }); var files = queues[queueType]; var byId = {}; files.forEach(function (f) { byId[f.__xamxamId] = f; }); var reordered = ids.map(function (id) { return byId[id]; }).filter(Boolean); files.forEach(function (f) { if (reordered.indexOf(f) === -1) reordered.push(f); }); queues[queueType] = reordered; markDirty(); } // ── Operations ───────────────────────────────────────────────────────── function addFiles(queueType, newFiles) { var errors = []; var addedFiles = []; for (var i = 0; i < newFiles.length; i++) { var err = validateFile(newFiles[i], queueType); if (err) { errors.push(err); } else { newFiles[i].__xamxamId = genFileId(); addedFiles.push(newFiles[i]); } } if (addedFiles.length > 0) { queues[queueType] = queues[queueType].concat(addedFiles); renderQueue(queueType); } return errors; } function removeFile(queueType, fileId) { var idx = -1; for (var i = 0; i < queues[queueType].length; i++) { if (queues[queueType][i].__xamxamId === fileId) { idx = i; break; } } if (idx >= 0) { queues[queueType].splice(idx, 1); renderQueue(queueType); } } // ── FormData injection on submit ─────────────────────────────────────── function injectQueuesIntoFormData(form, nativeFormData) { var fd = nativeFormData || new FormData(); QUEUE_TYPES.forEach(function (qt) { var files = queues[qt]; for (var i = 0; i < files.length; i++) { fd.append("queue_file[" + qt + "][]", files[i], files[i].name); } }); // Append queue order hints so the server can validate user ordering QUEUE_TYPES.forEach(function (qt) { var ids = queues[qt].map(function (f) { return f.__xamxamId; }); if (ids.length > 0) { fd.append("queue_order[" + qt + "]", JSON.stringify(ids)); } }); return fd; } // ── Dirty tracking integration ───────────────────────────────────────── function markDirty() { // Set a global flag that beforeunload-guard.js can check window.__xamxamDirty = true; } function markClean() { window.__xamxamDirty = false; } // ── Validate callback for inline validation fragments ────────────────── function showValidationMsg(input, msg) { // Find the .file-validation-msg element scoped to this input's form var form = input.closest(".file-validation-form"); var msgEl = form ? form.querySelector(".file-validation-msg") : null; if (msgEl) { msgEl.innerHTML = '' + esc(msg) + ''; } } function clearValidationMsg(input) { var form = input.closest(".file-validation-form"); var msgEl = form ? form.querySelector(".file-validation-msg") : null; if (msgEl) { msgEl.innerHTML = ""; } } // ── Binding ──────────────────────────────────────────────────────────── function bindQueueInputs() { document.querySelectorAll(".tfe-file-picker").forEach(function (input) { if (input.dataset.xamxamBound) return; input.dataset.xamxamBound = "1"; var queueType = null; // Determine queue type from input attributes if (input.id === "tfe-files-input") queueType = "tfe"; else if (input.id === "video-files-input") queueType = "video"; else if (input.id === "audio-files-input") queueType = "audio"; else if (input.id === "annexe-files-input") queueType = "annexe"; else if (input.getAttribute("hx-vals")) { // Legacy: try to parse from hx-vals try { var vals = JSON.parse(input.getAttribute("hx-vals")); if (vals && vals.queue_type && QUEUE_TYPES.indexOf(vals.queue_type) >= 0) { queueType = vals.queue_type; } } catch (_) { /* ignore */ } } if (!queueType) return; input.addEventListener("change", function () { clearValidationMsg(input); var files = Array.from(input.files || []); var errors = addFiles(queueType, files); if (errors.length > 0) { showValidationMsg(input, errors.join("; ")); } // Reset file input so the same file can be re-selected input.value = ""; }); }); } /** * Delegate click events for remove buttons (these are regenerated on * every renderQueue call, so live delegation is needed). */ function bindRemoveButtons() { document.addEventListener("click", function (e) { var btn = e.target.closest("button[data-action='xamxam-remove']"); if (!btn) return; e.preventDefault(); var qt = btn.getAttribute("data-queue"); var fid = btn.getAttribute("data-file-id"); if (qt && fid) removeFile(qt, fid); }); } // ── Form submit interception ─────────────────────────────────────────── function bindFormSubmit() { document.querySelectorAll("form[data-beforeunload-guard]").forEach(function (form) { if (form.dataset.xamxamFormBound) return; form.dataset.xamxamFormBound = "1"; form.addEventListener("submit", function (e) { var hasFiles = QUEUE_TYPES.some(function (qt) { return queues[qt].length > 0; }); if (!hasFiles) return; // Normal submit // Check if the form can accept multipart if (form.enctype !== "multipart/form-data") return; e.preventDefault(); // Build FormData from form fields + inject queued files var fd = new FormData(form); // Remove stale file input artifacts (consumed by change handler) QUEUE_TYPES.forEach(function (qt) { fd.delete("tfe"); fd.delete("video"); fd.delete("audio"); fd.delete("annexe"); }); fd = injectQueuesIntoFormData(form, fd); markClean(); // Use fetch so we can inspect the response before navigation. // The server either redirects (302 → we navigate to that URL) // or returns the form page with errors (200 → replace the page). fetch(form.getAttribute("action") || "", { method: "POST", body: fd, redirect: "manual", }).then(function (resp) { if (resp.type === "opaqueredirect" || resp.status === 302 || resp.status === 301) { // Browser redirect — navigate the whole page window.location.href = resp.headers.get("Location") || form.getAttribute("action"); return; } // Success or error — render the HTML response return resp.text(); }).then(function (html) { if (!html) return; document.open(); document.write(html); document.close(); // Re-bind after DOM replacement (for error re-renders) setTimeout(window.XamxamInitFileUploads, 0); }).catch(function () { // Network error — fall back to native submit form.submit(); }); }); }); } // ── Public API (called by HTMX afterSwap scripts) ────────────────────── window.XamxamInitFileUploads = function () { bindQueueInputs(); }; window.XamxamInitQueues = function () { // Re-render all queues from in-memory state (used when format switching // replaces the queues container via HTMX). QUEUE_TYPES.forEach(function (qt) { var container = document.getElementById(getQueueContainerId(qt)); if (container && queues[qt].length > 0) { renderQueue(qt); } }); }; // ── Bootstrap ────────────────────────────────────────────────────────── bindRemoveButtons(); bindFormSubmit(); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", window.XamxamInitFileUploads); } else { window.XamxamInitFileUploads(); } })();