113 lines
4.7 KiB
JavaScript
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 });
|
|
})();
|