mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
import type { FormEvent } from "react";
|
|
|
|
export type InputElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
|
|
export function ignoreTarget(target: InputElement | Element | null, form?: HTMLFormElement) {
|
|
const tagName = target?.tagName.toLowerCase() ?? "";
|
|
const tagNames = ["input", "select", "textarea"];
|
|
|
|
return (
|
|
!target ||
|
|
!form?.contains(target) ||
|
|
!tagNames.includes(tagName) ||
|
|
!("name" in target) ||
|
|
target.hasAttribute("data-ignore") ||
|
|
target.closest("[data-ignore]")
|
|
);
|
|
}
|
|
|
|
export function getFormTarget(e: FormEvent<HTMLFormElement>): InputElement | null {
|
|
const form = e.currentTarget;
|
|
const target = e.target as InputElement | null;
|
|
|
|
return ignoreTarget(target, form) ? null : target;
|
|
}
|
|
|
|
export function getTargetsByName(form: HTMLFormElement, name: string): InputElement[] {
|
|
const query = form.querySelectorAll(`[name="${name}"]`);
|
|
return Array.from(query).filter((e) => ignoreTarget(e)) as InputElement[];
|
|
}
|
|
|
|
export function coerce(target: InputElement | null, value?: any) {
|
|
if (!target) return value;
|
|
const required = target.required;
|
|
if (!value && !required) return undefined;
|
|
|
|
if (target.type === "number") {
|
|
const num = Number(value);
|
|
if (Number.isNaN(num) && !required) return undefined;
|
|
|
|
const min = "min" in target && target.min.length > 0 ? Number(target.min) : undefined;
|
|
const max = "max" in target && target.max.length > 0 ? Number(target.max) : undefined;
|
|
const step = "step" in target && target.step.length > 0 ? Number(target.step) : undefined;
|
|
|
|
if (min && num < min) return min;
|
|
if (max && num > max) return max;
|
|
if (step && step !== 1) return Math.round(num / step) * step;
|
|
|
|
return num;
|
|
} else if (target.type === "text") {
|
|
const maxLength =
|
|
"maxLength" in target && target.maxLength > -1 ? Number(target.maxLength) : undefined;
|
|
const pattern = "pattern" in target ? new RegExp(target.pattern) : undefined;
|
|
|
|
if (maxLength && value.length > maxLength) return value.slice(0, maxLength);
|
|
if (pattern && !pattern.test(value)) return "";
|
|
|
|
return value;
|
|
} else if (target.type === "checkbox") {
|
|
if ("checked" in target) return !!target.checked;
|
|
return ["on", "1", "true", 1, true].includes(value);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
export type CleanOptions = {
|
|
empty?: any[];
|
|
emptyInArray?: any[];
|
|
keepEmptyArray?: boolean;
|
|
};
|
|
export function cleanObject<Obj extends { [key: string]: any }>(
|
|
obj: Obj,
|
|
_opts?: CleanOptions,
|
|
): Obj {
|
|
if (!obj) return obj;
|
|
const _empty = [null, undefined, ""];
|
|
const opts = {
|
|
empty: _opts?.empty ?? _empty,
|
|
emptyInArray: _opts?.emptyInArray ?? _empty,
|
|
keepEmptyArray: _opts?.keepEmptyArray ?? false,
|
|
};
|
|
|
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
if (value && Array.isArray(value) && value.some((v) => typeof v === "object")) {
|
|
const nested = value.map((o) => cleanObject(o, opts));
|
|
if (nested.length > 0 || opts?.keepEmptyArray) {
|
|
acc[key] = nested;
|
|
}
|
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
const nested = cleanObject(value, opts);
|
|
if (Object.keys(nested).length > 0) {
|
|
acc[key] = nested;
|
|
}
|
|
} else if (Array.isArray(value)) {
|
|
const nested = value.filter((v) => !opts.emptyInArray.includes(v));
|
|
if (nested.length > 0 || opts?.keepEmptyArray) {
|
|
acc[key] = nested;
|
|
}
|
|
} else if (!opts.empty.includes(value)) {
|
|
acc[key] = value;
|
|
}
|
|
return acc;
|
|
}, {} as any);
|
|
}
|
|
|
|
export function setPath(object, _path, value) {
|
|
let path = _path;
|
|
|
|
// Optional string-path support.
|
|
// You can remove this `if` block if you don't need it.
|
|
if (typeof path === "string") {
|
|
const isQuoted = (str) => str[0] === '"' && str.at(-1) === '"';
|
|
path = path
|
|
.split(/[.\[\]]+/)
|
|
.filter((x) => x)
|
|
.map((x) => (!Number.isNaN(Number(x)) ? Number(x) : x))
|
|
.map((x) => (typeof x === "string" && isQuoted(x) ? x.slice(1, -1) : x));
|
|
}
|
|
|
|
if (path.length === 0) {
|
|
throw new Error("The path must have at least one entry in it");
|
|
}
|
|
|
|
const [head, ...tail] = path;
|
|
|
|
if (tail.length === 0) {
|
|
object[head] = value;
|
|
return object;
|
|
}
|
|
|
|
if (!(head in object)) {
|
|
object[head] = typeof tail[0] === "number" ? [] : {};
|
|
}
|
|
|
|
setPath(object[head], tail, value);
|
|
return object;
|
|
}
|