import React, { useState, useEffect, useCallback, useRef } from "react"; import classNames from "classnames"; import moment from "moment"; import { ControlledProps, useDefaultValue } from "../form/controlled"; import CalendarPart from "../calendar/CalendarPart"; import { CalendarTable } from "../calendar/CalendarTable"; import { Input } from "../input/Input"; import { useTranslation } from "../i18n"; import { genShowHourMinuteSecond, getValidTimeValue, isValidTimeValue, } from "./util"; import { TimeDisabledProps, TimePickerProps } from "./TimeProps"; import { DropdownBox } from "../dropdown"; import { RangeDateType } from "../calendar/DateProps"; import { CommonDatePickerProps } from "../datepicker"; import { Button } from "../button"; import { Popover } from "../popover/Popover"; import { DatePickerTrigger, isAfter } from "../datepicker/util"; import { useDefault } from "../_util/use-default"; import { useConfig } from "../_util/config-context"; import { forwardRefWithStatics } from "../_util/forward-ref-with-statics"; import { mergeEventProps } from "../_util/merge-event-props"; import { KeyMap } from "../_util/key-map"; export interface TimeRangePickerProps extends CommonDatePickerProps, ControlledProps, Pick { /** * 是否禁用选择值自动顺序校正,使得可以选择跨天类型的时间 * * 如 22:00 ~ 00:00,23:00 ~ 01:00, 10:00 ~ 08:00 等 * * @default false */ disableAutoAdjust?: boolean; /** * 分隔符 * @default ~ */ separator?: string; /** * placeholder * @default “选择时间” */ placeholder?: string; /** * 日期展示格式 * @default "HH:mm:ss" */ format?: string; /** * 不可选的时间 */ disabledTime?: ( dates: RangeDateType, partial: "start" | "end" ) => TimeDisabledProps; /** * 标题渲染 * @default ["开始时间","结束时间"] * @since 2.0.14 */ caption?: [React.ReactNode, React.ReactNode]; } const getDefaultMoment = ({ range, timeDisabled, hourStep = 1, minuteStep = 1, secondStep = 1, }: Partial & { timeDisabled: TimeDisabledProps }) => { return getValidTimeValue(moment("00:00:00", "HH:mm:ss"), { range, hourStep, minuteStep, secondStep, ...timeDisabled, }); }; function isValidRangeMomentValue(value: any) { return ( Array.isArray(value) && moment.isMoment(value[0]) && moment.isMoment(value[1]) ); } export const TimeRangePicker = forwardRefWithStatics( function TimeRangePicker( props: TimeRangePickerProps, ref: React.Ref ) { const { classPrefix } = useConfig(); const t = useTranslation(moment); const { className, style, header, value, onChange, disabled, separator = "~", format = "HH:mm:ss", placeholder = t.selectTime, defaultOpen = false, open, onOpenChange = () => null, placement = "bottom-start", placementOffset = 5, closeOnScroll = true, escapeWithReference, popupContainer, overlayClassName, overlayStyle, range, disabledTime = () => ({}), hourStep = 1, minuteStep = 1, secondStep = 1, disableAutoAdjust, caption = [t.startTime, t.endTime], } = useDefaultValue(props, [null, null]); const { showMilliseconds } = genShowHourMinuteSecond(format); // 当前选中时间 const [curValue, setCurValue] = useState( isValidRangeMomentValue(value) ? [value[0].clone(), value[1].clone()] : [null, null] ); // 选择器是否展开 const [active, setActive] = useDefault(open, defaultOpen, onOpenChange); // 输入框显示值 const getInputValue = useCallback( (value: RangeDateType): string => { const [start, end] = value || [null, null]; if (moment.isMoment(start) && moment.isMoment(end)) { return `${start .locale(t.locale) .format(format)} ${separator} ${end .locale(t.locale) .format(format)}`; } return ""; }, [format, separator, t.locale] ); const [inputValue, setInputValue] = useState( getInputValue(curValue) ); useEffect(() => { const [start, end] = value; const curStart = moment.isMoment(start) ? start.clone() : getDefaultMoment({ range, hourStep, minuteStep, secondStep, timeDisabled: disabledTime(value, "start"), }); const curEnd = moment.isMoment(end) ? end.clone() : getDefaultMoment({ range, hourStep, minuteStep, secondStep, timeDisabled: disabledTime([curStart, end], "end"), }); setCurValue([curStart, curEnd]); setInputValue(getInputValue(value)); }, [format, separator, value]); // eslint-disable-line react-hooks/exhaustive-deps const timerRef = useRef(null); useEffect( () => () => { clearTimeout(timerRef.current); }, [] ); function handleInputChange(content: string) { setInputValue(content); // 格式校验 const [startStr, endStr] = content.split(separator).map(i => i.trim()); const value: RangeDateType = [ moment(startStr, format, true), moment(endStr, format, true), ]; if (!value[0].isValid() || !value[1].isValid()) { return; } const startValid = isValidTimeValue(value[0], { format, hourStep, minuteStep, secondStep, range, ...disabledTime(value, "start"), }); const endValid = isValidTimeValue(value[1], { format, hourStep, minuteStep, secondStep, range, ...disabledTime(value, "end"), }); if (startValid && endValid) { setCurValue(value); } } function handleKeyDown(event: React.KeyboardEvent) { if (!active) { handleOpen(); return; } switch (event.key) { case KeyMap.Enter: handleOk(event); break; case KeyMap.Esc: handleClose(); break; } } function handleChange( partial: "start" | "end", value: RangeDateType // context: DateChangeContext ): void { const [start, end] = value; const fullValue: RangeDateType = [ start || getDefaultMoment({ range, hourStep, minuteStep, secondStep, timeDisabled: disabledTime(value, "start"), }), end || getDefaultMoment({ range, hourStep, minuteStep, secondStep, timeDisabled: disabledTime(value, "end"), }), ]; // 调整至合法时间 if (partial === "start") { fullValue[1] = getValidTimeValue(fullValue[1], { range, hourStep, minuteStep, secondStep, ...disabledTime(fullValue, "end"), }); } else { fullValue[0] = getValidTimeValue(fullValue[0], { range, hourStep, minuteStep, secondStep, ...disabledTime(fullValue, "start"), }); } setCurValue(fullValue); // [TODO] moment 更改后直接获取值(format)可能拿到是之前值? timerRef.current = setTimeout( () => setInputValue(getInputValue(fullValue)), 0 ); } function handleOk(event): void { let value = curValue; if ( !disableAutoAdjust && isValidRangeMomentValue(curValue) && isAfter(curValue[0], curValue[1]) ) { value = [curValue[1], curValue[0]]; setCurValue(value); } onChange(value, { event }); handleClose(); } function handleOpen(): void { if (disabled) { return; } setActive(true); } function handleClose(): void { setInputValue(getInputValue(value)); setActive(false); } const timeProps = { hourStep, minuteStep, secondStep, format, }; return ( { if (!visible) { handleClose(); } else { setActive(visible); } }} placement={placement} placementOffset={placementOffset} closeOnScroll={closeOnScroll} escapeWithReference={escapeWithReference} popupContainer={popupContainer} overlayClassName={overlayClassName} overlayStyle={overlayStyle} overlay={ {!!header && {header}} handleChange("start", value as RangeDateType) } /> handleChange("end", value as RangeDateType) } /> } /> } >
{ if (active) { event.stopPropagation(); } }} />
); }, { defaultLabelAlign: "middle" } ); TimeRangePicker.displayName = "TimeRangePicker";