Lumi/src/web/public/lumi-request.js
2026-07-22 10:20:32 +02:00

113 lines
4.7 KiB
JavaScript

(() => {
class LumiRequestError extends Error {
constructor(message, details = {}) {
super(message || "The request could not be completed.");
this.name = "LumiRequestError";
this.kind = details.kind || "server";
this.status = details.status || 0;
this.fieldErrors = details.fieldErrors || {};
this.data = details.data || {};
}
}
const enhancedHeaders = {
Accept: "application/json, text/html;q=0.8",
"X-Lumi-Enhanced": "1",
"X-Requested-With": "XMLHttpRequest"
};
function normalize(payload, response) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
return {
ok: response.ok,
message: response.ok ? "Saved." : "The request could not be completed.",
fieldErrors: {},
data: {},
patch: {},
redirect: null,
reloadRequired: false
};
}
const explicitOk = payload.ok ?? payload.success;
return {
...payload,
ok: explicitOk === undefined ? response.ok : Boolean(explicitOk),
message: payload.message || payload.error || (response.ok ? "Saved." : "The request could not be completed."),
fieldErrors: payload.fieldErrors && typeof payload.fieldErrors === "object" ? payload.fieldErrors : {},
data: payload.data && typeof payload.data === "object" ? payload.data : {},
patch: payload.patch && typeof payload.patch === "object" ? payload.patch : {},
redirect: payload.redirect || null,
reloadRequired: payload.reloadRequired === true
};
}
async function parseResponse(response) {
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
if (contentType.includes("application/json")) {
return normalize(await response.json().catch(() => null), response);
}
const html = await response.text().catch(() => "");
if (response.ok) return normalize(null, response);
const parsed = new DOMParser().parseFromString(html, "text/html");
const message = parsed.querySelector("[role='alert'], .flash.error, .error-message, main h1 + p")?.textContent?.trim();
return normalize({ ok: false, message: message || `The server returned ${response.status}.` }, response);
}
async function request(url, options = {}) {
const headers = new Headers(options.headers || {});
Object.entries(enhancedHeaders).forEach(([name, value]) => {
if (!headers.has(name)) headers.set(name, value);
});
try {
const response = await fetch(url, {
credentials: "same-origin",
redirect: "follow",
...options,
headers
});
const result = await parseResponse(response);
if (!response.ok || !result.ok) {
throw new LumiRequestError(result.message, {
kind: response.status === 401 || response.status === 403 ? "permission" : response.status === 422 || response.status === 400 ? "validation" : "server",
status: response.status,
fieldErrors: result.fieldErrors,
data: result.data
});
}
return result;
} catch (error) {
if (error instanceof LumiRequestError || error?.name === "AbortError") throw error;
throw new LumiRequestError("Lumi could not be reached. Your changes are still in this page; check the connection and try again.", {
kind: "network"
});
}
}
function formRequest(form, options = {}) {
const submitter = options.submitter || null;
const method = String(submitter?.getAttribute?.("formmethod") || form.getAttribute("method") || "POST").toUpperCase();
const url = submitter?.getAttribute?.("formaction") || form.getAttribute("action") || window.location.href;
const formData = submitter ? new FormData(form, submitter) : new FormData(form);
const hasFile = Array.from(formData.values()).some((value) => value instanceof File && value.size > 0);
let body = formData;
const headers = new Headers(options.headers || {});
if (!hasFile && method !== "GET") {
body = new URLSearchParams();
for (const [name, value] of formData.entries()) {
if (typeof value === "string") body.append(name, value);
}
headers.set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
}
if (method === "GET") {
const next = new URL(url, window.location.href);
for (const [name, value] of formData.entries()) {
if (typeof value === "string") next.searchParams.append(name, value);
}
return request(next.href, { ...options, method, body: undefined, headers });
}
return request(url, { ...options, method, body, headers });
}
window.LumiRequest = Object.freeze({ request, form: formRequest, Error: LumiRequestError });
})();