570 lines
15 KiB
Vue
570 lines
15 KiB
Vue
/**
|
|
* Logic:
|
|
* When `mode` === `picker`,
|
|
* click will trigger `onSelect` (if value changed trigger `onChange` also).
|
|
* Panel change will not trigger `onSelect` but trigger `onPanelChange`
|
|
*/
|
|
|
|
import * as React from 'react';
|
|
import classNames from 'classnames';
|
|
import KeyCode from 'rc-util/lib/KeyCode';
|
|
import warning from 'rc-util/lib/warning';
|
|
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
|
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 PanelContext from './PanelContext';
|
|
import type { DateRender } from './panels/DatePanel/DateBody';
|
|
import { PickerModeMap } from './utils/uiUtil';
|
|
import type { MonthCellRender } from './panels/MonthPanel/MonthBody';
|
|
import RangeContext from './RangeContext';
|
|
import getExtraFooter from './utils/getExtraFooter';
|
|
import getRanges from './utils/getRanges';
|
|
import { getLowerBoundTime, setDateTime, setTime } from './utils/timeUtil';
|
|
|
|
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) => React.ReactNode;
|
|
|
|
// Event
|
|
onSelect?: (value: DateType) => void;
|
|
onChange?: (value: DateType) => void;
|
|
onPanelChange?: OnPanelChange<DateType>;
|
|
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
|
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>(props: PickerPanelProps<DateType>) {
|
|
const {
|
|
prefixCls = 'rc-picker',
|
|
className,
|
|
style,
|
|
locale,
|
|
generateConfig,
|
|
value,
|
|
defaultValue,
|
|
pickerValue,
|
|
defaultPickerValue,
|
|
disabledDate,
|
|
mode,
|
|
picker = 'date',
|
|
tabIndex = 0,
|
|
showNow,
|
|
showTime,
|
|
showToday,
|
|
renderExtraFooter,
|
|
hideHeader,
|
|
onSelect,
|
|
onChange,
|
|
onPanelChange,
|
|
onMouseDown,
|
|
onPickerValueChange,
|
|
onOk,
|
|
components,
|
|
direction,
|
|
hourStep = 1,
|
|
minuteStep = 1,
|
|
secondStep = 1,
|
|
} = props as MergedPickerPanelProps<DateType>;
|
|
|
|
const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time';
|
|
|
|
const isHourStepValid = 24 % hourStep === 0;
|
|
const isMinuteStepValid = 60 % minuteStep === 0;
|
|
const isSecondStepValid = 60 % secondStep === 0;
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `value`.');
|
|
warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `defaultValue`.');
|
|
warning(isHourStepValid, `\`hourStep\` ${hourStep} is invalid. It should be a factor of 24.`);
|
|
warning(
|
|
isMinuteStepValid,
|
|
`\`minuteStep\` ${minuteStep} is invalid. It should be a factor of 60.`,
|
|
);
|
|
warning(
|
|
isSecondStepValid,
|
|
`\`secondStep\` ${secondStep} is invalid. It should be a factor of 60.`,
|
|
);
|
|
}
|
|
|
|
// ============================ State =============================
|
|
|
|
const panelContext = React.useContext(PanelContext);
|
|
const {
|
|
operationRef,
|
|
panelRef: panelDivRef,
|
|
onSelect: onContextSelect,
|
|
hideRanges,
|
|
defaultOpenValue,
|
|
} = panelContext;
|
|
|
|
const { inRange, panelPosition, rangedValue, hoverRangedValue } = React.useContext(RangeContext);
|
|
const panelRef = React.useRef<PanelRefProps>({});
|
|
|
|
// Handle init logic
|
|
const initRef = React.useRef(true);
|
|
|
|
// Value
|
|
const [mergedValue, setInnerValue] = useMergedState(null, {
|
|
value,
|
|
defaultValue,
|
|
postState: (val) => {
|
|
if (!val && defaultOpenValue && picker === 'time') {
|
|
return defaultOpenValue;
|
|
}
|
|
return val;
|
|
},
|
|
});
|
|
|
|
// View date control
|
|
const [viewDate, setInnerViewDate] = useMergedState<DateType | null, DateType>(null, {
|
|
value: pickerValue,
|
|
defaultValue: defaultPickerValue || mergedValue,
|
|
postState: (date) => {
|
|
const now = generateConfig.getNow();
|
|
if (!date) return now;
|
|
// When value is null and set showTime
|
|
if (!mergedValue && 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 (onPickerValueChange) {
|
|
onPickerValueChange(date);
|
|
}
|
|
};
|
|
|
|
// Panel control
|
|
const getInternalNextMode = (nextMode: PanelMode): PanelMode => {
|
|
const getNextMode = PickerModeMap[picker!];
|
|
if (getNextMode) {
|
|
return getNextMode(nextMode);
|
|
}
|
|
|
|
return nextMode;
|
|
};
|
|
|
|
// Save panel is changed from which panel
|
|
const [mergedMode, setInnerMode] = useMergedState(
|
|
() => {
|
|
if (picker === 'time') {
|
|
return 'time';
|
|
}
|
|
return getInternalNextMode('date');
|
|
},
|
|
{
|
|
value: mode,
|
|
},
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
setInnerMode(picker);
|
|
}, [picker]);
|
|
|
|
const [sourceMode, setSourceMode] = React.useState<PanelMode>(() => mergedMode);
|
|
|
|
const onInternalPanelChange = (newMode: PanelMode | null, viewValue: DateType) => {
|
|
const nextMode = getInternalNextMode(newMode || mergedMode);
|
|
setSourceMode(mergedMode);
|
|
setInnerMode(nextMode);
|
|
|
|
if (onPanelChange && (mergedMode !== nextMode || isEqual(generateConfig, viewDate, viewDate))) {
|
|
onPanelChange(viewValue, nextMode);
|
|
}
|
|
};
|
|
|
|
const triggerSelect = (
|
|
date: DateType,
|
|
type: 'key' | 'mouse' | 'submit',
|
|
forceTriggerSelect: boolean = false,
|
|
) => {
|
|
if (mergedMode === picker || forceTriggerSelect) {
|
|
setInnerValue(date);
|
|
|
|
if (onSelect) {
|
|
onSelect(date);
|
|
}
|
|
|
|
if (onContextSelect) {
|
|
onContextSelect(date, type);
|
|
}
|
|
|
|
if (onChange && !isEqual(generateConfig, date, mergedValue) && !disabledDate?.(date)) {
|
|
onChange(date);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ========================= Interactive ==========================
|
|
const onInternalKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
|
|
if (panelRef.current && panelRef.current.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.current.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: React.FocusEventHandler<HTMLElement> = (e) => {
|
|
if (panelRef.current && panelRef.current.onBlur) {
|
|
panelRef.current.onBlur(e);
|
|
}
|
|
};
|
|
|
|
if (operationRef && panelPosition !== 'right') {
|
|
operationRef.current = {
|
|
onKeyDown: onInternalKeyDown,
|
|
onClose: () => {
|
|
if (panelRef.current && panelRef.current.onClose) {
|
|
panelRef.current.onClose();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
// ============================ Effect ============================
|
|
React.useEffect(() => {
|
|
if (value && !initRef.current) {
|
|
setInnerViewDate(value);
|
|
}
|
|
}, [value]);
|
|
|
|
React.useEffect(() => {
|
|
initRef.current = false;
|
|
}, []);
|
|
|
|
// ============================ Panels ============================
|
|
let panelNode: React.ReactNode;
|
|
|
|
const pickerProps = {
|
|
...(props as MergedPickerPanelProps<DateType>),
|
|
operationRef: panelRef,
|
|
prefixCls,
|
|
viewDate,
|
|
value: mergedValue,
|
|
onViewDateChange: setViewDate,
|
|
sourceMode,
|
|
onPanelChange: onInternalPanelChange,
|
|
disabledDate,
|
|
};
|
|
delete pickerProps.onChange;
|
|
delete pickerProps.onSelect;
|
|
|
|
switch (mergedMode) {
|
|
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: React.ReactNode;
|
|
let rangesNode: React.ReactNode;
|
|
|
|
const onNow = () => {
|
|
const now = generateConfig.getNow();
|
|
const lowerBoundTime = getLowerBoundTime(
|
|
generateConfig.getHour(now),
|
|
generateConfig.getMinute(now),
|
|
generateConfig.getSecond(now),
|
|
isHourStepValid ? hourStep : 1,
|
|
isMinuteStepValid ? minuteStep : 1,
|
|
isSecondStepValid ? secondStep : 1,
|
|
);
|
|
const adjustedNow = setTime(
|
|
generateConfig,
|
|
now,
|
|
lowerBoundTime[0], // hour
|
|
lowerBoundTime[1], // minute
|
|
lowerBoundTime[2], // second
|
|
);
|
|
triggerSelect(adjustedNow, 'submit');
|
|
};
|
|
|
|
if (!hideRanges) {
|
|
extraFooter = getExtraFooter(prefixCls, mergedMode, renderExtraFooter);
|
|
rangesNode = getRanges({
|
|
prefixCls,
|
|
components,
|
|
needConfirmButton,
|
|
okDisabled: !mergedValue || (disabledDate && disabledDate(mergedValue)),
|
|
locale,
|
|
showNow,
|
|
onNow: needConfirmButton && onNow,
|
|
onOk: () => {
|
|
if (mergedValue) {
|
|
triggerSelect(mergedValue, 'submit', true);
|
|
if (onOk) {
|
|
onOk(mergedValue);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
let todayNode: React.ReactNode;
|
|
|
|
if (showToday && mergedMode === '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 (
|
|
<PanelContext.Provider
|
|
value={{
|
|
...panelContext,
|
|
mode: mergedMode,
|
|
hideHeader: 'hideHeader' in props ? hideHeader : panelContext.hideHeader,
|
|
hidePrevBtn: inRange && panelPosition === 'right',
|
|
hideNextBtn: inRange && panelPosition === 'left',
|
|
}}
|
|
>
|
|
<div
|
|
tabindex={tabIndex}
|
|
class={classNames(`${prefixCls}-panel`, className, {
|
|
[`${prefixCls}-panel-has-range`]: rangedValue && rangedValue[0] && rangedValue[1],
|
|
[`${prefixCls}-panel-has-range-hover`]:
|
|
hoverRangedValue && hoverRangedValue[0] && hoverRangedValue[1],
|
|
[`${prefixCls}-panel-rtl`]: direction === 'rtl',
|
|
})}
|
|
style={style}
|
|
onKeydown={onInternalKeyDown}
|
|
onBlur={onInternalBlur}
|
|
onMousedown={onMouseDown}
|
|
ref={panelDivRef}
|
|
>
|
|
{panelNode}
|
|
{extraFooter || rangesNode || todayNode ? (
|
|
<div class={`${prefixCls}-footer`}>
|
|
{extraFooter}
|
|
{rangesNode}
|
|
{todayNode}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</PanelContext.Provider>
|
|
);
|
|
}
|
|
|
|
export default PickerPanel;
|
|
/* eslint-enable */
|