import React, { useState, useEffect, useRef, useCallback } from "react"; import classNames from "classnames"; import moment, { Moment } from "moment"; import { ControlledProps, useDefaultValue } from "../form/controlled"; import CalendarPart from "../calendar/CalendarPart"; import { CalendarTable, getTimeRange } from "../calendar/CalendarTable"; import { CalendarWrapper } from "./CalendarWrapper"; import { Input } from "../input/Input"; import { useTranslation } from "../i18n"; import { CommonDatePickerProps } from "./DatePickerProps"; import { getYearMonthDate, DatePickerTrigger, isSame, isAfter, isBefore, isValidDate, isValidMonth, } from "./util"; import { genShowHourMinuteSecond, getHourMinuteSecond, getValidTimeValue, isValidTimeValue, } from "../timepicker/util"; import { TimeDisabledProps } from "../timepicker/TimeProps"; import { DropdownBox } from "../dropdown"; import { RangeDateType, showTimeType, CalendarTableType, DateChangeContext, } from "../calendar/DateProps"; import { useDefault } from "../_util/use-default"; import { Popover } from "../popover/Popover"; import { useConfig } from "../_util/config-context"; import { Icon } from "../icon"; import { noop } from "../_util/noop"; import { RangePickerContext } from "./RangePickerContext"; import { forwardRefWithStatics } from "../_util/forward-ref-with-statics"; import { mergeEventProps } from "../_util/merge-event-props"; import { KeyMap } from "../_util/key-map"; export interface RangePickerProps extends CommonDatePickerProps, ControlledProps { /** * 分隔符 * @default ~ */ separator?: string; /** * 是否开启时间选择,可传递对象设定时间选择配置 */ showTime?: showTimeType; /** * 不可选的日期 */ disabledDate?: (date: Moment, startDate?: Moment) => boolean; /** * 不可选的月份 */ disabledMonth?: (date: Moment, startDate?: Moment) => boolean; /** * 不可选的季度 */ disabledQuarter?: (date: Moment, startDate?: Moment) => boolean; /** * 日历类型 * @default date */ calendarType?: "date" | "month" | "quarter"; /** * 不可选的时间 */ disabledTime?: ( dates: RangeDateType, partial: "start" | "end" ) => TimeDisabledProps; } interface RangeInputValue { inputValueStart: string; separator: string; inputValueEnd: string; } type InputValueType = RangeInputValue | string; const getFormat = (showTime, calendarType) => { if (calendarType === "date") { return showTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD"; } if (calendarType === "month") { return "YYYY-MM"; } return "YYYY"; }; export function isValidRangeValue(value: any) { return ( Array.isArray(value) && moment.isMoment(value[0]) && moment.isMoment(value[1]) ); } export const RangePicker = forwardRefWithStatics( function RangePicker( props: RangePickerProps, ref: React.Ref ) { const t = useTranslation(moment); const { classPrefix } = useConfig(); const { header, className, style, value, onChange, showTime, disabled, separator = "~", format: _format, placeholder = showTime ? t.selectTime : t.selectDate, defaultOpen = false, open, onOpenChange = noop, onInputValueChange = noop, placement = "bottom-start", placementOffset = 5, closeOnScroll = true, escapeWithReference, popupContainer, overlayClassName, overlayStyle, clearable, calendarType = "date", ...restProps } = useDefaultValue(props, [null, null]); const parentUnit = ["month", "quarter"].includes(calendarType) ? "year" : "month"; const [hover, setHover] = useState(false); const format = _format || getFormat(showTime, calendarType); // 当前面板类型 const [type, setType] = useState<[CalendarTableType, CalendarTableType]>([ calendarType, calendarType, ]); // 当前左/右面板展示时间 const [curStartView, setStartCurView] = useState( getDefaultViewMoment(0) ); const [curEndView, setEndCurView] = useState( getDefaultViewMoment(1) ); // 当前选中日期 const [curValue, setCurValue] = useState( isValidRangeValue(value) ? [value[0].clone(), value[1].clone()] : [null, null] ); // 当前 hover 的日期 const [hovered, setHovered] = useState(null); // 上次选中日期 const preValidValueRef = useRef(value || [null, null]); // 选择器是否展开 const [active, setActive] = useDefault(open, defaultOpen, onOpenChange); /** 是否显示毫秒 */ const { showMilliseconds } = genShowHourMinuteSecond(format); // 输入框显示值 const startInputRef = useRef(null); const endInputRef = useRef(null); // 2.7.4 后改为两个输入框显示 const getInputValue = useCallback( (value: RangeDateType): InputValueType => { const [start, end] = value || [null, null]; if (moment.isMoment(start) && moment.isMoment(end)) { const startMoment = start.locale(t.locale); const endMoment = end.locale(t.locale); if (calendarType === "quarter") { return { inputValueStart: `${startMoment.format( "YYYY" )}-Q${startMoment.quarter()}`, separator, inputValueEnd: `${endMoment.format( "YYYY" )}-Q${endMoment.quarter()}`, }; } return { inputValueStart: startMoment.format(format), separator, inputValueEnd: endMoment.format(format), }; } return ""; }, [calendarType, format, separator, t.locale] ); const [inputValue, setInputValue] = useState( getInputValue(curValue) ); useEffect(() => { setCurValue( isValidRangeValue(value) ? [value[0].clone(), value[1].clone()] : [null, null] ); // [TODO] moment 更改后直接获取值(format)可能拿到是之前值? const timer = setTimeout(() => setInputValue(getInputValue(value)), 0); return () => { clearTimeout(timer); }; }, [format, separator, value, getInputValue, active]); useEffect(() => { preValidValueRef.current = value || [null, null]; }, [value]); const timerRef = useRef(null); useEffect( () => () => { clearTimeout(timerRef.current); }, [] ); useEffect(() => { // 同一个月不跳转面板 | 同一个年不跳转面板 const limit = calendarType === "date" ? "M" : "y"; if (!curValue[0]?.isSame(curValue[1], limit) && curValue[1]) { setStartCurView(curValue[0]); } if (!curValue[1]?.isSame(curValue[0], limit) && curValue[1]) { setEndCurView(curValue[1]); } }, [calendarType, curValue]); function handleChange( value: RangeDateType, context: DateChangeContext ): void { // 同步日期/时间 if (showTime && isValidRangeValue(value)) { value = syncDate(value, context.type); // eslint-disable-line no-param-reassign } setCurValue(value); // [TODO] moment 更改后直接获取值(format)可能拿到是之前值? timerRef.current = setTimeout( () => setInputValue(getInputValue(value)), 0 ); // 没有时间选择时没有二次确认选中 if (!showTime && isValidRangeValue(value)) { onChange(value, context); handleClose(); } } function handleOk(event): void { let value = curValue; if (isValidRangeValue(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); setType([calendarType, calendarType]); } function handleClose(): void { setActive(false); } /** * 同步日期/时间 * 如果当前修改为日期,则同步上次时间并修正; * 如果当前修改为时间,则同步上次日期; */ function syncDate( value: RangeDateType, type: CalendarTableType ): RangeDateType { const preValidValue = preValidValueRef.current; const { range, disabledTime = () => ({}) } = props; // 如果包含上次选择,则以上次选择为基准同步 if (isValidRangeValue(preValidValue)) { if (type === "date") { const timeStart = getValidTimeValue(preValidValue[0], { range: getTimeRange(value[0], range), ...disabledTime(value, "start"), hourStep: showTime?.hourStep || 1, minuteStep: showTime?.minuteStep || 1, secondStep: showTime?.secondStep || 1, }); const timeEnd = getValidTimeValue(preValidValue[1], { range: getTimeRange(value[1], range), ...disabledTime(value, "end"), hourStep: showTime?.hourStep || 1, minuteStep: showTime?.minuteStep || 1, secondStep: showTime?.secondStep || 1, }); value[0].set(getHourMinuteSecond(timeStart)); value[1].set(getHourMinuteSecond(timeEnd)); } if (type === "time") { value[0].set(getYearMonthDate(preValidValue[0])); value[1].set(getYearMonthDate(preValidValue[1])); } // 如果是首次选择,则将当前时间进行修正 } else if (type === "date") { // 合并 showTime.defaultValue if (isValidRangeValue(showTime?.defaultValue)) { const [start, end] = showTime.defaultValue; value[0].set(getHourMinuteSecond(start)); value[1].set(getHourMinuteSecond(end)); } const timeStart = getValidTimeValue(value[0], { range: getTimeRange(value[0], range), ...disabledTime(value, "start"), hourStep: showTime?.hourStep || 1, minuteStep: showTime?.minuteStep || 1, secondStep: showTime?.secondStep || 1, }); const timeEnd = getValidTimeValue(value[1], { range: getTimeRange(value[1], range), ...disabledTime(value, "end"), hourStep: showTime?.hourStep || 1, minuteStep: showTime?.minuteStep || 1, secondStep: showTime?.secondStep || 1, }); value[0].set(getHourMinuteSecond(timeStart)); value[1].set(getHourMinuteSecond(timeEnd)); } preValidValueRef.current = value; return value; } /** * 获取 ShowTime 参数 * @param index 左/右面板 - 0/1 */ function getShowTime(index: number): showTimeType { if (typeof showTime === "object") { if (Array.isArray(showTime.defaultValue)) { return { ...showTime, defaultValue: showTime.defaultValue[index] }; } return showTime as showTimeType; } return !!showTime; } /** * 获取左右面板 range */ function getRange(range): [RangeDateType, RangeDateType] { const [rangeMin, rangeMax] = range || [null, null]; const endPreEnd = moment(curEndView) .subtract(1, parentUnit) .endOf(parentUnit); const startNxtStart = moment(curStartView) .add(1, parentUnit) .startOf(parentUnit); return [ [rangeMin, isAfter(endPreEnd, rangeMax) ? rangeMax : endPreEnd], [ isBefore(startNxtStart, rangeMin) ? rangeMin : startNxtStart, rangeMax, ], ]; } /** * 获取面板默认展示时间 * @param index 左/右面板 - 0/1 */ function getDefaultViewMoment(index: number): Moment { const { range } = props; const showTime = getShowTime(index); const time = showTime && typeof showTime === "object" ? showTime.defaultValue : undefined; // 没有初始值 if (!isValidRangeValue(value)) { let m; if (!time) { m = moment(); } else { m = moment(getHourMinuteSecond(time as Moment)); } // range 判断 if (Array.isArray(range)) { let [start, end] = range; if (!moment.isMoment(start)) { start = moment(0); } if (!moment.isMoment(end)) { end = moment(2 ** 52); } if (isBefore(end, m, parentUnit)) { return index === 0 ? moment(end).subtract(1, parentUnit) : end; } if (isAfter(start, m, parentUnit)) { return index === 0 ? start : moment(start).add(1, parentUnit); } if (moment.isMoment(start) && isSame(start, m, parentUnit)) { return index === 0 ? m : moment(m).add(1, parentUnit); } } return index === 0 ? moment(m).subtract(1, parentUnit) : m; } // 初始值在同个月/年 if (isSame(value[0], value[1], parentUnit)) { // 根据 range 判断 if (Array.isArray(range)) { // 当前月前一月不可选时,向后显示一月 if ( moment.isMoment(range[0]) && isSame(range[0], value[1], parentUnit) ) { return index === 0 ? value[1] : moment(value[1]).add(1, parentUnit); } } return index === 0 ? moment(value[1]).subtract(1, parentUnit) : value[1]; } return value[index]; } /** * 是否改变当前视图时间 * 连续两月情况下,点击中间交错日期不改变当前视图时间 * @param from 当前视图变更事件来源 * @param nextDate 要变更到的时间 * @param type 当前视图类型 */ function shouldCurViewChange( from: string, nextDate: Moment, type: "start" | "end" = "start" ): boolean { if (from !== "outside-date") { return true; } const diffMonths = (start, end) => { const years = end.year() - start.year(); return end.month() - start.month() + years * 12; }; if (diffMonths(curStartView, curEndView) === 1) { if (type === "start") { return diffMonths(curStartView, nextDate) !== 1; } return diffMonths(nextDate, curEndView) !== 1; } return true; } function handleStartInputChange(content: string) { setInputValue({ ...(inputValue as RangeInputValue), inputValueStart: content, }); let startValue = moment(content, format, true); let endValue = moment( (inputValue as RangeInputValue).inputValueEnd, format, true ); let temp; if (!startValue.isValid()) { onInputValueChange(content, { valid: false }); return; } const isValid = calendarType === "month" ? isValidMonth : isValidDate; const valid = isValid(startValue, props, endValue); const validTime = showTime ? isValidTimeValue(startValue, { ...showTime, ...props, ...props.disabledTime?.([startValue, endValue], "start"), }) : true; if (valid && validTime) { // 输入的开始日期大于结束日期互换 if (startValue.isAfter(endValue, "d")) { temp = endValue; endValue = startValue; startValue = temp; } setCurValue([startValue, endValue]); } onInputValueChange(content, { valid: false }); } function handleEndInputChange(content: string) { setInputValue({ ...(inputValue as RangeInputValue), inputValueEnd: content, }); let startValue = moment( (inputValue as RangeInputValue).inputValueStart, format, true ); let endValue = moment(content, format, true); let temp; if (!endValue.isValid()) { onInputValueChange(content, { valid: false }); return; } const isValid = calendarType === "month" ? isValidMonth : isValidDate; const valid = isValid(endValue, props, startValue); const validTime = showTime ? isValidTimeValue(endValue, { ...showTime, ...props, ...props.disabledTime?.([startValue, endValue], "end"), }) : true; if (valid && validTime) { // 输入的结束日期小于开始日期互换 if (endValue.isBefore(startValue, "d")) { temp = endValue; endValue = startValue; startValue = temp; } setCurValue([startValue, endValue]); } onInputValueChange(content, { valid: false }); } function handleKeyDown(event: React.KeyboardEvent) { if (!active) { handleOpen(); return; } switch (event.key) { case KeyMap.Enter: handleOk(event); break; case KeyMap.Esc: handleClose(); break; } } return ( {!!header && ( {header} )} setType(types as [CalendarTableType, CalendarTableType]) } > {({ type, onTypeChange, range, ...props }) => { const [startRange, endRange] = getRange(range); return ( <> onTypeChange([t, type[1]])} showTime={getShowTime(0)} curViewMoment={curStartView} onCurViewMomentChange={(date, { from } = {}) => { if (!shouldCurViewChange(from, date)) { return; } if (!isAfter(curEndView, date, parentUnit)) { setEndCurView(moment(date).add(1, parentUnit)); } setStartCurView(date); }} dateRangeInRangePicker={range || [null, null]} hovered={hovered} onHoveredChange={setHovered} /> onTypeChange([type[0], t])} showTime={getShowTime(1)} curViewMoment={curEndView} onCurViewMomentChange={(date, { from } = {}) => { if (!shouldCurViewChange(from, date, "end")) { return; } if (!isBefore(curStartView, date, parentUnit)) { setStartCurView( moment(date).subtract(1, parentUnit) ); } setEndCurView(date); }} dateRangeInRangePicker={range || [null, null]} hovered={hovered} onHoveredChange={setHovered} /> ); }} } >
setHover(true)} onMouseLeave={() => setHover(false)} > calendarType === "quarter" && startInputRef.current.blur() } onClick={(event: React.MouseEvent) => { if (active) { event.stopPropagation(); } }} /> {typeof inputValue !== "string" && ( {inputValue.separator} )} calendarType === "quarter" && endInputRef.current.blur() } onClick={(event: React.MouseEvent) => { if (active) { event.stopPropagation(); } }} /> {clearable && !disabled && ( { event.stopPropagation(); onChange([null, null], { event }); }} /> )}
); }, { defaultLabelAlign: "middle" } ); RangePicker.displayName = "RangePicker";