vuecssuiant-designantdreactantantd-vueenterprisefrontendui-designvue-antdvue-antd-uivue3vuecomponent
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
613 lines
19 KiB
613 lines
19 KiB
/** |
|
* 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<DateType> = { |
|
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<DateType>; |
|
|
|
// 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<DateType>; |
|
monthCellRender?: MonthCellRender<DateType>; |
|
renderExtraFooter?: (mode: PanelMode) => VueNode; |
|
|
|
// Event |
|
onSelect?: (value: DateType) => void; |
|
onChange?: (value: DateType) => void; |
|
onPanelChange?: OnPanelChange<DateType>; |
|
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<DateType> = { |
|
picker: Exclude<PickerMode, 'date' | 'time'>; |
|
} & PickerPanelSharedProps<DateType>; |
|
|
|
export type PickerPanelDateProps<DateType> = { |
|
picker?: 'date'; |
|
showToday?: boolean; |
|
showNow?: boolean; |
|
|
|
// Time |
|
showTime?: boolean | SharedTimeProps<DateType>; |
|
disabledTime?: DisabledTime<DateType>; |
|
} & PickerPanelSharedProps<DateType>; |
|
|
|
export type PickerPanelTimeProps<DateType> = { |
|
picker: 'time'; |
|
} & PickerPanelSharedProps<DateType> & |
|
SharedTimeProps<DateType>; |
|
|
|
export type PickerPanelProps<DateType> = |
|
| PickerPanelBaseProps<DateType> |
|
| PickerPanelDateProps<DateType> |
|
| PickerPanelTimeProps<DateType>; |
|
|
|
// TMP type to fit for ts 3.9.2 |
|
type OmitType<DateType> = Omit<PickerPanelBaseProps<DateType>, 'picker'> & |
|
Omit<PickerPanelDateProps<DateType>, 'picker'> & |
|
Omit<PickerPanelTimeProps<DateType>, 'picker'>; |
|
type MergedPickerPanelProps<DateType> = { |
|
picker?: PickerMode; |
|
} & OmitType<DateType>; |
|
|
|
function PickerPanel<DateType>() { |
|
return defineComponent<MergedPickerPanelProps<DateType>>({ |
|
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<PanelRefProps>({}); |
|
// Value |
|
const [mergedValue, setInnerValue] = useMergedState<DateType | null>(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<DateType | null>(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<DateType>), |
|
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 = ( |
|
<DecadePanel<DateType> |
|
{...pickerProps} |
|
onSelect={(date, type) => { |
|
setViewDate(date); |
|
triggerSelect(date, type); |
|
}} |
|
/> |
|
); |
|
break; |
|
|
|
case 'year': |
|
panelNode = ( |
|
<YearPanel<DateType> |
|
{...pickerProps} |
|
onSelect={(date, type) => { |
|
setViewDate(date); |
|
triggerSelect(date, type); |
|
}} |
|
/> |
|
); |
|
break; |
|
|
|
case 'month': |
|
panelNode = ( |
|
<MonthPanel<DateType> |
|
{...pickerProps} |
|
onSelect={(date, type) => { |
|
setViewDate(date); |
|
triggerSelect(date, type); |
|
}} |
|
/> |
|
); |
|
break; |
|
|
|
case 'quarter': |
|
panelNode = ( |
|
<QuarterPanel<DateType> |
|
{...pickerProps} |
|
onSelect={(date, type) => { |
|
setViewDate(date); |
|
triggerSelect(date, type); |
|
}} |
|
/> |
|
); |
|
break; |
|
|
|
case 'week': |
|
panelNode = ( |
|
<WeekPanel |
|
{...pickerProps} |
|
onSelect={(date, type) => { |
|
setViewDate(date); |
|
triggerSelect(date, type); |
|
}} |
|
/> |
|
); |
|
break; |
|
|
|
case 'time': |
|
delete pickerProps.showTime; |
|
panelNode = ( |
|
<TimePanel<DateType> |
|
{...pickerProps} |
|
{...(typeof showTime === 'object' ? showTime : null)} |
|
onSelect={(date, type) => { |
|
setViewDate(date); |
|
triggerSelect(date, type); |
|
}} |
|
/> |
|
); |
|
break; |
|
|
|
default: |
|
if (showTime) { |
|
panelNode = ( |
|
<DatetimePanel |
|
{...pickerProps} |
|
onSelect={(date, type) => { |
|
setViewDate(date); |
|
triggerSelect(date, type); |
|
}} |
|
/> |
|
); |
|
} else { |
|
panelNode = ( |
|
<DatePanel<DateType> |
|
{...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 = ( |
|
<a |
|
class={classNames(todayCls, disabled && `${todayCls}-disabled`)} |
|
aria-disabled={disabled} |
|
onClick={() => { |
|
if (!disabled) { |
|
triggerSelect(now, 'mouse', true); |
|
} |
|
}} |
|
> |
|
{locale.today} |
|
</a> |
|
); |
|
} |
|
return ( |
|
<div |
|
tabindex={tabindex} |
|
class={classNames(classString.value, attrs.class)} |
|
style={attrs.style} |
|
onKeydown={onInternalKeydown} |
|
onBlur={onInternalBlur} |
|
onMousedown={onMousedown} |
|
ref={panelDivRef} |
|
> |
|
{panelNode} |
|
{extraFooter || rangesNode || todayNode ? ( |
|
<div class={`${prefixCls}-footer`}> |
|
{extraFooter} |
|
{rangesNode} |
|
{todayNode} |
|
</div> |
|
) : null} |
|
</div> |
|
); |
|
}; |
|
}, |
|
}); |
|
} |
|
const InterPickerPanel = PickerPanel<any>(); |
|
export default <DateType extends any>(props: MergedPickerPanelProps<DateType>): JSX.Element => |
|
createVNode(InterPickerPanel, props);
|
|
|