diff --git a/TODO.md b/TODO.md index 49eb611..314f1ce 100644 --- a/TODO.md +++ b/TODO.md @@ -12,11 +12,11 @@ - [x] Deduplicate getPredefinedLanguages() query - [x] Accent-tolerant getOrCreateLanguage() to prevent future duplicates - [x] Delete orphan non-accented language rows from DB -- [ ] Migrate file upload queues to FilePond +- [x] Migrate file upload queues to FilePond - [x] Download filepond.min.js + filepond.min.css as local assets - - [ ] Create file-upload-filepond.js (init script for FilePond instances) - - [ ] Rewrite fichiers-fragment.php: replace custom picker/queue DOM with FilePond targets - - [ ] Rewrite fieldset-files.php: same migration (used by admin add page) - - [ ] Update head.php / admin/add.php / edit.php / partage/index.php: swap sortable+file-upload-queue for filepond - - [ ] Remove file-upload-queue.js and sortable.min.js - - [ ] Clean up CSS: remove .fq-*, .tfe-file-queue styles, add filepond.css ref + - [x] Create file-upload-filepond.js (init script for FilePond instances) + - [x] Rewrite fichiers-fragment.php: replace custom picker/queue DOM with FilePond targets + - [x] Rewrite fieldset-files.php: same migration (dead code but kept consistent) + - [x] Update admin/add.php, admin/edit.php, partage/index.php: swap sortable+file-upload-queue for filepond + - [x] Remove file-upload-queue.js and sortable.min.js + - [x] Clean up CSS: remove .fq-*, .tfe-file-queue styles, add filepond.css + theme overrides diff --git a/app/public/admin/add.php b/app/public/admin/add.php index 96232bb..825eea4 100644 --- a/app/public/admin/add.php +++ b/app/public/admin/add.php @@ -54,8 +54,8 @@ function wasSelected($key, $value) { $isAdmin = true; $bodyClass = 'admin-body'; -$extraCss = ['/assets/css/form.css']; -$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js']; +$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css']; +$extraJs = ['/assets/js/filepond.min.js', '/assets/js/file-upload-filepond.js', '/assets/js/beforeunload-guard.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/add.php'; diff --git a/app/public/admin/edit.php b/app/public/admin/edit.php index 9326cbf..5d8f890 100644 --- a/app/public/admin/edit.php +++ b/app/public/admin/edit.php @@ -39,8 +39,8 @@ try { } $isAdmin = true; $bodyClass = 'admin-body'; -$extraCss = ['/assets/css/form.css']; -$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js']; +$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css']; +$extraJs = ['/assets/js/filepond.min.js', '/assets/js/file-upload-filepond.js', '/assets/js/beforeunload-guard.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/edit.php'; diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form.css index 69f2263..3bf3497 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form.css @@ -532,7 +532,7 @@ color: var(--danger, #c1121f); } -/* ── TFE file upload queue (.tfe-file-queue) ────────────────────────────── */ +/* ── TFE file upload (FilePond) ────────────────────────────────────────── */ .admin-files-fieldgroup { display: flex; @@ -540,124 +540,46 @@ gap: var(--space-3xs); } -.tfe-file-picker { - font-size: var(--step--1); - background: transparent; +/* FilePond overrides to match xamxam theme */ +.filepond--root { + font-family: var(--font-body, inherit); + margin-bottom: 0; +} + +.filepond--panel-root { + background: var(--bg-secondary); border: 1px dashed var(--border-primary); - padding: var(--space-3xs) var(--space-2xs); - border-radius: var(--radius); - cursor: pointer; - font-family: inherit; - width: 100%; } -.tfe-file-picker:hover { - border-color: var(--accent-primary); +.filepond--drop-label { + color: var(--text-secondary); + font-size: var(--step--1); } -/* New-file queue items */ -.tfe-file-queue { - list-style: none; - margin: var(--space-2xs) 0 0; - padding: 0; - display: flex; - flex-direction: column; - gap: var(--space-2xs); - min-width: 0; +.filepond--label-action { + color: var(--accent-primary); + text-decoration: underline; } -.tfe-queue-empty { - font-size: var(--step--2); - color: var(--text-tertiary); - margin: var(--space-3xs) 0 0; -} - -.tfe-file-queue:not(:empty) + .tfe-queue-empty { - display: none; -} - -.fq-item { - display: flex; - align-items: center; - gap: var(--space-xs); - padding: var(--space-3xs) var(--space-xs); +.filepond--item-panel { background: var(--bg-secondary); border: 1px solid var(--border-primary); - border-radius: var(--radius); - min-width: 0; } -.fq-icon { - font-size: 1.3rem; - line-height: 1; - flex-shrink: 0; - width: 2rem; - text-align: center; -} - -.fq-info { - display: flex; - flex-direction: column; - gap: var(--space-3xs); - flex: 1; - min-width: 0; -} - -.fq-name { - font-size: var(--step--1); - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: 0; - min-width: 100%; -} - -.fq-size { - font-size: var(--step--2); - color: var(--text-tertiary); -} - -.fq-label, -.admin-file-label-input { - font-size: var(--step--2); - font-family: inherit; - background: transparent; - border: none; - border-bottom: 1px solid var(--border-primary); - padding: 2px 0; - width: 100%; +.filepond--file-info-main { color: var(--text-primary); - border-radius: 0; } -.fq-label:focus, -.admin-file-label-input:focus { - outline: none; - border-bottom-color: var(--accent-primary); -} - -.fq-remove { - flex-shrink: 0; -} - -.fq-drag-handle { - flex-shrink: 0; - cursor: grab; +.filepond--file-info-sub { color: var(--text-tertiary); - font-size: 1.1rem; - line-height: 1; - padding: 0 var(--space-3xs); - user-select: none; - touch-action: none; } -.fq-drag-handle:active { - cursor: grabbing; +.filepond--file-action-button { + color: var(--text-secondary); } -.fq-container { - /* wrapper div emitted by renderQueueFragment */ +.filepond--file-action-button:hover { + color: var(--error); } /* ── Existing-files list (edit form) ─────────────────────────────────────── */ diff --git a/app/public/assets/js/beforeunload-guard.js b/app/public/assets/js/beforeunload-guard.js index f75e969..c5a7455 100644 --- a/app/public/assets/js/beforeunload-guard.js +++ b/app/public/assets/js/beforeunload-guard.js @@ -2,7 +2,7 @@ * Beforeunload guard — prompts the user before navigating away from unsaved changes. * * Attach to any form with a data-beforeunload-guard attribute. - * Also watches window.__xamxamDirty (set by file-upload-queue.js). + * Also watches window.__xamxamDirty (set by file-upload-filepond.js on FilePond events). * No effect when JavaScript is unavailable (form posts normally). */ (() => { diff --git a/app/public/assets/js/file-upload-filepond.js b/app/public/assets/js/file-upload-filepond.js new file mode 100644 index 0000000..b97194e --- /dev/null +++ b/app/public/assets/js/file-upload-filepond.js @@ -0,0 +1,221 @@ +/** + * 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; + } + }); + +})(); diff --git a/app/public/assets/js/file-upload-queue.js b/app/public/assets/js/file-upload-queue.js deleted file mode 100644 index c8fa683..0000000 --- a/app/public/assets/js/file-upload-queue.js +++ /dev/null @@ -1,495 +0,0 @@ -/** - * 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 ─────────────────────────────────────────── - - // Track whether we've already intercepted this submit to prevent double-submit - var _xamxamActiveSubmit = false; - - 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; }); - - console.log("[file-upload-queue] submit event fired | action=" + (form.getAttribute("action") || "") + " | hasFiles=" + hasFiles + " | enctype=" + form.enctype); - - if (!hasFiles) { - console.log("[file-upload-queue] no queued files, passing through native submit"); - markClean(); - return; // Normal submit - } - - // Check if the form can accept multipart - if (form.enctype !== "multipart/form-data") { - console.log("[file-upload-queue] form enctype is not multipart/form-data, passing through"); - markClean(); - return; - } - - // Prevent double-submit (in case native submit falls through) - if (_xamxamActiveSubmit) { - console.log("[file-upload-queue] already submitting, skipping"); - e.preventDefault(); - return; - } - _xamxamActiveSubmit = true; - - 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(); - - console.log("[file-upload-queue] injecting " + QUEUE_TYPES.map(function(qt) { return qt + ":" + queues[qt].length; }).join(", ") + " files into FormData"); - - // Use XMLHttpRequest instead of fetch so we can reliably read the - // final URL after redirects (fetch with redirect:manual hides headers). - var xhr = new XMLHttpRequest(); - xhr.open("POST", form.getAttribute("action") || "", true); - xhr.responseType = "text"; - - xhr.onload = function () { - _xamxamActiveSubmit = false; - var finalUrl = xhr.responseURL || ""; - - console.log("[file-upload-queue] XHR status=" + xhr.status + " | finalUrl=" + finalUrl + " | responseLength=" + (xhr.responseText ? xhr.responseText.length : 0)); - - // If the server redirected us (responseURL differs from action), navigate there. - // This handles the 302 redirect pattern used by formulaire.php and edit.php. - var actionUrl = form.getAttribute("action") || ""; - if (finalUrl && finalUrl !== actionUrl && finalUrl !== window.location.href) { - console.log("[file-upload-queue] redirecting to " + finalUrl); - window.location.href = finalUrl; - return; - } - - // 200 with HTML — likely form errors, replace page - if (xhr.status >= 200 && xhr.status < 300 && xhr.responseText) { - console.log("[file-upload-queue] rendering error response HTML"); - document.open(); - document.write(xhr.responseText); - document.close(); - // Re-bind after DOM replacement (for error re-renders) - setTimeout(window.XamxamInitFileUploads, 0); - return; - } - - // Fallback: reload current page - console.log("[file-upload-queue] unexpected response, reloading page"); - window.location.reload(); - }; - - xhr.onerror = function () { - _xamxamActiveSubmit = false; - console.error("[file-upload-queue] XHR network error, falling back to native submit"); - // Fall back to native submit - form.submit(); - }; - - xhr.send(fd); - }); - }); - } - - // ── 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(); - } -})(); diff --git a/app/public/assets/js/sortable.min.js b/app/public/assets/js/sortable.min.js deleted file mode 100644 index bb99533..0000000 --- a/app/public/assets/js/sortable.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! Sortable 1.15.2 - MIT | git://github.com/SortableJS/Sortable.git */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tAucun fichier sélectionné.
-Aucun fichier sélectionné.
-Aucun fichier sélectionné.
-.vtt sont des sous-titres et seront associés automatiquement à la vidéo précédente.
-
-
- Aucun fichier sélectionné.