107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { groupCommas, parseNum, toWords } from "../lib/numutil";
|
|
import { cn } from "../lib/utils";
|
|
|
|
export interface NumberInputProps {
|
|
value: number | null;
|
|
onChange: (n: number | null) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
inputClassName?: string;
|
|
align?: "right" | "center" | "left"; // تنظیم جهت متن و اعداد (پیشفرض: راستچین)
|
|
showWords?: boolean; // نمایش معادل فارسی زیر input
|
|
inputMode?: "numeric" | "decimal" | undefined;
|
|
maxDecimals?: number; // حداکثر رقم اعشار
|
|
disabled?: boolean;
|
|
}
|
|
|
|
// ورودی عددی با فرمتدهی خودکار (groupCommas) و نمایش کلمات فارسی
|
|
export function NumberInput({
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
className,
|
|
inputClassName,
|
|
align = "right",
|
|
showWords = false,
|
|
inputMode = "numeric",
|
|
maxDecimals,
|
|
disabled = false,
|
|
}: NumberInputProps) {
|
|
// فرمت با رعایت حداکثر اعشار (گرد کردن)
|
|
const formatValue = (n: number): string => {
|
|
let v = n;
|
|
if (maxDecimals != null) {
|
|
const f = Math.pow(10, maxDecimals);
|
|
v = Math.round(n * f) / f;
|
|
}
|
|
return groupCommas(v, false);
|
|
};
|
|
|
|
const [text, setText] = useState<string>(() => (value != null ? formatValue(value) : ""));
|
|
|
|
useEffect(() => {
|
|
const cur = parseNum(text);
|
|
if (value != null && value !== cur) {
|
|
setText(formatValue(value));
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [value]);
|
|
|
|
const commit = (raw: string) => {
|
|
const n = parseNum(raw);
|
|
if (n != null) {
|
|
const formatted = formatValue(n);
|
|
setText(formatted);
|
|
onChange(n);
|
|
} else {
|
|
setText(raw);
|
|
onChange(null);
|
|
}
|
|
};
|
|
|
|
const n = parseNum(text);
|
|
|
|
const alignClass =
|
|
align === "center"
|
|
? "!text-center text-center"
|
|
: align === "left"
|
|
? "!text-left text-left"
|
|
: "!text-right text-right";
|
|
|
|
return (
|
|
<div className={cn("min-w-0 w-full", className)}>
|
|
<input
|
|
type="text"
|
|
disabled={disabled}
|
|
className={cn(
|
|
"num-input h-9 w-full rounded-lg border border-line bg-panel px-3 py-1 text-sm text-ink shadow-xs transition-colors placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-60",
|
|
alignClass,
|
|
inputClassName,
|
|
)}
|
|
value={text}
|
|
placeholder={placeholder}
|
|
inputMode={inputMode}
|
|
onChange={(e) => {
|
|
const raw = e.target.value;
|
|
const parsed = parseNum(raw);
|
|
const trailingSep = /[.,،/٬٫]$/.test(raw);
|
|
if (parsed != null && !trailingSep) {
|
|
setText(formatValue(parsed));
|
|
onChange(parsed);
|
|
} else {
|
|
setText(raw);
|
|
onChange(parsed);
|
|
}
|
|
}}
|
|
onBlur={() => commit(text)}
|
|
/>
|
|
{showWords && n != null && (
|
|
<div className="mt-1 block text-right text-[11px] text-muted">
|
|
<span className="tabular-nums font-semibold text-ink/80">{groupCommas(n, true)}</span>{" "}
|
|
<span className="text-muted">({toWords(n)})</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |