/** * Logic: * When `mode` === `picker`, * click will trigger `onSelect` (if value changed trigger `onChange` also). * Panel change will not trigger `onSelect` but trigger `onPanelChange` */ import type { SharedTimeProps } from './panels/TimePanel'; import TimePanel from './panels/TimePanel'; import DatetimePanel from './panels/DatetimePanel'; import DatePanel from './panels/DatePanel'; import WeekPanel from './panels/WeekPanel'; import MonthPanel from './panels/MonthPanel'; import QuarterPanel from './panels/QuarterPanel'; import YearPanel from './panels/YearPanel'; import DecadePanel from './panels/DecadePanel'; import type { GenerateConfig } from './generate'; import type { Locale, PanelMode, PanelRefProps, PickerMode, DisabledTime, OnPanelChange, Components, } from './interface'; import { isEqual } from './utils/dateUtil'; import { useInjectPanel, useProvidePanel } from './PanelContext'; import type { DateRender } from './panels/DatePanel/DateBody'; import { PickerModeMap } from './utils/uiUtil'; import type { MonthCellRender } from './panels/MonthPanel/MonthBody'; import { useInjectRange } from './RangeContext'; import getExtraFooter from './utils/getExtraFooter'; import getRanges from './utils/getRanges'; import { getLowerBoundTime, setDateTime, setTime } from './utils/timeUtil'; import type { VueNode } from '../_util/type'; import { computed, createVNode, defineComponent, ref, toRef, watch, watchEffect } from 'vue'; import useMergedState from '../_util/hooks/useMergedState'; import { warning } from '../vc-util/warning'; import KeyCode from '../_util/KeyCode'; import classNames from '../_util/classNames'; export type PickerPanelSharedProps = { prefixCls?: string; // className?: string; // style?: React.CSSProperties; /** @deprecated Will be removed in next big version. Please use `mode` instead */ mode?: PanelMode; tabindex?: number; // Locale locale: Locale; generateConfig: GenerateConfig; // Value value?: DateType | null; defaultValue?: DateType; /** [Legacy] Set default display picker view date */ pickerValue?: DateType; /** [Legacy] Set default display picker view date */ defaultPickerValue?: DateType; // Date disabledDate?: (date: DateType) => boolean; // Render dateRender?: DateRender; monthCellRender?: MonthCellRender; renderExtraFooter?: (mode: PanelMode) => VueNode; // Event onSelect?: (value: DateType) => void; onChange?: (value: DateType) => void; onPanelChange?: OnPanelChange; onMousedown?: (e: MouseEvent) => void; onOk?: (date: DateType) => void; direction?: 'ltr' | 'rtl'; /** @private This is internal usage. Do not use in your production env */ hideHeader?: boolean; /** @private This is internal usage. Do not use in your production env */ onPickerValueChange?: (date: DateType) => void; /** @private Internal usage. Do not use in your production env */ components?: Components; }; export type PickerPanelBaseProps = { picker: Exclude; } & PickerPanelSharedProps; export type PickerPanelDateProps = { picker?: 'date'; showToday?: boolean; showNow?: boolean; // Time showTime?: boolean | SharedTimeProps; disabledTime?: DisabledTime; } & PickerPanelSharedProps; export type PickerPanelTimeProps = { picker: 'time'; } & PickerPanelSharedProps & SharedTimeProps; export type PickerPanelProps = | PickerPanelBaseProps | PickerPanelDateProps | PickerPanelTimeProps; // TMP type to fit for ts 3.9.2 type OmitType = Omit, 'picker'> & Omit, 'picker'> & Omit, 'picker'>; type MergedPickerPanelProps = { picker?: PickerMode; } & OmitType; function PickerPanel() { return defineComponent>({ name: 'PickerPanel', inheritAttrs: false, props: { prefixCls: String, locale: Object, generateConfig: Object, value: Object, defaultValue: Object, pickerValue: Object, defaultPickerValue: Object, disabledDate: Function, mode: String, picker: { type: String, default: 'date' }, tabindex: { type: [Number, String], default: 0 }, showNow: { type: Boolean, default: undefined }, showTime: [Boolean, Object], showToday: Boolean, renderExtraFooter: Function, dateRender: Function, hideHeader: { type: Boolean, default: undefined }, onSelect: Function, onChange: Function, onPanelChange: Function, onMousedown: Function, onPickerValueChange: Function, onOk: Function, components: Object, direction: String, hourStep: { type: Number, default: 1 }, minuteStep: { type: Number, default: 1 }, secondStep: { type: Number, default: 1 }, } as any, setup(props, { attrs }) { const needConfirmButton = computed( () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', ); const isHourStepValid = computed(() => 24 % props.hourStep === 0); const isMinuteStepValid = computed(() => 60 % props.minuteStep === 0); const isSecondStepValid = computed(() => 60 % props.secondStep === 0); if (process.env.NODE_ENV !== 'production') { watchEffect(() => { const { generateConfig, value, hourStep = 1, minuteStep = 1, secondStep = 1 } = props; warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `value`.'); warning( !value || generateConfig.isValidate(value), 'Invalidate date pass to `defaultValue`.', ); warning( isHourStepValid.value, `\`hourStep\` ${hourStep} is invalid. It should be a factor of 24.`, ); warning( isMinuteStepValid.value, `\`minuteStep\` ${minuteStep} is invalid. It should be a factor of 60.`, ); warning( isSecondStepValid.value, `\`secondStep\` ${secondStep} is invalid. It should be a factor of 60.`, ); }); } const panelContext = useInjectPanel(); const { operationRef, panelRef: panelDivRef, onSelect: onContextSelect, hideRanges, defaultOpenValue, } = panelContext; const { inRange, panelPosition, rangedValue, hoverRangedValue } = useInjectRange(); const panelRef = ref({}); // Value const [mergedValue, setInnerValue] = useMergedState(null, { value: toRef(props, 'value'), defaultValue: props.defaultValue, postState: val => { if (!val && defaultOpenValue?.value && props.picker === 'time') { return defaultOpenValue.value; } return val; }, }); // View date control const [viewDate, setInnerViewDate] = useMergedState(null, { value: toRef(props, 'pickerValue'), defaultValue: props.defaultPickerValue || mergedValue.value, postState: date => { const { generateConfig, showTime, defaultValue } = props; const now = generateConfig.getNow(); if (!date) return now; // When value is null and set showTime if (!mergedValue.value && props.showTime) { if (typeof showTime === 'object') { return setDateTime(generateConfig, date, showTime.defaultValue || now); } if (defaultValue) { return setDateTime(generateConfig, date, defaultValue); } return setDateTime(generateConfig, date, now); } return date; }, }); const setViewDate = (date: DateType) => { setInnerViewDate(date); if (props.onPickerValueChange) { props.onPickerValueChange(date); } }; // Panel control const getInternalNextMode = (nextMode: PanelMode): PanelMode => { const getNextMode = PickerModeMap[props.picker!]; if (getNextMode) { return getNextMode(nextMode); } return nextMode; }; // Save panel is changed from which panel const [mergedMode, setInnerMode] = useMergedState( () => { if (props.picker === 'time') { return 'time'; } return getInternalNextMode('date'); }, { value: toRef(props, 'mode'), }, ); watch( () => props.picker, () => { setInnerMode(props.picker); }, ); const sourceMode = ref(mergedMode.value); const setSourceMode = (val: PanelMode) => { sourceMode.value = val; }; const onInternalPanelChange = (newMode: PanelMode | null, viewValue: DateType) => { const { onPanelChange, generateConfig } = props; const nextMode = getInternalNextMode(newMode || mergedMode.value); setSourceMode(mergedMode.value); setInnerMode(nextMode); if ( onPanelChange && (mergedMode.value !== nextMode || isEqual(generateConfig, viewDate.value, viewDate.value)) ) { onPanelChange(viewValue, nextMode); } }; const triggerSelect = ( date: DateType, type: 'key' | 'mouse' | 'submit', forceTriggerSelect = false, ) => { const { picker, generateConfig, onSelect, onChange, disabledDate } = props; if (mergedMode.value === picker || forceTriggerSelect) { setInnerValue(date); if (onSelect) { onSelect(date); } if (onContextSelect) { onContextSelect(date, type); } if ( onChange && !isEqual(generateConfig, date, mergedValue.value) && !disabledDate?.(date) ) { onChange(date); } } }; // ========================= Interactive ========================== const onInternalKeydown = (e: KeyboardEvent) => { if (panelRef.value && panelRef.value.onKeydown) { if ( [ KeyCode.LEFT, KeyCode.RIGHT, KeyCode.UP, KeyCode.DOWN, KeyCode.PAGE_UP, KeyCode.PAGE_DOWN, KeyCode.ENTER, ].includes(e.which) ) { e.preventDefault(); } return panelRef.value.onKeydown(e); } /* istanbul ignore next */ /* eslint-disable no-lone-blocks */ { warning( false, 'Panel not correct handle keyDown event. Please help to fire issue about this.', ); return false; } /* eslint-enable no-lone-blocks */ }; const onInternalBlur = (e: FocusEvent) => { if (panelRef.value && panelRef.value.onBlur) { panelRef.value.onBlur(e); } }; const onNow = () => { const { generateConfig, hourStep, minuteStep, secondStep } = props; const now = generateConfig.getNow(); const lowerBoundTime = getLowerBoundTime( generateConfig.getHour(now), generateConfig.getMinute(now), generateConfig.getSecond(now), isHourStepValid.value ? hourStep : 1, isMinuteStepValid.value ? minuteStep : 1, isSecondStepValid.value ? secondStep : 1, ); const adjustedNow = setTime( generateConfig, now, lowerBoundTime[0], // hour lowerBoundTime[1], // minute lowerBoundTime[2], // second ); triggerSelect(adjustedNow, 'submit'); }; const classString = computed(() => { const { prefixCls, direction } = props; return classNames(`${prefixCls}-panel`, { [`${prefixCls}-panel-has-range`]: rangedValue && rangedValue.value && rangedValue.value[0] && rangedValue.value[1], [`${prefixCls}-panel-has-range-hover`]: hoverRangedValue && hoverRangedValue.value && hoverRangedValue.value[0] && hoverRangedValue.value[1], [`${prefixCls}-panel-rtl`]: direction === 'rtl', }); }); useProvidePanel({ ...panelContext, mode: mergedMode, hideHeader: computed(() => props.hideHeader !== undefined ? props.hideHeader : panelContext.hideHeader?.value, ), hidePrevBtn: computed(() => inRange.value && panelPosition.value === 'right'), hideNextBtn: computed(() => inRange.value && panelPosition.value === 'left'), }); watch( () => props.value, () => { if (props.value) { setInnerViewDate(props.value); } }, ); return () => { const { prefixCls = 'ant-picker', locale, generateConfig, disabledDate, picker = 'date', tabindex = 0, showNow, showTime, showToday, renderExtraFooter, onMousedown, onOk, components, } = props; if (operationRef && panelPosition.value !== 'right') { operationRef.value = { onKeydown: onInternalKeydown, onClose: () => { if (panelRef.value && panelRef.value.onClose) { panelRef.value.onClose(); } }, }; } // ============================ Panels ============================ let panelNode: VueNode; const pickerProps = { ...attrs, ...(props as MergedPickerPanelProps), operationRef: panelRef, prefixCls, viewDate: viewDate.value, value: mergedValue.value, onViewDateChange: setViewDate, sourceMode: sourceMode.value, onPanelChange: onInternalPanelChange, disabledDate, }; delete pickerProps.onChange; delete pickerProps.onSelect; switch (mergedMode.value) { case 'decade': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'year': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'month': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'quarter': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'week': panelNode = ( { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'time': delete pickerProps.showTime; panelNode = ( {...pickerProps} {...(typeof showTime === 'object' ? showTime : null)} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; default: if (showTime) { panelNode = ( { setViewDate(date); triggerSelect(date, type); }} /> ); } else { panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); } } // ============================ Footer ============================ let extraFooter: VueNode; let rangesNode: VueNode; if (!hideRanges?.value) { extraFooter = getExtraFooter(prefixCls, mergedMode.value, renderExtraFooter); rangesNode = getRanges({ prefixCls, components, needConfirmButton: needConfirmButton.value, okDisabled: !mergedValue.value || (disabledDate && disabledDate(mergedValue.value)), locale, showNow, onNow: needConfirmButton.value && onNow, onOk: () => { if (mergedValue.value) { triggerSelect(mergedValue.value, 'submit', true); if (onOk) { onOk(mergedValue.value); } } }, }); } let todayNode: VueNode; if (showToday && mergedMode.value === 'date' && picker === 'date' && !showTime) { const now = generateConfig.getNow(); const todayCls = `${prefixCls}-today-btn`; const disabled = disabledDate && disabledDate(now); todayNode = ( { if (!disabled) { triggerSelect(now, 'mouse', true); } }} > {locale.today} ); } return (
{panelNode} {extraFooter || rangesNode || todayNode ? (
{extraFooter} {rangesNode} {todayNode}
) : null}
); }; }, }); } const InterPickerPanel = PickerPanel(); export default (props: MergedPickerPanelProps): JSX.Element => createVNode(InterPickerPanel, props);