blade
Version:
React at the edge.
460 lines (453 loc) • 15.9 kB
JavaScript
import { a as useLocation, c as usePopulatePathname, d as useUniversalContext, g as construct, h as assign, m as FORM_TARGET_PREFIX, s as useParams } from "./hooks-BCUm8YC_.js";
import { t as wrapClientComponent } from "./wrap-client-DyCFsAJ7.js";
import { h as TriggerError } from "./utils-BeDKhSpT.js";
import { n as useMutation, t as useLinkEvents } from "./hooks-AV4d-jqO.js";
import { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
//#region ../../node_modules/radash/dist/esm/string.mjs
const capitalize = (str) => {
if (!str || str.length === 0) return "";
const lower = str.toLowerCase();
return lower.substring(0, 1).toUpperCase() + lower.substring(1, lower.length);
};
const dash = (str) => {
const parts = str?.replace(/([A-Z])+/g, capitalize)?.split(/(?=[A-Z])|[\.\-\s_]/).map((x) => x.toLowerCase()) ?? [];
if (parts.length === 0) return "";
if (parts.length === 1) return parts[0];
return parts.reduce((acc, part) => {
return `${acc}-${part.toLowerCase()}`;
});
};
//#endregion
//#region private/client/components/link.tsx
/**
* Normalizes a `LinkURL` object to a `URL` instance.
*
* @param url - The `LinkURL` or `URL` object to normalize.
* @param currentURL - The currently active URL.
*
* @returns A new `URL` instance.
*/
const normalizeURL = (url, currentURL) => {
if (url instanceof URL) return url;
const newURL = new URL(currentURL);
for (const [key, value] of Object.entries(url)) switch (key) {
case "search":
if (typeof url.search === "string") newURL.search = url.search;
else if (url.search) {
const params = Object.entries(url.search).filter(([, value$1]) => {
return typeof value$1 !== "undefined" && value$1 !== null;
});
newURL.search = new URLSearchParams(params).toString();
}
break;
case "hash":
case "host":
case "hostname":
case "href":
case "password":
case "pathname":
case "port":
case "protocol":
case "username": newURL[key] = value;
}
return newURL;
};
/**
* Get the pathname (including query parameters) of a URL.
*
* @param url - The URL to compose the pathname for.
*
* @returns The pathname (including query parameters) of the provided URL.
*/
const getPathFromURL = (url, currentURL) => {
const normalized = normalizeURL(url, currentURL);
return normalized.pathname + normalized.search + normalized.hash;
};
const Link = ({ href: hrefDefault, segments, children, prefetch = true,...extraProps }) => {
const universalContext = useUniversalContext();
const href = typeof hrefDefault === "string" ? hrefDefault : getPathFromURL(hrefDefault, universalContext.url);
const destination = usePopulatePathname()(href, segments);
const linkEventHandlers = useLinkEvents(destination);
const isExternal = href.startsWith("https://") || href.startsWith("http://");
let eventHandlers = prefetch ? linkEventHandlers : {
onClick: linkEventHandlers.onClick,
onMouseEnter: void 0,
onTouchStart: void 0
};
if (href.startsWith("#") || isExternal) eventHandlers = void 0;
return /* @__PURE__ */ jsx("a", {
href: destination,
...isExternal ? {
target: "_blank",
rel: "noreferrer"
} : {},
...extraProps,
onClick: extraProps.onClick || eventHandlers?.onClick,
onMouseEnter: extraProps.onMouseEnter || eventHandlers?.onMouseEnter,
onTouchStart: extraProps.onTouchStart || eventHandlers?.onTouchStart,
children
});
};
wrapClientComponent(Link, "Link");
//#endregion
//#region private/client/components/image.tsx
const supportedFitValues = [
"fill",
"contain",
"cover"
];
const Image = forwardRef(({ src: input, alt, size: defaultSize, width: defaultWidth, height: defaultHeight, fit = "cover", format = "webp", quality = 80, aspect, loading, style, className }, ref) => {
const imageElement = useRef(null);
const renderTime = useRef(Date.now());
const isMediaObject = typeof input === "object" && input !== null;
const width = defaultSize || defaultWidth;
const height = defaultSize || defaultHeight;
const onLoad = useCallback(() => {
if (Date.now() - renderTime.current > 150) imageElement.current?.animate([{
filter: "blur(4px)",
opacity: 0
}, {
filter: "blur(0px)",
opacity: 1
}], { duration: 200 });
}, []);
if (!(height || width)) throw new Error("Either `width`, `height`, or `size` must be defined for `Image`.");
if (quality && (quality < 0 || quality > 100)) throw new Error("The given `quality` was not in the range between 0 and 100.");
const optimizationParams = new URLSearchParams({
...width ? { w: width.toString() } : {},
...height ? { h: height.toString() } : {},
...format !== "original" ? { fm: format } : {},
fit: supportedFitValues.includes(fit) ? fit : "cover",
q: quality.toString()
});
const responsiveOptimizationParams = new URLSearchParams({
...width ? { h: (width * 2).toString() } : {},
...height ? { h: (height * 2).toString() } : {},
...format !== "original" ? { fm: format } : {},
fit: supportedFitValues.includes(fit) ? fit : "cover",
q: quality.toString()
});
const source = isMediaObject ? `${input.src}?${optimizationParams}` : input;
const responsiveSource = isMediaObject ? `${input.src}?${optimizationParams} 1x, ${input.src}?${responsiveOptimizationParams} 2x` : input;
const placeholder = input && typeof input !== "string" ? input.placeholder?.base64 : null;
return /* @__PURE__ */ jsxs("div", {
ref,
className,
style: {
position: "relative",
overflow: "hidden",
flexShrink: 0,
width: width || "100%",
height: height || "100%",
aspectRatio: aspect === "video" ? "16/9" : aspect === "square" ? "1/1" : "auto",
...style
},
children: [placeholder && /* @__PURE__ */ jsx("img", {
style: {
position: "absolute",
width: "100%",
height: "100%",
objectFit: fit
},
src: placeholder,
alt
}), /* @__PURE__ */ jsx("img", {
alt,
style: {
position: "absolute",
width: "100%",
height: "100%",
objectFit: fit
},
decoding: "async",
onLoad,
loading,
ref: imageElement,
src: source,
srcSet: responsiveSource
})]
});
});
wrapClientComponent(Image, "Image");
//#endregion
//#region private/client/components/form-element.tsx
const FormElement = ({ children, allowGlobalSave,...rest }) => {
const form = useContext(FormContext);
if (!form) throw new Error("`FormElement` can only be used within `Form`.");
const formId = useRef("");
const formRef = useRef(null);
useEffect(() => {
formId.current = form.registerForm(formRef);
return () => form.unregisterForm(formId.current);
}, []);
useEffect(() => {
if (!allowGlobalSave) return;
const listener = async (event) => {
if ((window.navigator.userAgent.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === "s") {
event.preventDefault();
if (form?.waiting) return;
if (!form) return;
await form.submit();
}
};
document.addEventListener("keydown", listener, false);
return () => document.removeEventListener("keydown", listener);
}, [allowGlobalSave, form]);
return /* @__PURE__ */ jsxs("form", {
id: form.key,
onKeyDown: (event) => {
/**
* When the user presses Ctrl+Enter (or Cmd+Enter on macOS) while focused
* on an input field, we want to submit the form.
*/
const focusedTagName = document.activeElement?.tagName;
if (!focusedTagName) return;
if (!(event.currentTarget.id === form.key)) return;
const isFocusedOnInput = ["INPUT", "TEXTAREA"].includes(focusedTagName);
if ((window.navigator.userAgent.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === "Enter" && isFocusedOnInput && !form.waiting) form.submit();
},
onSubmit: (event) => {
event.preventDefault();
if (form?.submit && !form?.waiting) form.submit();
},
ref: formRef,
...rest,
children: [children, /* @__PURE__ */ jsx("button", {
hidden: true,
type: "submit"
})]
});
};
wrapClientComponent(FormElement, "FormElement");
//#endregion
//#region private/client/components/form.tsx
const parseFormValue = (value, type) => {
if (typeof value === "string" && value?.length === 0 || value === null || value === "null") return null;
switch (type) {
case "BOOL": return typeof value === "boolean" ? value : value === "true";
case "STRING": return typeof value === "string" ? value : String(value);
case "INT64":
case "FLOAT64": return typeof value === "number" ? value : Number.parseFloat(value);
case "DATE":
case "TIMESTAMP": return value instanceof Date ? value : new Date(value);
case "ARRAY":
case "JSON": return typeof value === "object" ? value : JSON.parse(value);
case "SUBQUERY":
case "READABLE": return value;
default: throw new Error("Unsupported field type.");
}
};
const FormContext = createContext(null);
const Form = ({ model, clearOnSuccess = false, children, onSuccess, onError, redirect, disabled: defaultDisabled, disabledWhileWaiting, recordSlug, database, including, excludeEmptyFields, newRecordSlug, noElement, set: shouldSet, remove: shouldRemove,...rest }) => {
const forms = useRef({});
const { set, add, remove } = useMutation();
const { pathname } = useLocation();
const params = useParams();
const populatePathname = usePopulatePathname();
const [waiting, setWaiting] = useState(false);
const [result, setResult] = useState(null);
const registeredProperties = useRef({});
const registeredRedirect = useRef(redirect || null);
const disabled = Boolean(defaultDisabled || disabledWhileWaiting && waiting);
const registerForm = ({ current }) => {
const id = crypto.randomUUID();
forms.current[id] = current;
return id;
};
const unregisterForm = (id) => {
delete forms.current[id];
};
const submit = async () => {
setWaiting(true);
const values = {};
const elements = [];
for (const form of Object.values(forms.current)) {
new FormData(form).forEach((value, key$1) => {
if (key$1.endsWith("[]")) {
const strippedKey = key$1.replace("[]", "");
if (values[strippedKey] && value !== "null") values[strippedKey].push(value);
if (!values[strippedKey]) values[strippedKey] = value === "null" ? [] : [value];
} else if (key$1.startsWith(FORM_TARGET_PREFIX)) {
const expanded = construct({ [key$1.replace(FORM_TARGET_PREFIX, "")]: value });
if (shouldRemove) shouldRemove = assign(typeof shouldRemove === "object" ? shouldRemove : {}, expanded);
else shouldSet = assign(typeof shouldSet === "object" ? shouldSet : {}, expanded);
} else values[key$1] = value;
});
elements.push(...Array.from(form.elements));
}
const currentTime = /* @__PURE__ */ new Date();
for (const prop in registeredProperties.current) try {
values[prop] = await registeredProperties.current[prop]?.getValue();
} catch (err) {
const error$1 = err;
setWaiting(false);
setResult({
error: error$1,
updatedAt: currentTime
});
if (onError) onError(error$1);
return;
}
let valuesNormalized = {};
for (const key$1 in values) {
let type;
switch (key$1) {
case "id":
type = "STRING";
break;
case "ronin":
type = "JSON";
break;
default: type = registeredProperties.current[key$1]?.type || void 0;
}
if (!type) type = elements.find(({ name }) => name === key$1 || name === `${key$1}[]`)?.getAttribute("data-type");
if (!type) throw new Error(`No \`data-type\` provided for field "${key$1}".`);
const isChildOfJSON = key$1.includes(".");
const isArray = Array.isArray(values[key$1]);
const value = values[key$1];
const normalizedValue = parseFormValue(value, type);
if (excludeEmptyFields?.includes(key$1) && normalizedValue === null) continue;
if (isChildOfJSON) {
const expandable = {};
expandable[key$1] = normalizedValue;
valuesNormalized = assign(valuesNormalized, construct(expandable));
} else if (isArray) valuesNormalized[key$1] = values[key$1].map((value$1) => parseFormValue(value$1, type));
else valuesNormalized[key$1] = normalizedValue;
}
let slugRedirect = null;
if (recordSlug) {
const currentParam = Array.isArray(params[recordSlug.param]) ? params[recordSlug.param]?.[0] : params[recordSlug.param];
const desiredParam = recordSlug.formatAs === "dash-case" ? dash(valuesNormalized[recordSlug.field]) : valuesNormalized[recordSlug.field];
if (currentParam !== desiredParam) {
const newParam = desiredParam || (currentParam === newRecordSlug ? `{0.${recordSlug.field}}` : null);
if (newParam) slugRedirect = populatePathname(pathname, { [recordSlug.param]: newParam });
}
}
let result$1;
let error;
try {
const queryOptions = {
redirect: slugRedirect || registeredRedirect.current || void 0,
database
};
if (shouldSet) result$1 = await set[model]({
with: typeof shouldSet === "object" ? shouldSet : {},
to: valuesNormalized,
using: including
}, queryOptions);
else if (shouldRemove) result$1 = await remove[model]({
with: typeof shouldRemove === "object" ? shouldRemove : {},
using: including
}, queryOptions);
else result$1 = await add[model]({
with: valuesNormalized,
using: including
}, queryOptions);
} catch (err) {
error = err;
if (!(error instanceof TriggerError)) console.error(error);
}
setWaiting(false);
if (result$1) {
setResult({
value: result$1,
updatedAt: currentTime
});
if (onSuccess) onSuccess(result$1);
if (clearOnSuccess) clearForms();
} else if (error) {
setResult({
error,
updatedAt: currentTime
});
if (onError) onError(error);
}
};
const clearForms = () => {
Object.values(forms.current).map((form) => form.reset());
};
const reset = (clearFields) => {
setWaiting(false);
setResult(null);
if (clearFields) clearForms();
};
const registerProperty = (name, type, getValue) => {
registeredProperties.current[name] = {
type,
getValue
};
};
const unregisterProperty = (name) => {
delete registeredProperties.current[name];
};
const registerRedirect = (destination) => {
registeredRedirect.current = destination;
};
const unregisterRedirect = () => {
registeredRedirect.current = null;
};
useEffect(() => {
registeredRedirect.current = redirect || null;
}, [redirect]);
const key = [
model,
typeof shouldSet === "object" ? JSON.stringify(shouldSet) : "",
typeof shouldRemove === "object" ? JSON.stringify(shouldRemove) : ""
].join("");
return /* @__PURE__ */ jsx(FormContext.Provider, {
value: {
key,
waiting,
disabled,
clearOnSuccess,
result: result?.value || null,
error: result?.error || null,
updatedAt: result?.updatedAt || null,
submit,
reset,
registerProperty,
unregisterProperty,
registerRedirect,
unregisterRedirect,
registerForm,
unregisterForm
},
children: noElement ? children : /* @__PURE__ */ jsx(FormElement, {
...rest,
children
})
});
};
wrapClientComponent(Form, "Form");
//#endregion
//#region private/client/components/input.tsx
const stringifyFormValue = (value) => {
let newValue;
if (value === null) newValue = "null";
else if (typeof value === "boolean") newValue = value.toString();
else if (value instanceof Date) newValue = value.toISOString();
else if (typeof value === "object") newValue = JSON.stringify(value);
else newValue = value || "";
return newValue;
};
const Input = ({ value, fieldType, target,...rest }) => {
const initialValue = typeof value === "undefined" ? value : stringifyFormValue(value);
if (!fieldType) switch (rest.type) {
case "number":
fieldType = "INT64";
break;
case "checkbox":
fieldType = "BOOL";
break;
default: fieldType = "STRING";
}
return /* @__PURE__ */ jsx("input", {
"data-type": fieldType,
value: initialValue,
...rest,
name: target && rest.name ? FORM_TARGET_PREFIX + rest.name : rest.name
});
};
//#endregion
export { Image as a, FormElement as i, Form as n, Link as o, FormContext as r, Input as t };