mirror of
https://github.com/hex248/ob248.com.git
synced 2026-02-08 02:33:02 +00:00
452 lines
19 KiB
JavaScript
452 lines
19 KiB
JavaScript
"use client";
|
|
|
|
// src/form.tsx
|
|
import * as React from "react";
|
|
import { composeEventHandlers } from "@radix-ui/primitive";
|
|
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
import { createContextScope } from "@radix-ui/react-context";
|
|
import { useId } from "@radix-ui/react-id";
|
|
import { Label as LabelPrimitive } from "@radix-ui/react-label";
|
|
import { Primitive } from "@radix-ui/react-primitive";
|
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
var [createFormContext, createFormScope] = createContextScope("Form");
|
|
var FORM_NAME = "Form";
|
|
var [ValidationProvider, useValidationContext] = createFormContext(FORM_NAME);
|
|
var [AriaDescriptionProvider, useAriaDescriptionContext] = createFormContext(FORM_NAME);
|
|
var Form = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { __scopeForm, onClearServerErrors = () => {
|
|
}, ...rootProps } = props;
|
|
const formRef = React.useRef(null);
|
|
const composedFormRef = useComposedRefs(forwardedRef, formRef);
|
|
const [validityMap, setValidityMap] = React.useState({});
|
|
const getFieldValidity = React.useCallback(
|
|
(fieldName) => validityMap[fieldName],
|
|
[validityMap]
|
|
);
|
|
const handleFieldValidityChange = React.useCallback(
|
|
(fieldName, validity) => setValidityMap((prevValidityMap) => ({
|
|
...prevValidityMap,
|
|
[fieldName]: { ...prevValidityMap[fieldName] ?? {}, ...validity }
|
|
})),
|
|
[]
|
|
);
|
|
const handleFieldValiditionClear = React.useCallback((fieldName) => {
|
|
setValidityMap((prevValidityMap) => ({ ...prevValidityMap, [fieldName]: void 0 }));
|
|
setCustomErrorsMap((prevCustomErrorsMap) => ({ ...prevCustomErrorsMap, [fieldName]: {} }));
|
|
}, []);
|
|
const [customMatcherEntriesMap, setCustomMatcherEntriesMap] = React.useState({});
|
|
const getFieldCustomMatcherEntries = React.useCallback(
|
|
(fieldName) => customMatcherEntriesMap[fieldName] ?? [],
|
|
[customMatcherEntriesMap]
|
|
);
|
|
const handleFieldCustomMatcherAdd = React.useCallback((fieldName, matcherEntry) => {
|
|
setCustomMatcherEntriesMap((prevCustomMatcherEntriesMap) => ({
|
|
...prevCustomMatcherEntriesMap,
|
|
[fieldName]: [...prevCustomMatcherEntriesMap[fieldName] ?? [], matcherEntry]
|
|
}));
|
|
}, []);
|
|
const handleFieldCustomMatcherRemove = React.useCallback((fieldName, matcherEntryId) => {
|
|
setCustomMatcherEntriesMap((prevCustomMatcherEntriesMap) => ({
|
|
...prevCustomMatcherEntriesMap,
|
|
[fieldName]: (prevCustomMatcherEntriesMap[fieldName] ?? []).filter(
|
|
(matcherEntry) => matcherEntry.id !== matcherEntryId
|
|
)
|
|
}));
|
|
}, []);
|
|
const [customErrorsMap, setCustomErrorsMap] = React.useState({});
|
|
const getFieldCustomErrors = React.useCallback(
|
|
(fieldName) => customErrorsMap[fieldName] ?? {},
|
|
[customErrorsMap]
|
|
);
|
|
const handleFieldCustomErrorsChange = React.useCallback((fieldName, customErrors) => {
|
|
setCustomErrorsMap((prevCustomErrorsMap) => ({
|
|
...prevCustomErrorsMap,
|
|
[fieldName]: { ...prevCustomErrorsMap[fieldName] ?? {}, ...customErrors }
|
|
}));
|
|
}, []);
|
|
const [messageIdsMap, setMessageIdsMap] = React.useState({});
|
|
const handleFieldMessageIdAdd = React.useCallback((fieldName, id) => {
|
|
setMessageIdsMap((prevMessageIdsMap) => {
|
|
const fieldDescriptionIds = new Set(prevMessageIdsMap[fieldName]).add(id);
|
|
return { ...prevMessageIdsMap, [fieldName]: fieldDescriptionIds };
|
|
});
|
|
}, []);
|
|
const handleFieldMessageIdRemove = React.useCallback((fieldName, id) => {
|
|
setMessageIdsMap((prevMessageIdsMap) => {
|
|
const fieldDescriptionIds = new Set(prevMessageIdsMap[fieldName]);
|
|
fieldDescriptionIds.delete(id);
|
|
return { ...prevMessageIdsMap, [fieldName]: fieldDescriptionIds };
|
|
});
|
|
}, []);
|
|
const getFieldDescription = React.useCallback(
|
|
(fieldName) => Array.from(messageIdsMap[fieldName] ?? []).join(" ") || void 0,
|
|
[messageIdsMap]
|
|
);
|
|
return /* @__PURE__ */ jsx(
|
|
ValidationProvider,
|
|
{
|
|
scope: __scopeForm,
|
|
getFieldValidity,
|
|
onFieldValidityChange: handleFieldValidityChange,
|
|
getFieldCustomMatcherEntries,
|
|
onFieldCustomMatcherEntryAdd: handleFieldCustomMatcherAdd,
|
|
onFieldCustomMatcherEntryRemove: handleFieldCustomMatcherRemove,
|
|
getFieldCustomErrors,
|
|
onFieldCustomErrorsChange: handleFieldCustomErrorsChange,
|
|
onFieldValiditionClear: handleFieldValiditionClear,
|
|
children: /* @__PURE__ */ jsx(
|
|
AriaDescriptionProvider,
|
|
{
|
|
scope: __scopeForm,
|
|
onFieldMessageIdAdd: handleFieldMessageIdAdd,
|
|
onFieldMessageIdRemove: handleFieldMessageIdRemove,
|
|
getFieldDescription,
|
|
children: /* @__PURE__ */ jsx(
|
|
Primitive.form,
|
|
{
|
|
...rootProps,
|
|
ref: composedFormRef,
|
|
onInvalid: composeEventHandlers(props.onInvalid, (event) => {
|
|
const firstInvalidControl = getFirstInvalidControl(event.currentTarget);
|
|
if (firstInvalidControl === event.target) firstInvalidControl.focus();
|
|
event.preventDefault();
|
|
}),
|
|
onSubmit: composeEventHandlers(props.onSubmit, onClearServerErrors, {
|
|
checkForDefaultPrevented: false
|
|
}),
|
|
onReset: composeEventHandlers(props.onReset, onClearServerErrors)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
}
|
|
);
|
|
}
|
|
);
|
|
Form.displayName = FORM_NAME;
|
|
var FIELD_NAME = "FormField";
|
|
var [FormFieldProvider, useFormFieldContext] = createFormContext(FIELD_NAME);
|
|
var FormField = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { __scopeForm, name, serverInvalid = false, ...fieldProps } = props;
|
|
const validationContext = useValidationContext(FIELD_NAME, __scopeForm);
|
|
const validity = validationContext.getFieldValidity(name);
|
|
const id = useId();
|
|
return /* @__PURE__ */ jsx(FormFieldProvider, { scope: __scopeForm, id, name, serverInvalid, children: /* @__PURE__ */ jsx(
|
|
Primitive.div,
|
|
{
|
|
"data-valid": getValidAttribute(validity, serverInvalid),
|
|
"data-invalid": getInvalidAttribute(validity, serverInvalid),
|
|
...fieldProps,
|
|
ref: forwardedRef
|
|
}
|
|
) });
|
|
}
|
|
);
|
|
FormField.displayName = FIELD_NAME;
|
|
var LABEL_NAME = "FormLabel";
|
|
var FormLabel = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { __scopeForm, ...labelProps } = props;
|
|
const validationContext = useValidationContext(LABEL_NAME, __scopeForm);
|
|
const fieldContext = useFormFieldContext(LABEL_NAME, __scopeForm);
|
|
const htmlFor = labelProps.htmlFor || fieldContext.id;
|
|
const validity = validationContext.getFieldValidity(fieldContext.name);
|
|
return /* @__PURE__ */ jsx(
|
|
LabelPrimitive,
|
|
{
|
|
"data-valid": getValidAttribute(validity, fieldContext.serverInvalid),
|
|
"data-invalid": getInvalidAttribute(validity, fieldContext.serverInvalid),
|
|
...labelProps,
|
|
ref: forwardedRef,
|
|
htmlFor
|
|
}
|
|
);
|
|
}
|
|
);
|
|
FormLabel.displayName = LABEL_NAME;
|
|
var CONTROL_NAME = "FormControl";
|
|
var FormControl = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { __scopeForm, ...controlProps } = props;
|
|
const validationContext = useValidationContext(CONTROL_NAME, __scopeForm);
|
|
const fieldContext = useFormFieldContext(CONTROL_NAME, __scopeForm);
|
|
const ariaDescriptionContext = useAriaDescriptionContext(CONTROL_NAME, __scopeForm);
|
|
const ref = React.useRef(null);
|
|
const composedRef = useComposedRefs(forwardedRef, ref);
|
|
const name = controlProps.name || fieldContext.name;
|
|
const id = controlProps.id || fieldContext.id;
|
|
const customMatcherEntries = validationContext.getFieldCustomMatcherEntries(name);
|
|
const { onFieldValidityChange, onFieldCustomErrorsChange, onFieldValiditionClear } = validationContext;
|
|
const updateControlValidity = React.useCallback(
|
|
async (control) => {
|
|
if (hasBuiltInError(control.validity)) {
|
|
const controlValidity2 = validityStateToObject(control.validity);
|
|
onFieldValidityChange(name, controlValidity2);
|
|
return;
|
|
}
|
|
const formData = control.form ? new FormData(control.form) : new FormData();
|
|
const matcherArgs = [control.value, formData];
|
|
const syncCustomMatcherEntries = [];
|
|
const ayncCustomMatcherEntries = [];
|
|
customMatcherEntries.forEach((customMatcherEntry) => {
|
|
if (isAsyncCustomMatcherEntry(customMatcherEntry, matcherArgs)) {
|
|
ayncCustomMatcherEntries.push(customMatcherEntry);
|
|
} else if (isSyncCustomMatcherEntry(customMatcherEntry)) {
|
|
syncCustomMatcherEntries.push(customMatcherEntry);
|
|
}
|
|
});
|
|
const syncCustomErrors = syncCustomMatcherEntries.map(({ id: id2, match }) => {
|
|
return [id2, match(...matcherArgs)];
|
|
});
|
|
const syncCustomErrorsById = Object.fromEntries(syncCustomErrors);
|
|
const hasSyncCustomErrors = Object.values(syncCustomErrorsById).some(Boolean);
|
|
const hasCustomError = hasSyncCustomErrors;
|
|
control.setCustomValidity(hasCustomError ? DEFAULT_INVALID_MESSAGE : "");
|
|
const controlValidity = validityStateToObject(control.validity);
|
|
onFieldValidityChange(name, controlValidity);
|
|
onFieldCustomErrorsChange(name, syncCustomErrorsById);
|
|
if (!hasSyncCustomErrors && ayncCustomMatcherEntries.length > 0) {
|
|
const promisedCustomErrors = ayncCustomMatcherEntries.map(
|
|
({ id: id2, match }) => match(...matcherArgs).then((matches) => [id2, matches])
|
|
);
|
|
const asyncCustomErrors = await Promise.all(promisedCustomErrors);
|
|
const asyncCustomErrorsById = Object.fromEntries(asyncCustomErrors);
|
|
const hasAsyncCustomErrors = Object.values(asyncCustomErrorsById).some(Boolean);
|
|
const hasCustomError2 = hasAsyncCustomErrors;
|
|
control.setCustomValidity(hasCustomError2 ? DEFAULT_INVALID_MESSAGE : "");
|
|
const controlValidity2 = validityStateToObject(control.validity);
|
|
onFieldValidityChange(name, controlValidity2);
|
|
onFieldCustomErrorsChange(name, asyncCustomErrorsById);
|
|
}
|
|
},
|
|
[customMatcherEntries, name, onFieldCustomErrorsChange, onFieldValidityChange]
|
|
);
|
|
React.useEffect(() => {
|
|
const control = ref.current;
|
|
if (control) {
|
|
const handleChange = () => updateControlValidity(control);
|
|
control.addEventListener("change", handleChange);
|
|
return () => control.removeEventListener("change", handleChange);
|
|
}
|
|
}, [updateControlValidity]);
|
|
const resetControlValidity = React.useCallback(() => {
|
|
const control = ref.current;
|
|
if (control) {
|
|
control.setCustomValidity("");
|
|
onFieldValiditionClear(name);
|
|
}
|
|
}, [name, onFieldValiditionClear]);
|
|
React.useEffect(() => {
|
|
const form = ref.current?.form;
|
|
if (form) {
|
|
form.addEventListener("reset", resetControlValidity);
|
|
return () => form.removeEventListener("reset", resetControlValidity);
|
|
}
|
|
}, [resetControlValidity]);
|
|
React.useEffect(() => {
|
|
const control = ref.current;
|
|
const form = control?.closest("form");
|
|
if (form && fieldContext.serverInvalid) {
|
|
const firstInvalidControl = getFirstInvalidControl(form);
|
|
if (firstInvalidControl === control) firstInvalidControl.focus();
|
|
}
|
|
}, [fieldContext.serverInvalid]);
|
|
const validity = validationContext.getFieldValidity(name);
|
|
return /* @__PURE__ */ jsx(
|
|
Primitive.input,
|
|
{
|
|
"data-valid": getValidAttribute(validity, fieldContext.serverInvalid),
|
|
"data-invalid": getInvalidAttribute(validity, fieldContext.serverInvalid),
|
|
"aria-invalid": fieldContext.serverInvalid ? true : void 0,
|
|
"aria-describedby": ariaDescriptionContext.getFieldDescription(name),
|
|
title: "",
|
|
...controlProps,
|
|
ref: composedRef,
|
|
id,
|
|
name,
|
|
onInvalid: composeEventHandlers(props.onInvalid, (event) => {
|
|
const control = event.currentTarget;
|
|
updateControlValidity(control);
|
|
}),
|
|
onChange: composeEventHandlers(props.onChange, (_event) => {
|
|
resetControlValidity();
|
|
})
|
|
}
|
|
);
|
|
}
|
|
);
|
|
FormControl.displayName = CONTROL_NAME;
|
|
var DEFAULT_INVALID_MESSAGE = "This value is not valid";
|
|
var DEFAULT_BUILT_IN_MESSAGES = {
|
|
badInput: DEFAULT_INVALID_MESSAGE,
|
|
patternMismatch: "This value does not match the required pattern",
|
|
rangeOverflow: "This value is too large",
|
|
rangeUnderflow: "This value is too small",
|
|
stepMismatch: "This value does not match the required step",
|
|
tooLong: "This value is too long",
|
|
tooShort: "This value is too short",
|
|
typeMismatch: "This value does not match the required type",
|
|
valid: void 0,
|
|
valueMissing: "This value is missing"
|
|
};
|
|
var MESSAGE_NAME = "FormMessage";
|
|
var FormMessage = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { match, name: nameProp, ...messageProps } = props;
|
|
const fieldContext = useFormFieldContext(MESSAGE_NAME, props.__scopeForm);
|
|
const name = nameProp ?? fieldContext.name;
|
|
if (match === void 0) {
|
|
return /* @__PURE__ */ jsx(FormMessageImpl, { ...messageProps, ref: forwardedRef, name, children: props.children || DEFAULT_INVALID_MESSAGE });
|
|
} else if (typeof match === "function") {
|
|
return /* @__PURE__ */ jsx(FormCustomMessage, { match, ...messageProps, ref: forwardedRef, name });
|
|
} else {
|
|
return /* @__PURE__ */ jsx(FormBuiltInMessage, { match, ...messageProps, ref: forwardedRef, name });
|
|
}
|
|
}
|
|
);
|
|
FormMessage.displayName = MESSAGE_NAME;
|
|
var FormBuiltInMessage = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { match, forceMatch = false, name, children, ...messageProps } = props;
|
|
const validationContext = useValidationContext(MESSAGE_NAME, messageProps.__scopeForm);
|
|
const validity = validationContext.getFieldValidity(name);
|
|
const matches = forceMatch || validity?.[match];
|
|
if (matches) {
|
|
return /* @__PURE__ */ jsx(FormMessageImpl, { ref: forwardedRef, ...messageProps, name, children: children ?? DEFAULT_BUILT_IN_MESSAGES[match] });
|
|
}
|
|
return null;
|
|
}
|
|
);
|
|
var FormCustomMessage = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { match, forceMatch = false, name, id: idProp, children, ...messageProps } = props;
|
|
const validationContext = useValidationContext(MESSAGE_NAME, messageProps.__scopeForm);
|
|
const ref = React.useRef(null);
|
|
const composedRef = useComposedRefs(forwardedRef, ref);
|
|
const _id = useId();
|
|
const id = idProp ?? _id;
|
|
const customMatcherEntry = React.useMemo(() => ({ id, match }), [id, match]);
|
|
const { onFieldCustomMatcherEntryAdd, onFieldCustomMatcherEntryRemove } = validationContext;
|
|
React.useEffect(() => {
|
|
onFieldCustomMatcherEntryAdd(name, customMatcherEntry);
|
|
return () => onFieldCustomMatcherEntryRemove(name, customMatcherEntry.id);
|
|
}, [customMatcherEntry, name, onFieldCustomMatcherEntryAdd, onFieldCustomMatcherEntryRemove]);
|
|
const validity = validationContext.getFieldValidity(name);
|
|
const customErrors = validationContext.getFieldCustomErrors(name);
|
|
const hasMatchingCustomError = customErrors[id];
|
|
const matches = forceMatch || validity && !hasBuiltInError(validity) && hasMatchingCustomError;
|
|
if (matches) {
|
|
return /* @__PURE__ */ jsx(FormMessageImpl, { id, ref: composedRef, ...messageProps, name, children: children ?? DEFAULT_INVALID_MESSAGE });
|
|
}
|
|
return null;
|
|
}
|
|
);
|
|
var FormMessageImpl = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { __scopeForm, id: idProp, name, ...messageProps } = props;
|
|
const ariaDescriptionContext = useAriaDescriptionContext(MESSAGE_NAME, __scopeForm);
|
|
const _id = useId();
|
|
const id = idProp ?? _id;
|
|
const { onFieldMessageIdAdd, onFieldMessageIdRemove } = ariaDescriptionContext;
|
|
React.useEffect(() => {
|
|
onFieldMessageIdAdd(name, id);
|
|
return () => onFieldMessageIdRemove(name, id);
|
|
}, [name, id, onFieldMessageIdAdd, onFieldMessageIdRemove]);
|
|
return /* @__PURE__ */ jsx(Primitive.span, { id, ...messageProps, ref: forwardedRef });
|
|
}
|
|
);
|
|
var VALIDITY_STATE_NAME = "FormValidityState";
|
|
var FormValidityState = (props) => {
|
|
const { __scopeForm, name: nameProp, children } = props;
|
|
const validationContext = useValidationContext(VALIDITY_STATE_NAME, __scopeForm);
|
|
const fieldContext = useFormFieldContext(VALIDITY_STATE_NAME, __scopeForm);
|
|
const name = nameProp ?? fieldContext.name;
|
|
const validity = validationContext.getFieldValidity(name);
|
|
return /* @__PURE__ */ jsx(Fragment, { children: children(validity) });
|
|
};
|
|
FormValidityState.displayName = VALIDITY_STATE_NAME;
|
|
var SUBMIT_NAME = "FormSubmit";
|
|
var FormSubmit = React.forwardRef(
|
|
(props, forwardedRef) => {
|
|
const { __scopeForm, ...submitProps } = props;
|
|
return /* @__PURE__ */ jsx(Primitive.button, { type: "submit", ...submitProps, ref: forwardedRef });
|
|
}
|
|
);
|
|
FormSubmit.displayName = SUBMIT_NAME;
|
|
function validityStateToObject(validity) {
|
|
const object = {};
|
|
for (const key in validity) {
|
|
object[key] = validity[key];
|
|
}
|
|
return object;
|
|
}
|
|
function isHTMLElement(element) {
|
|
return element instanceof HTMLElement;
|
|
}
|
|
function isFormControl(element) {
|
|
return "validity" in element;
|
|
}
|
|
function isInvalid(control) {
|
|
return isFormControl(control) && (control.validity.valid === false || control.getAttribute("aria-invalid") === "true");
|
|
}
|
|
function getFirstInvalidControl(form) {
|
|
const elements = form.elements;
|
|
const [firstInvalidControl] = Array.from(elements).filter(isHTMLElement).filter(isInvalid);
|
|
return firstInvalidControl;
|
|
}
|
|
function isAsyncCustomMatcherEntry(entry, args) {
|
|
return entry.match.constructor.name === "AsyncFunction" || returnsPromise(entry.match, args);
|
|
}
|
|
function isSyncCustomMatcherEntry(entry) {
|
|
return entry.match.constructor.name === "Function";
|
|
}
|
|
function returnsPromise(func, args) {
|
|
return func(...args) instanceof Promise;
|
|
}
|
|
function hasBuiltInError(validity) {
|
|
let error = false;
|
|
for (const validityKey in validity) {
|
|
const key = validityKey;
|
|
if (key !== "valid" && key !== "customError" && validity[key]) {
|
|
error = true;
|
|
break;
|
|
}
|
|
}
|
|
return error;
|
|
}
|
|
function getValidAttribute(validity, serverInvalid) {
|
|
if (validity?.valid === true && !serverInvalid) return true;
|
|
return void 0;
|
|
}
|
|
function getInvalidAttribute(validity, serverInvalid) {
|
|
if (validity?.valid === false || serverInvalid) return true;
|
|
return void 0;
|
|
}
|
|
var Root = Form;
|
|
var Field = FormField;
|
|
var Label = FormLabel;
|
|
var Control = FormControl;
|
|
var Message = FormMessage;
|
|
var ValidityState = FormValidityState;
|
|
var Submit = FormSubmit;
|
|
export {
|
|
Control,
|
|
Field,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormLabel,
|
|
FormMessage,
|
|
FormSubmit,
|
|
FormValidityState,
|
|
Label,
|
|
Message,
|
|
Root,
|
|
Submit,
|
|
ValidityState,
|
|
createFormScope
|
|
};
|
|
//# sourceMappingURL=index.mjs.map
|