/* eslint-disable max-len */ import BaseFoundation, { DefaultAdapter } from '../base/foundation'; import { strings } from './constants'; import { format, set, addMonths, subMonths, subYears, addYears, differenceInCalendarMonths, differenceInCalendarYears, isSameDay, parseISO } from 'date-fns'; import { isBefore, isValidDate, getDefaultFormatToken, getFullDateOffset } from './_utils/index'; import { formatFullDate, WeekStartNumber } from './_utils/getMonthTable'; import { compatibleParse } from './_utils/parser'; import { includes, isSet, isEqual, isFunction } from 'lodash'; import { zonedTimeToUtc } from '../utils/date-fns-extra'; import { getDefaultFormatTokenByType } from './_utils/getDefaultFormatToken'; import isNullOrUndefined from '../utils/isNullOrUndefined'; import { BaseValueType, PresetPosition, ValueType } from './foundation'; import { MonthDayInfo } from './monthFoundation'; import { ArrayElement } from '../utils/type'; const dateDiffFns = { month: differenceInCalendarMonths, year: differenceInCalendarYears, }; const dateCalcFns = { prevMonth: subMonths, nextMonth: addMonths, prevYear: subYears, nextYear: addYears, }; type Type = ArrayElement; interface MonthsGridElementProps { // navPrev?: React.ReactNode; navPrev?: any; // navNext?: React.ReactNode; navNext?: any; // renderDate?: () => React.ReactNode; renderDate?: () => any; // renderFullDate?: () => React.ReactNode; renderFullDate?: () => any; } export type PanelType = 'left' | 'right'; export type YearMonthChangeType = 'prevMonth' | 'nextMonth' | 'prevYear' | 'nextYear'; export interface MonthsGridFoundationProps extends MonthsGridElementProps { type?: Type; defaultValue?: ValueType; defaultPickerValue?: ValueType; multiple?: boolean; max?: number; splitPanels?: boolean; weekStartsOn?: WeekStartNumber; disabledDate?: (date: Date, options?: { rangeStart: string; rangeEnd: string }) => boolean; disabledTime?: (date: Date | Date[], panelType: PanelType) => void; disabledTimePicker?: boolean; hideDisabledOptions?: boolean; onMaxSelect?: (v?: any) => void; timePickerOpts?: any; isControlledComponent?: boolean; rangeStart?: string; rangeInputFocus?: boolean | string; locale?: any; localeCode?: string; format?: string; startDateOffset?: () => void; endDateOffset?: () => void; autoSwitchDate?: boolean; motionEnd?: boolean; density?: string; dateFnsLocale?: any; timeZone?: string | number; syncSwitchMonth?: boolean; onChange?: ( value: [Date] | [Date, Date], options?: { closePanel?: boolean; needCheckFocusRecord?: boolean } ) => void; onPanelChange?: (date: Date | Date[], dateString: string | string[]) => void; setRangeInputFocus?: (rangeInputFocus: 'rangeStart' | 'rangeEnd') => void; isAnotherPanelHasOpened?: (currentRangeInput: 'rangeStart' | 'rangeEnd') => boolean; focusRecordsRef?: any; triggerRender?: (props: Record) => any; insetInput: boolean; presetPosition?: PresetPosition; renderQuickControls?: React.ReactNode; renderDateInput?: React.ReactNode; } export interface MonthInfo { pickerDate: Date; showDate: Date; isTimePickerOpen: boolean; isYearPickerOpen: boolean; } export interface MonthsGridFoundationState { selected: Set; monthLeft: MonthInfo; monthRight: MonthInfo; maxWeekNum: number; // Maximum number of weeks left and right for manual height adjustment hoverDay: string; // Real-time hover date rangeStart: string; // Start date for range selection rangeEnd: string; // End date of range selection currentPanelHeight: number; // current month panel height, offsetRangeStart: string; offsetRangeEnd: string; weeksRowNum?: number; } export interface MonthsGridDateAdapter { updateDaySelected: (selected: Set) => void; } export interface MonthsGridRangeAdapter { setRangeStart: (rangeStart: string) => void; setRangeEnd: (rangeEnd: string) => void; setHoverDay: (hoverDay: string) => void; setWeeksHeight: (maxWeekNum: number) => void; setOffsetRangeStart: (offsetRangeStart: string) => void; setOffsetRangeEnd: (offsetRangeEnd: string) => void; } export interface MonthsGridAdapter extends DefaultAdapter, MonthsGridRangeAdapter, MonthsGridDateAdapter { updateMonthOnLeft: (v: MonthInfo) => void; updateMonthOnRight: (v: MonthInfo) => void; notifySelectedChange: MonthsGridFoundationProps['onChange']; notifyMaxLimit: MonthsGridFoundationProps['onMaxSelect']; notifyPanelChange: MonthsGridFoundationProps['onPanelChange']; setRangeInputFocus: MonthsGridFoundationProps['setRangeInputFocus']; isAnotherPanelHasOpened: MonthsGridFoundationProps['isAnotherPanelHasOpened']; } export default class MonthsGridFoundation extends BaseFoundation { newBiMonthPanelDate: [Date, Date]; constructor(adapter: MonthsGridAdapter) { super({ ...adapter }); // Date change data when double panels this.newBiMonthPanelDate = [this.getState('monthLeft').pickerDate, this.getState('monthRight').pickerDate]; } init() { const defaultValue = this.getProp('defaultValue'); this.initDefaultPickerValue(); this.updateSelectedFromProps(defaultValue); } initDefaultPickerValue() { const defaultPickerValue = compatibleParse(this.getProp('defaultPickerValue')); if (defaultPickerValue && isValidDate(defaultPickerValue)) { this._updatePanelDetail(strings.PANEL_TYPE_LEFT, { pickerDate: defaultPickerValue, }); this._updatePanelDetail(strings.PANEL_TYPE_RIGHT, { pickerDate: addMonths(defaultPickerValue, 1), }); } } updateSelectedFromProps(values: ValueType, refreshPicker = true) { const type: Type = this.getProp('type'); const { selected, rangeStart, rangeEnd } = this.getStates(); if (values && (values as BaseValueType[]).length) { switch (type) { case 'date': this._initDatePickerFromValue(values as BaseValueType[], refreshPicker); break; case 'dateRange': this._initDateRangePickerFromValue(values as BaseValueType[]); break; case 'dateTime': this._initDateTimePickerFromValue(values as BaseValueType[]); break; case 'dateTimeRange': this._initDateTimeRangePickerFormValue(values as BaseValueType[]); break; default: break; } } else if (Array.isArray(values) && !values.length || !values) { // Empty panel when value is empty Select date if (isSet(selected) && selected.size) { this._adapter.updateDaySelected(new Set()); } if (rangeStart) { this._adapter.setRangeStart(''); } if (rangeEnd) { this._adapter.setRangeEnd(''); } } } calcDisabledTime(panelType: PanelType) { const { disabledTime, type } = this.getProps(); if (typeof disabledTime === 'function' && panelType && ['dateTime', 'dateTimeRange'].includes(type)) { const { rangeStart, rangeEnd, monthLeft } = this.getStates(); const selected = []; if (type === 'dateTimeRange') { if (rangeStart) { selected.push(rangeStart); } if (rangeStart && rangeEnd) { selected.push(rangeEnd); } } else if (monthLeft && monthLeft.showDate) { selected.push(monthLeft.showDate); } const selectedDates = selected.map(str => (str instanceof Date ? str : parseISO(str))); const cbDates = type === 'dateTimeRange' ? selectedDates : selectedDates[0]; return disabledTime(cbDates, panelType); } } _initDatePickerFromValue(values: BaseValueType[], refreshPicker = true) { const monthLeft = this.getState('monthLeft'); const newMonthLeft = { ...monthLeft }; // REMOVE: this._adapter.updateMonthOnLeft(newMonthLeft); const newSelected = new Set(); if (!this._isMultiple()) { values[0] && newSelected.add(format(values[0] as Date, strings.FORMAT_FULL_DATE)); } else { values.forEach(date => { date && newSelected.add(format(date as Date, strings.FORMAT_FULL_DATE)); }); } if (refreshPicker) { this.handleShowDateAndTime(strings.PANEL_TYPE_LEFT, values[0] || newMonthLeft.pickerDate); } else { // FIXME: this.handleShowDateAndTime(strings.PANEL_TYPE_LEFT, newMonthLeft.pickerDate); } this._adapter.updateDaySelected(newSelected); } _initDateRangePickerFromValue(values: BaseValueType[], withTime = false) { // init month panel const monthLeft = this.getState('monthLeft'); const monthRight = this.getState('monthRight'); const adjustResult = this._autoAdjustMonth( { ...monthLeft, pickerDate: values[0] || monthLeft.pickerDate }, { ...monthRight, pickerDate: values[1] || monthRight.pickerDate } ); this.handleShowDateAndTime(strings.PANEL_TYPE_LEFT, adjustResult.monthLeft.pickerDate); this.handleShowDateAndTime(strings.PANEL_TYPE_RIGHT, adjustResult.monthRight.pickerDate); // init range const formatToken = withTime ? strings.FORMAT_DATE_TIME : strings.FORMAT_FULL_DATE; let rangeStart = values[0] && format(values[0] as Date, formatToken); let rangeEnd = values[1] && format(values[1] as Date, formatToken); if (this._isNeedSwap(rangeStart, rangeEnd)) { [rangeStart, rangeEnd] = [rangeEnd, rangeStart]; } this._adapter.setRangeStart(rangeStart); this._adapter.setRangeEnd(rangeEnd); this._adapter.setHoverDay(rangeEnd); } _initDateTimePickerFromValue(values: BaseValueType[]) { this._initDatePickerFromValue(values); } _initDateTimeRangePickerFormValue(values: BaseValueType[]) { this._initDateRangePickerFromValue(values, true); } // eslint-disable-next-line @typescript-eslint/no-empty-function destroy() { } /** * sync change another panel month when change months from the else yam panel * call it when * - current change panel targe date month is same with another panel date * * @example * - panelType=right, target=new Date('2022-09-01') and left panel is in '2022-09' => call it, left panel minus one month to '2022-08' * - panelType=left, target=new Date('2021-12-01') and right panel is in '2021-12' => call it, right panel add one month to '2021-01' */ handleSyncChangeMonths(options: { panelType: PanelType, target: Date }) { const { panelType, target } = options; const { type } = this._adapter.getProps(); const { monthLeft, monthRight } = this._adapter.getStates(); if (this.isRangeType(type)) { if (panelType === 'right' && differenceInCalendarMonths(target, monthLeft.pickerDate) === 0) { this.handleYearOrMonthChange('prevMonth', 'left', 1, true); } else if (panelType === 'left' && differenceInCalendarMonths(monthRight.pickerDate, target) === 0) { this.handleYearOrMonthChange('nextMonth', 'right', 1, true); } } } /** * Get the target date based on the panel type and switch type */ getTargetChangeDate(options: { panelType: PanelType, switchType: YearMonthChangeType }) { const { panelType, switchType } = options; const { monthRight, monthLeft } = this._adapter.getStates(); const currentDate = panelType === 'left' ? monthLeft.pickerDate : monthRight.pickerDate; let target: Date; switch (switchType) { case 'prevMonth': target = addMonths(currentDate, -1); break; case 'nextMonth': target = addMonths(currentDate, 1); break; case 'prevYear': target = addYears(currentDate, -1); break; case 'nextYear': target = addYears(currentDate, 1); break; } return target; } /** * Change month by yam panel */ toMonth(panelType: PanelType, target: Date) { const { type } = this._adapter.getProps(); const diff = this._getDiff('month', target, panelType); this.handleYearOrMonthChange(diff < 0 ? 'prevMonth' : 'nextMonth', panelType, Math.abs(diff), false); if (this.isRangeType(type)) { this.handleSyncChangeMonths({ panelType, target }); } } toYear(panelType: PanelType, target: Date) { const diff = this._getDiff('year', target, panelType); this.handleYearOrMonthChange(diff < 0 ? 'prevYear' : 'nextYear', panelType, Math.abs(diff), false); } toYearMonth(panelType: PanelType, target: Date) { this.toYear(panelType, target); this.toMonth(panelType, target); } isRangeType(type?: Type) { const { type: typeFromProp } = this.getProps(); const realType = type ? type : typeFromProp; return typeof realType === 'string' && /range/i.test(realType); } handleSwitchMonthOrYear(switchType: YearMonthChangeType, panelType: PanelType) { const { type, syncSwitchMonth } = this.getProps(); const rangeType = this.isRangeType(type); // range type and syncSwitchMonth, we should change panels at same time if (rangeType && syncSwitchMonth) { this.handleYearOrMonthChange(switchType, 'left', 1, true); this.handleYearOrMonthChange(switchType, 'right', 1, true); } else { this.handleYearOrMonthChange(switchType, panelType); /** * default behavior (v2.2.0) * In order to prevent the two panels from being the same month, this will confuse the user when selecting the range * https://github.com/DouyinFE/semi-design/issues/260 */ if (rangeType) { const target = this.getTargetChangeDate({ panelType, switchType }); this.handleSyncChangeMonths({ panelType, target }); } } } prevMonth(panelType: PanelType) { this.handleSwitchMonthOrYear('prevMonth', panelType); } nextMonth(panelType: PanelType) { this.handleSwitchMonthOrYear('nextMonth', panelType); } prevYear(panelType: PanelType) { this.handleSwitchMonthOrYear('prevYear', panelType); } nextYear(panelType: PanelType) { this.handleSwitchMonthOrYear('nextYear', panelType); } /** * Calculate the year and month difference */ _getDiff(type: 'month' | 'year', target: Date, panelType: PanelType) { const panelDetail = this._getPanelDetail(panelType); const diff = dateDiffFns[type] && dateDiffFns[type](target, panelDetail.pickerDate); return diff; } _getPanelDetail(panelType: PanelType) { return panelType === strings.PANEL_TYPE_RIGHT ? this.getState('monthRight') : this.getState('monthLeft'); } /** * Format locale date * locale get from LocaleProvider * @param {Date} date * @param {String} token * @returns */ localeFormat(date: Date, token: string) { const dateFnsLocale = this._adapter.getProp('dateFnsLocale'); return format(date, token, { locale: dateFnsLocale }); } isValidTimeZone(timeZone?: string | number) { const propTimeZone = this.getProp('timeZone'); const _timeZone = isNullOrUndefined(timeZone) ? propTimeZone : timeZone; return ['string', 'number'].includes(typeof _timeZone) && _timeZone !== ''; } /** * 根据 type 处理 onChange 返回的参数 * * - 返回的日期需要把用户时间转换为设置的时区时间 * - 用户时间:用户计算机系统时间 * - 时区时间:通过 ConfigProvider 设置的 timeZone * - 例子:用户设置时区为+9,计算机所在时区为+8区,然后用户选择了22:00 * - DatePicker 内部保存日期 state 为 +8 的 22:00 => a = new Date("2021-05-25 22:00:00") * - 传出去时,需要把 +8 的 22:00 => +9 的 22:00 => b = zonedTimeToUtc(a, "+09:00"); * * The parameters returned by onChange are processed according to type * * -The returned date needs to convert the user time to the set time zone time * -User time: user computer system time * -Time zone: timeZone set by ConfigProvider * -Example: The user sets the time zone to + 9, and the time zone where the computer is located is + 8, and then the user selects 22:00 * -DatePicker internal save date state is + 8 22:00 = > a = new Date ("2021-05-25 22:00:00") * -When passing out, you need to put + 8's 22:00 = > + 9's 22:00 = > b = zonedTimeToUtc (a, "+ 09:00"); * * e.g. * let a = new Date ("2021-05-25 22:00:00"); * = > Tue May 25 2021 22:00:00 GMT + 0800 (China Standard Time) * let b = zonedTimeToUtc (a, "+ 09:00"); * = > Tue May 25 2021 21:00:00 GMT + 0800 (China Standard Time) * * @param {Date|Date[]} value */ disposeCallbackArgs(value: Date | Date[]) { let _value = Array.isArray(value) ? value : (value && [value]) || []; if (this.isValidTimeZone()) { const timeZone = this.getProp('timeZone'); _value = _value.map(date => zonedTimeToUtc(date, timeZone)); } const type = this.getProp('type'); const formatToken = this.getProp('format') || getDefaultFormatTokenByType(type); let notifyValue, notifyDate; switch (type) { case 'date': case 'dateTime': case 'month': if (!this._isMultiple()) { notifyValue = _value[0] && this.localeFormat(_value[0], formatToken); [notifyDate] = _value; } else { notifyValue = _value.map(v => v && this.localeFormat(v, formatToken)); notifyDate = [..._value]; } break; case 'dateRange': case 'dateTimeRange': notifyValue = _value.map(v => v && this.localeFormat(v, formatToken)); notifyDate = [..._value]; break; default: break; } return { notifyValue, notifyDate, }; } handleYearOrMonthChange( type: YearMonthChangeType, panelType: PanelType = strings.PANEL_TYPE_LEFT, step = 1, notSeparateInRange = false ) { const { autoSwitchDate, type: datePanelType } = this.getProps(); const { monthLeft, monthRight } = this.getStates(); const isRangeType = this.isRangeType(datePanelType); const isLeftPanelInRange = isRangeType && panelType === strings.PANEL_TYPE_LEFT; const panelDetail = this._getPanelDetail(panelType); const { pickerDate } = panelDetail; const fn = dateCalcFns[type]; const targetMonth = fn(pickerDate, step); // Determine if the date has changed const panelDateHasUpdate = (panelType === strings.PANEL_TYPE_LEFT && !isEqual(targetMonth, monthLeft.pickerDate)) || (panelType === strings.PANEL_TYPE_RIGHT && !isEqual(targetMonth, monthRight.pickerDate)); this._updatePanelDetail(panelType, { pickerDate: targetMonth }); if (panelDateHasUpdate) { // When the date changes if (!isRangeType) { // Single Panel Type const { notifyValue, notifyDate } = this.disposeCallbackArgs(targetMonth); this._adapter.notifyPanelChange(notifyDate, notifyValue); } else { // Double Panel Type if (isLeftPanelInRange) { // Left panel this.newBiMonthPanelDate[0] = targetMonth; } else { // Right panel this.newBiMonthPanelDate[1] = targetMonth; } if (!(isLeftPanelInRange && notSeparateInRange)) { // Not synchronously switching the left panel in the scene const { notifyValue, notifyDate } = this.disposeCallbackArgs(this.newBiMonthPanelDate); this._adapter.notifyPanelChange(notifyDate, notifyValue); } } } if (autoSwitchDate) { this.updateDateAfterChangeYM(type, targetMonth); } } /** * You have chosen to switch the year and month in the future to directly update the Date without closing the date panel * @param {*} type * @param {*} targetDate */ updateDateAfterChangeYM( type: YearMonthChangeType, targetDate: Date ) { const { multiple, disabledDate } = this.getProps(); const { selected: selectedSet, rangeStart, rangeEnd } = this.getStates(); // FIXME: const includeRange = ['dateRange', 'dateTimeRange'].includes(type); const options = { closePanel: false }; if (!multiple && !includeRange && selectedSet.size) { const selectedStr = Array.from(selectedSet)[0] as string; const selectedDate = new Date(selectedStr); const year = targetDate.getFullYear(); const month = targetDate.getMonth(); const fullDate = set(selectedDate, { year, month }); if (disabledDate(fullDate, { rangeStart, rangeEnd })) { return; } this._adapter.notifySelectedChange([fullDate], options); } } _isMultiple() { return Boolean(this.getProp('multiple')) && this.getProp('type') === 'date'; } _isRange() { // return this._adapter.getProp('type') === dateRangeTypeKey; } handleDayClick(day: MonthDayInfo, panelType: PanelType) { const type = this.getProp('type'); switch (true) { case type === 'date' || type === 'dateTime': this.handleDateSelected(day, panelType); break; case type === 'dateRange' || type === 'dateTimeRange': this.handleRangeSelected(day); break; default: break; } } handleDateSelected(day: { fullDate: string; fullValidDate?: Date }, panelType: PanelType) { const { max, type, isControlledComponent, dateFnsLocale } = this.getProps(); const multiple = this._isMultiple(); const { selected } = this.getStates(); const monthDetail = this._getPanelDetail(panelType); const newSelected = new Set(multiple ? [...selected] : []); const { fullDate } = day; const time = monthDetail.pickerDate; const dateStr = type === 'dateTime' ? this._mergeDateAndTime(fullDate, time) : fullDate; if (!multiple) { newSelected.add(dateStr); } else { if (newSelected.has(dateStr)) { newSelected.delete(dateStr); } else if (max && newSelected.size === max) { this._adapter.notifyMaxLimit(); } else { newSelected.add(dateStr); } } const dateFormat = this.getValidDateFormat(); // When passed to the upper layer, it is converted into a Date object to ensure that the input parameter format of initFormDefaultValue is consistent const newSelectedDates = [...newSelected].map(_dateStr => compatibleParse(_dateStr, dateFormat, undefined, dateFnsLocale)); this.handleShowDateAndTime(panelType, time); if (!isControlledComponent) { // Uncontrolled components, update internal values when operating, and notify external // MonthGrid internally uses string to represent fullDate for easy rendering this._adapter.updateDaySelected(newSelected); } this._adapter.notifySelectedChange(newSelectedDates as [Date]); } handleShowDateAndTime(panelType: PanelType, pickerDate: number | Date, showDate?: Date) { const _showDate = showDate || pickerDate; this._updatePanelDetail(panelType, { showDate: _showDate, pickerDate }); } /** * link date and time * * @param {Date|string} date * @param {Date|string} time * @returns {Date} */ _mergeDateAndTime(date: Date | string, time: Date | string) { const dateFnsLocale = this._adapter.getProp('dateFnsLocale'); const dateStr = format( isValidDate(date) ? date as Date : compatibleParse(date as string, strings.FORMAT_FULL_DATE, undefined, dateFnsLocale), strings.FORMAT_FULL_DATE ); const timeStr = format( isValidDate(time) ? time as Date : compatibleParse(time as string, strings.FORMAT_TIME_PICKER, undefined, dateFnsLocale), strings.FORMAT_TIME_PICKER ); const timeFormat = this.getValidTimeFormat(); return compatibleParse(`${dateStr} ${timeStr}`, timeFormat, undefined, dateFnsLocale); } handleRangeSelected(day: MonthDayInfo) { let { rangeStart, rangeEnd } = this.getStates(); const { startDateOffset, endDateOffset, type, dateFnsLocale, rangeInputFocus, triggerRender } = this._adapter.getProps(); const { fullDate } = day; let rangeStartReset = false; let rangeEndReset = false; const isDateRangeAndHasOffset = (startDateOffset || endDateOffset) && type === 'dateRange'; if (isDateRangeAndHasOffset) { rangeStart = getFullDateOffset(startDateOffset, fullDate); rangeEnd = getFullDateOffset(endDateOffset, fullDate); } else { if (rangeInputFocus === 'rangeEnd') { rangeEnd = fullDate; // rangStart Parten in dateTime: 'yyyy-MM-dd HH:MM:SS', rangeEnd parten: 'yyyy-MM-dd' if ((rangeStart && rangeEnd) && isBefore(rangeEnd, rangeStart.trim().split(/\s+/)[0])) { rangeStart = null; rangeStartReset = true; } // Compatible to select date after opening the panel without click input } else if (rangeInputFocus === 'rangeStart' || !rangeInputFocus) { rangeStart = fullDate; // rangEnd Parten in dateTime: 'yyyy-MM-dd HH:MM:SS', rangeStart parten: 'yyyy-MM-dd' if ((rangeStart && rangeEnd) && isBefore(rangeEnd.trim().split(/\s+/)[0], rangeStart)) { rangeEnd = null; rangeEndReset = true; } } } // next focus logic const isRangeType = /range/i.test(type); if (isRangeType) { if (isDateRangeAndHasOffset) { this._adapter.setRangeStart(rangeStart); this._adapter.setRangeEnd(rangeEnd); } else { if (rangeInputFocus === 'rangeEnd') { this._adapter.setRangeEnd(rangeEnd); if (rangeStartReset) { this._adapter.setRangeStart(rangeStart); } if (!this._adapter.isAnotherPanelHasOpened('rangeEnd') || !rangeStart) { this._adapter.setRangeInputFocus('rangeStart'); } } else if (rangeInputFocus === 'rangeStart' || !rangeInputFocus) { this._adapter.setRangeStart(rangeStart); if (rangeEndReset) { this._adapter.setRangeEnd(rangeEnd); } if (!this._adapter.isAnotherPanelHasOpened('rangeStart') || !rangeEnd) { this._adapter.setRangeInputFocus('rangeEnd'); } } } } const dateFormat = this.getValidDateFormat(); // only notify when choose completed if (rangeStart || rangeEnd) { const [startDate, endDate] = [ compatibleParse(rangeStart, dateFormat, undefined, dateFnsLocale), compatibleParse(rangeEnd, dateFormat, undefined, dateFnsLocale), ]; let date: [Date, Date] = [startDate, endDate]; // If the type is dateRangeTime, add the value of time if (type === 'dateTimeRange') { const startTime = this.getState('monthLeft').pickerDate; const endTime = this.getState('monthRight').pickerDate; const start = rangeStart ? this._mergeDateAndTime(rangeStart, startTime) : null; const end = rangeEnd ? this._mergeDateAndTime(rangeEnd, endTime) : null; if (isSameDay(startDate, endDate) && isBefore(end, start)) { date = [start, start]; } else { date = [start, end]; } } /** * no need to check focus then * - dateRange and isDateRangeAndHasOffset */ const needCheckFocusRecord = !(type === 'dateRange' && isDateRangeAndHasOffset); this._adapter.notifySelectedChange(date, { needCheckFocusRecord }); } } _isNeedSwap(rangeStart: Date | string, rangeEnd: Date | string) { // Check whether the start and end are reasonable and whether they need to be reversed return rangeStart && rangeEnd && isBefore(rangeEnd, rangeStart); } /** * Day may be empty, this is unhover state * @param {*} day */ handleDayHover(day = { fullDate: '' }, panelType?: PanelType) { const { fullDate } = day; const { startDateOffset, endDateOffset, type } = this.getProps(); this._adapter.setHoverDay(fullDate); if ((startDateOffset || endDateOffset) && type === 'dateRange') { const offsetRangeStart = getFullDateOffset(startDateOffset, fullDate); const offsetRangeEnd = getFullDateOffset(endDateOffset, fullDate); this._adapter.setOffsetRangeStart(offsetRangeStart); this._adapter.setOffsetRangeEnd(offsetRangeEnd); } } // Guarantee that monthLeft, monthRight will not appear in the same month or monthLeft is greater than MonthRight _autoAdjustMonth(monthLeft: MonthInfo, monthRight: MonthInfo) { let newMonthLeft = monthLeft; let newMonthRight = monthRight; const difference = differenceInCalendarMonths(monthLeft.pickerDate, monthRight.pickerDate); if (difference > 0) { // The month on the left is larger than the month on the right, swap newMonthLeft = { ...monthRight }; newMonthRight = { ...monthLeft }; } else if (difference === 0) { // Around the same month, the number of months on the right + 1 newMonthLeft = monthLeft; newMonthRight = { ...monthRight, pickerDate: addMonths(monthRight.pickerDate, 1) }; } return { monthLeft: newMonthLeft, monthRight: newMonthRight }; } getValidTimeFormat() { const formatProp = this.getProp('format') || strings.FORMAT_TIME_PICKER; const timeFormatTokens = []; if (includes(formatProp, 'h') || includes(formatProp, 'H')) { timeFormatTokens.push('HH'); } if (includes(formatProp, 'm')) { timeFormatTokens.push('mm'); } if (includes(formatProp, 's')) { timeFormatTokens.push('ss'); } return timeFormatTokens.join(':'); } getValidDateFormat() { return this.getProp('format') || getDefaultFormatToken(this.getProp('type')); } handleTimeChange(newTime: { timeStampValue: number }, panelType: PanelType) { const { rangeEnd, rangeStart } = this.getStates(); const dateFnsLocale = this.getProp('dateFnsLocale'); const ts = newTime.timeStampValue; const type = this.getProp('type'); const panelDetail = this._getPanelDetail(panelType); const { showDate } = panelDetail; const timeDate = new Date(ts); const dateFormat = this.getValidDateFormat(); const destRange = panelType === strings.PANEL_TYPE_RIGHT ? rangeEnd : rangeStart; let year, monthNo, date; // if (pickerDate && isValidDate(pickerDate)) { // year = pickerDate.getFullYear(); // monthNo = pickerDate.getMonth(); // date = pickerDate.getDate(); // } else if (type === 'dateTimeRange' && destRange) { const rangeDate = compatibleParse(destRange, dateFormat, undefined, dateFnsLocale); year = rangeDate.getFullYear(); monthNo = rangeDate.getMonth(); date = rangeDate.getDate(); } else { year = showDate.getFullYear(); monthNo = showDate.getMonth(); date = showDate.getDate(); } const hours = timeDate.getHours(); const minutes = timeDate.getMinutes(); const seconds = timeDate.getSeconds(); const milSeconds = timeDate.getMilliseconds(); const dateArgs = [year, monthNo, date, hours, minutes, seconds, milSeconds] as const; const fullValidDate = new Date(...dateArgs); if (type === 'dateTimeRange') { this.handleShowDateAndTime(panelType, fullValidDate, showDate); this._updateTimeInDateRange(panelType, fullValidDate); } else { const fullDate = formatFullDate(year, monthNo + 1, date); this.handleDateSelected( { fullDate, fullValidDate, }, panelType ); this.handleShowDateAndTime(panelType, fullValidDate); this._adapter.notifySelectedChange([fullValidDate]); } } /** * Update the time part in the range * @param {string} panelType * @param {Date} timeDate */ _updateTimeInDateRange(panelType: PanelType, timeDate: Date) { const { isControlledComponent, dateFnsLocale } = this.getProps(); let rangeStart = this.getState('rangeStart'); let rangeEnd = this.getState('rangeEnd'); const dateFormat = this.getValidDateFormat(); // TODO: Modify a time individually if (rangeStart && rangeEnd) { let startDate = compatibleParse(rangeStart, dateFormat, undefined, dateFnsLocale); let endDate = compatibleParse(rangeEnd, dateFormat, undefined, dateFnsLocale); // console.log('_updateTimeInDateRange()', rangeStart, rangeEnd, startDate, endDate); if (panelType === strings.PANEL_TYPE_RIGHT) { endDate = this._mergeDateAndTime(timeDate, timeDate); rangeEnd = format(endDate, strings.FORMAT_DATE_TIME); if (this._isNeedSwap(rangeStart, rangeEnd)) { [rangeStart, rangeEnd] = [rangeEnd, rangeStart]; [startDate, endDate] = [endDate, startDate]; } if (!isControlledComponent) { this._adapter.setRangeEnd(rangeEnd); } } else { startDate = this._mergeDateAndTime(timeDate, timeDate); rangeStart = format(startDate, strings.FORMAT_DATE_TIME); if (this._isNeedSwap(rangeStart, rangeEnd)) { [rangeStart, rangeEnd] = [rangeEnd, rangeStart]; [startDate, endDate] = [endDate, startDate]; } if (!isControlledComponent) { this._adapter.setRangeStart(rangeStart); } } // console.log('_updateTimeInDateRange()', rangeStart, rangeEnd, startDate, endDate); this._adapter.notifySelectedChange([startDate, endDate]); } } _updatePanelDetail( panelType: PanelType, kvs: { showDate?: number | Date; pickerDate?: number | Date; isTimePickerOpen?: boolean; isYearPickerOpen?: boolean; } ) { const { monthLeft, monthRight } = this.getStates(); if (panelType === strings.PANEL_TYPE_RIGHT) { this._adapter.updateMonthOnRight({ ...monthRight, ...kvs }); } else { this._adapter.updateMonthOnLeft({ ...monthLeft, ...kvs }); } } showYearPicker(panelType: PanelType) { this._updatePanelDetail(panelType, { isTimePickerOpen: false, isYearPickerOpen: true }); } showTimePicker(panelType: PanelType, opt?: boolean) { if (this.getProp('disabledTimePicker')) { return; } this._updatePanelDetail(panelType, { isTimePickerOpen: true, isYearPickerOpen: false }); } showDatePanel(panelType: PanelType) { this._updatePanelDetail(panelType, { isTimePickerOpen: false, isYearPickerOpen: false }); } /** * Get year and month panel open type * * It is useful info to set minHeight of weeks. * - When yam open type is 'left' or 'right', weeks minHeight should be set * If the minHeight is not set, the change of the number of weeks will cause the scrollList to be unstable */ getYAMOpenType() { const { monthLeft, monthRight } = this._adapter.getStates(); const leftYearPickerOpen = monthLeft.isYearPickerOpen; const rightYearPickerOpen = monthRight.isYearPickerOpen; if (leftYearPickerOpen && rightYearPickerOpen) { return 'both'; } else if (leftYearPickerOpen) { return 'left'; } else if (rightYearPickerOpen) { return 'right'; } else { return 'none'; } } }