This commit is contained in:
rizky 2024-04-20 22:55:13 -07:00
parent c35c4c41f8
commit cf2b1c02e5
15 changed files with 384 additions and 40 deletions

View File

@ -18,7 +18,7 @@ export function AutoHeightTextarea({
paddingBottom, paddingBottom,
paddingTop, paddingTop,
} = window.getComputedStyle(ref.current); } = window.getComputedStyle(ref.current);
ref.current.style.height = lineHeight; // set height temporarily to a single row to obtain scrollHeight, disregarding empty space after text (otherwise, scrollHeight would be equal to the height of the element) - this solves auto-shrinking of the textarea (it's not needed for auto-growing it) ref.current.style.minHeight = lineHeight; // set height temporarily to a single row to obtain scrollHeight, disregarding empty space after text (otherwise, scrollHeight would be equal to the height of the element) - this solves auto-shrinking of the textarea (it's not needed for auto-growing it)
const { scrollHeight } = ref.current; // scrollHeight = content height + padding top + padding bottom const { scrollHeight } = ref.current; // scrollHeight = content height + padding top + padding bottom
if (boxSizing === "border-box") { if (boxSizing === "border-box") {
@ -32,12 +32,12 @@ export function AutoHeightTextarea({
scrollHeight + scrollHeight +
parseFloat(borderTopWidth) + parseFloat(borderTopWidth) +
parseFloat(borderBottomWidth); parseFloat(borderBottomWidth);
ref.current.style.height = `${Math.max(minHeight, allTextHeight)}px`; ref.current.style.minHeight = `${Math.max(minHeight, allTextHeight)}px`;
} else if (boxSizing === "content-box") { } else if (boxSizing === "content-box") {
const minHeight = parseFloat(lineHeight) * minRows; const minHeight = parseFloat(lineHeight) * minRows;
const allTextHeight = const allTextHeight =
scrollHeight - parseFloat(paddingTop) - parseFloat(paddingBottom); scrollHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
ref.current.style.height = `${Math.max(minHeight, allTextHeight)}px`; ref.current.style.minHeight = `${Math.max(minHeight, allTextHeight)}px`;
} else { } else {
console.error("Unknown box-sizing value."); console.error("Unknown box-sizing value.");
} }

View File

@ -34,6 +34,7 @@ export const Breadcrumb: FC<BreadcrumbProps> = (_arg) => {
if (local.status === "init") { if (local.status === "init") {
let should_load = true; let should_load = true;
local.status = "loading";
if (isEditor && item && breadcrumbData[item.id]) { if (isEditor && item && breadcrumbData[item.id]) {
local.list = breadcrumbData[item.id]; local.list = breadcrumbData[item.id];

View File

@ -1,12 +1,13 @@
import { useLocal } from "@/utils/use-local"; import { useLocal } from "@/utils/use-local";
import get from "lodash.get"; import get from "lodash.get";
import { FC, useRef } from "react"; import { FC, useEffect, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { FMInternal, FMProps } from "./typings"; import { FMInternal, FMProps } from "./typings";
import { editorFormData } from "./utils/ed-data"; import { editorFormData } from "./utils/ed-data";
import { formInit } from "./utils/init"; import { formInit } from "./utils/init";
import { formReload } from "./utils/reload"; import { formReload } from "./utils/reload";
import { getPathname } from "../../..";
const editorFormWidth = {} as Record<string, { w: number; f: any }>; const editorFormWidth = {} as Record<string, { w: number; f: any }>;
@ -83,6 +84,13 @@ export const Form: FC<FMProps> = (props) => {
}), }),
}); });
useEffect(() => {
if (fm.status === "ready") {
fm.status = "init";
fm.render();
}
}, [getPathname()]);
if (fm.status === "init") { if (fm.status === "init") {
formInit(fm, props); formInit(fm, props);
fm.reload(); fm.reload();
@ -95,6 +103,14 @@ export const Form: FC<FMProps> = (props) => {
} }
const toaster_el = document.getElementsByClassName("prasi-toaster")[0]; const toaster_el = document.getElementsByClassName("prasi-toaster")[0];
if (fm.status === "resizing") {
setTimeout(() => {
fm.status = "ready";
fm.render();
}, 100);
return null;
}
return ( return (
<form <form
onSubmit={(e) => { onSubmit={(e) => {

58
comps/form/base/BaseField.tsx Executable file
View File

@ -0,0 +1,58 @@
import { FMLocal } from "../typings";
import { BaseLabel } from "./BaseLabel";
import { BaseFieldProps } from "./utils/type/field";
import { useField } from "./utils/use-field";
export const BaseField = <T extends Record<string, any>>(
arg: BaseFieldProps<T> & { fm: FMLocal }
) => {
const field = useField<T>(arg);
const fm = arg.fm;
const mode = fm.props.label_mode || "vertical";
const props = arg.props;
const w = field.width;
const errors = fm.error.get(field.name);
return (
<label
className={cx(
"field",
"c-flex",
css`
padding: 5px 0px 0px 10px;
`,
w === "auto" && fm.size.field === "full" && "c-w-full",
w === "auto" && fm.size.field === "half" && "c-w-1/2",
w === "full" && "c-w-full",
w === "¾" && "c-w-3/4",
w === "½" && "c-w-1/2",
w === "⅓" && "c-w-1/3",
w === "¼" && "c-w-1/4",
mode === "horizontal" && "c-flex-row c-items-center",
mode === "vertical" && "c-flex-col c-space-y-1"
)}
{...props}
>
{mode !== "hidden" && <BaseLabel field={field} fm={fm} />}
<div className="field-inner c-flex c-flex-1 c-flex-col">
{field.desc && (
<div className={cx("c-p-2 c-text-xs", errors.length > 0 && "c-pb-1")}>
{field.desc}
</div>
)}
{errors.length > 0 && (
<div
className={cx(
"c-p-2 c-text-xs c-text-red-600",
field.desc && "c-pt-0"
)}
>
{errors.map((err) => {
return <div>{err}</div>;
})}
</div>
)}
</div>
</label>
);
};

51
comps/form/base/BaseForm.tsx Executable file
View File

@ -0,0 +1,51 @@
import { useLocal } from "@/utils/use-local";
import { FC } from "react";
import { BaseField } from "./BaseField";
import { initSimpleForm as initBaseForm } from "./utils/init";
import { BaseFieldProps, BaseFormProps } from "./utils/type/field";
type Children<T extends Record<string, any>> = Exclude<
BaseFormProps<T>["children"],
undefined
>;
export const BaseForm = <T extends Record<string, any>>(
arg: BaseFormProps<T>
) => {
const fm = useLocal<FMInternal>({ status: "init" } as any);
const local = useLocal({
Field: null as null | FC<BaseFieldProps<T>>,
children: null as unknown as Children<T>,
});
if (fm.status === "init") {
local.Field = (props) => {
return <BaseField fm={fm} {...props} />;
};
if (arg.children) local.children = arg.children;
else {
local.children = ({ Field }) => {
const data = Object.entries(fm.data);
return (
<>
{data.map(([key, value]) => {
return <Field name={key} />;
})}
</>
);
};
}
initBaseForm(fm, arg);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
fm.submit();
}}
>
{local.Field && local.children({ Field: local.Field })}
</form>
);
};

49
comps/form/base/BaseLabel.tsx Executable file
View File

@ -0,0 +1,49 @@
import { FC } from "react";
import { BaseFieldLocal } from "./utils/type/field";
import { FMLocal } from "../typings";
export const BaseLabel = <T extends Record<string, any>>({
field,
fm,
}: {
field: BaseFieldLocal<T>;
fm: FMLocal;
}) => {
const errors = fm.error.get(field.name);
return (
<div
className={cx(
"label c-text-sm c-flex c-items-center",
fm.props.label_mode === "horizontal" &&
css`
width: ${fm.props.label_width}px;
`,
fm.props.label_mode === "vertical" && "c-mt-3"
)}
>
<span className={cx(errors.length > 0 && `c-text-red-600`)}>
{field.label}
</span>
{field.required && (
<span className="c-text-red-600 c-mb-2 c-ml-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 6v12" />
<path d="M17.196 9 6.804 15" />
<path d="m6.804 9 10.392 6" />
</svg>
</span>
)}
</div>
);
};

35
comps/form/base/utils/init.ts Executable file
View File

@ -0,0 +1,35 @@
import { FMLocal } from "../../typings";
import { formError } from "../../utils/error";
import { reloadBaseForm } from "./reload";
import { submitBaseForm } from "./submit";
import { BaseFormProps } from "./type/field";
export const initSimpleForm = <T extends Record<string, any>>(
fm: FMLocal,
arg: BaseFormProps<T>
) => {
fm.data = {};
fm.error = formError(fm);
fm.events = {
on_change(name, new_value) {},
};
fm.field_def = {};
fm.fields = {};
fm.internal = {
submit: { done: [], promises: [], timeout: null as any },
reload: { done: [], promises: [], timeout: null as any },
};
fm.props = {} as any;
fm.reload = async () => {
await reloadBaseForm(fm, arg);
};
fm.size = {
width: 0,
height: 0,
field: "full",
};
fm.submit = () => {
return submitBaseForm(fm, arg);
};
fm.status = "ready";
};

View File

@ -0,0 +1,7 @@
import { FMLocal } from "../../typings";
import { BaseFormProps } from "./type/field";
export const reloadBaseForm = async <T extends Record<string, any>>(
fm: FMLocal,
arg: BaseFormProps<T>
) => {};

View File

@ -0,0 +1,9 @@
import { FMLocal } from "../../typings";
import { BaseFormProps } from "./type/field";
export const submitBaseForm = async <T extends Record<string, any>>(
fm: FMLocal,
arg: BaseFormProps<T>
) => {
return true;
};

View File

@ -0,0 +1,57 @@
import { FC, ReactNode } from "react";
export type BaseFieldWidth =
| "auto"
| "full"
| "¾"
| "½"
| "⅓"
| "¼"
| "1/2"
| "1/3"
| "1/4"
| "3/4";
export type BaseFieldType = "text" | "relation";
export type BaseFieldProps<T extends Record<string, any>> = {
name: keyof T;
label?: string;
type?: BaseFieldType;
props?: any;
desc?: string;
on_change?: (arg: { value: any }) => void | Promise<void>;
prefix?: any;
suffix?: any;
required?: boolean;
required_msg?: (name: string) => string;
disabled?: boolean;
width?: BaseFieldWidth;
};
export type BaseFieldInternal<T extends Record<string, any>> = {
name: keyof T;
label: ReactNode;
type: BaseFieldType;
desc: string;
on_change: (arg: { value: any }) => void | Promise<void>;
prefix: any;
suffix: any;
required: boolean;
required_msg: (name: string) => string;
disabled: boolean;
width: BaseFieldWidth;
status: "init" | "loading" | "ready";
PassProp: any;
child: any;
};
export type BaseFieldLocal<T extends Record<string, any>> =
BaseFieldInternal<T>;

View File

@ -0,0 +1,8 @@
import { FC, ReactNode } from "react";
import { BaseFieldProps } from "./field";
export type BaseFormProps<T extends Record<string, any>> = {
onLoad: () => Promise<T | null>;
onSubmit: (arg: { data: T | null }) => Promise<boolean>;
children?: (arg: { Field: FC<BaseFieldProps<T>> }) => ReactNode;
};

View File

@ -0,0 +1,28 @@
import { useLocal } from "@/utils/use-local";
import { FMLocal } from "../../typings";
import {
BaseFieldInternal,
BaseFieldLocal,
BaseFieldProps,
} from "./type/field";
import { formatName } from "@/gen/utils";
export const useField = <T extends Record<string, any>>(
arg: BaseFieldProps<T> & { fm: FMLocal }
) => {
const fm = arg.fm;
const local = useLocal<BaseFieldInternal<T>>({
status: "init",
} as any) as BaseFieldLocal<T>;
if (local.status === "init") {
local.name = arg.name;
local.label = arg.label || formatName(arg.name as string);
local.type = arg.type || "text";
local.desc = arg.desc || "";
local.status = "ready";
local.width = arg.width || fm.size.field === "full" ? "full" : "1/2";
}
return local as BaseFieldLocal<T>;
};

View File

@ -2,9 +2,10 @@ import { FC } from "react";
import { FMLocal, FieldLocal } from "../../typings"; import { FMLocal, FieldLocal } from "../../typings";
import { useLocal } from "@/utils/use-local"; import { useLocal } from "@/utils/use-local";
import parser from "any-date-parser"; import parser from "any-date-parser";
import { AutoHeightTextarea } from "@/comps/custom/AutoHeightTextarea";
export type PropTypeText = { export type PropTypeText = {
type: "text" | "password" | "number" | "date" | "datetime"; type: "text" | "password" | "number" | "date" | "datetime" | "textarea";
}; };
const parse = parser.exportAsFunctionAny("en-US"); const parse = parser.exportAsFunctionAny("en-US");
@ -31,25 +32,46 @@ export const FieldTypeText: FC<{
return ( return (
<> <>
<input {prop.type === "textarea" ? (
type={prop.type} <AutoHeightTextarea
onChange={(ev) => { onChange={(ev) => {
fm.data[field.name] = ev.currentTarget.value; fm.data[field.name] = ev.currentTarget.value;
fm.render(); fm.render();
}} }}
value={value || ""} value={value || ""}
disabled={field.disabled} disabled={field.disabled}
className="c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full" className="c-flex-1 c-bg-transparent c-outline-none c-p-2 c-text-sm c-w-full"
spellCheck={false} spellCheck={false}
onFocus={() => { onFocus={() => {
field.focused = true; field.focused = true;
field.render(); field.render();
}} }}
onBlur={() => { onBlur={() => {
field.focused = false; field.focused = false;
field.render(); field.render();
}} }}
/> />
) : (
<input
type={prop.type}
onChange={(ev) => {
fm.data[field.name] = ev.currentTarget.value;
fm.render();
}}
value={value || ""}
disabled={field.disabled}
className="c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full"
spellCheck={false}
onFocus={() => {
field.focused = true;
field.render();
}}
onBlur={() => {
field.focused = false;
field.render();
}}
/>
)}
</> </>
); );
}; };

View File

@ -73,9 +73,9 @@ export type FMInternal = {
error: { error: {
readonly object: Record<string, string>; readonly object: Record<string, string>;
readonly list: { name: string; error: string[] }[]; readonly list: { name: string; error: string[] }[];
set: (name: string, error: string[]) => void; set: (name: string | number | symbol, error: string[]) => void;
get: (name: string) => string[]; get: (name: string | number | symbol) => string[];
clear: (name?: string) => void; clear: (name?: string | number | symbol) => void;
}; };
internal: { internal: {
reload: { reload: {

View File

@ -1,18 +1,21 @@
export const getPathname = () => { export const getPathname = (url?: string) => {
// if (["localhost", "prasi.avolut.com"].includes(location.hostname)) { // if (["localhost", "prasi.avolut.com"].includes(location.hostname)) {
if ( if (
location.pathname.startsWith("/vi") || location.pathname.startsWith("/vi") ||
location.pathname.startsWith("/prod") || location.pathname.startsWith("/prod") ||
location.pathname.startsWith("/deploy") location.pathname.startsWith("/deploy")
) { ) {
const hash = location.hash; const hash = location.hash;
if (url?.startsWith("/prod")) {
if (hash !== "") { return "/" + url.split("/").slice(3).join("/");
return "/" + location.pathname.split("/").slice(3).join("/") + hash;
} else {
return "/" + location.pathname.split("/").slice(3).join("/");
}
} }
if (hash !== "") {
return "/" + location.pathname.split("/").slice(3).join("/") + hash;
} else {
return "/" + location.pathname.split("/").slice(3).join("/");
}
}
// } // }
return location.pathname; return location.pathname;
}; };