/** * file-upload-filepond.js * * Thin FilePond wrapper — replaces the old custom file-upload-queue.js. * * Architecture: * 1. Each is upgraded to a FilePond instance. * 2. FilePond handles drag-to-reorder, thumbnails, remove, validation — zero custom DOM. * 3. storeAsFile: true preserves native multipart form submission. * Server receives files via $_FILES indexed by each input's name attribute * (e.g. queue_file[tfe][], queue_file[video][], etc.). * 4. Validation rules are derived from ALLOWED_BY_TYPE (same as before). */ (function () { "use strict"; // ── Constants (mirrors file-upload-queue.js ALLOWED_BY_TYPE) ────────── var ALLOWED_BY_TYPE = { tfe: { exts: ["jpg","jpeg","png","gif","webp","pdf","mp4","webm","ogv","mov","mp3","ogg","oga","wav","flac","aac","m4a","vtt","zip","tar","gz","tgz"], maxSize: function (f) { return (/\.pdf$/i.test(f.name) ? 100 : 500) * 1024 * 1024; }, }, video: { exts: ["mp4","webm","ogv","mov"], maxSize: function () { return 500 * 1024 * 1024; }, }, audio: { exts: ["mp3","ogg","oga","wav","flac","aac","m4a"], maxSize: function () { return 500 * 1024 * 1024; }, }, annexe: { exts: ["pdf","zip","tar","gz","tgz"], maxSize: function () { return 500 * 1024 * 1024; }, }, }; // Map input id → queue type var INPUT_ID_TO_TYPE = { "tfe-files-input": "tfe", "tfe-files-input-2": "tfe", "video-files-input": "video", "audio-files-input": "audio", "annexe-files-input": "annexe", }; function ext(fn) { var m = fn.match(/\.([^./]+)$/); return m ? m[1].toLowerCase() : ""; } // ── FilePond configuration per queue type ───────────────────────────── function buildFilePondOptions(queueType, input) { var rules = ALLOWED_BY_TYPE[queueType]; if (!rules) return null; // Build acceptedFileTypes from extensions var mimeMap = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", pdf: "application/pdf", mp4: "video/mp4", webm: "video/webm", ogv: "video/ogg", mov: "video/quicktime", mp3: "audio/mpeg", ogg: "audio/ogg", oga: "audio/ogg", wav: "audio/wav", flac: "audio/flac", aac: "audio/aac", m4a: "audio/mp4", vtt: "text/vtt", zip: "application/zip", tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip", }; var accepted = rules.exts.map(function(e) { return mimeMap[e] || ("." + e); }); return { allowMultiple: (queueType !== "video" && queueType !== "audio"), allowReorder: true, storeAsFile: true, labelIdle: "Glissez-déposez vos fichiers ou Parcourir", acceptedFileTypes: accepted, labelFileTypeNotAllowed: "Type de fichier non accepté", fileValidateTypeLabelExpectedTypes: "Types acceptés : " + rules.exts.map(function(e) { return "." + e; }).join(", "), fileValidateSizeLabelMaxFileSize: function (fileSize) { var max = rules.maxSize({name: "", size: 0}); return "Taille maximale : " + Math.round(max / 1024 / 1024) + " MB"; }, maxFileSize: function () { // We can't do per-file max based on extension easily with FilePond. // Use the larger limit and validate PDFs as a special case in the // beforeAddFile callback. return "500MB"; }, beforeAddFile: function (item) { var f = item.file; var max = rules.maxSize(f); if (f.size > max) { var maxMb = Math.round(max / 1024 / 1024); return { status: "error", main: "Fichier trop volumineux (" + (f.size / 1024 / 1024).toFixed(1) + " MB)", sub: "Maximum : " + maxMb + " MB." }; } return true; }, }; } // ── Instance tracking ──────────────────────────────────────────────── var _ponds = {}; // ── Public API ──────────────────────────────────────────────────────── /** * Upgrade .tfe-file-picker inputs to FilePond instances. * Called on page load and after HTMX swaps. */ function initFilePonds() { document.querySelectorAll(".tfe-file-picker").forEach(function (input) { // Skip already upgraded inputs if (input.dataset.filepondUpgraded) return; // Skip if input is inside an existing FilePond root if (input.closest(".filepond--root")) return; var id = input.id; var queueType = INPUT_ID_TO_TYPE[id]; if (!queueType) { // Try to infer from data attr on the container var container = input.closest("[data-queue-type]"); if (container) queueType = container.dataset.queueType; } if (!queueType) return; var options = buildFilePondOptions(queueType, input); if (!options) return; // Preserve the input's original name for form submission options.name = input.getAttribute("name") || input.name || ""; var pond = FilePond.create(input, options); input.dataset.filepondUpgraded = "1"; // Track by id for cleanup var key = id || queueType; _ponds[key] = pond; }); } /** * Destroy all FilePond instances and restore original inputs. * Called before HTMX swaps that replace the file block. */ function destroyFilePonds() { Object.keys(_ponds).forEach(function (key) { try { _ponds[key].destroy(); } catch (_) { /* ignore */ } delete _ponds[key]; }); // Also catch any stray instances (HTMX may have replaced DOM) document.querySelectorAll(".tfe-file-picker[data-filepond-upgraded]").forEach(function (input) { delete input.dataset.filepondUpgraded; }); } // ── HTMX integration ───────────────────────────────────────────────── /** * Called before HTMX replaces the #format-fichiers-block. * We must destroy FilePond instances on the soon-to-be-removed DOM nodes * to avoid leaks and file-state conflicts. */ function onHtmxBeforeSwap(evt) { // Only care about format-fichiers-block swaps if (evt.detail.target && ( evt.detail.target.id === "format-fichiers-block" || evt.detail.target.closest && evt.detail.target.closest("#format-fichiers-block") )) { destroyFilePonds(); } } // ── Bootstrap ───────────────────────────────────────────────────────── // Hook into HTMX events if htmx is loaded if (window.htmx) { window.htmx.on("htmx:beforeSwap", onHtmxBeforeSwap); } // Initialise on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { initFilePonds(); // Re-init handles HTMX after-swap if (window.htmx) { window.htmx.on("htmx:afterSwap", function () { initFilePonds(); }); } }); } else { initFilePonds(); if (window.htmx) { window.htmx.on("htmx:afterSwap", function () { initFilePonds(); }); } } // ── Mark form dirty on FilePond changes (beforeunload guard) ───────── document.addEventListener("FilePond:addfile", function () { window.__xamxamDirty = true; }); // Clean dirty flag on form submit (matches beforeunload-guard.js) document.addEventListener("submit", function (e) { var form = e.target; if (form && form.hasAttribute && form.hasAttribute("data-beforeunload-guard")) { window.__xamxamDirty = false; } }); })();