import { isValidDate } from "@web3r/flowerkit/date"; import type { ReactNode, FocusEvent, Ref } from "react"; import { useEffect, useCallback, useRef, useState, } from "react"; import type { ReactMaskOpts } from "react-imask"; import { useIMask } from "react-imask"; import { useCN } from "../../../utils/react/useCN"; import { today } from "../config/constants"; import type { TDatePickerInputProps } from "../config/types"; /** * Поле пользовательского ввода. Всегда имеет маску. * @returns {ReactNode} */ export const DatePickerCustomInput = ({ baseClass = "formInput", extraClasses = {}, isDisabled = false, children, onBlur, onClick, onKeyDown, date, maxDate, minDate, pickerRef, mask, format, handleChange, isTime, isRange, isAutoFix, timeIntervals, placeholder, ref, ...rest }: TDatePickerInputProps): ReactNode => { const getMaskValue = () => { if (typeof mask === "function") { const props = mask({ isRange, isAutoFix, format, minDate, maxDate, isTime, today, }); return !!props ? props : null; } else { return null; } }; // Опции маски (может не быть, если range или маска убрана вручную) const [ maskOptions, setMaskOptions ] = useState(() => getMaskValue()); const prevMaskOptions = useRef(maskOptions); const { setTypedValue, setValue, ref: maskRef, maskRef: maskInstance, value: maskValue, } = useIMask(maskOptions || {}, { onComplete: () => { if (pickerRef.current) { pickerRef.current.setOpen(false); } }, onAccept: (value, instance) => { if (instance.masked && typeof instance.masked.parse === "function") { const parsedDate = value ? instance.masked.parse(value, instance.masked) : undefined; if (!!parsedDate && isValidDate(parsedDate) && !!pickerRef?.current?.calendar) { const mm = parsedDate.getMonth(); pickerRef.current.calendar.changeMonth(mm); } } }, }); const { getCN } = useCN(baseClass); // Предыдущее значение const prevDate = useRef(date); // Сброс некорректно введенной даты const handleBlur = useCallback((e: FocusEvent) => { if (!!maskOptions) { const masked = maskInstance.current?.masked; // Проверка готовности маски if (!!masked && typeof masked.parse === "function") { // То, что ввёл пользователь const currentValue = e.target.value; // Парсинг данных из маски const parsedDate = currentValue ? masked.parse(currentValue, masked) : undefined; if (!!parsedDate && isValidDate(parsedDate)) { // дата пользователя валидна const formattedValue = (date: Date) => { return typeof masked.format === "function" ? masked.format(date as never, masked) : ""; }; switch (true) { // пользователь ввел дату больше, чем нужно case (parsedDate > maxDate!): setValue(isAutoFix ? formattedValue(maxDate!) : ""); handleChange(isAutoFix ? maxDate : null, e); console.debug(`[DatePicker] Parsed date "${parsedDate}" fixed to max "${maxDate}"`); break; // пользователь ввел дату меньше, чем нужно case (parsedDate < minDate!): setValue(isAutoFix ? formattedValue(minDate!) : ""); handleChange(isAutoFix ? minDate : null, e); console.debug(`[DatePicker] Parsed date "${parsedDate}" fixed to min "${minDate}"`); break; default: // пользователь ввёл корректную дату if (isTime && Number.isFinite(timeIntervals)) { // дополнительная проверка на кратность минут, если `isTime` const MM = parsedDate.getMinutes(); if (MM % timeIntervals!) { const roundedMM = Math.round(MM / timeIntervals!) * timeIntervals!; parsedDate.setMinutes(roundedMM); setValue(formattedValue(parsedDate)); console.debug(`[DatePicker] Parsed date "${parsedDate}" fixed: corrected minutes "${roundedMM}"`); } } handleChange(parsedDate, e); } } else { // дата пользователя невалидна setValue(""); setTypedValue(""); handleChange(null, e); } } } if (typeof onBlur === "function") { onBlur(e); } }, [ maskOptions, onBlur, maskInstance, maxDate, setValue, isAutoFix, handleChange, minDate, isTime, timeIntervals, setTypedValue ]); useEffect(() => { const updatedMask = getMaskValue(); if (updatedMask !== prevMaskOptions.current) { setMaskOptions(updatedMask); prevMaskOptions.current = updatedMask; } }, [ isRange, isAutoFix, format, minDate, maxDate, isTime ]); useEffect(() => { if (!!maskOptions) { if (isValidDate(date)) { // валидная дата const masked = maskInstance?.current?.masked; if (!!masked && typeof masked?.format === "function") { const formattedValue = masked.format(date as never, masked); setValue(formattedValue); } } else if (prevDate.current !== date) { // сброс даты setValue(""); maskInstance.current!.value = ""; // обновление UI iMask } prevDate.current = date; } }, [ date ]); const computedProps = { onChange: () => {}, // prevent react warning onBlur: handleBlur, className: getCN("", extraClasses), disabled: isDisabled, placeholder, onClick, onKeyDown, value: !!maskOptions ? maskValue : rest.value, // conditional value ref: (!!maskOptions ? maskRef : ref) as Ref, // conditional ref, max: undefined, min: undefined, inputMode: "numeric" as const, }; return ( <> {children} ); };