Updates
This commit is contained in:
@@ -66,7 +66,7 @@ export default function Checkbox({
|
||||
const finalSize = size || 20;
|
||||
|
||||
const [checked, setChecked] = React.useState(
|
||||
defaultChecked || externalChecked || false
|
||||
defaultChecked || externalChecked || false,
|
||||
);
|
||||
|
||||
const finalTitle = title
|
||||
@@ -93,7 +93,7 @@ export default function Checkbox({
|
||||
"flex items-start md:items-center gap-2 flex-wrap md:flex-nowrap",
|
||||
readOnly ? "opacity-70 pointer-events-none" : "",
|
||||
wrapperClassName,
|
||||
wrapperProps?.className
|
||||
wrapperProps?.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setChecked(!checked);
|
||||
@@ -108,7 +108,7 @@ export default function Checkbox({
|
||||
? "bg-primary twui-checkbox-checked text-white outline-slate-400"
|
||||
: "dark:outline-white/50 outline-2 -outline-offset-2 twui-checkbox-unchecked",
|
||||
"twui-checkbox",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
minWidth: finalSize + "px",
|
||||
@@ -125,7 +125,7 @@ export default function Checkbox({
|
||||
{...labelProps}
|
||||
className={twMerge(
|
||||
"select-none whitespace-normal md:whitespace-nowrap",
|
||||
labelProps?.className
|
||||
labelProps?.className,
|
||||
)}
|
||||
>
|
||||
{label || finalTitle}
|
||||
@@ -134,8 +134,8 @@ export default function Checkbox({
|
||||
)}
|
||||
</div>
|
||||
{info && (
|
||||
<Row className="gap-1" title={info.toString()}>
|
||||
<Info size={12} className="opacity-40" />
|
||||
<Row className="gap-1 flex-nowrap" title={info.toString()}>
|
||||
<Info size={13} className="opacity-40 min-w-[20px]" />
|
||||
<Span size="smaller" className="opacity-70">
|
||||
{info}
|
||||
</Span>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import _ from "lodash";
|
||||
import { DetailedHTMLProps, FormHTMLAttributes } from "react";
|
||||
import { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props<T extends { [key: string]: any } = { [key: string]: any }> =
|
||||
DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||
submitHandler?: (e: React.FormEvent<HTMLFormElement>, data: T) => void;
|
||||
changeHandler?: (e: React.FormEvent<HTMLFormElement>, data: T) => void;
|
||||
formRef?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -13,8 +14,8 @@ type Props<T extends { [key: string]: any } = { [key: string]: any }> =
|
||||
* @className twui-form
|
||||
*/
|
||||
export default function Form<
|
||||
T extends { [key: string]: any } = { [key: string]: any }
|
||||
>({ ...props }: Props<T>) {
|
||||
T extends { [key: string]: any } = { [key: string]: any },
|
||||
>({ formRef, ...props }: Props<T>) {
|
||||
const finalProps = _.omit(props, ["submitHandler", "changeHandler"]);
|
||||
|
||||
return (
|
||||
@@ -23,7 +24,7 @@ export default function Form<
|
||||
className={twMerge(
|
||||
"flex flex-col items-stretch gap-2 w-full bg-transparent",
|
||||
"twui-form",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -42,6 +43,7 @@ export default function Form<
|
||||
props.changeHandler?.(e, data);
|
||||
props.onChange?.(e);
|
||||
}}
|
||||
ref={formRef}
|
||||
>
|
||||
{props.children}
|
||||
</form>
|
||||
|
||||
@@ -10,13 +10,15 @@ import imageInputToBase64, {
|
||||
} from "../utils/form/imageInputToBase64";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Tag from "../elements/Tag";
|
||||
import Input from "./Input";
|
||||
import Row from "../layout/Row";
|
||||
|
||||
type ImageUploadProps = DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
onChangeHandler?: (
|
||||
imgData: ImageInputToBase64FunctionReturn | undefined
|
||||
imgData: ImageInputToBase64FunctionReturn | undefined,
|
||||
) => any;
|
||||
fileInputProps?: DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
@@ -45,6 +47,7 @@ type ImageUploadProps = DetailedHTMLProps<
|
||||
React.SetStateAction<ImageInputToBase64FunctionReturn[] | undefined>
|
||||
>;
|
||||
setLoading?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setImgURL?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
externalImage?: ImageInputToBase64FunctionReturn;
|
||||
restoreImageFn?: () => void;
|
||||
};
|
||||
@@ -67,6 +70,7 @@ export default function ImageUpload({
|
||||
multiple,
|
||||
restoreImageFn,
|
||||
setLoading,
|
||||
setImgURL,
|
||||
...props
|
||||
}: ImageUploadProps) {
|
||||
const [imageObject, setImageObject] = React.useState<
|
||||
@@ -74,6 +78,11 @@ export default function ImageUpload({
|
||||
>(externalImage);
|
||||
const [src, setSrc] = React.useState<string | undefined>(existingImageUrl);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const imageUrlRef = React.useRef("");
|
||||
|
||||
React.useEffect(() => {
|
||||
setImgURL?.(src);
|
||||
}, [src]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (existingImageUrl) setSrc(existingImageUrl);
|
||||
@@ -84,7 +93,7 @@ export default function ImageUpload({
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"w-full h-[300px] overflow-hidden",
|
||||
props?.className
|
||||
props?.className,
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -123,7 +132,7 @@ export default function ImageUpload({
|
||||
externalSetImage?.(res);
|
||||
fileInputProps?.onChange?.(e);
|
||||
setLoading?.(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -138,7 +147,7 @@ export default function ImageUpload({
|
||||
{label && (
|
||||
<label
|
||||
className={twMerge(
|
||||
"absolute top-0 left-0 text-xs z-50"
|
||||
"absolute top-0 left-0 text-xs z-50",
|
||||
)}
|
||||
>
|
||||
<Tag color="gray">
|
||||
@@ -157,10 +166,10 @@ export default function ImageUpload({
|
||||
{...previewImageProps}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute p-1 top-2 right-2 z-20 bg-background-light dark:bg-background-dark"
|
||||
"absolute p-1 top-2 right-2 z-20 bg-background-light dark:bg-background-dark",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
setSrc(undefined);
|
||||
@@ -174,13 +183,13 @@ export default function ImageUpload({
|
||||
title="Cancel Image Upload Button"
|
||||
>
|
||||
<X className="text-slate-950 dark:text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
className={twMerge(
|
||||
"w-full h-full cursor-pointer hover:bg-slate-100 dark:hover:bg-white/20",
|
||||
placeHolderWrapper?.className
|
||||
placeHolderWrapper?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
const targetEl = e.target as HTMLElement | undefined;
|
||||
@@ -199,6 +208,30 @@ export default function ImageUpload({
|
||||
<Span size="smaller" variant="faded">
|
||||
{label || "Click to Upload Image"}
|
||||
</Span>
|
||||
<Stack className="cancel-upload w-full items-stretch gap-1">
|
||||
<Input
|
||||
placeholder="Eg. https://example.com/img.png"
|
||||
className="text-sm twui-image-url-input"
|
||||
title="Enter Image URL"
|
||||
wrapperWrapperProps={{ className: "mt-2" }}
|
||||
changeHandler={(value) => {
|
||||
imageUrlRef.current = value;
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Button
|
||||
title="Restore Image Button"
|
||||
size="smaller"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
if (!imageUrlRef.current) return;
|
||||
setSrc(imageUrlRef.current);
|
||||
}}
|
||||
>
|
||||
Set Image URL
|
||||
</Button>
|
||||
</Stack>
|
||||
{existingImageUrl && (
|
||||
<Button
|
||||
title="Restore Image Button"
|
||||
|
||||
@@ -8,7 +8,7 @@ let pressInterval: any;
|
||||
let pressTimeout: any;
|
||||
|
||||
type Props = Pick<InputProps<any>, "min" | "max" | "step"> & {
|
||||
updateValue: (v: string) => void;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
getNormalizedValue: (v: string) => void;
|
||||
buttonDownRef: React.MutableRefObject<boolean>;
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
@@ -19,7 +19,7 @@ type Props = Pick<InputProps<any>, "min" | "max" | "step"> & {
|
||||
*/
|
||||
export default function NumberInputButtons({
|
||||
getNormalizedValue,
|
||||
updateValue,
|
||||
setValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
@@ -65,12 +65,14 @@ export default function NumberInputButtons({
|
||||
const existingNumberValue = twuiNumberfy(existingValue);
|
||||
|
||||
if (max && existingNumberValue >= twuiNumberfy(max)) {
|
||||
return updateValue(String(max));
|
||||
return setValue(String(max));
|
||||
} else if (min && existingNumberValue < twuiNumberfy(min)) {
|
||||
return updateValue(String(min));
|
||||
return setValue(String(min));
|
||||
} else {
|
||||
updateValue(
|
||||
String(existingNumberValue + twuiNumberfy(step || DEFAULT_STEP))
|
||||
setValue(
|
||||
String(
|
||||
existingNumberValue + twuiNumberfy(step || DEFAULT_STEP),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -80,10 +82,12 @@ export default function NumberInputButtons({
|
||||
const existingNumberValue = twuiNumberfy(existingValue);
|
||||
|
||||
if (min && existingNumberValue <= twuiNumberfy(min)) {
|
||||
updateValue(String(min));
|
||||
setValue(String(min));
|
||||
} else {
|
||||
updateValue(
|
||||
String(existingNumberValue - twuiNumberfy(step || DEFAULT_STEP))
|
||||
setValue(
|
||||
String(
|
||||
existingNumberValue - twuiNumberfy(step || DEFAULT_STEP),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,16 @@ let timeout: any;
|
||||
let validationFnTimeout: any;
|
||||
let externalValueChangeTimeout: any;
|
||||
|
||||
export type InputProps<KeyType extends string> = DetailedHTMLProps<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
export type InputProps<KeyType extends string> = Omit<
|
||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||
"prefix" | "suffix"
|
||||
> &
|
||||
DetailedHTMLProps<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
HTMLTextAreaElement
|
||||
Omit<
|
||||
DetailedHTMLProps<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
HTMLTextAreaElement
|
||||
>,
|
||||
"prefix" | "suffix"
|
||||
> & {
|
||||
label?: string;
|
||||
variant?: "normal" | "warning" | "error" | "inactive";
|
||||
@@ -60,16 +63,14 @@ export type InputProps<KeyType extends string> = DetailedHTMLProps<
|
||||
invalidMessage?: string;
|
||||
validationFunction?: (
|
||||
value: string,
|
||||
element?: HTMLInputElement | HTMLTextAreaElement
|
||||
element?: HTMLInputElement | HTMLTextAreaElement,
|
||||
) => Promise<TWUISelectValidityObject>;
|
||||
changeHandler?: (
|
||||
value: string,
|
||||
element?: HTMLInputElement | HTMLTextAreaElement
|
||||
) => void;
|
||||
changeHandler?: (value: string) => void;
|
||||
autoComplete?: (typeof AutocompleteOptions)[number];
|
||||
name?: KeyType;
|
||||
valueUpdate?: string;
|
||||
numberText?: boolean;
|
||||
rawNumber?: boolean;
|
||||
setReady?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
decimal?: number;
|
||||
info?: string | ReactNode;
|
||||
@@ -91,7 +92,7 @@ let refreshes = 0;
|
||||
* @className twui-clear-input-field-button
|
||||
*/
|
||||
export default function Input<KeyType extends string>(
|
||||
inputProps: InputProps<KeyType>
|
||||
inputProps: InputProps<KeyType>,
|
||||
) {
|
||||
const {
|
||||
label,
|
||||
@@ -119,10 +120,12 @@ export default function Input<KeyType extends string>(
|
||||
changeHandler,
|
||||
validity: existingValidity,
|
||||
clearInputProps,
|
||||
rawNumber,
|
||||
...props
|
||||
} = inputProps;
|
||||
|
||||
function getFinalValue(v: any) {
|
||||
if (rawNumber) return twuiNumberfy(v);
|
||||
if (numberText) {
|
||||
return (
|
||||
twuiNumberfy(v, decimal).toLocaleString() +
|
||||
@@ -141,16 +144,19 @@ export default function Input<KeyType extends string>(
|
||||
const [validity, setValidity] = React.useState<TWUISelectValidityObject>(
|
||||
existingValidity || {
|
||||
isValid: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
|
||||
const textAreaRef = componentRef || React.useRef<HTMLTextAreaElement>(null);
|
||||
const buttonDownRef = React.useRef(false);
|
||||
|
||||
const [value, setValue] = React.useState(
|
||||
props.defaultValue ? String(props.defaultValue) : "",
|
||||
);
|
||||
const [focus, setFocus] = React.useState(false);
|
||||
const [inputType, setInputType] = React.useState(
|
||||
numberText ? "text" : props.type
|
||||
numberText ? "text" : props.type,
|
||||
);
|
||||
|
||||
const DEFAULT_DEBOUNCE = 500;
|
||||
@@ -180,23 +186,20 @@ export default function Input<KeyType extends string>(
|
||||
setValidity(existingValidity);
|
||||
}, [existingValidity]);
|
||||
|
||||
const updateValueFn = (
|
||||
val: string,
|
||||
el?: HTMLInputElement | HTMLTextAreaElement
|
||||
) => {
|
||||
const updateValueFn = (val: string) => {
|
||||
if (buttonDownRef.current) return;
|
||||
|
||||
if (changeHandler) {
|
||||
window.clearTimeout(externalValueChangeTimeout);
|
||||
externalValueChangeTimeout = setTimeout(() => {
|
||||
changeHandler(val, el);
|
||||
changeHandler(val);
|
||||
}, finalDebounce);
|
||||
}
|
||||
|
||||
if (typeof val == "string") {
|
||||
if (!val.match(/./)) {
|
||||
setValidity({ isValid: true });
|
||||
props.value = "";
|
||||
setValue("");
|
||||
if (istextarea && textAreaRef.current) {
|
||||
textAreaRef.current.value = "";
|
||||
} else if (inputRef?.current) {
|
||||
@@ -223,7 +226,7 @@ export default function Input<KeyType extends string>(
|
||||
if (validationRegex && !validationRegex.test(val)) {
|
||||
return;
|
||||
}
|
||||
validationFunction(val, el).then((res) => {
|
||||
validationFunction(val).then((res) => {
|
||||
setValidity(res);
|
||||
});
|
||||
}, finalDebounce);
|
||||
@@ -233,28 +236,36 @@ export default function Input<KeyType extends string>(
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof props.value !== "string" || !props.value.match(/./)) return;
|
||||
updateValueFn(String(props.value));
|
||||
setValue(String(props.value));
|
||||
}, [props.value]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (istextarea && textAreaRef.current) {
|
||||
} else if (inputRef?.current) {
|
||||
inputRef.current.value = getFinalValue(value);
|
||||
}
|
||||
updateValueFn(value);
|
||||
}, [value]);
|
||||
|
||||
function handleValueChange(
|
||||
e: React.ChangeEvent<HTMLInputElement> &
|
||||
React.ChangeEvent<HTMLTextAreaElement>
|
||||
React.ChangeEvent<HTMLTextAreaElement>,
|
||||
) {
|
||||
const newValue = e.target.value;
|
||||
updateValue(newValue, e.target);
|
||||
setValue(newValue);
|
||||
props.onChange?.(e);
|
||||
}
|
||||
|
||||
function updateValue(
|
||||
v: string,
|
||||
el?: HTMLInputElement | HTMLTextAreaElement
|
||||
) {
|
||||
if (istextarea && textAreaRef.current) {
|
||||
} else if (inputRef?.current) {
|
||||
inputRef.current.value = getFinalValue(v);
|
||||
}
|
||||
updateValueFn(v, el);
|
||||
}
|
||||
// function updateValue(
|
||||
// v: string,
|
||||
// el?: HTMLInputElement | HTMLTextAreaElement,
|
||||
// ) {
|
||||
// if (istextarea && textAreaRef.current) {
|
||||
// } else if (inputRef?.current) {
|
||||
// inputRef.current.value = getFinalValue(v);
|
||||
// }
|
||||
// updateValueFn(v);
|
||||
// }
|
||||
|
||||
const targetComponent = istextarea ? (
|
||||
<textarea
|
||||
@@ -265,7 +276,7 @@ export default function Input<KeyType extends string>(
|
||||
className={twMerge(
|
||||
"w-full outline-none bg-transparent grow",
|
||||
"twui-textarea",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
ref={textAreaRef}
|
||||
onFocus={(e) => {
|
||||
@@ -294,7 +305,7 @@ export default function Input<KeyType extends string>(
|
||||
"dark:bg-transparent dark:outline-none dark:border-none",
|
||||
"p-0 grow",
|
||||
"twui-input",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
ref={inputRef}
|
||||
onFocus={(e) => {
|
||||
@@ -318,8 +329,8 @@ export default function Input<KeyType extends string>(
|
||||
title={`${finalLabel}${props.required ? " (Required)" : ""}`}
|
||||
{...wrapperWrapperProps}
|
||||
className={twMerge(
|
||||
"w-full gap-1.5 relative z-0 hover:z-10",
|
||||
wrapperWrapperProps?.className
|
||||
"w-full gap-1.5 relative z-0 hover:z-100",
|
||||
wrapperWrapperProps?.className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -353,7 +364,7 @@ export default function Input<KeyType extends string>(
|
||||
: "opacity-50 pointer-events-none"
|
||||
: undefined,
|
||||
"twui-input-wrapper",
|
||||
wrapperProps?.className
|
||||
wrapperProps?.className,
|
||||
)}
|
||||
>
|
||||
{showLabel && (
|
||||
@@ -365,7 +376,7 @@ export default function Input<KeyType extends string>(
|
||||
"dark:text-foreground-dark/80 dark:bg-background-dark whitespace-nowrap",
|
||||
"overflow-hidden overflow-ellipsis z-20 px-1.5 rounded-t-default",
|
||||
"twui-input-label",
|
||||
labelProps?.className
|
||||
labelProps?.className,
|
||||
)}
|
||||
>
|
||||
{finalLabel}
|
||||
@@ -378,11 +389,7 @@ export default function Input<KeyType extends string>(
|
||||
</label>
|
||||
)}
|
||||
|
||||
{prefix && (
|
||||
<div className="opacity-60 pointer-events-none whitespace-nowrap">
|
||||
{prefix}
|
||||
</div>
|
||||
)}
|
||||
{prefix && prefix}
|
||||
|
||||
{targetComponent}
|
||||
|
||||
@@ -394,7 +401,7 @@ export default function Input<KeyType extends string>(
|
||||
"p-1 -my-2 -mx-1 opacity-0 cursor-pointer w-7 h-7",
|
||||
"bg-background-light dark:bg-background-dark",
|
||||
"twui-clear-input-field-button",
|
||||
clearInputProps?.className
|
||||
clearInputProps?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -406,7 +413,7 @@ export default function Input<KeyType extends string>(
|
||||
textAreaRef.current.value = "";
|
||||
}
|
||||
|
||||
updateValue("");
|
||||
setValue("");
|
||||
clearInputProps?.onClick?.(e);
|
||||
}}
|
||||
>
|
||||
@@ -439,21 +446,11 @@ export default function Input<KeyType extends string>(
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{suffix ? (
|
||||
<div
|
||||
{...suffixProps}
|
||||
className={twMerge(
|
||||
"opacity-60 pointer-events-none whitespace-nowrap",
|
||||
suffixProps?.className
|
||||
)}
|
||||
>
|
||||
{suffix}
|
||||
</div>
|
||||
) : null}
|
||||
{suffix ? suffix : null}
|
||||
|
||||
{numberText ? (
|
||||
<NumberInputButtons
|
||||
updateValue={updateValue}
|
||||
setValue={setValue}
|
||||
inputRef={inputRef}
|
||||
getNormalizedValue={getNormalizedValue}
|
||||
max={props.max}
|
||||
@@ -468,18 +465,22 @@ export default function Input<KeyType extends string>(
|
||||
target={
|
||||
<Row className="gap-1">
|
||||
<Info size={12} className="opacity-40" />
|
||||
<Span size="smaller" className="opacity-70">
|
||||
<Span
|
||||
size="smaller"
|
||||
className="opacity-70 hover:opacity-100"
|
||||
>
|
||||
{info}
|
||||
</Span>
|
||||
</Row>
|
||||
}
|
||||
openDebounce={700}
|
||||
className="z-1000"
|
||||
hoverOpen
|
||||
>
|
||||
<Paper
|
||||
className={twMerge(
|
||||
"min-w-[250px] shadow-lg shadow-slate-200 dark:shadow-white/10",
|
||||
"max-w-[300px] w-full"
|
||||
"max-w-[300px] w-full bg-background-light! dark:bg-background-dark! z-1000",
|
||||
)}
|
||||
>
|
||||
<Stack className="gap-2 items-center">
|
||||
|
||||
Reference in New Issue
Block a user