mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-01 03:32:09 +03:00
b53a5515d6
persian-calendar-suite seeds today's date and emits it whenever it mounts without a value. Clearing the expiry remounts the picker with a null value, so the library immediately fired onChange(today) and the date came straight back — and it also painted that seeded date into its read-only input. Swallow the mount-time emit (re-armed on every clear-remount) and hide the seeded text while the value is empty, so a cleared expiry stays empty and a fresh client/inbound form no longer silently adopts today as its expiry.
145 lines
4.5 KiB
TypeScript
145 lines
4.5 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { CloseCircleFilled } from '@ant-design/icons';
|
|
import { DatePicker } from 'antd';
|
|
import dayjs from 'dayjs';
|
|
import type { Dayjs } from 'dayjs';
|
|
import { PersianDateTimePicker } from 'persian-calendar-suite';
|
|
|
|
import { useDatepicker } from '@/hooks/useDatepicker';
|
|
import { useTheme } from '@/hooks/useTheme';
|
|
import './DateTimePicker.css';
|
|
|
|
interface DateTimePickerProps {
|
|
value: Dayjs | null;
|
|
onChange: (next: Dayjs | null) => void;
|
|
showTime?: boolean;
|
|
format?: string;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
const LIGHT_THEME = {
|
|
primaryColor: '#1677ff',
|
|
backgroundColor: '#ffffff',
|
|
borderColor: '#d9d9d9',
|
|
hoverColor: 'rgba(22, 119, 255, 0.10)',
|
|
selectedTextColor: '#ffffff',
|
|
textColor: 'rgba(0, 0, 0, 0.88)',
|
|
};
|
|
|
|
const DARK_THEME = {
|
|
primaryColor: '#1677ff',
|
|
backgroundColor: '#23252b',
|
|
borderColor: 'rgba(255, 255, 255, 0.12)',
|
|
hoverColor: 'rgba(22, 119, 255, 0.18)',
|
|
selectedTextColor: '#ffffff',
|
|
textColor: 'rgba(255, 255, 255, 0.88)',
|
|
};
|
|
|
|
const ULTRA_DARK_THEME = {
|
|
primaryColor: '#1677ff',
|
|
backgroundColor: '#101013',
|
|
borderColor: 'rgba(255, 255, 255, 0.08)',
|
|
hoverColor: 'rgba(22, 119, 255, 0.16)',
|
|
selectedTextColor: '#ffffff',
|
|
textColor: 'rgba(255, 255, 255, 0.88)',
|
|
};
|
|
|
|
export default function DateTimePicker({
|
|
value,
|
|
onChange,
|
|
showTime = true,
|
|
format = 'YYYY-MM-DD HH:mm:ss',
|
|
placeholder = '',
|
|
disabled = false,
|
|
}: DateTimePickerProps) {
|
|
const { t } = useTranslation();
|
|
const { datepicker } = useDatepicker();
|
|
const { isDark, isUltra } = useTheme();
|
|
const jalaliRef = useRef<HTMLDivElement>(null);
|
|
// Bumped on clear: persian-calendar-suite reads `value` only on mount, so
|
|
// remounting via key is the only way to reflect an externally cleared value.
|
|
const [clearNonce, setClearNonce] = useState(0);
|
|
// Mounted without a value, persian-calendar-suite seeds today and emits it —
|
|
// which would instantly undo a clear. Armed across every (re)mount.
|
|
const suppressMountEmit = useRef(true);
|
|
|
|
useEffect(() => {
|
|
suppressMountEmit.current = false;
|
|
return () => {
|
|
suppressMountEmit.current = true;
|
|
};
|
|
}, [clearNonce]);
|
|
|
|
const persianTheme = useMemo(() => {
|
|
if (isUltra) return ULTRA_DARK_THEME;
|
|
if (isDark) return DARK_THEME;
|
|
return LIGHT_THEME;
|
|
}, [isDark, isUltra]);
|
|
|
|
// The library hardcodes a Persian placeholder and exposes no working prop to
|
|
// override it, so clear it (or apply the caller's) on the input directly so
|
|
// the empty field shows no leftover Persian text. No dep array: re-apply
|
|
// after every render (incl. clear-remounts).
|
|
useEffect(() => {
|
|
if (datepicker !== 'jalalian') return;
|
|
const input = jalaliRef.current?.querySelector('input');
|
|
if (input) input.placeholder = placeholder;
|
|
});
|
|
|
|
if (datepicker === 'jalalian') {
|
|
return (
|
|
<div ref={jalaliRef} className={`jdp-wrap${isDark ? ' jdp-dark' : ''}${isUltra ? ' jdp-ultra' : ''}${disabled ? ' jdp-disabled' : ''}${value ? '' : ' jdp-empty'}`}>
|
|
<PersianDateTimePicker
|
|
key={clearNonce}
|
|
value={value ? value.valueOf() : null}
|
|
onChange={(next: number | string | null) => {
|
|
if (suppressMountEmit.current) return;
|
|
if (next == null || next === '') {
|
|
onChange(null);
|
|
return;
|
|
}
|
|
const ms = typeof next === 'number' ? next : Number(next);
|
|
if (Number.isFinite(ms)) onChange(dayjs(ms));
|
|
}}
|
|
showTime={showTime}
|
|
outputFormat="timestamp"
|
|
persianNumbers
|
|
rtlCalendar
|
|
theme={persianTheme}
|
|
/>
|
|
{value && !disabled && (
|
|
<button
|
|
type="button"
|
|
className="jdp-clear"
|
|
aria-label={t('clear')}
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onChange(null);
|
|
setClearNonce((n) => n + 1);
|
|
}}
|
|
>
|
|
<CloseCircleFilled />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DatePicker
|
|
value={value}
|
|
onChange={(next) => onChange(next || null)}
|
|
onCalendarChange={(next) => onChange((Array.isArray(next) ? next[0] : next) || null)}
|
|
showTime={showTime ? { format: 'HH:mm:ss' } : false}
|
|
needConfirm={false}
|
|
format={format}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
);
|
|
}
|