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.
ant-design-vue/components/vc-picker/PickerPanel.tsx

621 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 type { CSSProperties } from 'vue';
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,
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,
Array.isArray(date) ? date[0] : date,
showTime.defaultValue || now,
);
}
if (defaultValue) {
return setDateTime(
generateConfig,
Array.isArray(date) ? date[0] : date,
defaultValue,
);
}
return setDateTime(generateConfig, Array.isArray(date) ? date[0] : 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 as CSSProperties}
onKeydown={onInternalKeydown}
onBlur={onInternalBlur}
onMousedown={onMousedown}
>
{panelNode}
{extraFooter || rangesNode || todayNode ? (
<div class={`${prefixCls}-footer`}>
{extraFooter}
{rangesNode}
{todayNode}
</div>
) : null}
</div>
);
};
},
});
}
const InterPickerPanel = PickerPanel<any>();
export default <DateType,>(props: MergedPickerPanelProps<DateType>): JSX.Element =>
createVNode(InterPickerPanel, props);