mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
703 lines
23 KiB
JavaScript
703 lines
23 KiB
JavaScript
/**
|
|
* file-upload-filepond.js
|
|
*
|
|
* FilePond wrapper with async server round-trip architecture.
|
|
*
|
|
* Architecture:
|
|
* 1. Each <input type="file" class="tfe-file-picker"> is upgraded to a FilePond instance.
|
|
* 2. FilePond handles drag-to-reorder, thumbnails, remove, validation — zero custom DOM.
|
|
* 3. Async upload: files are POSTed to /admin/actions/filepond/process.php immediately.
|
|
* The server returns a file_id stored as item.serverId.
|
|
* 4. Form submit sends only file_ids (tiny payload), not the files themselves.
|
|
* 5. Type + size validation: via native FilePond options + FileValidateType/Size plugins
|
|
* plus fileValidateSizeFilterItem for per-extension size caps.
|
|
* 6. Order serialization: hidden inputs track file order using serverId (not filename).
|
|
* 7. HTMX cleanup: generic destroyFilePondsIn(target) for all swaps, not just known IDs.
|
|
* 8. Edit mode: loads existing files via data-existing-files JSON + server.load.
|
|
*/
|
|
|
|
(() => {
|
|
// ── Per-queue-type configuration ────────────────────────────────────
|
|
// Single source of truth for validation. These specificatons are also
|
|
// reflected in the PHP-synthesised accept attributes on inputs.
|
|
|
|
var QUEUE_CONFIG = {
|
|
tfe: {
|
|
acceptedFileTypes: [
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/gif",
|
|
"image/webp",
|
|
"application/pdf",
|
|
"video/mp4",
|
|
"video/webm",
|
|
"video/ogg",
|
|
"video/quicktime",
|
|
"audio/mpeg",
|
|
"audio/ogg",
|
|
"audio/flac",
|
|
"audio/x-wav",
|
|
"audio/aac",
|
|
"audio/mp4",
|
|
"text/vtt",
|
|
"application/zip",
|
|
"application/x-tar",
|
|
"application/gzip",
|
|
],
|
|
labelFileTypeNotAllowed: "Format non accepté",
|
|
fileValidateTypeLabelExpectedTypes:
|
|
"PDF, Images, Vidéos, Audio, VTT, Archives",
|
|
maxFileSize: "500MB",
|
|
labelMaxFileSizeExceeded: "Fichier trop volumineux",
|
|
labelMaxFileSize: "Taille max: {filesize}",
|
|
allowMultiple: true,
|
|
// Per-extension size limits: certain types get higher caps.
|
|
perExtensionMaxSize: {
|
|
pdf: "100MB",
|
|
mp4: "2GB",
|
|
webm: "2GB",
|
|
ogv: "2GB",
|
|
mov: "2GB",
|
|
mp3: "2GB",
|
|
ogg: "2GB",
|
|
oga: "2GB",
|
|
wav: "2GB",
|
|
flac: "2GB",
|
|
aac: "2GB",
|
|
m4a: "2GB",
|
|
},
|
|
},
|
|
annexe: {
|
|
acceptedFileTypes: [
|
|
"application/pdf",
|
|
"application/zip",
|
|
"application/x-tar",
|
|
"application/gzip",
|
|
],
|
|
labelFileTypeNotAllowed: "Format non accepté",
|
|
fileValidateTypeLabelExpectedTypes: "PDF, ZIP, TAR, GZ",
|
|
maxFileSize: "500MB",
|
|
labelMaxFileSizeExceeded: "Fichier trop volumineux",
|
|
labelMaxFileSize: "Taille max: {filesize}",
|
|
allowMultiple: true,
|
|
},
|
|
cover: {
|
|
acceptedFileTypes: ["image/jpeg", "image/png", "image/webp"],
|
|
labelFileTypeNotAllowed: "Seulement JPG, PNG ou WEBP",
|
|
fileValidateTypeLabelExpectedTypes: "JPG, PNG, WEBP",
|
|
maxFileSize: "20MB",
|
|
labelMaxFileSizeExceeded: "Fichier trop volumineux",
|
|
labelMaxFileSize: "Taille max: {filesize}",
|
|
allowMultiple: false,
|
|
},
|
|
note_intention: {
|
|
acceptedFileTypes: ["application/pdf"],
|
|
labelFileTypeNotAllowed: "Seulement PDF",
|
|
fileValidateTypeLabelExpectedTypes: "PDF",
|
|
maxFileSize: "100MB",
|
|
labelMaxFileSizeExceeded: "Fichier trop volumineux",
|
|
labelMaxFileSize: "Taille max: {filesize}",
|
|
allowMultiple: false,
|
|
},
|
|
};
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Parse a size string like "500MB" or "2GB" to bytes.
|
|
*/
|
|
function parseSize(str) {
|
|
var m = str.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$/i);
|
|
if (!m) return 0;
|
|
var val = parseFloat(m[1]);
|
|
var unit = m[2].toUpperCase();
|
|
var mult = {
|
|
B: 1,
|
|
KB: 1024,
|
|
MB: 1024 * 1024,
|
|
GB: 1024 * 1024 * 1024,
|
|
TB: 1024 * 1024 * 1024 * 1024,
|
|
};
|
|
return Math.round(val * (mult[unit] || 1));
|
|
}
|
|
|
|
/**
|
|
* Get extension from filename (lowercase).
|
|
*/
|
|
function getExt(name) {
|
|
var m = name.match(/\.([^./]+)$/);
|
|
return m ? m[1].toLowerCase() : "";
|
|
}
|
|
|
|
/**
|
|
* Get the CSRF token from the meta tag.
|
|
*/
|
|
function getCsrfToken() {
|
|
var meta = document.querySelector('meta[name="csrf-token"]');
|
|
return meta ? meta.getAttribute("content") : "";
|
|
}
|
|
|
|
/**
|
|
* Get the FilePond endpoint base URL from meta tag.
|
|
* Defaults to /admin/actions/filepond/ for backward compat.
|
|
*/
|
|
function getFilepondBase() {
|
|
var meta = document.querySelector('meta[name="filepond-base"]');
|
|
return meta ? meta.getAttribute("content") : "/admin/actions/filepond";
|
|
}
|
|
|
|
// ── Order serialization ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Create/update a hidden input that serializes the file order for a queue.
|
|
* Name: queue_file[<queueType>][] for each file_id.
|
|
* Name: queue_order[<queueType>] for the pipe-separated order.
|
|
*/
|
|
function syncOrderInput(queueType, pond) {
|
|
if (!pond || !pond.element) return;
|
|
var form = pond.element.closest("form");
|
|
if (!form) return;
|
|
|
|
var files = pond.getFiles();
|
|
|
|
// Remove old order input and all queue_file hidden inputs for this queueType
|
|
var oldOrder = form.querySelector(
|
|
`input[name='queue_order[${queueType}]']`,
|
|
);
|
|
if (oldOrder) oldOrder.remove();
|
|
|
|
var oldHidden = form.querySelectorAll(
|
|
`input[name='queue_file[${queueType}][]'][data-filepond-id]`,
|
|
);
|
|
for (var h = 0; h < oldHidden.length; h++) {
|
|
oldHidden[h].remove();
|
|
}
|
|
|
|
if (files.length === 0) return;
|
|
|
|
// Create hidden inputs per file: queue_file[<queueType>][] = serverId
|
|
var ids = [];
|
|
for (var i = 0; i < files.length; i++) {
|
|
var f = files[i];
|
|
// Only include files that have been uploaded and have a serverId
|
|
var id = f.serverId || null;
|
|
if (id) {
|
|
ids.push(id);
|
|
var hidden = document.createElement("input");
|
|
hidden.type = "hidden";
|
|
hidden.name = `queue_file[${queueType}][]`;
|
|
hidden.value = id;
|
|
hidden.setAttribute("data-filepond-id", "1");
|
|
form.appendChild(hidden);
|
|
}
|
|
}
|
|
|
|
// Create order input
|
|
if (ids.length > 0) {
|
|
var orderInput = document.createElement("input");
|
|
orderInput.type = "hidden";
|
|
orderInput.name = `queue_order[${queueType}]`;
|
|
orderInput.value = ids.join("|");
|
|
form.appendChild(orderInput);
|
|
}
|
|
}
|
|
|
|
// ── Server config builder ─────────────────────────────────────────────
|
|
|
|
function buildServerConfig(queueType) {
|
|
var csrfToken = getCsrfToken();
|
|
console.log(
|
|
"[filepond] buildServerConfig | queueType=" +
|
|
queueType +
|
|
" | csrfToken=" +
|
|
(csrfToken ? `${csrfToken.substring(0, 8)}...` : "MISSING"),
|
|
);
|
|
|
|
var base = getFilepondBase();
|
|
console.log(
|
|
"[filepond] buildServerConfig | queueType=" +
|
|
queueType +
|
|
" | csrfToken=" +
|
|
(csrfToken ? `${csrfToken.substring(0, 8)}...` : "MISSING") +
|
|
" | base=" +
|
|
base,
|
|
);
|
|
|
|
return {
|
|
process: {
|
|
url: `${base}/process.php`,
|
|
method: "POST",
|
|
headers: { "X-CSRF-Token": csrfToken },
|
|
ondata: (formData) => {
|
|
formData.append("queue_type", queueType);
|
|
console.log(`[filepond] process ondata | queueType=${queueType}`);
|
|
return formData;
|
|
},
|
|
onload: (response) => {
|
|
var id = response.trim();
|
|
console.log(`[filepond] process onload | serverId=${id}`);
|
|
return id; // file_id stored as serverId
|
|
},
|
|
onerror: (response) => {
|
|
console.error(
|
|
"[filepond] process onerror | status=" +
|
|
response.status +
|
|
" | body=" +
|
|
response,
|
|
);
|
|
return response;
|
|
},
|
|
},
|
|
|
|
revert: {
|
|
url: `${base}/revert.php`,
|
|
method: "DELETE",
|
|
headers: { "X-CSRF-Token": csrfToken },
|
|
onload: () => {
|
|
console.log("[filepond] revert OK");
|
|
},
|
|
onerror: (r) => {
|
|
console.error(`[filepond] revert ERROR | body=${r}`);
|
|
},
|
|
},
|
|
|
|
load: `${base}/load.php?id=`,
|
|
// FilePond appends the source value (db_id) automatically
|
|
|
|
remove: (source, load, error) => {
|
|
console.log(`[filepond] remove called | db_id=${source}`);
|
|
fetch(`${base}/remove.php`, {
|
|
method: "DELETE",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRF-Token": csrfToken,
|
|
},
|
|
body: JSON.stringify({ db_id: source }),
|
|
})
|
|
.then((r) => {
|
|
console.log(
|
|
"[filepond] remove response | ok=" +
|
|
r.ok +
|
|
" | status=" +
|
|
r.status,
|
|
);
|
|
r.ok ? load() : error("Erreur suppression");
|
|
})
|
|
.catch((e) => {
|
|
console.error("[filepond] remove fetch error", e);
|
|
error("Erreur réseau");
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// ── FilePond configuration per queue type ─────────────────────────────
|
|
|
|
function buildFilePondOptions(queueType, _input) {
|
|
var cfg = QUEUE_CONFIG[queueType];
|
|
if (!cfg) return null;
|
|
|
|
// Per-type max size overrides (for TFE: PDF=100MB, video/audio=2GB)
|
|
var perExtMax = cfg.perExtensionMaxSize || {};
|
|
|
|
return {
|
|
allowMultiple: cfg.allowMultiple,
|
|
allowReorder: true,
|
|
|
|
// ── Async server model (replaces storeAsFile + allowProcess: false) ──
|
|
server: buildServerConfig(queueType),
|
|
|
|
// ── Native FilePond validation ──
|
|
acceptedFileTypes: cfg.acceptedFileTypes,
|
|
labelFileTypeNotAllowed: cfg.labelFileTypeNotAllowed,
|
|
fileValidateTypeLabelExpectedTypes:
|
|
cfg.fileValidateTypeLabelExpectedTypes,
|
|
maxFileSize: cfg.maxFileSize,
|
|
labelMaxFileSizeExceeded: cfg.labelMaxFileSizeExceeded,
|
|
labelMaxFileSize: cfg.labelMaxFileSize,
|
|
|
|
// ── French labels ──
|
|
labelIdle:
|
|
"Glissez-déposez vos fichiers ou <span class='filepond--label-action'>Parcourir</span>",
|
|
labelFileProcessing: "Chargement en cours",
|
|
labelFileProcessingComplete: "Chargement terminé",
|
|
labelFileProcessingAborted: "Chargement annulé",
|
|
labelFileProcessingError: "Erreur lors du chargement",
|
|
labelTapToCancel: "Appuyez pour annuler",
|
|
labelTapToRetry: "Appuyez pour réessayer",
|
|
labelTapToUndo: "Appuyez pour annuler",
|
|
labelButtonRemoveItem: "Supprimer",
|
|
labelButtonAbortItemLoad: "Annuler",
|
|
labelButtonRetryItemLoad: "Réessayer",
|
|
labelButtonProcessItem: "Charger",
|
|
|
|
// ── Per-extension size validation ──────────────────────────────
|
|
// Uses fileValidateSizeFilterItem if the FileValidateSize plugin supports it.
|
|
// Falls back to beforeAddFile for silent rejection (the plugin shows the error).
|
|
fileValidateSizeFilterItem: (item) => {
|
|
var ext = getExt(item.filename);
|
|
if (ext && perExtMax[ext]) {
|
|
return parseSize(perExtMax[ext]); // per-extension cap for this item
|
|
}
|
|
return parseSize(cfg.maxFileSize); // queue default
|
|
},
|
|
|
|
// Fallback: if fileValidateSizeFilterItem is not available,
|
|
// beforeAddFile enforces per-extension limits (silent rejection).
|
|
beforeAddFile: (item) => {
|
|
// This check is redundant if fileValidateSizeFilterItem works,
|
|
// but serves as a fallback.
|
|
if (typeof item.file === "undefined") return true;
|
|
var f = item.file;
|
|
var ext = getExt(f.name);
|
|
if (ext && perExtMax[ext]) {
|
|
var limit = parseSize(perExtMax[ext]);
|
|
if (limit > 0 && f.size > limit) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
// ── Order serialization on add/remove/reorder ──
|
|
onaddfile: function () {
|
|
syncOrderInput(queueType, this);
|
|
},
|
|
onremovefile: function () {
|
|
syncOrderInput(queueType, this);
|
|
},
|
|
onreorderfiles: function () {
|
|
syncOrderInput(queueType, this);
|
|
},
|
|
onupdatefiles: function () {
|
|
syncOrderInput(queueType, this);
|
|
},
|
|
|
|
// Re-sync after async upload completes (serverId is now set)
|
|
onprocessfile: function (error, _item) {
|
|
if (!error) syncOrderInput(queueType, this);
|
|
},
|
|
};
|
|
}
|
|
|
|
// ── Public API ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Upgrade .tfe-file-picker inputs to FilePond instances.
|
|
* Called on page load and after HTMX swaps.
|
|
*/
|
|
window.XamxamInitFilePonds = () => {
|
|
document.querySelectorAll(".tfe-file-picker").forEach((input) => {
|
|
// Canonical duplicate check: FilePond.find() is the authoritative source
|
|
if (FilePond.find(input)) return;
|
|
|
|
// Queue type: always from data-queue-type attribute
|
|
var queueType = input.dataset.queueType || null;
|
|
if (!queueType) return;
|
|
|
|
var options = buildFilePondOptions(queueType, input);
|
|
if (!options) return;
|
|
|
|
var pond = FilePond.create(input, options);
|
|
console.log(
|
|
"[filepond] Created instance | queueType=" +
|
|
queueType +
|
|
" | inputId=" +
|
|
(input.id || "none") +
|
|
" | inputName=" +
|
|
(input.getAttribute("name") || input.name || "?"),
|
|
);
|
|
|
|
// Initial order serialization
|
|
syncOrderInput(queueType, pond);
|
|
|
|
// ── Edit mode: load existing files ──
|
|
var existingFiles = [];
|
|
try {
|
|
existingFiles = JSON.parse(input.dataset.existingFiles || "[]");
|
|
} catch (_) {}
|
|
|
|
if (existingFiles.length) {
|
|
pond.addFiles(
|
|
existingFiles.map((f) => ({ source: f.source, options: f.options })),
|
|
);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Destroy FilePond instances inside a given container element.
|
|
* Generic: handles ANY HTMX swap target, not just known IDs.
|
|
*/
|
|
function destroyFilePondsIn(el) {
|
|
if (!el) return;
|
|
el.querySelectorAll(".tfe-file-picker").forEach((input) => {
|
|
var pond = FilePond.find(input);
|
|
if (pond) {
|
|
try {
|
|
// Remove order/hidden inputs before destroying
|
|
var form = input.closest("form");
|
|
if (form) {
|
|
var queueType = input.dataset.queueType || null;
|
|
if (queueType) {
|
|
var orderInput = form.querySelector(
|
|
`input[name='queue_order[${queueType}]']`,
|
|
);
|
|
if (orderInput) orderInput.remove();
|
|
var hiddenInputs = form.querySelectorAll(
|
|
"input[name='queue_file[" +
|
|
queueType +
|
|
"][]'][data-filepond-id]",
|
|
);
|
|
for (var h = 0; h < hiddenInputs.length; h++) {
|
|
hiddenInputs[h].remove();
|
|
}
|
|
}
|
|
}
|
|
pond.destroy();
|
|
} catch (_) {}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── HTMX integration ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Generic beforeSwap handler: destroy FilePonds in ANY swapped target.
|
|
* This prevents detached FilePond instances from leaking listeners.
|
|
*/
|
|
function onHtmxBeforeSwap(evt) {
|
|
var target = evt.detail.target;
|
|
if (target) {
|
|
destroyFilePondsIn(target);
|
|
}
|
|
}
|
|
|
|
// ── Bootstrap ─────────────────────────────────────────────────────────
|
|
|
|
// Global FilePond event listeners for debugging
|
|
document.addEventListener("FilePond:processfile", (e) => {
|
|
console.log(
|
|
"[filepond:event] processfile | id=" +
|
|
(e.detail.file ? e.detail.file.serverId : "") +
|
|
" | error=" +
|
|
(e.detail.error || "none"),
|
|
);
|
|
});
|
|
document.addEventListener("FilePond:processfilestart", (e) => {
|
|
console.log(
|
|
"[filepond:event] processfilestart | filename=" +
|
|
(e.detail.file ? e.detail.file.filename : "?"),
|
|
);
|
|
});
|
|
document.addEventListener("FilePond:processfileprogress", (e) => {
|
|
var pct = e.detail.progress;
|
|
if (pct && (pct === 0 || pct === 1 || Math.floor(pct * 100) % 25 === 0)) {
|
|
console.log(
|
|
"[filepond:event] processfileprogress | pct=" +
|
|
Math.floor(pct * 100) +
|
|
"%",
|
|
);
|
|
}
|
|
});
|
|
document.addEventListener("FilePond:processfileabort", (_e) => {
|
|
console.log("[filepond:event] processfileabort");
|
|
});
|
|
document.addEventListener("FilePond:processfilerevert", (_e) => {
|
|
console.log("[filepond:event] processfilerevert");
|
|
});
|
|
document.addEventListener("FilePond:error", (e) => {
|
|
console.error("[filepond:event] error", e.detail);
|
|
});
|
|
|
|
// Register FilePond plugins (idempotent)
|
|
if (typeof FilePondPluginFileValidateType !== "undefined") {
|
|
FilePond.registerPlugin(FilePondPluginFileValidateType);
|
|
}
|
|
if (typeof FilePondPluginFileValidateSize !== "undefined") {
|
|
FilePond.registerPlugin(FilePondPluginFileValidateSize);
|
|
}
|
|
if (typeof FilePondPluginImagePreview !== "undefined") {
|
|
FilePond.registerPlugin(FilePondPluginImagePreview);
|
|
}
|
|
if (typeof FilePondPluginImageExifOrientation !== "undefined") {
|
|
FilePond.registerPlugin(FilePondPluginImageExifOrientation);
|
|
}
|
|
|
|
// ── HTMX integration (register later once htmx is loaded) ───────────
|
|
// Note: htmx.min.js loads at the end of <body> (admin/footer.php),
|
|
// after this script. Use DOM polling or a listener to wire up.
|
|
function tryRegisterHtmx() {
|
|
if (!window.htmx) {
|
|
setTimeout(tryRegisterHtmx, 50);
|
|
return;
|
|
}
|
|
console.log('[filepond] htmx detected, registering swap listeners');
|
|
window.htmx.on("htmx:beforeSwap", onHtmxBeforeSwap);
|
|
window.htmx.on("htmx:afterSwap", () => {
|
|
_xamxamFilepondReady = false;
|
|
window.XamxamInitFilePonds();
|
|
setTimeout(() => { _xamxamFilepondReady = true; }, 0);
|
|
});
|
|
}
|
|
// Flag set after FilePond instances are fully initialised.
|
|
// Before this flag is set, FilePond:addfile events are from initial load
|
|
// (e.g. existing files loaded in edit mode) and should not mark the form dirty.
|
|
let _xamxamFilepondReady = false;
|
|
|
|
tryRegisterHtmx();
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
window.XamxamInitFilePonds();
|
|
_xamxamFilepondReady = true;
|
|
});
|
|
} else {
|
|
window.XamxamInitFilePonds();
|
|
_xamxamFilepondReady = true;
|
|
}
|
|
|
|
// ── Mark form dirty on FilePond changes (beforeunload guard) ─────────
|
|
document.addEventListener("FilePond:addfile", () => {
|
|
if (_xamxamFilepondReady) window.__xamxamDirty = true;
|
|
});
|
|
document.addEventListener("FilePond:removefile", () => {
|
|
if (_xamxamFilepondReady) window.__xamxamDirty = true;
|
|
});
|
|
|
|
document.addEventListener("submit", (e) => {
|
|
var form = e.target;
|
|
if (form?.hasAttribute?.("data-beforeunload-guard")) {
|
|
window.__xamxamDirty = false;
|
|
}
|
|
});
|
|
|
|
// ── Relink file browser ──────────────────────────────────────────
|
|
|
|
/**
|
|
* Relink a selected file to the thesis.
|
|
* Called from the onclick handler on file-browser entries.
|
|
* The file browser is loaded inside #relink-modal-body via HTMX.
|
|
*/
|
|
window.XamxamRelinkFile = (el) => {
|
|
var li = el.closest('.file-browser-entry');
|
|
console.log('[relink] XamxamRelinkFile called | el=', el, '| li=', li);
|
|
if (!li) return;
|
|
|
|
var ctx = window.__xamxamRelinkCtx || {};
|
|
var thesisId = ctx.thesisId;
|
|
var queueType = ctx.queueType;
|
|
|
|
var filePath = li.dataset.filePath;
|
|
var fileName = li.dataset.fileName;
|
|
var fileSize = parseInt(li.dataset.fileSize, 10) || 0;
|
|
var ext = li.dataset.fileExt || '';
|
|
|
|
console.log('[relink] data | thesisId=' + thesisId + ' | queueType=' + queueType + ' | filePath=' + filePath + ' | fileName=' + fileName + ' | ext=' + ext);
|
|
|
|
if (!filePath || !thesisId || !queueType) {
|
|
console.error('[relink] missing data', { filePath, thesisId, queueType });
|
|
return;
|
|
}
|
|
|
|
// Determine MIME from extension
|
|
var mimeMap = {
|
|
pdf: 'application/pdf',
|
|
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif',
|
|
mp4: 'video/mp4', webm: 'video/webm', ogv: 'video/ogg', mov: 'video/quicktime',
|
|
mp3: 'audio/mpeg', ogg: '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',
|
|
};
|
|
var mimeType = mimeMap[ext] || 'application/octet-stream';
|
|
|
|
var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
|
console.log('[relink] csrfToken=' + (csrfToken ? csrfToken.substring(0, 8) + '...' : 'MISSING'));
|
|
|
|
var bodyEl = document.getElementById('relink-modal-body');
|
|
if (bodyEl) bodyEl.innerHTML = '<p class="file-browser-loading">Reliage en cours…</p>';
|
|
|
|
fetch('/admin/actions/filepond/relink.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken,
|
|
},
|
|
body: JSON.stringify({
|
|
thesis_id: parseInt(thesisId, 10),
|
|
file_path: filePath,
|
|
file_name: fileName,
|
|
file_size: fileSize,
|
|
mime_type: mimeType,
|
|
queue_type: queueType,
|
|
}),
|
|
})
|
|
.then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data })))
|
|
.then(({ ok, status, data }) => {
|
|
if (!ok || (data && data.ok === false)) {
|
|
var msg = (data && data.error) ? data.error : (typeof data === 'string' ? data : 'Erreur ' + status);
|
|
if (bodyEl) bodyEl.innerHTML = `<p class="file-browser-error">Erreur : ${msg}</p>`;
|
|
return;
|
|
}
|
|
console.log('[relink] success | new_id=' + data.id);
|
|
|
|
// Add the new file to the FilePond pool, then close the modal.
|
|
// If the DOM was replaced (e.g. live-reload), refresh the
|
|
// form fragment via HTMX so the server re-renders the pools
|
|
// with the newly-linked file included.
|
|
var input = document.querySelector(`.tfe-file-picker[data-queue-type="${queueType}"]`);
|
|
console.log('[relink] looking for input | selector=' + `.tfe-file-picker[data-queue-type="${queueType}"]` + ' | found=' + !!input);
|
|
var closeAndRefresh = function() {
|
|
var modal = document.getElementById('relink-modal');
|
|
if (modal) modal.close();
|
|
// Re-fetch the fichiers fragment from the server so the
|
|
// newly-linked file appears in the FilePond pools.
|
|
var block = document.getElementById('format-fichiers-block');
|
|
if (block && window.htmx) {
|
|
var url = '/admin/fragments/fichiers.php';
|
|
if (window.__xamxamRelinkCtx && window.__xamxamRelinkCtx.thesisId) {
|
|
url += '?_thesis_id=' + encodeURIComponent(window.__xamxamRelinkCtx.thesisId);
|
|
}
|
|
htmx.ajax('GET', url, {
|
|
target: '#format-fichiers-block',
|
|
swap: 'outerHTML'
|
|
});
|
|
}
|
|
};
|
|
if (input) {
|
|
var pond = FilePond.find(input);
|
|
console.log('[relink] looking for pond | found=' + !!pond);
|
|
if (pond) {
|
|
pond.addFile(String(data.id), {
|
|
type: 'limbo',
|
|
file: {
|
|
name: fileName,
|
|
size: fileSize,
|
|
type: mimeType
|
|
}
|
|
}).then(function() {
|
|
console.log('[relink] addFile resolved | source=' + String(data.id) + ' | queueType=' + queueType);
|
|
closeAndRefresh();
|
|
}).catch(function(err) {
|
|
console.error('[relink] addFile rejected', err);
|
|
closeAndRefresh();
|
|
});
|
|
} else {
|
|
console.error('[relink] FilePond.find returned null for input', input);
|
|
closeAndRefresh();
|
|
}
|
|
} else {
|
|
console.warn('[relink] input not found, page may have reloaded | queueType=' + queueType);
|
|
closeAndRefresh();
|
|
}
|
|
|
|
// Mark form dirty
|
|
window.__xamxamDirty = true;
|
|
})
|
|
.catch(err => {
|
|
console.error('[relink] fetch error', err);
|
|
if (bodyEl) bodyEl.innerHTML = '<p class="file-browser-error">Erreur réseau.</p>';
|
|
});
|
|
};
|
|
})();
|