From a020c2f6816d2adc0ce1133a1a305ee8030d9cbb Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 15 Jun 2021 15:26:53 +0800 Subject: [PATCH] refactor: date --- components/vc-picker/PanelContext.tsx | 38 + components/vc-picker/Picker.tsx | 561 ++++++++ components/vc-picker/PickerPanel.tsx | 569 ++++++++ components/vc-picker/PickerTrigger.tsx | 104 ++ components/vc-picker/RangeContext.tsx | 27 + components/vc-picker/RangePicker.tsx | 1181 +++++++++++++++++ components/vc-picker/generate/dayjs.ts | 132 ++ components/vc-picker/generate/moment.ts | 140 ++ .../vc-picker/hooks/useCellClassName.ts | 109 ++ components/vc-picker/hooks/useHoverValue.ts | 44 + components/vc-picker/hooks/usePickerInput.ts | 171 +++ .../vc-picker/hooks/useRangeDisabled.ts | 113 ++ .../vc-picker/hooks/useRangeViewDates.ts | 121 ++ .../vc-picker/hooks/useTextValueMapping.ts | 31 + components/vc-picker/hooks/useValueTexts.ts | 42 + components/vc-picker/index.tsx | 13 + components/vc-picker/interface.ts | 109 ++ components/vc-picker/locale/ar_EG.ts | 32 + components/vc-picker/locale/az_AZ.ts | 33 + components/vc-picker/locale/bg_BG.ts | 31 + components/vc-picker/locale/by_BY.ts | 33 + components/vc-picker/locale/ca_ES.ts | 32 + components/vc-picker/locale/cs_CZ.ts | 32 + components/vc-picker/locale/da_DK.ts | 32 + components/vc-picker/locale/de_DE.ts | 32 + components/vc-picker/locale/el_GR.ts | 32 + components/vc-picker/locale/en_GB.ts | 32 + components/vc-picker/locale/en_US.ts | 33 + components/vc-picker/locale/es_ES.ts | 32 + components/vc-picker/locale/et_EE.ts | 32 + components/vc-picker/locale/fa_IR.ts | 32 + components/vc-picker/locale/fi_FI.ts | 32 + components/vc-picker/locale/fr_BE.ts | 32 + components/vc-picker/locale/fr_CA.ts | 32 + components/vc-picker/locale/fr_FR.ts | 32 + components/vc-picker/locale/ga_IE.ts | 33 + components/vc-picker/locale/gl_ES.ts | 32 + components/vc-picker/locale/he_IL.ts | 33 + components/vc-picker/locale/hi_IN.ts | 33 + components/vc-picker/locale/hr_HR.ts | 33 + components/vc-picker/locale/hu_HU.ts | 32 + components/vc-picker/locale/id_ID.ts | 33 + components/vc-picker/locale/is_IS.ts | 32 + components/vc-picker/locale/it_IT.ts | 32 + components/vc-picker/locale/ja_JP.ts | 32 + components/vc-picker/locale/kk_KZ.ts | 32 + components/vc-picker/locale/km_KH.ts | 32 + components/vc-picker/locale/kmr_IQ.ts | 32 + components/vc-picker/locale/kn_IN.ts | 33 + components/vc-picker/locale/ko_KR.ts | 32 + components/vc-picker/locale/lt_LT.ts | 32 + components/vc-picker/locale/lv_LV.ts | 32 + components/vc-picker/locale/mk_MK.ts | 32 + components/vc-picker/locale/ml_IN.ts | 33 + components/vc-picker/locale/mm_MM.ts | 33 + components/vc-picker/locale/mn_MN.ts | 33 + components/vc-picker/locale/ms_MY.ts | 32 + components/vc-picker/locale/nb_NO.ts | 33 + components/vc-picker/locale/nl_BE.ts | 32 + components/vc-picker/locale/nl_NL.ts | 32 + components/vc-picker/locale/pl_PL.ts | 32 + components/vc-picker/locale/pt_BR.ts | 47 + components/vc-picker/locale/pt_PT.ts | 32 + components/vc-picker/locale/ro_RO.ts | 33 + components/vc-picker/locale/ru_RU.ts | 32 + components/vc-picker/locale/sk_SK.ts | 32 + components/vc-picker/locale/sl_SI.ts | 32 + components/vc-picker/locale/sr_RS.ts | 32 + components/vc-picker/locale/sv_SE.ts | 32 + components/vc-picker/locale/ta_IN.ts | 33 + components/vc-picker/locale/th_TH.ts | 32 + components/vc-picker/locale/tr_TR.ts | 32 + components/vc-picker/locale/ug_CN.ts | 31 + components/vc-picker/locale/uk_UA.ts | 32 + components/vc-picker/locale/vi_VN.ts | 33 + components/vc-picker/locale/zh_CN.ts | 32 + components/vc-picker/locale/zh_TW.ts | 33 + .../vc-picker/panels/DatePanel/DateBody.tsx | 111 ++ .../vc-picker/panels/DatePanel/DateHeader.tsx | 105 ++ .../vc-picker/panels/DatePanel/index.tsx | 116 ++ .../vc-picker/panels/DatetimePanel/index.tsx | 186 +++ .../panels/DecadePanel/DecadeBody.tsx | 68 + .../panels/DecadePanel/DecadeHeader.tsx | 51 + .../vc-picker/panels/DecadePanel/index.tsx | 96 ++ components/vc-picker/panels/Header.tsx | 100 ++ .../vc-picker/panels/MonthPanel/MonthBody.tsx | 89 ++ .../panels/MonthPanel/MonthHeader.tsx | 58 + .../vc-picker/panels/MonthPanel/index.tsx | 86 ++ components/vc-picker/panels/PanelBody.tsx | 144 ++ .../panels/QuarterPanel/QuarterBody.tsx | 71 + .../panels/QuarterPanel/QuarterHeader.tsx | 57 + .../vc-picker/panels/QuarterPanel/index.tsx | 76 ++ .../vc-picker/panels/TimePanel/TimeBody.tsx | 249 ++++ .../vc-picker/panels/TimePanel/TimeHeader.tsx | 42 + .../panels/TimePanel/TimeUnitColumn.tsx | 97 ++ .../vc-picker/panels/TimePanel/index.tsx | 98 ++ .../vc-picker/panels/WeekPanel/index.tsx | 52 + .../vc-picker/panels/YearPanel/YearBody.tsx | 79 ++ .../vc-picker/panels/YearPanel/YearHeader.tsx | 50 + .../vc-picker/panels/YearPanel/index.tsx | 95 ++ components/vc-picker/utils/dateUtil.ts | 321 +++++ components/vc-picker/utils/getExtraFooter.tsx | 16 + components/vc-picker/utils/getRanges.tsx | 79 ++ components/vc-picker/utils/miscUtil.ts | 58 + components/vc-picker/utils/timeUtil.ts | 70 + components/vc-picker/utils/uiUtil.ts | 276 ++++ tsconfig.json | 17 +- 107 files changed, 8462 insertions(+), 6 deletions(-) create mode 100644 components/vc-picker/PanelContext.tsx create mode 100644 components/vc-picker/Picker.tsx create mode 100644 components/vc-picker/PickerPanel.tsx create mode 100644 components/vc-picker/PickerTrigger.tsx create mode 100644 components/vc-picker/RangeContext.tsx create mode 100644 components/vc-picker/RangePicker.tsx create mode 100644 components/vc-picker/generate/dayjs.ts create mode 100644 components/vc-picker/generate/moment.ts create mode 100644 components/vc-picker/hooks/useCellClassName.ts create mode 100644 components/vc-picker/hooks/useHoverValue.ts create mode 100644 components/vc-picker/hooks/usePickerInput.ts create mode 100644 components/vc-picker/hooks/useRangeDisabled.ts create mode 100644 components/vc-picker/hooks/useRangeViewDates.ts create mode 100644 components/vc-picker/hooks/useTextValueMapping.ts create mode 100644 components/vc-picker/hooks/useValueTexts.ts create mode 100644 components/vc-picker/index.tsx create mode 100644 components/vc-picker/interface.ts create mode 100644 components/vc-picker/locale/ar_EG.ts create mode 100644 components/vc-picker/locale/az_AZ.ts create mode 100644 components/vc-picker/locale/bg_BG.ts create mode 100644 components/vc-picker/locale/by_BY.ts create mode 100644 components/vc-picker/locale/ca_ES.ts create mode 100644 components/vc-picker/locale/cs_CZ.ts create mode 100644 components/vc-picker/locale/da_DK.ts create mode 100644 components/vc-picker/locale/de_DE.ts create mode 100644 components/vc-picker/locale/el_GR.ts create mode 100644 components/vc-picker/locale/en_GB.ts create mode 100644 components/vc-picker/locale/en_US.ts create mode 100644 components/vc-picker/locale/es_ES.ts create mode 100644 components/vc-picker/locale/et_EE.ts create mode 100644 components/vc-picker/locale/fa_IR.ts create mode 100644 components/vc-picker/locale/fi_FI.ts create mode 100644 components/vc-picker/locale/fr_BE.ts create mode 100644 components/vc-picker/locale/fr_CA.ts create mode 100644 components/vc-picker/locale/fr_FR.ts create mode 100644 components/vc-picker/locale/ga_IE.ts create mode 100644 components/vc-picker/locale/gl_ES.ts create mode 100644 components/vc-picker/locale/he_IL.ts create mode 100644 components/vc-picker/locale/hi_IN.ts create mode 100644 components/vc-picker/locale/hr_HR.ts create mode 100644 components/vc-picker/locale/hu_HU.ts create mode 100644 components/vc-picker/locale/id_ID.ts create mode 100644 components/vc-picker/locale/is_IS.ts create mode 100644 components/vc-picker/locale/it_IT.ts create mode 100644 components/vc-picker/locale/ja_JP.ts create mode 100644 components/vc-picker/locale/kk_KZ.ts create mode 100644 components/vc-picker/locale/km_KH.ts create mode 100644 components/vc-picker/locale/kmr_IQ.ts create mode 100644 components/vc-picker/locale/kn_IN.ts create mode 100644 components/vc-picker/locale/ko_KR.ts create mode 100644 components/vc-picker/locale/lt_LT.ts create mode 100644 components/vc-picker/locale/lv_LV.ts create mode 100644 components/vc-picker/locale/mk_MK.ts create mode 100644 components/vc-picker/locale/ml_IN.ts create mode 100644 components/vc-picker/locale/mm_MM.ts create mode 100644 components/vc-picker/locale/mn_MN.ts create mode 100644 components/vc-picker/locale/ms_MY.ts create mode 100644 components/vc-picker/locale/nb_NO.ts create mode 100644 components/vc-picker/locale/nl_BE.ts create mode 100644 components/vc-picker/locale/nl_NL.ts create mode 100644 components/vc-picker/locale/pl_PL.ts create mode 100644 components/vc-picker/locale/pt_BR.ts create mode 100644 components/vc-picker/locale/pt_PT.ts create mode 100644 components/vc-picker/locale/ro_RO.ts create mode 100644 components/vc-picker/locale/ru_RU.ts create mode 100644 components/vc-picker/locale/sk_SK.ts create mode 100644 components/vc-picker/locale/sl_SI.ts create mode 100644 components/vc-picker/locale/sr_RS.ts create mode 100644 components/vc-picker/locale/sv_SE.ts create mode 100644 components/vc-picker/locale/ta_IN.ts create mode 100644 components/vc-picker/locale/th_TH.ts create mode 100644 components/vc-picker/locale/tr_TR.ts create mode 100644 components/vc-picker/locale/ug_CN.ts create mode 100644 components/vc-picker/locale/uk_UA.ts create mode 100644 components/vc-picker/locale/vi_VN.ts create mode 100644 components/vc-picker/locale/zh_CN.ts create mode 100644 components/vc-picker/locale/zh_TW.ts create mode 100644 components/vc-picker/panels/DatePanel/DateBody.tsx create mode 100644 components/vc-picker/panels/DatePanel/DateHeader.tsx create mode 100644 components/vc-picker/panels/DatePanel/index.tsx create mode 100644 components/vc-picker/panels/DatetimePanel/index.tsx create mode 100644 components/vc-picker/panels/DecadePanel/DecadeBody.tsx create mode 100644 components/vc-picker/panels/DecadePanel/DecadeHeader.tsx create mode 100644 components/vc-picker/panels/DecadePanel/index.tsx create mode 100644 components/vc-picker/panels/Header.tsx create mode 100644 components/vc-picker/panels/MonthPanel/MonthBody.tsx create mode 100644 components/vc-picker/panels/MonthPanel/MonthHeader.tsx create mode 100644 components/vc-picker/panels/MonthPanel/index.tsx create mode 100644 components/vc-picker/panels/PanelBody.tsx create mode 100644 components/vc-picker/panels/QuarterPanel/QuarterBody.tsx create mode 100644 components/vc-picker/panels/QuarterPanel/QuarterHeader.tsx create mode 100644 components/vc-picker/panels/QuarterPanel/index.tsx create mode 100644 components/vc-picker/panels/TimePanel/TimeBody.tsx create mode 100644 components/vc-picker/panels/TimePanel/TimeHeader.tsx create mode 100644 components/vc-picker/panels/TimePanel/TimeUnitColumn.tsx create mode 100644 components/vc-picker/panels/TimePanel/index.tsx create mode 100644 components/vc-picker/panels/WeekPanel/index.tsx create mode 100644 components/vc-picker/panels/YearPanel/YearBody.tsx create mode 100644 components/vc-picker/panels/YearPanel/YearHeader.tsx create mode 100644 components/vc-picker/panels/YearPanel/index.tsx create mode 100644 components/vc-picker/utils/dateUtil.ts create mode 100644 components/vc-picker/utils/getExtraFooter.tsx create mode 100644 components/vc-picker/utils/getRanges.tsx create mode 100644 components/vc-picker/utils/miscUtil.ts create mode 100644 components/vc-picker/utils/timeUtil.ts create mode 100644 components/vc-picker/utils/uiUtil.ts diff --git a/components/vc-picker/PanelContext.tsx b/components/vc-picker/PanelContext.tsx new file mode 100644 index 000000000..85d8341e8 --- /dev/null +++ b/components/vc-picker/PanelContext.tsx @@ -0,0 +1,38 @@ +import { inject, InjectionKey, provide, Ref } from 'vue'; +import type { OnSelect, PanelMode } from './interface'; + +export type ContextOperationRefProps = { + onKeyDown?: (e: KeyboardEvent) => boolean; + onClose?: () => void; +}; + +export type PanelContextProps = { + operationRef?: Ref; + /** Only work with time panel */ + hideHeader?: boolean; + panelRef?: Ref; + hidePrevBtn?: boolean; + hideNextBtn?: boolean; + onDateMouseEnter?: (date: any) => void; + onDateMouseLeave?: (date: any) => void; + onSelect?: OnSelect; + hideRanges?: boolean; + open?: boolean; + mode?: PanelMode; + + /** Only used for TimePicker and this is a deprecated prop */ + defaultOpenValue?: any; +}; + + +const PanelContextKey: InjectionKey = Symbol('PanelContextProps'); + +export const useProvidePanel = (props: PanelContextProps) => { + provide(PanelContextKey, props); +}; + +export const useInjectPanel = () => { + return inject(PanelContextKey); +}; + +export default PanelContextKey; diff --git a/components/vc-picker/Picker.tsx b/components/vc-picker/Picker.tsx new file mode 100644 index 000000000..11b717bdc --- /dev/null +++ b/components/vc-picker/Picker.tsx @@ -0,0 +1,561 @@ +/** + * Removed: + * - getCalendarContainer: use `getPopupContainer` instead + * - onOk + * + * New Feature: + * - picker + * - allowEmpty + * - selectable + * + * Tips: Should add faq about `datetime` mode with `defaultValue` + */ + +import * as React from 'react'; +import classNames from 'classnames'; +import type { AlignType } from 'rc-trigger/lib/interface'; +import warning from 'rc-util/lib/warning'; +import useMergedState from 'rc-util/lib/hooks/useMergedState'; +import type { + PickerPanelBaseProps, + PickerPanelDateProps, + PickerPanelTimeProps, +} from './PickerPanel'; +import PickerPanel from './PickerPanel'; +import PickerTrigger from './PickerTrigger'; +import { formatValue, isEqual, parseValue } from './utils/dateUtil'; +import getDataOrAriaProps, { toArray } from './utils/miscUtil'; +import type { ContextOperationRefProps } from './PanelContext'; +import PanelContext from './PanelContext'; +import type { CustomFormat, PickerMode } from './interface'; +import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; +import usePickerInput from './hooks/usePickerInput'; +import useTextValueMapping from './hooks/useTextValueMapping'; +import useValueTexts from './hooks/useValueTexts'; +import useHoverValue from './hooks/useHoverValue'; + +export type PickerRefConfig = { + focus: () => void; + blur: () => void; +}; + +export type PickerSharedProps = { + dropdownClassName?: string; + dropdownAlign?: AlignType; + popupStyle?: React.CSSProperties; + transitionName?: string; + placeholder?: string; + allowClear?: boolean; + autoFocus?: boolean; + disabled?: boolean; + tabIndex?: number; + open?: boolean; + defaultOpen?: boolean; + /** Make input readOnly to avoid popup keyboard in mobile */ + inputReadOnly?: boolean; + id?: string; + + // Value + format?: string | CustomFormat | (string | CustomFormat)[]; + + // Render + suffixIcon?: React.ReactNode; + clearIcon?: React.ReactNode; + prevIcon?: React.ReactNode; + nextIcon?: React.ReactNode; + superPrevIcon?: React.ReactNode; + superNextIcon?: React.ReactNode; + getPopupContainer?: (node: HTMLElement) => HTMLElement; + panelRender?: (originPanel: React.ReactNode) => React.ReactNode; + + // Events + onChange?: (value: DateType | null, dateString: string) => void; + onOpenChange?: (open: boolean) => void; + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; + onMouseDown?: React.MouseEventHandler; + onMouseUp?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + onClick?: React.MouseEventHandler; + onContextMenu?: React.MouseEventHandler; + onKeyDown?: (event: React.KeyboardEvent, preventDefault: () => void) => void; + + // Internal + /** @private Internal usage, do not use in production mode!!! */ + pickerRef?: React.MutableRefObject; + + // WAI-ARIA + role?: string; + name?: string; + + autoComplete?: string; + direction?: 'ltr' | 'rtl'; +} & React.AriaAttributes; + +type OmitPanelProps = Omit< + Props, + 'onChange' | 'hideHeader' | 'pickerValue' | 'onPickerValueChange' +>; + +export type PickerBaseProps = {} & PickerSharedProps & + OmitPanelProps>; + +export type PickerDateProps = {} & PickerSharedProps & + OmitPanelProps>; + +export type PickerTimeProps = { + picker: 'time'; + /** + * @deprecated Please use `defaultValue` directly instead + * since `defaultOpenValue` will confuse user of current value status + */ + defaultOpenValue?: DateType; +} & PickerSharedProps & + Omit>, 'format'>; + +export type PickerProps = + | PickerBaseProps + | PickerDateProps + | PickerTimeProps; + +// TMP type to fit for ts 3.9.2 +type OmitType = Omit, 'picker'> & + Omit, 'picker'> & + Omit, 'picker'>; +type MergedPickerProps = { + picker?: PickerMode; +} & OmitType; + +function InnerPicker(props: PickerProps) { + const { + prefixCls = 'rc-picker', + id, + tabIndex, + style, + className, + dropdownClassName, + dropdownAlign, + popupStyle, + transitionName, + generateConfig, + locale, + inputReadOnly, + allowClear, + autoFocus, + showTime, + picker = 'date', + format, + use12Hours, + value, + defaultValue, + open, + defaultOpen, + defaultOpenValue, + suffixIcon, + clearIcon, + disabled, + disabledDate, + placeholder, + getPopupContainer, + pickerRef, + panelRender, + onChange, + onOpenChange, + onFocus, + onBlur, + onMouseDown, + onMouseUp, + onMouseEnter, + onMouseLeave, + onContextMenu, + onClick, + onKeyDown, + onSelect, + direction, + autoComplete = 'off', + } = props as MergedPickerProps; + + const inputRef = React.useRef(null); + + const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time'; + + // ============================= State ============================= + const formatList = toArray(getDefaultFormat(format, picker, showTime, use12Hours)); + + // Panel ref + const panelDivRef = React.useRef(null); + const inputDivRef = React.useRef(null); + + // Real value + const [mergedValue, setInnerValue] = useMergedState(null, { + value, + defaultValue, + }); + + // Selected value + const [selectedValue, setSelectedValue] = React.useState(mergedValue); + + // Operation ref + const operationRef: React.MutableRefObject = + React.useRef(null); + + // Open + const [mergedOpen, triggerInnerOpen] = useMergedState(false, { + value: open, + defaultValue: defaultOpen, + postState: (postOpen) => (disabled ? false : postOpen), + onChange: (newOpen) => { + if (onOpenChange) { + onOpenChange(newOpen); + } + + if (!newOpen && operationRef.current && operationRef.current.onClose) { + operationRef.current.onClose(); + } + }, + }); + + // ============================= Text ============================== + const [valueTexts, firstValueText] = useValueTexts(selectedValue, { + formatList, + generateConfig, + locale, + }); + + const [text, triggerTextChange, resetText] = useTextValueMapping({ + valueTexts, + onTextChange: (newText) => { + const inputDate = parseValue(newText, { + locale, + formatList, + generateConfig, + }); + if (inputDate && (!disabledDate || !disabledDate(inputDate))) { + setSelectedValue(inputDate); + } + }, + }); + + // ============================ Trigger ============================ + const triggerChange = (newValue: DateType | null) => { + setSelectedValue(newValue); + setInnerValue(newValue); + + if (onChange && !isEqual(generateConfig, mergedValue, newValue)) { + onChange( + newValue, + newValue ? formatValue(newValue, { generateConfig, locale, format: formatList[0] }) : '', + ); + } + }; + + const triggerOpen = (newOpen: boolean) => { + if (disabled && newOpen) { + return; + } + + triggerInnerOpen(newOpen); + }; + + const forwardKeyDown = (e: React.KeyboardEvent) => { + if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) { + // Let popup panel handle keyboard + return operationRef.current.onKeyDown(e); + } + + /* istanbul ignore next */ + /* eslint-disable no-lone-blocks */ + { + warning( + false, + 'Picker not correct forward KeyDown operation. Please help to fire issue about this.', + ); + return false; + } + }; + + const onInternalMouseUp: React.MouseEventHandler = (...args) => { + if (onMouseUp) { + onMouseUp(...args); + } + + if (inputRef.current) { + inputRef.current.focus(); + triggerOpen(true); + } + }; + + // ============================= Input ============================= + const [inputProps, { focused, typing }] = usePickerInput({ + blurToCancel: needConfirmButton, + open: mergedOpen, + value: text, + triggerOpen, + forwardKeyDown, + isClickOutside: (target) => + !elementsContains([panelDivRef.current, inputDivRef.current], target as HTMLElement), + onSubmit: () => { + if (disabledDate && disabledDate(selectedValue)) { + return false; + } + + triggerChange(selectedValue); + triggerOpen(false); + resetText(); + return true; + }, + onCancel: () => { + triggerOpen(false); + setSelectedValue(mergedValue); + resetText(); + }, + onKeyDown: (e, preventDefault) => { + onKeyDown?.(e, preventDefault); + }, + onFocus, + onBlur, + }); + + // ============================= Sync ============================== + // Close should sync back with text value + React.useEffect(() => { + if (!mergedOpen) { + setSelectedValue(mergedValue); + + if (!valueTexts.length || valueTexts[0] === '') { + triggerTextChange(''); + } else if (firstValueText !== text) { + resetText(); + } + } + }, [mergedOpen, valueTexts]); + + // Change picker should sync back with text value + React.useEffect(() => { + if (!mergedOpen) { + resetText(); + } + }, [picker]); + + // Sync innerValue with control mode + React.useEffect(() => { + // Sync select value + setSelectedValue(mergedValue); + }, [mergedValue]); + + // ============================ Private ============================ + if (pickerRef) { + pickerRef.current = { + focus: () => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, + blur: () => { + if (inputRef.current) { + inputRef.current.blur(); + } + }, + }; + } + + const [hoverValue, onEnter, onLeave] = useHoverValue(text, { + formatList, + generateConfig, + locale, + }); + + // ============================= Panel ============================= + const panelProps = { + // Remove `picker` & `format` here since TimePicker is little different with other panel + ...(props as Omit, 'picker' | 'format'>), + className: undefined, + style: undefined, + pickerValue: undefined, + onPickerValueChange: undefined, + onChange: null, + }; + + let panelNode: React.ReactNode = ( + + {...panelProps} + generateConfig={generateConfig} + className={classNames({ + [`${prefixCls}-panel-focused`]: !typing, + })} + value={selectedValue} + locale={locale} + tabIndex={-1} + onSelect={(date) => { + onSelect?.(date); + setSelectedValue(date); + }} + direction={direction} + onPanelChange={(viewDate, mode) => { + const { onPanelChange } = props; + onLeave(true); + onPanelChange?.(viewDate, mode); + }} + /> + ); + + if (panelRender) { + panelNode = panelRender(panelNode); + } + + const panel = ( +
{ + e.preventDefault(); + }} + > + {panelNode} +
+ ); + + let suffixNode: React.ReactNode; + if (suffixIcon) { + suffixNode = {suffixIcon}; + } + + let clearNode: React.ReactNode; + if (allowClear && mergedValue && !disabled) { + clearNode = ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onMouseUp={(e) => { + e.preventDefault(); + e.stopPropagation(); + triggerChange(null); + triggerOpen(false); + }} + className={`${prefixCls}-clear`} + role="button" + > + {clearIcon || } + + ); + } + + // ============================ Warning ============================ + if (process.env.NODE_ENV !== 'production') { + warning( + !defaultOpenValue, + '`defaultOpenValue` may confuse user for the current value status. Please use `defaultValue` instead.', + ); + } + + // ============================ Return ============================= + const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { + if (type === 'submit' || (type !== 'key' && !needConfirmButton)) { + // triggerChange will also update selected values + triggerChange(date); + triggerOpen(false); + } + }; + const popupPlacement = direction === 'rtl' ? 'bottomRight' : 'bottomLeft'; + + return ( + + +
+
+ { + triggerTextChange(e.target.value); + }} + autoFocus={autoFocus} + placeholder={placeholder} + ref={inputRef} + title={text} + {...inputProps} + size={getInputSize(picker, formatList[0], generateConfig)} + {...getDataOrAriaProps(props)} + autoComplete={autoComplete} + /> + {suffixNode} + {clearNode} +
+
+
+
+ ); +} + +// Wrap with class component to enable pass generic with instance method +class Picker extends React.Component> { + pickerRef = React.createRef(); + + focus = () => { + if (this.pickerRef.current) { + this.pickerRef.current.focus(); + } + }; + + blur = () => { + if (this.pickerRef.current) { + this.pickerRef.current.blur(); + } + }; + + render() { + return ( + + {...this.props} + pickerRef={this.pickerRef as React.MutableRefObject} + /> + ); + } +} + +export default Picker; diff --git a/components/vc-picker/PickerPanel.tsx b/components/vc-picker/PickerPanel.tsx new file mode 100644 index 000000000..9c7224691 --- /dev/null +++ b/components/vc-picker/PickerPanel.tsx @@ -0,0 +1,569 @@ +/** + * 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 = { + 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) => React.ReactNode; + + // Event + onSelect?: (value: DateType) => void; + onChange?: (value: DateType) => void; + onPanelChange?: OnPanelChange; + onMouseDown?: React.MouseEventHandler; + 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(props: PickerPanelProps) { + 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; + + 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({}); + + // 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(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(() => 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) => { + 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 = (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), + operationRef: panelRef, + prefixCls, + viewDate, + value: mergedValue, + onViewDateChange: setViewDate, + sourceMode, + onPanelChange: onInternalPanelChange, + disabledDate, + }; + delete pickerProps.onChange; + delete pickerProps.onSelect; + + switch (mergedMode) { + 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: 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 = ( + { + if (!disabled) { + triggerSelect(now, 'mouse', true); + } + }} + > + {locale.today} + + ); + } + + return ( + +
+ {panelNode} + {extraFooter || rangesNode || todayNode ? ( +
+ {extraFooter} + {rangesNode} + {todayNode} +
+ ) : null} +
+
+ ); +} + +export default PickerPanel; +/* eslint-enable */ diff --git a/components/vc-picker/PickerTrigger.tsx b/components/vc-picker/PickerTrigger.tsx new file mode 100644 index 000000000..d942442e1 --- /dev/null +++ b/components/vc-picker/PickerTrigger.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import Trigger from 'rc-trigger'; +import type { AlignType } from 'rc-trigger/lib/interface'; + +const BUILT_IN_PLACEMENTS = { + bottomLeft: { + points: ['tl', 'bl'], + offset: [0, 4], + overflow: { + adjustX: 1, + adjustY: 1, + }, + }, + bottomRight: { + points: ['tr', 'br'], + offset: [0, 4], + overflow: { + adjustX: 1, + adjustY: 1, + }, + }, + topLeft: { + points: ['bl', 'tl'], + offset: [0, -4], + overflow: { + adjustX: 0, + adjustY: 1, + }, + }, + topRight: { + points: ['br', 'tr'], + offset: [0, -4], + overflow: { + adjustX: 0, + adjustY: 1, + }, + }, +}; + +type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight'; + +export type PickerTriggerProps = { + prefixCls: string; + visible: boolean; + popupElement: React.ReactElement; + popupStyle?: React.CSSProperties; + children: React.ReactElement; + dropdownClassName?: string; + transitionName?: string; + getPopupContainer?: (node: HTMLElement) => HTMLElement; + dropdownAlign?: AlignType; + range?: boolean; + popupPlacement?: Placement; + direction?: 'ltr' | 'rtl'; +}; + +function PickerTrigger({ + prefixCls, + popupElement, + popupStyle, + visible, + dropdownClassName, + dropdownAlign, + transitionName, + getPopupContainer, + children, + range, + popupPlacement, + direction, +}: PickerTriggerProps) { + const dropdownPrefixCls = `${prefixCls}-dropdown`; + + const getPopupPlacement = () => { + if (popupPlacement !== undefined) { + return popupPlacement; + } + return direction === 'rtl' ? 'bottomRight' : 'bottomLeft'; + }; + + return ( + + {children} + + ); +} + +export default PickerTrigger; diff --git a/components/vc-picker/RangeContext.tsx b/components/vc-picker/RangeContext.tsx new file mode 100644 index 000000000..ef64a6195 --- /dev/null +++ b/components/vc-picker/RangeContext.tsx @@ -0,0 +1,27 @@ +import { inject, InjectionKey, provide } from 'vue'; +import type { NullableDateType, RangeValue } from './interface'; + +export type RangeContextProps = { + /** + * Set displayed range value style. + * Panel only has one value, this is only style effect. + */ + rangedValue?: [NullableDateType, NullableDateType] | null; + hoverRangedValue?: RangeValue; + inRange?: boolean; + panelPosition?: 'left' | 'right' | false; +}; + + +const RangeContextKey: InjectionKey = Symbol('RangeContextProps'); + +export const useProvideRange = (props: RangeContextProps) => { + provide(RangeContextKey, props); +}; + +export const useInjectRange = () => { + return inject(RangeContextKey); +}; + + +export default RangeContextKey; diff --git a/components/vc-picker/RangePicker.tsx b/components/vc-picker/RangePicker.tsx new file mode 100644 index 000000000..fec2ffa93 --- /dev/null +++ b/components/vc-picker/RangePicker.tsx @@ -0,0 +1,1181 @@ +import * as React from 'react'; +import { useRef, useEffect, useState } from 'react'; +import classNames from 'classnames'; +import warning from 'rc-util/lib/warning'; +import useMergedState from 'rc-util/lib/hooks/useMergedState'; +import type { DisabledTimes, PanelMode, PickerMode, RangeValue, EventValue } from './interface'; +import type { PickerBaseProps, PickerDateProps, PickerTimeProps, PickerRefConfig } from './Picker'; +import type { SharedTimeProps } from './panels/TimePanel'; +import PickerTrigger from './PickerTrigger'; +import PickerPanel from './PickerPanel'; +import usePickerInput from './hooks/usePickerInput'; +import getDataOrAriaProps, { toArray, getValue, updateValues } from './utils/miscUtil'; +import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; +import type { ContextOperationRefProps } from './PanelContext'; +import PanelContext from './PanelContext'; +import { + isEqual, + getClosingViewDate, + isSameDate, + isSameWeek, + isSameQuarter, + formatValue, + parseValue, +} from './utils/dateUtil'; +import useValueTexts from './hooks/useValueTexts'; +import useTextValueMapping from './hooks/useTextValueMapping'; +import type { GenerateConfig } from './generate'; +import type { PickerPanelProps } from '.'; +import RangeContext from './RangeContext'; +import useRangeDisabled from './hooks/useRangeDisabled'; +import getExtraFooter from './utils/getExtraFooter'; +import getRanges from './utils/getRanges'; +import useRangeViewDates from './hooks/useRangeViewDates'; +import type { DateRender } from './panels/DatePanel/DateBody'; +import useHoverValue from './hooks/useHoverValue'; + +function reorderValues( + values: RangeValue, + generateConfig: GenerateConfig, +): RangeValue { + if (values && values[0] && values[1] && generateConfig.isAfter(values[0], values[1])) { + return [values[1], values[0]]; + } + + return values; +} + +function canValueTrigger( + value: EventValue, + index: number, + disabled: [boolean, boolean], + allowEmpty?: [boolean, boolean] | null, +): boolean { + if (value) { + return true; + } + + if (allowEmpty && allowEmpty[index]) { + return true; + } + + if (disabled[(index + 1) % 2]) { + return true; + } + + return false; +} + +export type RangeType = 'start' | 'end'; + +export type RangeInfo = { + range: RangeType; +}; + +export type RangeDateRender = ( + currentDate: DateType, + today: DateType, + info: RangeInfo, +) => React.ReactNode; + +export type RangePickerSharedProps = { + id?: string; + value?: RangeValue; + defaultValue?: RangeValue; + defaultPickerValue?: [DateType, DateType]; + placeholder?: [string, string]; + disabled?: boolean | [boolean, boolean]; + disabledTime?: (date: EventValue, type: RangeType) => DisabledTimes; + ranges?: Record< + string, + Exclude, null> | (() => Exclude, null>) + >; + separator?: React.ReactNode; + allowEmpty?: [boolean, boolean]; + mode?: [PanelMode, PanelMode]; + onChange?: (values: RangeValue, formatString: [string, string]) => void; + onCalendarChange?: ( + values: RangeValue, + formatString: [string, string], + info: RangeInfo, + ) => void; + onPanelChange?: (values: RangeValue, modes: [PanelMode, PanelMode]) => void; + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + onOk?: (dates: RangeValue) => void; + direction?: 'ltr' | 'rtl'; + autoComplete?: string; + /** @private Internal control of active picker. Do not use since it's private usage */ + activePickerIndex?: 0 | 1; + dateRender?: RangeDateRender; + panelRender?: (originPanel: React.ReactNode) => React.ReactNode; +}; + +type OmitPickerProps = Omit< + Props, + | 'value' + | 'defaultValue' + | 'defaultPickerValue' + | 'placeholder' + | 'disabled' + | 'disabledTime' + | 'showToday' + | 'showTime' + | 'mode' + | 'onChange' + | 'onSelect' + | 'onPanelChange' + | 'pickerValue' + | 'onPickerValueChange' + | 'onOk' + | 'dateRender' +>; + +type RangeShowTimeObject = Omit, 'defaultValue'> & { + defaultValue?: DateType[]; +}; + +export type RangePickerBaseProps = {} & RangePickerSharedProps & OmitPickerProps>; + +export type RangePickerDateProps = { + showTime?: boolean | RangeShowTimeObject; +} & RangePickerSharedProps & OmitPickerProps>; + +export type RangePickerTimeProps = { + order?: boolean; +} & RangePickerSharedProps & OmitPickerProps>; + +export type RangePickerProps = + | RangePickerBaseProps + | RangePickerDateProps + | RangePickerTimeProps; + +// TMP type to fit for ts 3.9.2 +type OmitType = Omit, 'picker'> & + Omit, 'picker'> & + Omit, 'picker'>; + +type MergedRangePickerProps = { + picker?: PickerMode; +} & OmitType; + +function InnerRangePicker(props: RangePickerProps) { + const { + prefixCls = 'rc-picker', + id, + style, + className, + popupStyle, + dropdownClassName, + transitionName, + dropdownAlign, + getPopupContainer, + generateConfig, + locale, + placeholder, + autoFocus, + disabled, + format, + picker = 'date', + showTime, + use12Hours, + separator = '~', + value, + defaultValue, + defaultPickerValue, + open, + defaultOpen, + disabledDate, + disabledTime, + dateRender, + panelRender, + ranges, + allowEmpty, + allowClear, + suffixIcon, + clearIcon, + pickerRef, + inputReadOnly, + mode, + renderExtraFooter, + onChange, + onOpenChange, + onPanelChange, + onCalendarChange, + onFocus, + onBlur, + onMouseEnter, + onMouseLeave, + onOk, + onKeyDown, + components, + order, + direction, + activePickerIndex, + autoComplete = 'off', + } = props as MergedRangePickerProps; + + const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time'; + + // We record opened status here in case repeat open with picker + const openRecordsRef = useRef>({}); + + const containerRef = useRef(null); + const panelDivRef = useRef(null); + const startInputDivRef = useRef(null); + const endInputDivRef = useRef(null); + const separatorRef = useRef(null); + const startInputRef = useRef(null); + const endInputRef = useRef(null); + + // ============================= Misc ============================== + const formatList = toArray(getDefaultFormat(format, picker, showTime, use12Hours)); + + // Active picker + const [mergedActivePickerIndex, setMergedActivePickerIndex] = useMergedState<0 | 1>(0, { + value: activePickerIndex, + }); + + // Operation ref + const operationRef: React.MutableRefObject = useRef< + ContextOperationRefProps + >(null); + + const mergedDisabled = React.useMemo<[boolean, boolean]>(() => { + if (Array.isArray(disabled)) { + return disabled; + } + + return [disabled || false, disabled || false]; + }, [disabled]); + + // ============================= Value ============================= + const [mergedValue, setInnerValue] = useMergedState>(null, { + value, + defaultValue, + postState: values => + picker === 'time' && !order ? values : reorderValues(values, generateConfig), + }); + + // =========================== View Date =========================== + // Config view panel + const [getViewDate, setViewDate] = useRangeViewDates({ + values: mergedValue, + picker, + defaultDates: defaultPickerValue, + generateConfig, + }); + + // ========================= Select Values ========================= + const [selectedValue, setSelectedValue] = useMergedState(mergedValue, { + postState: values => { + let postValues = values; + + if (mergedDisabled[0] && mergedDisabled[1]) { + return postValues; + } + + // Fill disabled unit + for (let i = 0; i < 2; i += 1) { + if (mergedDisabled[i] && !getValue(postValues, i) && !getValue(allowEmpty, i)) { + postValues = updateValues(postValues, generateConfig.getNow(), i); + } + } + return postValues; + }, + }); + + // ============================= Modes ============================= + const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>([picker, picker], { + value: mode, + }); + + useEffect(() => { + setInnerModes([picker, picker]); + }, [picker]); + + const triggerModesChange = (modes: [PanelMode, PanelMode], values: RangeValue) => { + setInnerModes(modes); + + if (onPanelChange) { + onPanelChange(values, modes); + } + }; + + // ========================= Disable Date ========================== + const [disabledStartDate, disabledEndDate] = useRangeDisabled( + { + picker, + selectedValue, + locale, + disabled: mergedDisabled, + disabledDate, + generateConfig, + }, + openRecordsRef.current[1], + openRecordsRef.current[0], + ); + + // ============================= Open ============================== + const [mergedOpen, triggerInnerOpen] = useMergedState(false, { + value: open, + defaultValue: defaultOpen, + postState: postOpen => (mergedDisabled[mergedActivePickerIndex] ? false : postOpen), + onChange: newOpen => { + if (onOpenChange) { + onOpenChange(newOpen); + } + + if (!newOpen && operationRef.current && operationRef.current.onClose) { + operationRef.current.onClose(); + } + }, + }); + + const startOpen = mergedOpen && mergedActivePickerIndex === 0; + const endOpen = mergedOpen && mergedActivePickerIndex === 1; + + // ============================= Popup ============================= + // Popup min width + const [popupMinWidth, setPopupMinWidth] = useState(0); + useEffect(() => { + if (!mergedOpen && containerRef.current) { + setPopupMinWidth(containerRef.current.offsetWidth); + } + }, [mergedOpen]); + + // ============================ Trigger ============================ + const triggerRef = React.useRef(); + + function triggerOpen(newOpen: boolean, index: 0 | 1) { + if (newOpen) { + clearTimeout(triggerRef.current); + openRecordsRef.current[index] = true; + + setMergedActivePickerIndex(index); + triggerInnerOpen(newOpen); + + // Open to reset view date + if (!mergedOpen) { + setViewDate(null, index); + } + } else if (mergedActivePickerIndex === index) { + triggerInnerOpen(newOpen); + + // Clean up async + // This makes ref not quick refresh in case user open another input with blur trigger + const openRecords = openRecordsRef.current; + triggerRef.current = setTimeout(() => { + if (openRecords === openRecordsRef.current) { + openRecordsRef.current = {}; + } + }); + } + } + + function triggerOpenAndFocus(index: 0 | 1) { + triggerOpen(true, index); + // Use setTimeout to make sure panel DOM exists + setTimeout(() => { + const inputRef = [startInputRef, endInputRef][index]; + if (inputRef.current) { + inputRef.current.focus(); + } + }, 0); + } + + function triggerChange(newValue: RangeValue, sourceIndex: 0 | 1) { + let values = newValue; + let startValue = getValue(values, 0); + let endValue = getValue(values, 1); + + // >>>>> Format start & end values + if (startValue && endValue && generateConfig.isAfter(startValue, endValue)) { + if ( + // WeekPicker only compare week + (picker === 'week' && !isSameWeek(generateConfig, locale.locale, startValue, endValue)) || + // QuotaPicker only compare week + (picker === 'quarter' && !isSameQuarter(generateConfig, startValue, endValue)) || + // Other non-TimePicker compare date + (picker !== 'week' && + picker !== 'quarter' && + picker !== 'time' && + !isSameDate(generateConfig, startValue, endValue)) + ) { + // Clean up end date when start date is after end date + if (sourceIndex === 0) { + values = [startValue, null]; + endValue = null; + } else { + startValue = null; + values = [null, endValue]; + } + + // Clean up cache since invalidate + openRecordsRef.current = { + [sourceIndex]: true, + }; + } else if (picker !== 'time' || order !== false) { + // Reorder when in same date + values = reorderValues(values, generateConfig); + } + } + + setSelectedValue(values); + + const startStr = + values && values[0] + ? formatValue(values[0], { generateConfig, locale, format: formatList[0] }) + : ''; + const endStr = + values && values[1] + ? formatValue(values[1], { generateConfig, locale, format: formatList[0] }) + : ''; + + if (onCalendarChange) { + const info: RangeInfo = { range: sourceIndex === 0 ? 'start' : 'end' }; + + onCalendarChange(values, [startStr, endStr], info); + } + + // >>>>> Trigger `onChange` event + const canStartValueTrigger = canValueTrigger(startValue, 0, mergedDisabled, allowEmpty); + const canEndValueTrigger = canValueTrigger(endValue, 1, mergedDisabled, allowEmpty); + + const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); + + if (canTrigger) { + // Trigger onChange only when value is validate + setInnerValue(values); + + if ( + onChange && + (!isEqual(generateConfig, getValue(mergedValue, 0), startValue) || + !isEqual(generateConfig, getValue(mergedValue, 1), endValue)) + ) { + onChange(values, [startStr, endStr]); + } + } + + // >>>>> Open picker when + + // Always open another picker if possible + let nextOpenIndex: 0 | 1 = null; + if (sourceIndex === 0 && !mergedDisabled[1]) { + nextOpenIndex = 1; + } else if (sourceIndex === 1 && !mergedDisabled[0]) { + nextOpenIndex = 0; + } + + if ( + nextOpenIndex !== null && + nextOpenIndex !== mergedActivePickerIndex && + (!openRecordsRef.current[nextOpenIndex] || !getValue(values, nextOpenIndex)) && + getValue(values, sourceIndex) + ) { + // Delay to focus to avoid input blur trigger expired selectedValues + triggerOpenAndFocus(nextOpenIndex); + } else { + triggerOpen(false, sourceIndex); + } + } + + const forwardKeyDown = (e: React.KeyboardEvent) => { + if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) { + // Let popup panel handle keyboard + return operationRef.current.onKeyDown(e); + } + + /* istanbul ignore next */ + /* eslint-disable no-lone-blocks */ + { + warning( + false, + 'Picker not correct forward KeyDown operation. Please help to fire issue about this.', + ); + return false; + } + }; + + // ============================= Text ============================== + const sharedTextHooksProps = { + formatList, + generateConfig, + locale, + }; + + const [startValueTexts, firstStartValueText] = useValueTexts( + getValue(selectedValue, 0), + sharedTextHooksProps, + ); + + const [endValueTexts, firstEndValueText] = useValueTexts( + getValue(selectedValue, 1), + sharedTextHooksProps, + ); + + const onTextChange = (newText: string, index: 0 | 1) => { + const inputDate = parseValue(newText, { + locale, + formatList, + generateConfig, + }); + + const disabledFunc = index === 0 ? disabledStartDate : disabledEndDate; + + if (inputDate && !disabledFunc(inputDate)) { + setSelectedValue(updateValues(selectedValue, inputDate, index)); + setViewDate(inputDate, index); + } + }; + + const [startText, triggerStartTextChange, resetStartText] = useTextValueMapping({ + valueTexts: startValueTexts, + onTextChange: newText => onTextChange(newText, 0), + }); + + const [endText, triggerEndTextChange, resetEndText] = useTextValueMapping({ + valueTexts: endValueTexts, + onTextChange: newText => onTextChange(newText, 1), + }); + + const [rangeHoverValue, setRangeHoverValue] = useState>(null); + + // ========================== Hover Range ========================== + const [hoverRangedValue, setHoverRangedValue] = useState>(null); + + const [startHoverValue, onStartEnter, onStartLeave] = useHoverValue(startText, { + formatList, + generateConfig, + locale, + }); + + const [endHoverValue, onEndEnter, onEndLeave] = useHoverValue(endText, { + formatList, + generateConfig, + locale, + }); + + const onDateMouseEnter = (date: DateType) => { + setHoverRangedValue(updateValues(selectedValue, date, mergedActivePickerIndex)); + if (mergedActivePickerIndex === 0) { + onStartEnter(date); + } else { + onEndEnter(date); + } + }; + + const onDateMouseLeave = () => { + setHoverRangedValue(updateValues(selectedValue, null, mergedActivePickerIndex)); + if (mergedActivePickerIndex === 0) { + onStartLeave(); + } else { + onEndLeave(); + } + }; + + // ============================= Input ============================= + const getSharedInputHookProps = (index: 0 | 1, resetText: () => void) => ({ + blurToCancel: needConfirmButton, + forwardKeyDown, + onBlur, + isClickOutside: (target: EventTarget | null) => + !elementsContains( + [panelDivRef.current, startInputDivRef.current, endInputDivRef.current], + target as HTMLElement, + ), + onFocus: (e: React.FocusEvent) => { + setMergedActivePickerIndex(index); + if (onFocus) { + onFocus(e); + } + }, + triggerOpen: (newOpen: boolean) => { + triggerOpen(newOpen, index); + }, + onSubmit: () => { + triggerChange(selectedValue, index); + resetText(); + }, + onCancel: () => { + triggerOpen(false, index); + setSelectedValue(mergedValue); + resetText(); + }, + }); + + const [startInputProps, { focused: startFocused, typing: startTyping }] = usePickerInput({ + ...getSharedInputHookProps(0, resetStartText), + open: startOpen, + value: startText, + onKeyDown: (e, preventDefault) => { + onKeyDown?.(e, preventDefault); + }, + }); + + const [endInputProps, { focused: endFocused, typing: endTyping }] = usePickerInput({ + ...getSharedInputHookProps(1, resetEndText), + open: endOpen, + value: endText, + onKeyDown: (e, preventDefault) => { + onKeyDown?.(e, preventDefault); + }, + }); + + // ========================== Click Picker ========================== + const onPickerClick = (e: MouseEvent) => { + // When click inside the picker & outside the picker's input elements + // the panel should still be opened + if ( + !mergedOpen && + !startInputRef.current.contains(e.target as Node) && + !endInputRef.current.contains(e.target as Node) + ) { + if (!mergedDisabled[0]) { + triggerOpenAndFocus(0); + } else if (!mergedDisabled[1]) { + triggerOpenAndFocus(1); + } + } + }; + + const onPickerMouseDown = (e: MouseEvent) => { + // shouldn't affect input elements if picker is active + if ( + mergedOpen && + (startFocused || endFocused) && + !startInputRef.current.contains(e.target as Node) && + !endInputRef.current.contains(e.target as Node) + ) { + e.preventDefault(); + } + }; + + // ============================= Sync ============================== + // Close should sync back with text value + const startStr = + mergedValue && mergedValue[0] + ? formatValue(mergedValue[0], { + locale, + format: 'YYYYMMDDHHmmss', + generateConfig, + }) + : ''; + const endStr = + mergedValue && mergedValue[1] + ? formatValue(mergedValue[1], { + locale, + format: 'YYYYMMDDHHmmss', + generateConfig, + }) + : ''; + + useEffect(() => { + if (!mergedOpen) { + setSelectedValue(mergedValue); + + if (!startValueTexts.length || startValueTexts[0] === '') { + triggerStartTextChange(''); + } else if (firstStartValueText !== startText) { + resetStartText(); + } + if (!endValueTexts.length || endValueTexts[0] === '') { + triggerEndTextChange(''); + } else if (firstEndValueText !== endText) { + resetEndText(); + } + } + }, [mergedOpen, startValueTexts, endValueTexts]); + + // Sync innerValue with control mode + useEffect(() => { + setSelectedValue(mergedValue); + }, [startStr, endStr]); + + // ============================ Warning ============================ + if (process.env.NODE_ENV !== 'production') { + if ( + value && + Array.isArray(disabled) && + ((getValue(disabled, 0) && !getValue(value, 0)) || + (getValue(disabled, 1) && !getValue(value, 1))) + ) { + warning( + false, + '`disabled` should not set with empty `value`. You should set `allowEmpty` or `value` instead.', + ); + } + } + + // ============================ Private ============================ + if (pickerRef) { + pickerRef.current = { + focus: () => { + if (startInputRef.current) { + startInputRef.current.focus(); + } + }, + blur: () => { + if (startInputRef.current) { + startInputRef.current.blur(); + } + if (endInputRef.current) { + endInputRef.current.blur(); + } + }, + }; + } + + // ============================ Ranges ============================= + const rangeLabels = Object.keys(ranges || {}); + + const rangeList = rangeLabels.map(label => { + const range = ranges![label]; + const newValues = typeof range === 'function' ? range() : range; + + return { + label, + onClick: () => { + triggerChange(newValues, null); + triggerOpen(false, mergedActivePickerIndex); + }, + onMouseEnter: () => { + setRangeHoverValue(newValues); + }, + onMouseLeave: () => { + setRangeHoverValue(null); + }, + }; + }); + + // ============================= Panel ============================= + function renderPanel( + panelPosition: 'left' | 'right' | false = false, + panelProps: Partial> = {}, + ) { + let panelHoverRangedValue: RangeValue = null; + if ( + mergedOpen && + hoverRangedValue && + hoverRangedValue[0] && + hoverRangedValue[1] && + generateConfig.isAfter(hoverRangedValue[1], hoverRangedValue[0]) + ) { + panelHoverRangedValue = hoverRangedValue; + } + + let panelShowTime: + | boolean + | SharedTimeProps + | undefined = showTime as SharedTimeProps; + if (showTime && typeof showTime === 'object' && showTime.defaultValue) { + const timeDefaultValues: DateType[] = showTime.defaultValue!; + panelShowTime = { + ...showTime, + defaultValue: getValue(timeDefaultValues, mergedActivePickerIndex) || undefined, + }; + } + + let panelDateRender: DateRender | null = null; + if (dateRender) { + panelDateRender = (date, today) => + dateRender(date, today, { + range: mergedActivePickerIndex ? 'end' : 'start', + }); + } + + return ( + + + {...(props as any)} + {...panelProps} + dateRender={panelDateRender} + showTime={panelShowTime} + mode={mergedModes[mergedActivePickerIndex]} + generateConfig={generateConfig} + style={undefined} + direction={direction} + disabledDate={mergedActivePickerIndex === 0 ? disabledStartDate : disabledEndDate} + disabledTime={date => { + if (disabledTime) { + return disabledTime(date, mergedActivePickerIndex === 0 ? 'start' : 'end'); + } + return false; + }} + className={classNames({ + [`${prefixCls}-panel-focused`]: + mergedActivePickerIndex === 0 ? !startTyping : !endTyping, + })} + value={getValue(selectedValue, mergedActivePickerIndex)} + locale={locale} + tabIndex={-1} + onPanelChange={(date, newMode) => { + // clear hover value when panel change + if (mergedActivePickerIndex === 0) { + onStartLeave(true); + } + if (mergedActivePickerIndex === 1) { + onEndLeave(true); + } + triggerModesChange( + updateValues(mergedModes, newMode, mergedActivePickerIndex), + updateValues(selectedValue, date, mergedActivePickerIndex), + ); + + let viewDate = date; + if (panelPosition === 'right' && mergedModes[mergedActivePickerIndex] === newMode) { + viewDate = getClosingViewDate(viewDate, newMode as any, generateConfig, -1); + } + setViewDate(viewDate, mergedActivePickerIndex); + }} + onOk={null} + onSelect={undefined} + onChange={undefined} + defaultValue={ + mergedActivePickerIndex === 0 ? getValue(selectedValue, 1) : getValue(selectedValue, 0) + } + defaultPickerValue={undefined} + /> + + ); + } + + let arrowLeft: number = 0; + let panelLeft: number = 0; + if ( + mergedActivePickerIndex && + startInputDivRef.current && + separatorRef.current && + panelDivRef.current + ) { + // Arrow offset + arrowLeft = startInputDivRef.current.offsetWidth + separatorRef.current.offsetWidth; + + if (panelDivRef.current.offsetWidth && arrowLeft > panelDivRef.current.offsetWidth) { + panelLeft = arrowLeft; + } + } + + const arrowPositionStyle = direction === 'rtl' ? { right: arrowLeft } : { left: arrowLeft }; + + function renderPanels() { + let panels: React.ReactNode; + const extraNode = getExtraFooter( + prefixCls, + mergedModes[mergedActivePickerIndex], + renderExtraFooter, + ); + + const rangesNode = getRanges({ + prefixCls, + components, + needConfirmButton, + okDisabled: + !getValue(selectedValue, mergedActivePickerIndex) || + (disabledDate && disabledDate(selectedValue[mergedActivePickerIndex])), + locale, + rangeList, + onOk: () => { + if (getValue(selectedValue, mergedActivePickerIndex)) { + // triggerChangeOld(selectedValue); + triggerChange(selectedValue, mergedActivePickerIndex); + if (onOk) { + onOk(selectedValue); + } + } + }, + }); + + if (picker !== 'time' && !showTime) { + const viewDate = getViewDate(mergedActivePickerIndex); + const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig); + const currentMode = mergedModes[mergedActivePickerIndex]; + + const showDoublePanel = currentMode === picker; + const leftPanel = renderPanel(showDoublePanel ? 'left' : false, { + pickerValue: viewDate, + onPickerValueChange: newViewDate => { + setViewDate(newViewDate, mergedActivePickerIndex); + }, + }); + const rightPanel = renderPanel('right', { + pickerValue: nextViewDate, + onPickerValueChange: newViewDate => { + setViewDate( + getClosingViewDate(newViewDate, picker, generateConfig, -1), + mergedActivePickerIndex, + ); + }, + }); + + if (direction === 'rtl') { + panels = ( + <> + {rightPanel} + {showDoublePanel && leftPanel} + + ); + } else { + panels = ( + <> + {leftPanel} + {showDoublePanel && rightPanel} + + ); + } + } else { + panels = renderPanel(); + } + + let mergedNodes: React.ReactNode = ( + <> +
{panels}
+ {(extraNode || rangesNode) && ( +
+ {extraNode} + {rangesNode} +
+ )} + + ); + + if (panelRender) { + mergedNodes = panelRender(mergedNodes); + } + + return ( +
{ + e.preventDefault(); + }} + > + {mergedNodes} +
+ ); + } + + const rangePanel = ( +
+
+ + {renderPanels()} +
+ ); + + // ============================= Icons ============================= + let suffixNode: React.ReactNode; + if (suffixIcon) { + suffixNode = {suffixIcon}; + } + + let clearNode: React.ReactNode; + if ( + allowClear && + ((getValue(mergedValue, 0) && !mergedDisabled[0]) || + (getValue(mergedValue, 1) && !mergedDisabled[1])) + ) { + clearNode = ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onMouseUp={e => { + e.preventDefault(); + e.stopPropagation(); + let values = mergedValue; + + if (!mergedDisabled[0]) { + values = updateValues(values, null, 0); + } + if (!mergedDisabled[1]) { + values = updateValues(values, null, 1); + } + + triggerChange(values, null); + triggerOpen(false, mergedActivePickerIndex); + }} + className={`${prefixCls}-clear`} + > + {clearIcon || } + + ); + } + + const inputSharedProps = { + size: getInputSize(picker, formatList[0], generateConfig), + }; + + let activeBarLeft: number = 0; + let activeBarWidth: number = 0; + if (startInputDivRef.current && endInputDivRef.current && separatorRef.current) { + if (mergedActivePickerIndex === 0) { + activeBarWidth = startInputDivRef.current.offsetWidth; + } else { + activeBarLeft = arrowLeft; + activeBarWidth = endInputDivRef.current.offsetWidth; + } + } + const activeBarPositionStyle = + direction === 'rtl' ? { right: activeBarLeft } : { left: activeBarLeft }; + // ============================ Return ============================= + const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { + const values = updateValues(selectedValue, date, mergedActivePickerIndex); + + if (type === 'submit' || (type !== 'key' && !needConfirmButton)) { + // triggerChange will also update selected values + triggerChange(values, mergedActivePickerIndex); + // clear hover value style + if (mergedActivePickerIndex === 0) { + onStartLeave(); + } else { + onEndLeave(); + } + } else { + setSelectedValue(values); + } + }; + + return ( + + +
+
+ { + triggerStartTextChange(e.target.value); + }} + autoFocus={autoFocus} + placeholder={getValue(placeholder, 0) || ''} + ref={startInputRef} + {...startInputProps} + {...inputSharedProps} + autoComplete={autoComplete} + /> +
+
+ {separator} +
+
+ { + triggerEndTextChange(e.target.value); + }} + placeholder={getValue(placeholder, 1) || ''} + ref={endInputRef} + {...endInputProps} + {...inputSharedProps} + autoComplete={autoComplete} + /> +
+
+ {suffixNode} + {clearNode} +
+ + + ); +} + +// Wrap with class component to enable pass generic with instance method +class RangePicker extends React.Component> { + pickerRef = React.createRef(); + + focus = () => { + if (this.pickerRef.current) { + this.pickerRef.current.focus(); + } + }; + + blur = () => { + if (this.pickerRef.current) { + this.pickerRef.current.blur(); + } + }; + + render() { + return ( + + {...this.props} + pickerRef={this.pickerRef as React.MutableRefObject} + /> + ); + } +} + +export default RangePicker; diff --git a/components/vc-picker/generate/dayjs.ts b/components/vc-picker/generate/dayjs.ts new file mode 100644 index 000000000..702ae0018 --- /dev/null +++ b/components/vc-picker/generate/dayjs.ts @@ -0,0 +1,132 @@ +import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; +import weekday from 'dayjs/plugin/weekday'; +import localeData from 'dayjs/plugin/localeData'; +import weekOfYear from 'dayjs/plugin/weekOfYear'; +import weekYear from 'dayjs/plugin/weekYear'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; +import type { GenerateConfig } from '.'; +import { noteOnce } from '../../vc-util/warning'; + +dayjs.extend(customParseFormat); +dayjs.extend(advancedFormat); +dayjs.extend(weekday); +dayjs.extend(localeData); +dayjs.extend(weekOfYear); +dayjs.extend(weekYear); + +dayjs.extend((_o, c) => { + // todo support Wo (ISO week) + const proto = c.prototype; + const oldFormat = proto.format; + proto.format = function f(formatStr: string) { + const str = (formatStr || '').replace('Wo', 'wo'); + return oldFormat.bind(this)(str); + }; +}); + +type IlocaleMapObject = Record; +const localeMap: IlocaleMapObject = { + en_GB: 'en-gb', + en_US: 'en', + zh_CN: 'zh-cn', + zh_TW: 'zh-tw', +}; + +const parseLocale = (locale: string) => { + const mapLocale = localeMap[locale]; + return mapLocale || locale.split('_')[0]; +}; + +const parseNoMatchNotice = () => { + /* istanbul ignore next */ + noteOnce(false, 'Not match any format. Please help to fire a issue about this.'); +}; + +const generateConfig: GenerateConfig = { + // get + getNow: () => dayjs(), + getFixedDate: string => dayjs(string, 'YYYY-MM-DD'), + getEndDate: date => date.endOf('month'), + getWeekDay: date => { + const clone = date.locale('en'); + return clone.weekday() + clone.localeData().firstDayOfWeek(); + }, + getYear: date => date.year(), + getMonth: date => date.month(), + getDate: date => date.date(), + getHour: date => date.hour(), + getMinute: date => date.minute(), + getSecond: date => date.second(), + + // set + addYear: (date, diff) => date.add(diff, 'year'), + addMonth: (date, diff) => date.add(diff, 'month'), + addDate: (date, diff) => date.add(diff, 'day'), + setYear: (date, year) => date.year(year), + setMonth: (date, month) => date.month(month), + setDate: (date, num) => date.date(num), + setHour: (date, hour) => date.hour(hour), + setMinute: (date, minute) => date.minute(minute), + setSecond: (date, second) => date.second(second), + + // Compare + isAfter: (date1, date2) => date1.isAfter(date2), + isValidate: date => date.isValid(), + + locale: { + getWeekFirstDay: locale => + dayjs() + .locale(parseLocale(locale)) + .localeData() + .firstDayOfWeek(), + getWeekFirstDate: (locale, date) => date.locale(parseLocale(locale)).weekday(0), + getWeek: (locale, date) => date.locale(parseLocale(locale)).week(), + getShortWeekDays: locale => + dayjs() + .locale(parseLocale(locale)) + .localeData() + .weekdaysMin(), + getShortMonths: locale => + dayjs() + .locale(parseLocale(locale)) + .localeData() + .monthsShort(), + format: (locale, date, format) => date.locale(parseLocale(locale)).format(format), + parse: (locale, text, formats) => { + const localeStr = parseLocale(locale); + for (let i = 0; i < formats.length; i += 1) { + const format = formats[i]; + const formatText = text; + if (format.includes('wo') || format.includes('Wo')) { + // parse Wo + const year = formatText.split('-')[0]; + const weekStr = formatText.split('-')[1]; + const firstWeek = dayjs(year, 'YYYY') + .startOf('year') + .locale(localeStr); + for (let j = 0; j <= 52; j += 1) { + const nextWeek = firstWeek.add(j, 'week'); + if (nextWeek.format('Wo') === weekStr) { + return nextWeek; + } + } + parseNoMatchNotice(); + return null; + } + const date = dayjs(formatText, format).locale(localeStr); + if (date.isValid()) { + return date; + } + } + + if (text) { + parseNoMatchNotice(); + } + return null; + }, + }, +}; + +export default generateConfig; diff --git a/components/vc-picker/generate/moment.ts b/components/vc-picker/generate/moment.ts new file mode 100644 index 000000000..24ed8ce0d --- /dev/null +++ b/components/vc-picker/generate/moment.ts @@ -0,0 +1,140 @@ +import { noteOnce } from '../../vc-util/warning'; +import type { Moment } from 'moment'; +import moment from 'moment'; +import type { GenerateConfig } from '.'; + +const generateConfig: GenerateConfig = { + // get + getNow: () => moment(), + getFixedDate: string => moment(string, 'YYYY-MM-DD'), + getEndDate: date => { + const clone = date.clone(); + return clone.endOf('month'); + }, + getWeekDay: date => { + const clone = date.clone().locale('en_US'); + return clone.weekday() + clone.localeData().firstDayOfWeek(); + }, + getYear: date => date.year(), + getMonth: date => date.month(), + getDate: date => date.date(), + getHour: date => date.hour(), + getMinute: date => date.minute(), + getSecond: date => date.second(), + + // set + addYear: (date, diff) => { + const clone = date.clone(); + return clone.add(diff, 'year'); + }, + addMonth: (date, diff) => { + const clone = date.clone(); + return clone.add(diff, 'month'); + }, + addDate: (date, diff) => { + const clone = date.clone(); + return clone.add(diff, 'day'); + }, + setYear: (date, year) => { + const clone = date.clone(); + return clone.year(year); + }, + setMonth: (date, month) => { + const clone = date.clone(); + return clone.month(month); + }, + setDate: (date, num) => { + const clone = date.clone(); + return clone.date(num); + }, + setHour: (date, hour) => { + const clone = date.clone(); + return clone.hour(hour); + }, + setMinute: (date, minute) => { + const clone = date.clone(); + return clone.minute(minute); + }, + setSecond: (date, second) => { + const clone = date.clone(); + return clone.second(second); + }, + + // Compare + isAfter: (date1, date2) => date1.isAfter(date2), + isValidate: date => date.isValid(), + + locale: { + getWeekFirstDay: locale => { + const date = moment().locale(locale); + return date.localeData().firstDayOfWeek(); + }, + getWeekFirstDate: (locale, date) => { + const clone = date.clone(); + const result = clone.locale(locale); + return result.weekday(0); + }, + getWeek: (locale, date) => { + const clone = date.clone(); + const result = clone.locale(locale); + return result.week(); + }, + getShortWeekDays: locale => { + const date = moment().locale(locale); + return date.localeData().weekdaysMin(); + }, + getShortMonths: locale => { + const date = moment().locale(locale); + return date.localeData().monthsShort(); + }, + format: (locale, date, format) => { + const clone = date.clone(); + const result = clone.locale(locale); + return result.format(format); + }, + parse: (locale, text, formats) => { + const fallbackFormatList: string[] = []; + + for (let i = 0; i < formats.length; i += 1) { + let format = formats[i]; + let formatText = text; + + if (format.includes('wo') || format.includes('Wo')) { + format = format.replace(/wo/g, 'w').replace(/Wo/g, 'W'); + const matchFormat = format.match(/[-YyMmDdHhSsWwGg]+/g); + const matchText = formatText.match(/[-\d]+/g); + + if (matchFormat && matchText) { + format = matchFormat.join(''); + formatText = matchText.join(''); + } else { + fallbackFormatList.push(format.replace(/o/g, '')); + } + } + + const date = moment(formatText, format, locale, true); + if (date.isValid()) { + return date; + } + } + + // Fallback to fuzzy matching, this should always not reach match or need fire a issue + for (let i = 0; i < fallbackFormatList.length; i += 1) { + const date = moment(text, fallbackFormatList[i], locale, false); + + /* istanbul ignore next */ + if (date.isValid()) { + noteOnce( + false, + 'Not match any format strictly and fallback to fuzzy match. Please help to fire a issue about this.', + ); + return date; + } + } + + return null; + }, + }, +}; + +export default generateConfig; diff --git a/components/vc-picker/hooks/useCellClassName.ts b/components/vc-picker/hooks/useCellClassName.ts new file mode 100644 index 000000000..7aeb2b5f7 --- /dev/null +++ b/components/vc-picker/hooks/useCellClassName.ts @@ -0,0 +1,109 @@ +import { isInRange } from '../utils/dateUtil'; +import type { GenerateConfig } from '../generate'; +import type { RangeValue, NullableDateType } from '../interface'; +import { getValue } from '../utils/miscUtil'; + +export default function useCellClassName({ + cellPrefixCls, + generateConfig, + rangedValue, + hoverRangedValue, + isInView, + isSameCell, + offsetCell, + today, + value, +}: { + cellPrefixCls: string; + generateConfig: GenerateConfig; + isSameCell: ( + current: NullableDateType, + target: NullableDateType, + ) => boolean; + offsetCell: (date: DateType, offset: number) => DateType; + isInView: (date: DateType) => boolean; + rangedValue?: RangeValue; + hoverRangedValue?: RangeValue; + today?: NullableDateType; + value?: NullableDateType; +}) { + function getClassName(currentDate: DateType) { + const prevDate = offsetCell(currentDate, -1); + const nextDate = offsetCell(currentDate, 1); + + const rangeStart = getValue(rangedValue, 0); + const rangeEnd = getValue(rangedValue, 1); + + const hoverStart = getValue(hoverRangedValue, 0); + const hoverEnd = getValue(hoverRangedValue, 1); + + const isRangeHovered = isInRange( + generateConfig, + hoverStart, + hoverEnd, + currentDate, + ); + + function isRangeStart(date: DateType) { + return isSameCell(rangeStart, date); + } + function isRangeEnd(date: DateType) { + return isSameCell(rangeEnd, date); + } + const isHoverStart = isSameCell(hoverStart, currentDate); + const isHoverEnd = isSameCell(hoverEnd, currentDate); + + const isHoverEdgeStart = + (isRangeHovered || isHoverEnd) && + (!isInView(prevDate) || isRangeEnd(prevDate)); + const isHoverEdgeEnd = + (isRangeHovered || isHoverStart) && + (!isInView(nextDate) || isRangeStart(nextDate)); + + return { + // In view + [`${cellPrefixCls}-in-view`]: isInView(currentDate), + + // Range + [`${cellPrefixCls}-in-range`]: isInRange( + generateConfig, + rangeStart, + rangeEnd, + currentDate, + ), + [`${cellPrefixCls}-range-start`]: isRangeStart(currentDate), + [`${cellPrefixCls}-range-end`]: isRangeEnd(currentDate), + [`${cellPrefixCls}-range-start-single`]: + isRangeStart(currentDate) && !rangeEnd, + [`${cellPrefixCls}-range-end-single`]: + isRangeEnd(currentDate) && !rangeStart, + [`${cellPrefixCls}-range-start-near-hover`]: + isRangeStart(currentDate) && + (isSameCell(prevDate, hoverStart) || + isInRange(generateConfig, hoverStart, hoverEnd, prevDate)), + [`${cellPrefixCls}-range-end-near-hover`]: + isRangeEnd(currentDate) && + (isSameCell(nextDate, hoverEnd) || + isInRange(generateConfig, hoverStart, hoverEnd, nextDate)), + + // Range Hover + [`${cellPrefixCls}-range-hover`]: isRangeHovered, + [`${cellPrefixCls}-range-hover-start`]: isHoverStart, + [`${cellPrefixCls}-range-hover-end`]: isHoverEnd, + + // Range Edge + [`${cellPrefixCls}-range-hover-edge-start`]: isHoverEdgeStart, + [`${cellPrefixCls}-range-hover-edge-end`]: isHoverEdgeEnd, + [`${cellPrefixCls}-range-hover-edge-start-near-range`]: + isHoverEdgeStart && isSameCell(prevDate, rangeEnd), + [`${cellPrefixCls}-range-hover-edge-end-near-range`]: + isHoverEdgeEnd && isSameCell(nextDate, rangeStart), + + // Others + [`${cellPrefixCls}-today`]: isSameCell(today, currentDate), + [`${cellPrefixCls}-selected`]: isSameCell(value, currentDate), + }; + } + + return getClassName; +} diff --git a/components/vc-picker/hooks/useHoverValue.ts b/components/vc-picker/hooks/useHoverValue.ts new file mode 100644 index 000000000..e24cda03e --- /dev/null +++ b/components/vc-picker/hooks/useHoverValue.ts @@ -0,0 +1,44 @@ +import { useState, useEffect, useRef } from 'react'; +import type { ValueTextConfig } from './useValueTexts'; +import useValueTexts from './useValueTexts'; + +export default function useHoverValue( + valueText: string, + { formatList, generateConfig, locale }: ValueTextConfig, +): [string, (date: DateType) => void, (immediately?: boolean) => void] { + const [value, internalSetValue] = useState(null); + const raf = useRef(null); + + function setValue(val: DateType, immediately: boolean = false) { + cancelAnimationFrame(raf.current); + if (immediately) { + internalSetValue(val); + return; + } + raf.current = requestAnimationFrame(() => { + internalSetValue(val); + }); + } + + const [, firstText] = useValueTexts(value, { + formatList, + generateConfig, + locale, + }); + + function onEnter(date: DateType) { + setValue(date); + } + + function onLeave(immediately: boolean = false) { + setValue(null, immediately); + } + + useEffect(() => { + onLeave(true); + }, [valueText]); + + useEffect(() => () => cancelAnimationFrame(raf.current), []); + + return [firstText, onEnter, onLeave]; +} diff --git a/components/vc-picker/hooks/usePickerInput.ts b/components/vc-picker/hooks/usePickerInput.ts new file mode 100644 index 000000000..0d54e3fcf --- /dev/null +++ b/components/vc-picker/hooks/usePickerInput.ts @@ -0,0 +1,171 @@ +import type * as React from 'react'; +import { useState, useEffect, useRef } from 'react'; +import KeyCode from 'rc-util/lib/KeyCode'; +import { addGlobalMouseDownEvent, getTargetFromEvent } from '../utils/uiUtil'; + +export default function usePickerInput({ + open, + value, + isClickOutside, + triggerOpen, + forwardKeyDown, + onKeyDown, + blurToCancel, + onSubmit, + onCancel, + onFocus, + onBlur, +}: { + open: boolean; + value: string; + isClickOutside: (clickElement: EventTarget | null) => boolean; + triggerOpen: (open: boolean) => void; + forwardKeyDown: (e: React.KeyboardEvent) => boolean; + onKeyDown: (e: React.KeyboardEvent, preventDefault: () => void) => void; + blurToCancel?: boolean; + onSubmit: () => void | boolean; + onCancel: () => void; + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; +}): [React.DOMAttributes, { focused: boolean; typing: boolean }] { + const [typing, setTyping] = useState(false); + const [focused, setFocused] = useState(false); + + /** + * We will prevent blur to handle open event when user click outside, + * since this will repeat trigger `onOpenChange` event. + */ + const preventBlurRef = useRef(false); + + const valueChangedRef = useRef(false); + + const preventDefaultRef = useRef(false); + + const inputProps: React.DOMAttributes = { + onMouseDown: () => { + setTyping(true); + triggerOpen(true); + }, + onKeyDown: (e) => { + const preventDefault = (): void => { + preventDefaultRef.current = true; + }; + + onKeyDown(e, preventDefault); + + if (preventDefaultRef.current) return; + + switch (e.which) { + case KeyCode.ENTER: { + if (!open) { + triggerOpen(true); + } else if (onSubmit() !== false) { + setTyping(true); + } + + e.preventDefault(); + return; + } + + case KeyCode.TAB: { + if (typing && open && !e.shiftKey) { + setTyping(false); + e.preventDefault(); + } else if (!typing && open) { + if (!forwardKeyDown(e) && e.shiftKey) { + setTyping(true); + e.preventDefault(); + } + } + return; + } + + case KeyCode.ESC: { + setTyping(true); + onCancel(); + return; + } + } + + if (!open && ![KeyCode.SHIFT].includes(e.which)) { + triggerOpen(true); + } else if (!typing) { + // Let popup panel handle keyboard + forwardKeyDown(e); + } + }, + + onFocus: (e) => { + setTyping(true); + setFocused(true); + + if (onFocus) { + onFocus(e); + } + }, + + onBlur: (e) => { + if (preventBlurRef.current || !isClickOutside(document.activeElement)) { + preventBlurRef.current = false; + return; + } + + if (blurToCancel) { + setTimeout(() => { + let { activeElement } = document; + while (activeElement && activeElement.shadowRoot) { + activeElement = activeElement.shadowRoot.activeElement; + } + + if (isClickOutside(activeElement)) { + onCancel(); + } + }, 0); + } else if (open) { + triggerOpen(false); + + if (valueChangedRef.current) { + onSubmit(); + } + } + setFocused(false); + + if (onBlur) { + onBlur(e); + } + }, + }; + + // check if value changed + useEffect(() => { + valueChangedRef.current = false; + }, [open]); + + useEffect(() => { + valueChangedRef.current = true; + }, [value]); + + // Global click handler + useEffect(() => + addGlobalMouseDownEvent((e: MouseEvent) => { + const target = getTargetFromEvent(e); + + if (open) { + const clickedOutside = isClickOutside(target); + + if (!clickedOutside) { + preventBlurRef.current = true; + + // Always set back in case `onBlur` prevented by user + requestAnimationFrame(() => { + preventBlurRef.current = false; + }); + } else if (!focused || clickedOutside) { + triggerOpen(false); + } + } + }), + ); + + return [inputProps, { focused, typing }]; +} diff --git a/components/vc-picker/hooks/useRangeDisabled.ts b/components/vc-picker/hooks/useRangeDisabled.ts new file mode 100644 index 000000000..c01b81673 --- /dev/null +++ b/components/vc-picker/hooks/useRangeDisabled.ts @@ -0,0 +1,113 @@ +import * as React from 'react'; +import type { RangeValue, PickerMode, Locale } from '../interface'; +import { getValue } from '../utils/miscUtil'; +import type { GenerateConfig } from '../generate'; +import { isSameDate, getQuarter } from '../utils/dateUtil'; + +export default function useRangeDisabled( + { + picker, + locale, + selectedValue, + disabledDate, + disabled, + generateConfig, + }: { + picker: PickerMode; + selectedValue: RangeValue; + disabledDate?: (date: DateType) => boolean; + disabled: [boolean, boolean]; + locale: Locale; + generateConfig: GenerateConfig; + }, + disabledStart: boolean, + disabledEnd: boolean, +) { + const startDate = getValue(selectedValue, 0); + const endDate = getValue(selectedValue, 1); + + function weekFirstDate(date: DateType) { + return generateConfig.locale.getWeekFirstDate(locale.locale, date); + } + + function monthNumber(date: DateType) { + const year = generateConfig.getYear(date); + const month = generateConfig.getMonth(date); + return year * 100 + month; + } + + function quarterNumber(date: DateType) { + const year = generateConfig.getYear(date); + const quarter = getQuarter(generateConfig, date); + return year * 10 + quarter; + } + + const disabledStartDate = React.useCallback( + (date: DateType) => { + if (disabledDate && disabledDate(date)) { + return true; + } + + // Disabled range + if (disabled[1] && endDate) { + return !isSameDate(generateConfig, date, endDate) && generateConfig.isAfter(date, endDate); + } + + // Disabled part + if (disabledStart && endDate) { + switch (picker) { + case 'quarter': + return quarterNumber(date) > quarterNumber(endDate); + case 'month': + return monthNumber(date) > monthNumber(endDate); + case 'week': + return weekFirstDate(date) > weekFirstDate(endDate); + default: + return ( + !isSameDate(generateConfig, date, endDate) && generateConfig.isAfter(date, endDate) + ); + } + } + + return false; + }, + [disabledDate, disabled[1], endDate, disabledStart], + ); + + const disabledEndDate = React.useCallback( + (date: DateType) => { + if (disabledDate && disabledDate(date)) { + return true; + } + + // Disabled range + if (disabled[0] && startDate) { + return ( + !isSameDate(generateConfig, date, endDate) && generateConfig.isAfter(startDate, date) + ); + } + + // Disabled part + if (disabledEnd && startDate) { + switch (picker) { + case 'quarter': + return quarterNumber(date) < quarterNumber(startDate); + case 'month': + return monthNumber(date) < monthNumber(startDate); + case 'week': + return weekFirstDate(date) < weekFirstDate(startDate); + default: + return ( + !isSameDate(generateConfig, date, startDate) && + generateConfig.isAfter(startDate, date) + ); + } + } + + return false; + }, + [disabledDate, disabled[0], startDate, disabledEnd], + ); + + return [disabledStartDate, disabledEndDate]; +} diff --git a/components/vc-picker/hooks/useRangeViewDates.ts b/components/vc-picker/hooks/useRangeViewDates.ts new file mode 100644 index 000000000..df6e63453 --- /dev/null +++ b/components/vc-picker/hooks/useRangeViewDates.ts @@ -0,0 +1,121 @@ +import * as React from 'react'; +import type { RangeValue, PickerMode } from '../interface'; +import type { GenerateConfig } from '../generate'; +import { getValue, updateValues } from '../utils/miscUtil'; +import { getClosingViewDate, isSameYear, isSameMonth, isSameDecade } from '../utils/dateUtil'; + +function getStartEndDistance( + startDate: DateType, + endDate: DateType, + picker: PickerMode, + generateConfig: GenerateConfig, +): 'same' | 'closing' | 'far' { + const startNext = getClosingViewDate(startDate, picker, generateConfig, 1); + + function getDistance(compareFunc: (start: DateType | null, end: DateType | null) => boolean) { + if (compareFunc(startDate, endDate)) { + return 'same'; + } + if (compareFunc(startNext, endDate)) { + return 'closing'; + } + return 'far'; + } + + switch (picker) { + case 'year': + return getDistance((start, end) => isSameDecade(generateConfig, start, end)); + case 'quarter': + case 'month': + return getDistance((start, end) => isSameYear(generateConfig, start, end)); + default: + return getDistance((start, end) => isSameMonth(generateConfig, start, end)); + } +} + +function getRangeViewDate( + values: RangeValue, + index: 0 | 1, + picker: PickerMode, + generateConfig: GenerateConfig, +): DateType | null { + const startDate = getValue(values, 0); + const endDate = getValue(values, 1); + + if (index === 0) { + return startDate; + } + + if (startDate && endDate) { + const distance = getStartEndDistance(startDate, endDate, picker, generateConfig); + switch (distance) { + case 'same': + return startDate; + case 'closing': + return startDate; + default: + return getClosingViewDate(endDate, picker, generateConfig, -1); + } + } + + return startDate; +} + +export default function useRangeViewDates({ + values, + picker, + defaultDates, + generateConfig, +}: { + values: RangeValue; + picker: PickerMode; + defaultDates: RangeValue | undefined; + generateConfig: GenerateConfig; +}): [(activePickerIndex: 0 | 1) => DateType, (viewDate: DateType | null, index: 0 | 1) => void] { + const [defaultViewDates, setDefaultViewDates] = React.useState< + [DateType | null, DateType | null] + >(() => [getValue(defaultDates, 0), getValue(defaultDates, 1)]); + const [viewDates, setInternalViewDates] = React.useState>(null); + + const startDate = getValue(values, 0); + const endDate = getValue(values, 1); + + function getViewDate(index: 0 | 1): DateType { + // If set default view date, use it + if (defaultViewDates[index]) { + return defaultViewDates[index]!; + } + + return ( + getValue(viewDates, index) || + getRangeViewDate(values, index, picker, generateConfig) || + startDate || + endDate || + generateConfig.getNow() + ); + } + + function setViewDate(viewDate: DateType | null, index: 0 | 1) { + if (viewDate) { + let newViewDates = updateValues(viewDates, viewDate, index); + // Set view date will clean up default one + setDefaultViewDates( + // Should always be an array + updateValues(defaultViewDates, null, index) || [null, null], + ); + + // Reset another one when not have value + const anotherIndex = (index + 1) % 2; + if (!getValue(values, anotherIndex)) { + newViewDates = updateValues(newViewDates, viewDate, anotherIndex); + } + + setInternalViewDates(newViewDates); + } else if (startDate || endDate) { + // Reset all when has values when `viewDate` is `null` which means from open trigger + setInternalViewDates(null); + } + } + + return [getViewDate, setViewDate]; +} diff --git a/components/vc-picker/hooks/useTextValueMapping.ts b/components/vc-picker/hooks/useTextValueMapping.ts new file mode 100644 index 000000000..af63b47a4 --- /dev/null +++ b/components/vc-picker/hooks/useTextValueMapping.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; + +export default function useTextValueMapping({ + valueTexts, + onTextChange, +}: { + /** Must useMemo, to assume that `valueTexts` only match on the first change */ + valueTexts: string[]; + onTextChange: (text: string) => void; +}): [string, (text: string) => void, () => void] { + const [text, setInnerText] = React.useState(''); + const valueTextsRef = React.useRef([]); + valueTextsRef.current = valueTexts; + + function triggerTextChange(value: string) { + setInnerText(value); + onTextChange(value); + } + + function resetText() { + setInnerText(valueTextsRef.current[0]); + } + + React.useEffect(() => { + if (valueTexts.every(valText => valText !== text)) { + resetText(); + } + }, [valueTexts.join('||')]); + + return [text, triggerTextChange, resetText]; +} diff --git a/components/vc-picker/hooks/useValueTexts.ts b/components/vc-picker/hooks/useValueTexts.ts new file mode 100644 index 000000000..89f01f5d9 --- /dev/null +++ b/components/vc-picker/hooks/useValueTexts.ts @@ -0,0 +1,42 @@ +import shallowEqual from 'shallowequal'; +import useMemo from 'rc-util/lib/hooks/useMemo'; +import type { GenerateConfig } from '../generate'; +import type { CustomFormat, Locale } from '../interface'; +import { formatValue } from '../utils/dateUtil'; + +export type ValueTextConfig = { + formatList: (string | CustomFormat)[]; + generateConfig: GenerateConfig; + locale: Locale; +}; + +export default function useValueTexts( + value: DateType | null, + { formatList, generateConfig, locale }: ValueTextConfig, +) { + return useMemo<[string[], string]>( + () => { + if (!value) { + return [[''], '']; + } + + // We will convert data format back to first format + let firstValueText: string = ''; + const fullValueTexts: string[] = []; + + for (let i = 0; i < formatList.length; i += 1) { + const format = formatList[i]; + const formatStr = formatValue(value, { generateConfig, locale, format }); + fullValueTexts.push(formatStr); + + if (i === 0) { + firstValueText = formatStr; + } + } + + return [fullValueTexts, firstValueText]; + }, + [value, formatList], + (prev, next) => prev[0] !== next[0] || !shallowEqual(prev[1], next[1]), + ); +} diff --git a/components/vc-picker/index.tsx b/components/vc-picker/index.tsx new file mode 100644 index 000000000..4773cc35f --- /dev/null +++ b/components/vc-picker/index.tsx @@ -0,0 +1,13 @@ +import Picker, { PickerProps } from './Picker'; +import PickerPanel, { PickerPanelProps } from './PickerPanel'; +import RangePicker, { RangePickerProps } from './RangePicker'; + +export { + PickerPanel, + RangePicker, + PickerProps, + PickerPanelProps, + RangePickerProps, +}; + +export default Picker; diff --git a/components/vc-picker/interface.ts b/components/vc-picker/interface.ts new file mode 100644 index 000000000..2eba561be --- /dev/null +++ b/components/vc-picker/interface.ts @@ -0,0 +1,109 @@ +import type { GenerateConfig } from './generate'; + +export type Locale = { + locale: string; + + // ===================== Date Panel ===================== + /** Display month before year in date panel header */ + monthBeforeYear?: boolean; + yearFormat: string; + monthFormat?: string; + quarterFormat?: string; + + today: string; + now: string; + backToToday: string; + ok: string; + timeSelect: string; + dateSelect: string; + weekSelect?: string; + clear: string; + month: string; + year: string; + previousMonth: string; + nextMonth: string; + monthSelect: string; + yearSelect: string; + decadeSelect: string; + + dayFormat: string; + dateFormat: string; + dateTimeFormat: string; + previousYear: string; + nextYear: string; + previousDecade: string; + nextDecade: string; + previousCentury: string; + nextCentury: string; + + shortWeekDays?: string[]; + shortMonths?: string[]; +}; + +export type PanelMode = 'time' | 'date' | 'week' | 'month' | 'quarter' | 'year' | 'decade'; + +export type PickerMode = Exclude; + +export type PanelRefProps = { + onKeyDown?: (e: KeyboardEvent) => boolean; + onBlur?: (e: FocusEvent)=> void; + onClose?: () => void; +}; + +export type NullableDateType = DateType | null | undefined; + +export type OnSelect = (value: DateType, type: 'key' | 'mouse' | 'submit') => void; + +export type PanelSharedProps = { + prefixCls: string; + generateConfig: GenerateConfig; + value?: NullableDateType; + viewDate: DateType; + /** [Legacy] Set default display picker view date */ + defaultPickerValue?: DateType; + locale: Locale; + disabledDate?: (date: DateType) => boolean; + + // prevIcon?: React.ReactNode; + // nextIcon?: React.ReactNode; + // superPrevIcon?: React.ReactNode; + // superNextIcon?: React.ReactNode; + + // /** + // * Typescript can not handle generic type so we can not use `forwardRef` here. + // * Thus, move ref into operationRef. + // * This is little hack which should refactor after typescript support. + // */ + // operationRef: React.MutableRefObject; + + onSelect: OnSelect; + onViewDateChange: (value: DateType) => void; + onPanelChange: (mode: PanelMode | null, viewValue: DateType) => void; +}; + +export type DisabledTimes = { + disabledHours?: () => number[]; + disabledMinutes?: (hour: number) => number[]; + disabledSeconds?: (hour: number, minute: number) => number[]; +}; + +export type DisabledTime = (date: DateType | null) => DisabledTimes; + +export type OnPanelChange = (value: DateType, mode: PanelMode) => void; + +export type EventValue = DateType | null; +export type RangeValue = [EventValue, EventValue] | null; + +export type Components = { + button?: any; + rangeItem?: any; +}; + +export type RangeList = { + label: string; + onClick: () => void; + onMouseEnter: () => void; + onMouseLeave: () => void; +}[]; + +export type CustomFormat = (value: DateType) => string; diff --git a/components/vc-picker/locale/ar_EG.ts b/components/vc-picker/locale/ar_EG.ts new file mode 100644 index 000000000..14e9b6b27 --- /dev/null +++ b/components/vc-picker/locale/ar_EG.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ar_EG', + today: 'اليوم', + now: 'الأن', + backToToday: 'العودة إلى اليوم', + ok: 'تأكيد', + clear: 'مسح', + month: 'الشهر', + year: 'السنة', + timeSelect: 'اختيار الوقت', + dateSelect: 'اختيار التاريخ', + monthSelect: 'اختيار الشهر', + yearSelect: 'اختيار السنة', + decadeSelect: 'اختيار العقد', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'الشهر السابق (PageUp)', + nextMonth: 'الشهر التالى(PageDown)', + previousYear: 'العام السابق (Control + left)', + nextYear: 'العام التالى (Control + right)', + previousDecade: 'العقد السابق', + nextDecade: 'العقد التالى', + previousCentury: 'القرن السابق', + nextCentury: 'القرن التالى', +}; + +export default locale; diff --git a/components/vc-picker/locale/az_AZ.ts b/components/vc-picker/locale/az_AZ.ts new file mode 100644 index 000000000..a499805bc --- /dev/null +++ b/components/vc-picker/locale/az_AZ.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'az_AZ', + today: 'Bugün', + now: 'İndi', + backToToday: 'Bugünə qayıt', + ok: 'Təsdiq', + clear: 'Təmizlə', + month: 'Ay', + year: 'İl', + timeSelect: 'vaxtı seç', + dateSelect: 'tarixi seç', + weekSelect: 'Həftə seç', + monthSelect: 'Ay seç', + yearSelect: 'il seç', + decadeSelect: 'Onillik seçin', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Əvvəlki ay (PageUp)', + nextMonth: 'Növbəti ay (PageDown)', + previousYear: 'Sonuncu il (Control + left)', + nextYear: 'Növbəti il (Control + right)', + previousDecade: 'Sonuncu onillik', + nextDecade: 'Növbəti onillik', + previousCentury: 'Sonuncu əsr', + nextCentury: 'Növbəti əsr', +}; + +export default locale; diff --git a/components/vc-picker/locale/bg_BG.ts b/components/vc-picker/locale/bg_BG.ts new file mode 100644 index 000000000..07334a91d --- /dev/null +++ b/components/vc-picker/locale/bg_BG.ts @@ -0,0 +1,31 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'bg_BG', + today: 'Днес', + now: 'Сега', + backToToday: 'Към днес', + ok: 'Добре', + clear: 'Изчистване', + month: 'Месец', + year: 'Година', + timeSelect: 'Избор на час', + dateSelect: 'Избор на дата', + monthSelect: 'Избор на месец', + yearSelect: 'Избор на година', + decadeSelect: 'Десетилетие', + yearFormat: 'YYYY', + dateFormat: 'D M YYYY', + dayFormat: 'D', + dateTimeFormat: 'D M YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Предишен месец (PageUp)', + nextMonth: 'Следващ месец (PageDown)', + previousYear: 'Последна година (Control + left)', + nextYear: 'Следваща година (Control + right)', + previousDecade: 'Предишно десетилетие', + nextDecade: 'Следващо десетилетие', + previousCentury: 'Последен век', + nextCentury: 'Следващ век', +}; +export default locale; diff --git a/components/vc-picker/locale/by_BY.ts b/components/vc-picker/locale/by_BY.ts new file mode 100644 index 000000000..3339d970b --- /dev/null +++ b/components/vc-picker/locale/by_BY.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { +locale: 'by_BY', +today: 'Сёння', +now: 'Зараз', +backToToday: 'Дадзеная дата', +ok: 'Ok', +clear: 'Ачысціць', +month: 'Месяц', +year: 'Год', +timeSelect: 'Выбраць час', +dateSelect: 'Выбраць дату', +weekSelect: 'Выбраць тыдзень', +monthSelect: 'Выбраць месяц', +yearSelect: 'Выбраць год', +decadeSelect: 'Выбраць дзесяцігоддзе', +yearFormat: 'YYYY', +dateFormat: 'D-M-YYYY', +dayFormat: 'D', +dateTimeFormat: 'D-M-YYYY HH:mm:ss', +monthBeforeYear: true, +previousMonth: 'Папярэдні месяц (PageUp)', +nextMonth: 'Наступны месяц (PageDown)', +previousYear: 'Папярэдні год (Control + left)', +nextYear: 'Наступны год (Control + right)', +previousDecade: 'Папярэдняе дзесяцігоддзе', +nextDecade: 'Наступнае дзесяцігоддзе', +previousCentury: 'Папярэдні век', +nextCentury: 'Наступны век', +}; + +export default locale; diff --git a/components/vc-picker/locale/ca_ES.ts b/components/vc-picker/locale/ca_ES.ts new file mode 100644 index 000000000..f93f09397 --- /dev/null +++ b/components/vc-picker/locale/ca_ES.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ca_ES', + today: 'Avui', + now: 'Ara', + backToToday: 'Tornar a avui', + ok: 'Acceptar', + clear: 'Netejar', + month: 'Mes', + year: 'Any', + timeSelect: 'Seleccionar hora', + dateSelect: 'Seleccionar data', + monthSelect: 'Escollir un mes', + yearSelect: 'Escollir un any', + decadeSelect: 'Escollir una dècada', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Mes anterior (PageUp)', + nextMonth: 'Mes següent (PageDown)', + previousYear: 'Any anterior (Control + left)', + nextYear: 'Mes següent (Control + right)', + previousDecade: 'Dècada anterior', + nextDecade: 'Dècada següent', + previousCentury: 'Segle anterior', + nextCentury: 'Segle següent', +}; + +export default locale; diff --git a/components/vc-picker/locale/cs_CZ.ts b/components/vc-picker/locale/cs_CZ.ts new file mode 100644 index 000000000..3c1b2eb3c --- /dev/null +++ b/components/vc-picker/locale/cs_CZ.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'cs_CZ', + today: 'Dnes', + now: 'Nyní', + backToToday: 'Zpět na dnešek', + ok: 'Ok', + clear: 'Vymazat', + month: 'Měsíc', + year: 'Rok', + timeSelect: 'Vybrat čas', + dateSelect: 'Vybrat datum', + monthSelect: 'Vyberte měsíc', + yearSelect: 'Vyberte rok', + decadeSelect: 'Vyberte dekádu', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Předchozí měsíc (PageUp)', + nextMonth: 'Následující (PageDown)', + previousYear: 'Předchozí rok (Control + left)', + nextYear: 'Následující rok (Control + right)', + previousDecade: 'Předchozí dekáda', + nextDecade: 'Následující dekáda', + previousCentury: 'Předchozí století', + nextCentury: 'Následující století', +}; + +export default locale; diff --git a/components/vc-picker/locale/da_DK.ts b/components/vc-picker/locale/da_DK.ts new file mode 100644 index 000000000..f3fe17c6e --- /dev/null +++ b/components/vc-picker/locale/da_DK.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'da_DK', + today: 'I dag', + now: 'Nu', + backToToday: 'Gå til i dag', + ok: 'Ok', + clear: 'Ryd', + month: 'Måned', + year: 'År', + timeSelect: 'Vælg tidspunkt', + dateSelect: 'Vælg dato', + monthSelect: 'Vælg måned', + yearSelect: 'Vælg år', + decadeSelect: 'Vælg årti', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Forrige måned (Page Up)', + nextMonth: 'Næste måned (Page Down)', + previousYear: 'Forrige år (Ctrl-venstre pil)', + nextYear: 'Næste år (Ctrl-højre pil)', + previousDecade: 'Forrige årti', + nextDecade: 'Næste årti', + previousCentury: 'Forrige århundrede', + nextCentury: 'Næste århundrede', +}; + +export default locale; diff --git a/components/vc-picker/locale/de_DE.ts b/components/vc-picker/locale/de_DE.ts new file mode 100644 index 000000000..e053a6302 --- /dev/null +++ b/components/vc-picker/locale/de_DE.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'de_DE', + today: 'Heute', + now: 'Jetzt', + backToToday: 'Zurück zu Heute', + ok: 'OK', + clear: 'Zurücksetzen', + month: 'Monat', + year: 'Jahr', + timeSelect: 'Zeit wählen', + dateSelect: 'Datum wählen', + monthSelect: 'Wähle einen Monat', + yearSelect: 'Wähle ein Jahr', + decadeSelect: 'Wähle ein Jahrzehnt', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Vorheriger Monat (PageUp)', + nextMonth: 'Nächster Monat (PageDown)', + previousYear: 'Vorheriges Jahr (Ctrl + left)', + nextYear: 'Nächstes Jahr (Ctrl + right)', + previousDecade: 'Vorheriges Jahrzehnt', + nextDecade: 'Nächstes Jahrzehnt', + previousCentury: 'Vorheriges Jahrhundert', + nextCentury: 'Nächstes Jahrhundert', +}; + +export default locale; diff --git a/components/vc-picker/locale/el_GR.ts b/components/vc-picker/locale/el_GR.ts new file mode 100644 index 000000000..e8979a57a --- /dev/null +++ b/components/vc-picker/locale/el_GR.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'el_GR', + today: 'Σήμερα', + now: 'Τώρα', + backToToday: 'Πίσω στη σημερινή μέρα', + ok: 'Ok', + clear: 'Καθαρισμός', + month: 'Μήνας', + year: 'Έτος', + timeSelect: 'Επιλογή ώρας', + dateSelect: 'Επιλογή ημερομηνίας', + monthSelect: 'Επιλογή μήνα', + yearSelect: 'Επιλογή έτους', + decadeSelect: 'Επιλογή δεκαετίας', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Προηγούμενος μήνας (PageUp)', + nextMonth: 'Επόμενος μήνας (PageDown)', + previousYear: 'Προηγούμενο έτος (Control + αριστερά)', + nextYear: 'Επόμενο έτος (Control + δεξιά)', + previousDecade: 'Προηγούμενη δεκαετία', + nextDecade: 'Επόμενη δεκαετία', + previousCentury: 'Προηγούμενος αιώνας', + nextCentury: 'Επόμενος αιώνας', +}; + +export default locale; diff --git a/components/vc-picker/locale/en_GB.ts b/components/vc-picker/locale/en_GB.ts new file mode 100644 index 000000000..a875ae257 --- /dev/null +++ b/components/vc-picker/locale/en_GB.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'en_GB', + today: 'Today', + now: 'Now', + backToToday: 'Back to today', + ok: 'Ok', + clear: 'Clear', + month: 'Month', + year: 'Year', + timeSelect: 'Select time', + dateSelect: 'Select date', + monthSelect: 'Choose a month', + yearSelect: 'Choose a year', + decadeSelect: 'Choose a decade', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Previous month (PageUp)', + nextMonth: 'Next month (PageDown)', + previousYear: 'Last year (Control + left)', + nextYear: 'Next year (Control + right)', + previousDecade: 'Last decade', + nextDecade: 'Next decade', + previousCentury: 'Last century', + nextCentury: 'Next century', +}; + +export default locale; diff --git a/components/vc-picker/locale/en_US.ts b/components/vc-picker/locale/en_US.ts new file mode 100644 index 000000000..28d5fad58 --- /dev/null +++ b/components/vc-picker/locale/en_US.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'en_US', + today: 'Today', + now: 'Now', + backToToday: 'Back to today', + ok: 'Ok', + clear: 'Clear', + month: 'Month', + year: 'Year', + timeSelect: 'select time', + dateSelect: 'select date', + weekSelect: 'Choose a week', + monthSelect: 'Choose a month', + yearSelect: 'Choose a year', + decadeSelect: 'Choose a decade', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Previous month (PageUp)', + nextMonth: 'Next month (PageDown)', + previousYear: 'Last year (Control + left)', + nextYear: 'Next year (Control + right)', + previousDecade: 'Last decade', + nextDecade: 'Next decade', + previousCentury: 'Last century', + nextCentury: 'Next century', +}; + +export default locale; diff --git a/components/vc-picker/locale/es_ES.ts b/components/vc-picker/locale/es_ES.ts new file mode 100644 index 000000000..373db7dbf --- /dev/null +++ b/components/vc-picker/locale/es_ES.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'es_ES', + today: 'Hoy', + now: 'Ahora', + backToToday: 'Volver a hoy', + ok: 'Aceptar', + clear: 'Limpiar', + month: 'Mes', + year: 'Año', + timeSelect: 'Seleccionar hora', + dateSelect: 'Seleccionar fecha', + monthSelect: 'Elegir un mes', + yearSelect: 'Elegir un año', + decadeSelect: 'Elegir una década', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Mes anterior (PageUp)', + nextMonth: 'Mes siguiente (PageDown)', + previousYear: 'Año anterior (Control + left)', + nextYear: 'Año siguiente (Control + right)', + previousDecade: 'Década anterior', + nextDecade: 'Década siguiente', + previousCentury: 'Siglo anterior', + nextCentury: 'Siglo siguiente', +}; + +export default locale; diff --git a/components/vc-picker/locale/et_EE.ts b/components/vc-picker/locale/et_EE.ts new file mode 100644 index 000000000..19a4af593 --- /dev/null +++ b/components/vc-picker/locale/et_EE.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'et_EE', + today: 'Täna', + now: 'Praegu', + backToToday: 'Tagasi tänase juurde', + ok: 'Ok', + clear: 'Tühista', + month: 'Kuu', + year: 'Aasta', + timeSelect: 'Vali aeg', + dateSelect: 'Vali kuupäev', + monthSelect: 'Vali kuu', + yearSelect: 'Vali aasta', + decadeSelect: 'Vali dekaad', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Eelmine kuu (PageUp)', + nextMonth: 'Järgmine kuu (PageDown)', + previousYear: 'Eelmine aasta (Control + left)', + nextYear: 'Järgmine aasta (Control + right)', + previousDecade: 'Eelmine dekaad', + nextDecade: 'Järgmine dekaad', + previousCentury: 'Eelmine sajand', + nextCentury: 'Järgmine sajand', +}; + +export default locale; diff --git a/components/vc-picker/locale/fa_IR.ts b/components/vc-picker/locale/fa_IR.ts new file mode 100644 index 000000000..f7e1af3b5 --- /dev/null +++ b/components/vc-picker/locale/fa_IR.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'fa_IR', + today: 'امروز', + now: 'اکنون', + backToToday: 'بازگشت به روز', + ok: 'باشه', + clear: 'پاک کردن', + month: 'ماه', + year: 'سال', + timeSelect: 'انتخاب زمان', + dateSelect: 'انتخاب تاریخ', + monthSelect: 'یک ماه را انتخاب کنید', + yearSelect: 'یک سال را انتخاب کنید', + decadeSelect: 'یک دهه را انتخاب کنید', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'ماه قبل (PageUp)', + nextMonth: 'ماه بعد (PageDown)', + previousYear: 'سال قبل (Control + left)', + nextYear: 'سال بعد (Control + right)', + previousDecade: 'دهه قبل', + nextDecade: 'دهه بعد', + previousCentury: 'قرن قبل', + nextCentury: 'قرن بعد', +}; + +export default locale; diff --git a/components/vc-picker/locale/fi_FI.ts b/components/vc-picker/locale/fi_FI.ts new file mode 100644 index 000000000..9a02113bf --- /dev/null +++ b/components/vc-picker/locale/fi_FI.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'fi_FI', + today: 'Tänään', + now: 'Nyt', + backToToday: 'Tämä päivä', + ok: 'Ok', + clear: 'Tyhjennä', + month: 'Kuukausi', + year: 'Vuosi', + timeSelect: 'Valise aika', + dateSelect: 'Valitse päivä', + monthSelect: 'Valitse kuukausi', + yearSelect: 'Valitse vuosi', + decadeSelect: 'Valitse vuosikymmen', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Edellinen kuukausi (PageUp)', + nextMonth: 'Seuraava kuukausi (PageDown)', + previousYear: 'Edellinen vuosi (Control + left)', + nextYear: 'Seuraava vuosi (Control + right)', + previousDecade: 'Edellinen vuosikymmen', + nextDecade: 'Seuraava vuosikymmen', + previousCentury: 'Edellinen vuosisata', + nextCentury: 'Seuraava vuosisata', +}; + +export default locale; diff --git a/components/vc-picker/locale/fr_BE.ts b/components/vc-picker/locale/fr_BE.ts new file mode 100644 index 000000000..b39864474 --- /dev/null +++ b/components/vc-picker/locale/fr_BE.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'fr_BE', + today: "Aujourd'hui", + now: 'Maintenant', + backToToday: "Aujourd'hui", + ok: 'Ok', + clear: 'Rétablir', + month: 'Mois', + year: 'Année', + timeSelect: "Sélectionner l'heure", + dateSelect: "Sélectionner l'heure", + monthSelect: 'Choisissez un mois', + yearSelect: 'Choisissez une année', + decadeSelect: 'Choisissez une décennie', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Mois précédent (PageUp)', + nextMonth: 'Mois suivant (PageDown)', + previousYear: 'Année précédente (Ctrl + gauche)', + nextYear: 'Année prochaine (Ctrl + droite)', + previousDecade: 'Décennie précédente', + nextDecade: 'Décennie suivante', + previousCentury: 'Siècle précédent', + nextCentury: 'Siècle suivant', +}; + +export default locale; diff --git a/components/vc-picker/locale/fr_CA.ts b/components/vc-picker/locale/fr_CA.ts new file mode 100644 index 000000000..8bbaf2ff6 --- /dev/null +++ b/components/vc-picker/locale/fr_CA.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'fr_CA', + today: "Aujourd'hui", + now: 'Maintenant', + backToToday: "Aujourd'hui", + ok: 'Ok', + clear: 'Rétablir', + month: 'Mois', + year: 'Année', + timeSelect: "Sélectionner l'heure", + dateSelect: 'Sélectionner la date', + monthSelect: 'Choisissez un mois', + yearSelect: 'Choisissez une année', + decadeSelect: 'Choisissez une décennie', + yearFormat: 'YYYY', + dateFormat: 'DD/MM/YYYY', + dayFormat: 'DD', + dateTimeFormat: 'DD/MM/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Mois précédent (PageUp)', + nextMonth: 'Mois suivant (PageDown)', + previousYear: 'Année précédente (Ctrl + gauche)', + nextYear: 'Année prochaine (Ctrl + droite)', + previousDecade: 'Décennie précédente', + nextDecade: 'Décennie suivante', + previousCentury: 'Siècle précédent', + nextCentury: 'Siècle suivant', +}; + +export default locale; diff --git a/components/vc-picker/locale/fr_FR.ts b/components/vc-picker/locale/fr_FR.ts new file mode 100644 index 000000000..53239b592 --- /dev/null +++ b/components/vc-picker/locale/fr_FR.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'fr_FR', + today: "Aujourd'hui", + now: 'Maintenant', + backToToday: "Aujourd'hui", + ok: 'Ok', + clear: 'Rétablir', + month: 'Mois', + year: 'Année', + timeSelect: "Sélectionner l'heure", + dateSelect: 'Sélectionner la date', + monthSelect: 'Choisissez un mois', + yearSelect: 'Choisissez une année', + decadeSelect: 'Choisissez une décennie', + yearFormat: 'YYYY', + dateFormat: 'DD/MM/YYYY', + dayFormat: 'DD', + dateTimeFormat: 'DD/MM/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Mois précédent (PageUp)', + nextMonth: 'Mois suivant (PageDown)', + previousYear: 'Année précédente (Ctrl + gauche)', + nextYear: 'Année prochaine (Ctrl + droite)', + previousDecade: 'Décennie précédente', + nextDecade: 'Décennie suivante', + previousCentury: 'Siècle précédent', + nextCentury: 'Siècle suivant', +}; + +export default locale; diff --git a/components/vc-picker/locale/ga_IE.ts b/components/vc-picker/locale/ga_IE.ts new file mode 100644 index 000000000..326a209bd --- /dev/null +++ b/components/vc-picker/locale/ga_IE.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ga_IE', + today: 'inniu', + now: 'anois', + backToToday: 'Ar ais inniu', + ok: 'ceart go leor', + clear: 'soiléir', + month: 'mhí', + year: 'bhliain', + timeSelect: 'roghnaigh am', + dateSelect: 'roghnaigh dáta', + weekSelect: 'Roghnaigh seachtain', + monthSelect: 'Roghnaigh mí', + yearSelect: 'Roghnaigh bliain', + decadeSelect: 'Roghnaigh deich mbliana', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'An mhí roimhe seo (PageUp)', + nextMonth: 'An mhí seo chugainn (PageDown)', + previousYear: 'Anuraidh (Control + left)', + nextYear: 'An bhliain seo chugainn (Control + right)', + previousDecade: 'Le deich mbliana anuas', + nextDecade: 'Deich mbliana amach romhainn', + previousCentury: 'An chéid seo caite', + nextCentury: 'An chéad aois eile', +}; + +export default locale; diff --git a/components/vc-picker/locale/gl_ES.ts b/components/vc-picker/locale/gl_ES.ts new file mode 100644 index 000000000..13258764e --- /dev/null +++ b/components/vc-picker/locale/gl_ES.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'gl_ES', + today: 'Hoxe', + now: 'Agora', + backToToday: 'Voltar a hoxe', + ok: 'Aceptar', + clear: 'Limpar', + month: 'Mes', + year: 'Ano', + timeSelect: 'Seleccionar hora', + dateSelect: 'Seleccionar data', + monthSelect: 'Elexir un mes', + yearSelect: 'Elexir un año', + decadeSelect: 'Elexir unha década', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Mes anterior (PageUp)', + nextMonth: 'Mes seguinte (PageDown)', + previousYear: 'Ano anterior (Control + left)', + nextYear: 'Ano seguinte (Control + right)', + previousDecade: 'Década anterior', + nextDecade: 'Década seguinte', + previousCentury: 'Século anterior', + nextCentury: 'Século seguinte', +}; + +export default locale; diff --git a/components/vc-picker/locale/he_IL.ts b/components/vc-picker/locale/he_IL.ts new file mode 100644 index 000000000..ea3fc8b73 --- /dev/null +++ b/components/vc-picker/locale/he_IL.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'he_IL', + today: 'היום', + now: 'עכשיו', + backToToday: 'חזור להיום', + ok: 'אישור', + clear: 'איפוס', + month: 'חודש', + year: 'שנה', + timeSelect: 'בחר שעה', + dateSelect: 'בחר תאריך', + weekSelect: 'בחר שבוע', + monthSelect: 'בחר חודש', + yearSelect: 'בחר שנה', + decadeSelect: 'בחר עשור', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'חודש קודם (PageUp)', + nextMonth: 'חודש הבא (PageDown)', + previousYear: 'שנה שעברה (Control + left)', + nextYear: 'שנה הבאה (Control + right)', + previousDecade: 'העשור הקודם', + nextDecade: 'העשור הבא', + previousCentury: 'המאה הקודמת', + nextCentury: 'המאה הבאה', +}; + +export default locale; diff --git a/components/vc-picker/locale/hi_IN.ts b/components/vc-picker/locale/hi_IN.ts new file mode 100644 index 000000000..51ae22917 --- /dev/null +++ b/components/vc-picker/locale/hi_IN.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'hi_IN', + today: 'आज', + now: 'अभी', + backToToday: 'आज तक', + ok: 'ठीक', + clear: 'स्पष्ट', + month: 'महीना', + year: 'साल', + timeSelect: 'समय का चयन करें', + dateSelect: 'तारीख़ चुनें', + weekSelect: 'एक सप्ताह चुनें', + monthSelect: 'एक महीना चुनें', + yearSelect: 'एक वर्ष चुनें', + decadeSelect: 'एक दशक चुनें', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'पिछला महीना (पेजअप)', + nextMonth: 'अगले महीने (पेजडाउन)', + previousYear: 'पिछले साल (Ctrl + बाएं)', + nextYear: 'अगले साल (Ctrl + दाहिना)', + previousDecade: 'पिछला दशक', + nextDecade: 'अगले दशक', + previousCentury: 'पीछ्ली शताब्दी', + nextCentury: 'अगली सदी', +}; + +export default locale; diff --git a/components/vc-picker/locale/hr_HR.ts b/components/vc-picker/locale/hr_HR.ts new file mode 100644 index 000000000..a341396b5 --- /dev/null +++ b/components/vc-picker/locale/hr_HR.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'hr_HR', + today: 'Danas', + now: 'Sad', + backToToday: 'Natrag na danas', + ok: 'Ok', + clear: 'Očisti', + month: 'Mjesec', + year: 'Godina', + timeSelect: 'odaberite vrijeme', + dateSelect: 'odaberite datum', + weekSelect: 'Odaberite tjedan', + monthSelect: 'Odaberite mjesec', + yearSelect: 'Odaberite godinu', + decadeSelect: 'Odaberite desetljeće', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Prošli mjesec (PageUp)', + nextMonth: 'Sljedeći mjesec (PageDown)', + previousYear: 'Prošla godina (Control + left)', + nextYear: 'Sljedeća godina (Control + right)', + previousDecade: 'Prošlo desetljeće', + nextDecade: 'Sljedeće desetljeće', + previousCentury: 'Prošlo stoljeće', + nextCentury: 'Sljedeće stoljeće', +}; + +export default locale; diff --git a/components/vc-picker/locale/hu_HU.ts b/components/vc-picker/locale/hu_HU.ts new file mode 100644 index 000000000..b98603dc8 --- /dev/null +++ b/components/vc-picker/locale/hu_HU.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'hu_HU', + today: 'Ma', // 'Today', + now: 'Most', // 'Now', + backToToday: 'Vissza a mai napra', // 'Back to today', + ok: 'Ok', + clear: 'Törlés', // 'Clear', + month: 'Hónap', // 'Month', + year: 'Év', // 'Year', + timeSelect: 'Időpont kiválasztása', // 'Select time', + dateSelect: 'Dátum kiválasztása', // 'Select date', + monthSelect: 'Hónap kiválasztása', // 'Choose a month', + yearSelect: 'Év kiválasztása', // 'Choose a year', + decadeSelect: 'Évtized kiválasztása', // 'Choose a decade', + yearFormat: 'YYYY', + dateFormat: 'YYYY/MM/DD', // 'M/D/YYYY', + dayFormat: 'DD', // 'D', + dateTimeFormat: 'YYYY/MM/DD HH:mm:ss', // 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Előző hónap (PageUp)', // 'Previous month (PageUp)', + nextMonth: 'Következő hónap (PageDown)', // 'Next month (PageDown)', + previousYear: 'Múlt év (Control + left)', // 'Last year (Control + left)', + nextYear: 'Jövő év (Control + right)', // 'Next year (Control + right)', + previousDecade: 'Előző évtized', // 'Last decade', + nextDecade: 'Következő évtized', // 'Next decade', + previousCentury: 'Múlt évszázad', // 'Last century', + nextCentury: 'Jövő évszázad', // 'Next century', +}; + +export default locale; diff --git a/components/vc-picker/locale/id_ID.ts b/components/vc-picker/locale/id_ID.ts new file mode 100644 index 000000000..01c9d760b --- /dev/null +++ b/components/vc-picker/locale/id_ID.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'id_ID', + today: 'Hari ini', + now: 'Sekarang', + backToToday: 'Kembali ke hari ini', + ok: 'Baik', + clear: 'Bersih', + month: 'Bulan', + year: 'Tahun', + timeSelect: 'pilih waktu', + dateSelect: 'pilih tanggal', + weekSelect: 'Pilih satu minggu', + monthSelect: 'Pilih satu bulan', + yearSelect: 'Pilih satu tahun', + decadeSelect: 'Pilih satu dekade', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Bulan sebelumnya (PageUp)', + nextMonth: 'Bulan selanjutnya (PageDown)', + previousYear: 'Tahun lalu (Control + kiri)', + nextYear: 'Tahun selanjutnya (Kontrol + kanan)', + previousDecade: 'Dekade terakhir', + nextDecade: 'Dekade berikutnya', + previousCentury: 'Abad terakhir', + nextCentury: 'Abad berikutnya', +}; + +export default locale; diff --git a/components/vc-picker/locale/is_IS.ts b/components/vc-picker/locale/is_IS.ts new file mode 100644 index 000000000..efea36701 --- /dev/null +++ b/components/vc-picker/locale/is_IS.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'is_IS', + today: 'Í dag', + now: 'Núna', + backToToday: 'Til baka til dagsins í dag', + ok: 'Í lagi', + clear: 'Hreinsa', + month: 'Mánuður', + year: 'Ár', + timeSelect: 'Velja tíma', + dateSelect: 'Velja dag', + monthSelect: 'Velja mánuð', + yearSelect: 'Velja ár', + decadeSelect: 'Velja áratug', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Fyrri mánuður (PageUp)', + nextMonth: 'Næsti mánuður (PageDown)', + previousYear: 'Fyrra ár (Control + left)', + nextYear: 'Næsta ár (Control + right)', + previousDecade: 'Fyrri áratugur', + nextDecade: 'Næsti áratugur', + previousCentury: 'Fyrri öld', + nextCentury: 'Næsta öld', +}; + +export default locale; diff --git a/components/vc-picker/locale/it_IT.ts b/components/vc-picker/locale/it_IT.ts new file mode 100644 index 000000000..3b69a9f2f --- /dev/null +++ b/components/vc-picker/locale/it_IT.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'it_IT', + today: 'Oggi', + now: 'Adesso', + backToToday: 'Torna ad oggi', + ok: 'Ok', + clear: 'Cancella', + month: 'Mese', + year: 'Anno', + timeSelect: "Seleziona l'ora", + dateSelect: 'Seleziona la data', + monthSelect: 'Seleziona il mese', + yearSelect: "Seleziona l'anno", + decadeSelect: 'Seleziona il decennio', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Il mese scorso (PageUp)', + nextMonth: 'Il prossimo mese (PageDown)', + previousYear: "L'anno scorso (Control + sinistra)", + nextYear: "L'anno prossimo (Control + destra)", + previousDecade: 'Ultimo decennio', + nextDecade: 'Prossimo decennio', + previousCentury: 'Secolo precedente', + nextCentury: 'Prossimo secolo', +}; + +export default locale; diff --git a/components/vc-picker/locale/ja_JP.ts b/components/vc-picker/locale/ja_JP.ts new file mode 100644 index 000000000..f4d3985f9 --- /dev/null +++ b/components/vc-picker/locale/ja_JP.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ja_JP', + today: '今日', + now: '現在時刻', + backToToday: '今日に戻る', + ok: '決定', + timeSelect: '時間を選択', + dateSelect: '日時を選択', + weekSelect: '週を選択', + clear: 'クリア', + month: '月', + year: '年', + previousMonth: '前月 (ページアップキー)', + nextMonth: '翌月 (ページダウンキー)', + monthSelect: '月を選択', + yearSelect: '年を選択', + decadeSelect: '年代を選択', + yearFormat: 'YYYY年', + dayFormat: 'D日', + dateFormat: 'YYYY年M月D日', + dateTimeFormat: 'YYYY年M月D日 HH時mm分ss秒', + previousYear: '前年 (Controlを押しながら左キー)', + nextYear: '翌年 (Controlを押しながら右キー)', + previousDecade: '前の年代', + nextDecade: '次の年代', + previousCentury: '前の世紀', + nextCentury: '次の世紀', +}; + +export default locale; diff --git a/components/vc-picker/locale/kk_KZ.ts b/components/vc-picker/locale/kk_KZ.ts new file mode 100644 index 000000000..f3b6787ff --- /dev/null +++ b/components/vc-picker/locale/kk_KZ.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'kk_KZ', + today: 'Бүгін', + now: 'Қазір', + backToToday: 'Ағымдағы күн', + ok: 'Таңдау', + clear: 'Таза', + month: 'Ай', + year: 'Жыл', + timeSelect: 'Уақытты таңдау', + dateSelect: 'Күнді таңдау', + monthSelect: 'Айды таңдаңыз', + yearSelect: 'Жылды таңдаңыз', + decadeSelect: 'Онжылды таңдаңыз', + yearFormat: 'YYYY', + dateFormat: 'D-M-YYYY', + dayFormat: 'D', + dateTimeFormat: 'D-M-YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Алдыңғы ай (PageUp)', + nextMonth: 'Келесі ай (PageDown)', + previousYear: 'Алдыңғы жыл (Control + left)', + nextYear: 'Келесі жыл (Control + right)', + previousDecade: 'Алдыңғы онжылдық', + nextDecade: 'Келесі онжылдық', + previousCentury: 'Алдыңғы ғасыр', + nextCentury: 'Келесі ғасыр', +}; + +export default locale; diff --git a/components/vc-picker/locale/km_KH.ts b/components/vc-picker/locale/km_KH.ts new file mode 100644 index 000000000..2edb5da3d --- /dev/null +++ b/components/vc-picker/locale/km_KH.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'km', + today: 'ថ្ងៃនេះ', + now: 'ឥឡូវ​នេះ', + backToToday: 'ត្រលប់ទៅថ្ងៃនេះ', + ok: 'កំណត់', + timeSelect: 'រយៈពេលជ្រើសរើស', + dateSelect: 'ជ្រើសរើសកាលបរិច្ឆេទ', + weekSelect: 'ជ្រើសរើសសប្តាហ៍', + clear: 'ច្បាស់', + month: 'ខែ', + year: 'ឆ្នាំ', + previousMonth: 'ខែមុន (ឡើងទំព័រ)', + nextMonth: 'ខែបន្ទាប់ (ប៊ូតុងចុះទំព័រ)', + monthSelect: 'ជ្រើសរើសខែ', + yearSelect: 'ជ្រើសរើសឆ្នាំ', + decadeSelect: 'ជ្រើសរើសអាយុ', + yearFormat: 'YYYY', + dayFormat: 'D', + dateFormat: 'YYYY-M-D', + dateTimeFormat: 'YYYY-M-D HH:mm:ss', + previousYear: 'ឆ្នាំមុន (Controlគ្រាប់ចុចបូកព្រួញខាងឆ្វេង)', + nextYear: 'ឆ្នាំក្រោយ (Control គ្រាប់ចុចបូកព្រួញស្ដាំ)', + previousDecade: 'ជំនាន់ចុងក្រោយ', + nextDecade: 'ជំនាន់​ក្រោយ', + previousCentury: 'សតវត្សចុងក្រោយ', + nextCentury: 'សតវត្សរ៍បន្ទាប់', +}; + +export default locale; diff --git a/components/vc-picker/locale/kmr_IQ.ts b/components/vc-picker/locale/kmr_IQ.ts new file mode 100644 index 000000000..867ec68b0 --- /dev/null +++ b/components/vc-picker/locale/kmr_IQ.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ku', + today: 'Îro', + now: 'Niha', + backToToday: 'Vegere îro', + ok: 'Temam', + clear: 'Paqij bike', + month: 'Meh', + year: 'Sal', + timeSelect: 'Demê hilbijêre', + dateSelect: 'Dîrok hilbijêre', + monthSelect: 'Meh hilbijêre', + yearSelect: 'Sal hilbijêre', + decadeSelect: 'Dehsal hilbijêre', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Meha peş (PageUp))', + nextMonth: 'Meha paş (PageDown)', + previousYear: 'Sala peş (Control + şep)', + nextYear: 'Sala paş (Control + rast)', + previousDecade: 'Dehsalen peş', + nextDecade: 'Dehsalen paş', + previousCentury: 'Sedsalen peş', + nextCentury: 'Sedsalen paş', +}; + +export default locale; diff --git a/components/vc-picker/locale/kn_IN.ts b/components/vc-picker/locale/kn_IN.ts new file mode 100644 index 000000000..1731ebdaf --- /dev/null +++ b/components/vc-picker/locale/kn_IN.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'kn_IN', + today: 'ಇಂದು', + now: 'ಈಗ', + backToToday: 'ಇಂದು ಹಿಂದಿರುಗಿ', + ok: 'ಸರಿ', + clear: 'ಸ್ಪಷ್ಟ', + month: 'ತಿಂಗಳು', + year: 'ವರ್ಷ', + timeSelect: 'ಸಮಯ ಆಯ್ಕೆಮಾಡಿ', + dateSelect: 'ದಿನಾಂಕವನ್ನು ಆಯ್ಕೆ ಮಾಡಿ', + weekSelect: 'ಒಂದು ವಾರದ ಆರಿಸಿ', + monthSelect: 'ಒಂದು ತಿಂಗಳು ಆಯ್ಕೆಮಾಡಿ', + yearSelect: 'ಒಂದು ವರ್ಷ ಆರಿಸಿ', + decadeSelect: 'ಒಂದು ದಶಕದ ಆಯ್ಕೆಮಾಡಿ', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'ಹಿಂದಿನ ತಿಂಗಳು (ಪೇಜ್ಅಪ್)', + nextMonth: 'ಮುಂದಿನ ತಿಂಗಳು (ಪೇಜ್ಡೌನ್)', + previousYear: 'ಕಳೆದ ವರ್ಷ (Ctrl + ಎಡ)', + nextYear: 'ಮುಂದಿನ ವರ್ಷ (Ctrl + ಬಲ)', + previousDecade: 'ಕಳೆದ ದಶಕ', + nextDecade: 'ಮುಂದಿನ ದಶಕ', + previousCentury: 'ಕಳೆದ ಶತಮಾನ', + nextCentury: 'ಮುಂದಿನ ಶತಮಾನ', +}; + +export default locale; diff --git a/components/vc-picker/locale/ko_KR.ts b/components/vc-picker/locale/ko_KR.ts new file mode 100644 index 000000000..6f8416c7e --- /dev/null +++ b/components/vc-picker/locale/ko_KR.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ko_KR', + today: '오늘', + now: '현재 시각', + backToToday: '오늘로 돌아가기', + ok: '확인', + clear: '지우기', + month: '월', + year: '년', + timeSelect: '시간 선택', + dateSelect: '날짜 선택', + monthSelect: '달 선택', + yearSelect: '연 선택', + decadeSelect: '연대 선택', + yearFormat: 'YYYY년', + dateFormat: 'YYYY-MM-DD', + dayFormat: 'Do', + dateTimeFormat: 'YYYY-MM-DD HH:mm:ss', + monthBeforeYear: false, + previousMonth: '이전 달 (PageUp)', + nextMonth: '다음 달 (PageDown)', + previousYear: '이전 해 (Control + left)', + nextYear: '다음 해 (Control + right)', + previousDecade: '이전 연대', + nextDecade: '다음 연대', + previousCentury: '이전 세기', + nextCentury: '다음 세기', +}; + +export default locale; diff --git a/components/vc-picker/locale/lt_LT.ts b/components/vc-picker/locale/lt_LT.ts new file mode 100644 index 000000000..2ddb658c7 --- /dev/null +++ b/components/vc-picker/locale/lt_LT.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'lt_LT', + today: 'Šiandien', + now: 'Dabar', + backToToday: 'Rodyti šiandien', + ok: 'Gerai', + clear: 'Išvalyti', + month: 'Mėnesis', + year: 'Metai', + timeSelect: 'Pasirinkti laiką', + dateSelect: 'Pasirinkti datą', + monthSelect: 'Pasirinkti mėnesį', + yearSelect: 'Pasirinkti metus', + decadeSelect: 'Pasirinkti dešimtmetį', + yearFormat: 'YYYY', + dateFormat: 'YYYY-MM-DD', + dayFormat: 'DD', + dateTimeFormat: 'YYYY-MM-DD HH:MM:SS', + monthBeforeYear: true, + previousMonth: 'Buvęs mėnesis (PageUp)', + nextMonth: 'Sekantis mėnesis (PageDown)', + previousYear: 'Buvę metai (Control + left)', + nextYear: 'Sekantis metai (Control + right)', + previousDecade: 'Buvęs dešimtmetis', + nextDecade: 'Sekantis dešimtmetis', + previousCentury: 'Buvęs amžius', + nextCentury: 'Sekantis amžius', +}; + +export default locale; diff --git a/components/vc-picker/locale/lv_LV.ts b/components/vc-picker/locale/lv_LV.ts new file mode 100644 index 000000000..0529ff639 --- /dev/null +++ b/components/vc-picker/locale/lv_LV.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'lv_LV', + today: 'Šodien', + now: 'Tagad', + backToToday: 'Atpakaļ pie šodienas', + ok: 'Ok', + clear: 'Skaidrs', + month: 'Mēnesis', + year: 'Gads', + timeSelect: 'Izvēlieties laiku', + dateSelect: 'Izvēlieties datumu', + monthSelect: 'Izvēlieties mēnesi', + yearSelect: 'Izvēlieties gadu', + decadeSelect: 'Izvēlieties desmit gadus', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Iepriekšējais mēnesis (PageUp)', + nextMonth: 'Nākammēnes (PageDown)', + previousYear: 'Pagājušais gads (Control + left)', + nextYear: 'Nākamgad (Control + right)', + previousDecade: 'Pēdējā desmitgadē', + nextDecade: 'Nākamā desmitgade', + previousCentury: 'Pagājušajā gadsimtā', + nextCentury: 'Nākamajā gadsimtā', +}; + +export default locale; diff --git a/components/vc-picker/locale/mk_MK.ts b/components/vc-picker/locale/mk_MK.ts new file mode 100644 index 000000000..d1dd459ab --- /dev/null +++ b/components/vc-picker/locale/mk_MK.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'mk_MK', + today: 'Денес', + now: 'Сега', + backToToday: 'Назад до денес', + ok: 'ОК', + clear: 'Избриши', + month: 'Месец', + year: 'Година', + timeSelect: 'Избери време', + dateSelect: 'Избери датум', + monthSelect: 'Избери месец', + yearSelect: 'Избери година', + decadeSelect: 'Избери деценија', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Претходен месец (PageUp)', + nextMonth: 'Нареден месец (PageDown)', + previousYear: 'Претходна година (Control + left)', + nextYear: 'Наредна година (Control + right)', + previousDecade: 'Претходна деценија', + nextDecade: 'Наредна деценија', + previousCentury: 'Претходен век', + nextCentury: 'Нареден век', +}; + +export default locale; diff --git a/components/vc-picker/locale/ml_IN.ts b/components/vc-picker/locale/ml_IN.ts new file mode 100644 index 000000000..5c7b513e0 --- /dev/null +++ b/components/vc-picker/locale/ml_IN.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ml_IN', + today: 'ഇന്ന്', + now: 'ഇപ്പോൾ', + backToToday: 'ഇന്നത്തെ ദിവസത്തിലേക്ക് തിരിച്ചു പോകുക', + ok: 'ശരിയാണ്', + clear: 'നീക്കം ചെയ്യുക', + month: 'മാസം', + year: 'വർഷം', + timeSelect: 'സമയം തിരഞ്ഞെടുക്കുക', + dateSelect: 'ദിവസം തിരഞ്ഞെടുക്കുക', + weekSelect: 'വാരം തിരഞ്ഞെടുക്കുക', + monthSelect: 'മാസം തിരഞ്ഞെടുക്കുക', + yearSelect: 'വർഷം തിരഞ്ഞെടുക്കുക', + decadeSelect: 'ദശാബ്ദം തിരഞ്ഞെടുക്കുക', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'കഴിഞ്ഞ മാസം (PageUp)', + nextMonth: 'അടുത്ത മാസം (PageDown)', + previousYear: 'കഴിഞ്ഞ വർഷം (Control + left)', + nextYear: 'അടുത്ത വർഷം (Control + right)', + previousDecade: 'കഴിഞ്ഞ ദശാബ്ദം', + nextDecade: 'അടുത്ത ദശാബ്ദം', + previousCentury: 'കഴിഞ്ഞ നൂറ്റാണ്ട്', + nextCentury: 'അടുത്ത നൂറ്റാണ്ട്', +}; + +export default locale; diff --git a/components/vc-picker/locale/mm_MM.ts b/components/vc-picker/locale/mm_MM.ts new file mode 100644 index 000000000..b6a824156 --- /dev/null +++ b/components/vc-picker/locale/mm_MM.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'mm_MM', + today: 'ယနေ့', + now: 'ယခု', + backToToday: 'ယနေ့ မတိုင်ခင် သို့', + ok: 'Ok', + clear: 'ရှင်းမည်', + month: 'လ', + year: 'နှစ်', + timeSelect: 'အချိန်ကို ရွေး', + dateSelect: 'နေ့ကို ရွေး', + weekSelect: 'အပတ်ကို ရွေး', + monthSelect: 'လကို ရွေး', + yearSelect: 'နှစ်ကို ရွေး', + decadeSelect: 'ဆယ်စုနှစ်ကို ရွေး', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'ယခင် လ (PageUp)', + nextMonth: 'နောက် လ (PageDown)', + previousYear: 'ယခင် နှစ် (Control + left)', + nextYear: 'နောက် နှစ် (Control + right)', + previousDecade: 'ယခင် ဆယ်စုနှစ်', + nextDecade: 'နောက် ဆယ်စုနှစ်', + previousCentury: 'ယခင် ရာစုနှစ်', + nextCentury: 'နောက် ရာစုနှစ်', +}; + +export default locale; diff --git a/components/vc-picker/locale/mn_MN.ts b/components/vc-picker/locale/mn_MN.ts new file mode 100644 index 000000000..3b9e2ef7d --- /dev/null +++ b/components/vc-picker/locale/mn_MN.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'mn_MN', + today: 'Өнөөдөр', + now: 'Одоо', + backToToday: 'Өнөөдөрлүү буцах', + ok: 'Ok', + clear: 'Цэвэрлэх', + month: 'Сар', + year: 'Жил', + timeSelect: 'Цаг сонгох', + dateSelect: 'Огноо сонгох', + weekSelect: '7 хоног сонгох', + monthSelect: 'Сар сонгох', + yearSelect: 'Жил сонгох', + decadeSelect: 'Арван сонгох', + yearFormat: 'YYYY', + dateFormat: 'YYYY/MM/DD', + dayFormat: 'DD', + dateTimeFormat: 'YYYY/MM/DD HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Өмнөх сар (PageUp)', + nextMonth: 'Дараа сар (PageDown)', + previousYear: 'Өмнөх жил (Control + left)', + nextYear: 'Дараа жил (Control + right)', + previousDecade: 'Өмнөх арван', + nextDecade: 'Дараа арван', + previousCentury: 'Өмнөх зуун', + nextCentury: 'Дараа зуун', +}; + +export default locale; diff --git a/components/vc-picker/locale/ms_MY.ts b/components/vc-picker/locale/ms_MY.ts new file mode 100644 index 000000000..1e3784c9e --- /dev/null +++ b/components/vc-picker/locale/ms_MY.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ms_MY', + today: 'Hari ini', + now: 'Sekarang', + backToToday: 'Kembali ke hari ini', + ok: 'Ok', + timeSelect: 'Pilih masa', + dateSelect: 'Pilih tarikh', + weekSelect: 'Pilih minggu', + clear: 'Padam', + month: 'Bulan', + year: 'Tahun', + previousMonth: 'Bulan lepas', + nextMonth: 'Bulan depan', + monthSelect: 'Pilih bulan', + yearSelect: 'Pilih tahun', + decadeSelect: 'Pilih dekad', + yearFormat: 'YYYY', + dayFormat: 'D', + dateFormat: 'M/D/YYYY', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + previousYear: 'Tahun lepas (Ctrl+left)', + nextYear: 'Tahun depan (Ctrl+right)', + previousDecade: 'Dekad lepas', + nextDecade: 'Dekad depan', + previousCentury: 'Abad lepas', + nextCentury: 'Abad depan', +}; + +export default locale; diff --git a/components/vc-picker/locale/nb_NO.ts b/components/vc-picker/locale/nb_NO.ts new file mode 100644 index 000000000..a2ce4ea64 --- /dev/null +++ b/components/vc-picker/locale/nb_NO.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'nb_NO', + today: 'I dag', + now: 'Nå', + backToToday: 'Gå til i dag', + ok: 'Ok', + clear: 'Annuller', + month: 'Måned', + year: 'År', + timeSelect: 'Velg tidspunkt', + dateSelect: 'Velg dato', + weekSelect: 'Velg uke', + monthSelect: 'Velg måned', + yearSelect: 'Velg år', + decadeSelect: 'Velg tiår', + yearFormat: 'YYYY', + dateFormat: 'DD.MM.YYYY', + dayFormat: 'DD', + dateTimeFormat: 'DD.MM.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Forrige måned (PageUp)', + nextMonth: 'Neste måned (PageDown)', + previousYear: 'Forrige år (Control + venstre)', + nextYear: 'Neste år (Control + høyre)', + previousDecade: 'Forrige tiår', + nextDecade: 'Neste tiår', + previousCentury: 'Forrige århundre', + nextCentury: 'Neste århundre', +}; + +export default locale; diff --git a/components/vc-picker/locale/nl_BE.ts b/components/vc-picker/locale/nl_BE.ts new file mode 100644 index 000000000..e78fef000 --- /dev/null +++ b/components/vc-picker/locale/nl_BE.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'nl_BE', + today: 'Vandaag', + now: 'Nu', + backToToday: 'Terug naar vandaag', + ok: 'Ok', + clear: 'Reset', + month: 'Maand', + year: 'Jaar', + timeSelect: 'Selecteer tijd', + dateSelect: 'Selecteer datum', + monthSelect: 'Kies een maand', + yearSelect: 'Kies een jaar', + decadeSelect: 'Kies een decennium', + yearFormat: 'YYYY', + dateFormat: 'D-M-YYYY', + dayFormat: 'D', + dateTimeFormat: 'D-M-YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Vorige maand (PageUp)', + nextMonth: 'Volgende maand (PageDown)', + previousYear: 'Vorig jaar (Control + left)', + nextYear: 'Volgend jaar (Control + right)', + previousDecade: 'Vorig decennium', + nextDecade: 'Volgend decennium', + previousCentury: 'Vorige eeuw', + nextCentury: 'Volgende eeuw', +}; + +export default locale; diff --git a/components/vc-picker/locale/nl_NL.ts b/components/vc-picker/locale/nl_NL.ts new file mode 100644 index 000000000..cb33549e5 --- /dev/null +++ b/components/vc-picker/locale/nl_NL.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'nl_NL', + today: 'Vandaag', + now: 'Nu', + backToToday: 'Terug naar vandaag', + ok: 'Ok', + clear: 'Reset', + month: 'Maand', + year: 'Jaar', + timeSelect: 'Selecteer tijd', + dateSelect: 'Selecteer datum', + monthSelect: 'Kies een maand', + yearSelect: 'Kies een jaar', + decadeSelect: 'Kies een decennium', + yearFormat: 'YYYY', + dateFormat: 'D-M-YYYY', + dayFormat: 'D', + dateTimeFormat: 'D-M-YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Vorige maand (PageUp)', + nextMonth: 'Volgende maand (PageDown)', + previousYear: 'Vorig jaar (Control + left)', + nextYear: 'Volgend jaar (Control + right)', + previousDecade: 'Vorig decennium', + nextDecade: 'Volgend decennium', + previousCentury: 'Vorige eeuw', + nextCentury: 'Volgende eeuw', +}; + +export default locale; diff --git a/components/vc-picker/locale/pl_PL.ts b/components/vc-picker/locale/pl_PL.ts new file mode 100644 index 000000000..d37f20f8f --- /dev/null +++ b/components/vc-picker/locale/pl_PL.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'pl_PL', + today: 'Dzisiaj', + now: 'Teraz', + backToToday: 'Ustaw dzisiaj', + ok: 'Ok', + clear: 'Wyczyść', + month: 'Miesiąc', + year: 'Rok', + timeSelect: 'Ustaw czas', + dateSelect: 'Ustaw datę', + monthSelect: 'Wybierz miesiąc', + yearSelect: 'Wybierz rok', + decadeSelect: 'Wybierz dekadę', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Poprzedni miesiąc (PageUp)', + nextMonth: 'Następny miesiąc (PageDown)', + previousYear: 'Ostatni rok (Ctrl + left)', + nextYear: 'Następny rok (Ctrl + right)', + previousDecade: 'Ostatnia dekada', + nextDecade: 'Następna dekada', + previousCentury: 'Ostatni wiek', + nextCentury: 'Następny wiek', +}; + +export default locale; diff --git a/components/vc-picker/locale/pt_BR.ts b/components/vc-picker/locale/pt_BR.ts new file mode 100644 index 000000000..a6839ba1d --- /dev/null +++ b/components/vc-picker/locale/pt_BR.ts @@ -0,0 +1,47 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'pt_BR', + today: 'Hoje', + now: 'Agora', + backToToday: 'Voltar para hoje', + ok: 'Ok', + clear: 'Limpar', + month: 'Mês', + year: 'Ano', + timeSelect: 'Selecionar hora', + dateSelect: 'Selecionar data', + monthSelect: 'Escolher mês', + yearSelect: 'Escolher ano', + decadeSelect: 'Escolher década', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: false, + previousMonth: 'Mês anterior (PageUp)', + nextMonth: 'Próximo mês (PageDown)', + previousYear: 'Ano anterior (Control + esquerda)', + nextYear: 'Próximo ano (Control + direita)', + previousDecade: 'Década anterior', + nextDecade: 'Próxima década', + previousCentury: 'Século anterior', + nextCentury: 'Próximo século', + shortWeekDays: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], + shortMonths: [ + 'Jan', + 'Fev', + 'Mar', + 'Abr', + 'Mai', + 'Jun', + 'Jul', + 'Ago', + 'Set', + 'Out', + 'Nov', + 'Dez', + ], +}; + +export default locale; diff --git a/components/vc-picker/locale/pt_PT.ts b/components/vc-picker/locale/pt_PT.ts new file mode 100644 index 000000000..86a9a64cc --- /dev/null +++ b/components/vc-picker/locale/pt_PT.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'pt_PT', + today: 'Hoje', + now: 'Agora', + backToToday: 'Hoje', + ok: 'Ok', + clear: 'Limpar', + month: 'Mês', + year: 'Ano', + timeSelect: 'Selecionar hora', + dateSelect: 'Selecionar data', + monthSelect: 'Selecionar mês', + yearSelect: 'Selecionar ano', + decadeSelect: 'Selecionar década', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Mês anterior (PageUp)', + nextMonth: 'Mês seguinte (PageDown)', + previousYear: 'Ano anterior (Control + left)', + nextYear: 'Ano seguinte (Control + right)', + previousDecade: 'Década anterior', + nextDecade: 'Década seguinte', + previousCentury: 'Século anterior', + nextCentury: 'Século seguinte', +}; + +export default locale; diff --git a/components/vc-picker/locale/ro_RO.ts b/components/vc-picker/locale/ro_RO.ts new file mode 100644 index 000000000..7994b7bee --- /dev/null +++ b/components/vc-picker/locale/ro_RO.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ro_RO', + today: 'Azi', + now: 'Acum', + backToToday: 'Înapoi la azi', + ok: 'Ok', + clear: 'Șterge', + month: 'Lună', + year: 'An', + timeSelect: 'selectează timpul', + dateSelect: 'selectează data', + weekSelect: 'Alege o săptămână', + monthSelect: 'Alege o lună', + yearSelect: 'Alege un an', + decadeSelect: 'Alege un deceniu', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Luna anterioară (PageUp)', + nextMonth: 'Luna următoare (PageDown)', + previousYear: 'Anul anterior (Control + stânga)', + nextYear: 'Anul următor (Control + dreapta)', + previousDecade: 'Deceniul anterior', + nextDecade: 'Deceniul următor', + previousCentury: 'Secolul anterior', + nextCentury: 'Secolul următor', +}; + +export default locale; diff --git a/components/vc-picker/locale/ru_RU.ts b/components/vc-picker/locale/ru_RU.ts new file mode 100644 index 000000000..a93224c92 --- /dev/null +++ b/components/vc-picker/locale/ru_RU.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ru_RU', + today: 'Сегодня', + now: 'Сейчас', + backToToday: 'Текущая дата', + ok: 'ОК', + clear: 'Очистить', + month: 'Месяц', + year: 'Год', + timeSelect: 'Выбрать время', + dateSelect: 'Выбрать дату', + monthSelect: 'Выбрать месяц', + yearSelect: 'Выбрать год', + decadeSelect: 'Выбрать десятилетие', + yearFormat: 'YYYY', + dateFormat: 'D-M-YYYY', + dayFormat: 'D', + dateTimeFormat: 'D-M-YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Предыдущий месяц (PageUp)', + nextMonth: 'Следующий месяц (PageDown)', + previousYear: 'Предыдущий год (Control + left)', + nextYear: 'Следующий год (Control + right)', + previousDecade: 'Предыдущее десятилетие', + nextDecade: 'Следущее десятилетие', + previousCentury: 'Предыдущий век', + nextCentury: 'Следующий век', +}; + +export default locale; diff --git a/components/vc-picker/locale/sk_SK.ts b/components/vc-picker/locale/sk_SK.ts new file mode 100644 index 000000000..997f55e4f --- /dev/null +++ b/components/vc-picker/locale/sk_SK.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'sk_SK', + today: 'Dnes', + now: 'Teraz', + backToToday: 'Späť na dnes', + ok: 'Ok', + clear: 'Vymazať', + month: 'Mesiac', + year: 'Rok', + timeSelect: 'Vybrať čas', + dateSelect: 'Vybrať dátum', + monthSelect: 'Vybrať mesiac', + yearSelect: 'Vybrať rok', + decadeSelect: 'Vybrať dekádu', + yearFormat: 'YYYY', + dateFormat: 'D.M.YYYY', + dayFormat: 'D', + dateTimeFormat: 'D.M.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Predchádzajúci mesiac (PageUp)', + nextMonth: 'Nasledujúci mesiac (PageDown)', + previousYear: 'Predchádzajúci rok (Control + left)', + nextYear: 'Nasledujúci rok (Control + right)', + previousDecade: 'Predchádzajúca dekáda', + nextDecade: 'Nasledujúca dekáda', + previousCentury: 'Predchádzajúce storočie', + nextCentury: 'Nasledujúce storočie', +}; + +export default locale; diff --git a/components/vc-picker/locale/sl_SI.ts b/components/vc-picker/locale/sl_SI.ts new file mode 100644 index 000000000..4af01844f --- /dev/null +++ b/components/vc-picker/locale/sl_SI.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'sl_SI', + today: 'Danes', + now: 'Trenutno', + backToToday: 'Nazaj na danes', + ok: 'V redu', + clear: 'Počisti', + month: 'Mesec', + year: 'Leto', + timeSelect: 'Izberite čas', + dateSelect: 'Izberite datum', + monthSelect: 'Izberite mesec', + yearSelect: 'Izberite leto', + decadeSelect: 'Izberite desetletje', + yearFormat: 'YYYY', + dateFormat: 'DD.MM.YYYY', + dayFormat: 'D', + dateTimeFormat: 'DD.MM.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Prejšnji mesec (PageUp)', + nextMonth: 'Naslednji mesec (PageDown)', + previousYear: 'Prejšnje leto (Control + left)', + nextYear: 'Naslednje leto (Control + right)', + previousDecade: 'Prejšnje desetletje', + nextDecade: 'Naslednje desetletje', + previousCentury: 'Prejšnje stoletje', + nextCentury: 'Naslednje stoletje', +}; + +export default locale; diff --git a/components/vc-picker/locale/sr_RS.ts b/components/vc-picker/locale/sr_RS.ts new file mode 100644 index 000000000..d77eb97a4 --- /dev/null +++ b/components/vc-picker/locale/sr_RS.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'sr_RS', + today: 'Danas', + now: 'Sada', + backToToday: 'Vrati se na danas', + ok: 'U redu', + clear: 'Obriši', + month: 'Mesec', + year: 'Godina', + timeSelect: 'Izaberi vreme', + dateSelect: 'Izaberi datum', + monthSelect: 'Izaberi mesec', + yearSelect: 'Izaberi godinu', + decadeSelect: 'Izaberi deceniju', + yearFormat: 'YYYY', + dateFormat: 'DD.MM.YYYY', + dayFormat: 'D', + dateTimeFormat: 'DD.MM.YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Prethodni mesec (PageUp)', + nextMonth: 'Sledeći mesec (PageDown)', + previousYear: 'Prethodna godina (Control + left)', + nextYear: 'Sledeća godina (Control + right)', + previousDecade: 'Prethodna decenija', + nextDecade: 'Sledeća decenija', + previousCentury: 'Prethodni vek', + nextCentury: 'Sledeći vek', +}; + +export default locale; diff --git a/components/vc-picker/locale/sv_SE.ts b/components/vc-picker/locale/sv_SE.ts new file mode 100644 index 000000000..c42c29cc0 --- /dev/null +++ b/components/vc-picker/locale/sv_SE.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'sv_SE', + today: 'I dag', + now: 'Nu', + backToToday: 'Till idag', + ok: 'Ok', + clear: 'Avbryt', + month: 'Månad', + year: 'År', + timeSelect: 'Välj tidpunkt', + dateSelect: 'Välj datum', + monthSelect: 'Välj månad', + yearSelect: 'Välj år', + decadeSelect: 'Välj årtionde', + yearFormat: 'YYYY', + dateFormat: 'YYYY-MM-DD', + dayFormat: 'D', + dateTimeFormat: 'YYYY-MM-DD H:mm:ss', + monthBeforeYear: true, + previousMonth: 'Förra månaden (PageUp)', + nextMonth: 'Nästa månad (PageDown)', + previousYear: 'Föreg år (Control + left)', + nextYear: 'Nästa år (Control + right)', + previousDecade: 'Föreg årtionde', + nextDecade: 'Nästa årtionde', + previousCentury: 'Föreg århundrade', + nextCentury: 'Nästa århundrade', +}; + +export default locale; diff --git a/components/vc-picker/locale/ta_IN.ts b/components/vc-picker/locale/ta_IN.ts new file mode 100644 index 000000000..7f0e30568 --- /dev/null +++ b/components/vc-picker/locale/ta_IN.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ta_IN', + today: 'இன்று', + now: 'இப்போது', + backToToday: 'இன்றுக்கு திரும்பு', + ok: 'சரி', + clear: 'அழி', + month: 'மாதம்', + year: 'வருடம்', + timeSelect: 'நேரத்தைத் தேர்ந்தெடு', + dateSelect: 'தேதியைத் தேர்ந்தெடு', + weekSelect: 'வாரத்தைத் தேர்வுசெய்க', + monthSelect: 'மாதத்தைத் தேர்வுசெய்க', + yearSelect: 'வருடத்தைத் தேர்வுசெய்க', + decadeSelect: 'தசாப்தத்தைத் தேர்வுசெய்க', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'முந்தைய மாதம் (PageUp)', + nextMonth: 'அடுத்த மாதம் (PageDown)', + previousYear: 'முந்தைய வருடம் (Control + left)', + nextYear: 'அடுத்த வருடம் (Control + right)', + previousDecade: 'முந்தைய தசாப்தம்', + nextDecade: 'அடுத்த தசாப்தம்', + previousCentury: 'முந்தைய நூற்றாண்டு', + nextCentury: 'அடுத்த நூற்றாண்டு', +}; + +export default locale; diff --git a/components/vc-picker/locale/th_TH.ts b/components/vc-picker/locale/th_TH.ts new file mode 100644 index 000000000..bcc1c42ab --- /dev/null +++ b/components/vc-picker/locale/th_TH.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'th_TH', + today: 'วันนี้', + now: 'ตอนนี้', + backToToday: 'กลับไปยังวันนี้', + ok: 'ตกลง', + clear: 'ลบล้าง', + month: 'เดือน', + year: 'ปี', + timeSelect: 'เลือกเวลา', + dateSelect: 'เลือกวัน', + monthSelect: 'เลือกเดือน', + yearSelect: 'เลือกปี', + decadeSelect: 'เลือกทศวรรษ', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'เดือนก่อนหน้า (PageUp)', + nextMonth: 'เดือนถัดไป (PageDown)', + previousYear: 'ปีก่อนหน้า (Control + left)', + nextYear: 'ปีถัดไป (Control + right)', + previousDecade: 'ทศวรรษก่อนหน้า', + nextDecade: 'ทศวรรษถัดไป', + previousCentury: 'ศตวรรษก่อนหน้า', + nextCentury: 'ศตวรรษถัดไป', +}; + +export default locale; diff --git a/components/vc-picker/locale/tr_TR.ts b/components/vc-picker/locale/tr_TR.ts new file mode 100644 index 000000000..f2f10540b --- /dev/null +++ b/components/vc-picker/locale/tr_TR.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'tr_TR', + today: 'Bugün', + now: 'Şimdi', + backToToday: 'Bugüne Geri Dön', + ok: 'tamam', + clear: 'Temizle', + month: 'Ay', + year: 'Yıl', + timeSelect: 'Zaman Seç', + dateSelect: 'Tarih Seç', + monthSelect: 'Ay Seç', + yearSelect: 'Yıl Seç', + decadeSelect: 'On Yıl Seç', + yearFormat: 'YYYY', + dateFormat: 'M/D/YYYY', + dayFormat: 'D', + dateTimeFormat: 'M/D/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Önceki Ay (PageUp)', + nextMonth: 'Sonraki Ay (PageDown)', + previousYear: 'Önceki Yıl (Control + Sol)', + nextYear: 'Sonraki Yıl (Control + Sağ)', + previousDecade: 'Önceki On Yıl', + nextDecade: 'Sonraki On Yıl', + previousCentury: 'Önceki Yüzyıl', + nextCentury: 'Sonraki Yüzyıl', +}; + +export default locale; diff --git a/components/vc-picker/locale/ug_CN.ts b/components/vc-picker/locale/ug_CN.ts new file mode 100644 index 000000000..4e0241671 --- /dev/null +++ b/components/vc-picker/locale/ug_CN.ts @@ -0,0 +1,31 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'ug_CN', + today: 'بۈگۈن', + now: 'ھازىر', + backToToday: 'بۈگۈنگە قايتىش', + ok: 'مۇقىملاشتۇرۇش', + timeSelect: 'ۋاقىت تاللاش', + dateSelect: 'كۈن تاللاش', + clear: 'تازىلاش', + month: 'ئاي', + year: 'يىل', + previousMonth: 'ئالدىنقى ئاي(ئالدىنقى بەت )', + nextMonth: 'كېلەركى ئاي (كېلەركى بەت)', + monthSelect: 'ئاي تاللاش', + yearSelect: 'يىل تاللاش', + decadeSelect: 'يىللارنى تاللاش', + yearFormat: 'YYYY-يىلى', + dayFormat: 'D-كۈنى', + dateFormat: 'YYYY-يىلىM-ئاينىڭD-كۈنى', + dateTimeFormat: 'YYYY-يىلىM—ئاينىڭD-كۈنى، HH:mm:ss', + previousYear: 'ئالدىنقى يىلى (Controlبىلەن يۆنىلىش كونۇپكىسى)', + nextYear: 'كېلەركى يىلى (Controlبىلەن يۆنىلىش كونۇپكىسى)', + previousDecade: 'ئالدىنقى يىللار', + nextDecade: 'كېيىنكى يىللار', + previousCentury: 'ئالدىنقى ئەسىر', + nextCentury: 'كېيىنكى ئەسىر', +}; + +export default locale; diff --git a/components/vc-picker/locale/uk_UA.ts b/components/vc-picker/locale/uk_UA.ts new file mode 100644 index 000000000..eb56087fe --- /dev/null +++ b/components/vc-picker/locale/uk_UA.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'uk_UA', + today: 'Сьогодні', + now: 'Зараз', + backToToday: 'Поточна дата', + ok: 'Ok', + clear: 'Очистити', + month: 'Місяць', + year: 'Рік', + timeSelect: 'Обрати час', + dateSelect: 'Обрати дату', + monthSelect: 'Обрати місяць', + yearSelect: 'Обрати рік', + decadeSelect: 'Обрати десятиріччя', + yearFormat: 'YYYY', + dateFormat: 'D-M-YYYY', + dayFormat: 'D', + dateTimeFormat: 'D-M-YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Попередній місяць (PageUp)', + nextMonth: 'Наступний місяць (PageDown)', + previousYear: 'Попередній рік (Control + left)', + nextYear: 'Наступний рік (Control + right)', + previousDecade: 'Попереднє десятиріччя', + nextDecade: 'Наступне десятиріччя', + previousCentury: 'Попереднє століття', + nextCentury: 'Наступне століття', +}; + +export default locale; diff --git a/components/vc-picker/locale/vi_VN.ts b/components/vc-picker/locale/vi_VN.ts new file mode 100644 index 000000000..75302e70e --- /dev/null +++ b/components/vc-picker/locale/vi_VN.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'vi_VN', + today: 'Hôm nay', + now: 'Bây giờ', + backToToday: 'Trở về hôm nay', + ok: 'Ok', + clear: 'Xóa', + month: 'Tháng', + year: 'Năm', + timeSelect: 'Chọn thời gian', + dateSelect: 'Chọn ngày', + weekSelect: 'Chọn tuần', + monthSelect: 'Chọn tháng', + yearSelect: 'Chọn năm', + decadeSelect: 'Chọn thập kỷ', + yearFormat: 'YYYY', + dateFormat: 'D/M/YYYY', + dayFormat: 'D', + dateTimeFormat: 'D/M/YYYY HH:mm:ss', + monthBeforeYear: true, + previousMonth: 'Tháng trước (PageUp)', + nextMonth: 'Tháng sau (PageDown)', + previousYear: 'Năm trước (Control + left)', + nextYear: 'Năm sau (Control + right)', + previousDecade: 'Thập kỷ trước', + nextDecade: 'Thập kỷ sau', + previousCentury: 'Thế kỷ trước', + nextCentury: 'Thế kỷ sau', +}; + +export default locale; diff --git a/components/vc-picker/locale/zh_CN.ts b/components/vc-picker/locale/zh_CN.ts new file mode 100644 index 000000000..0b0495bae --- /dev/null +++ b/components/vc-picker/locale/zh_CN.ts @@ -0,0 +1,32 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'zh_CN', + today: '今天', + now: '此刻', + backToToday: '返回今天', + ok: '确定', + timeSelect: '选择时间', + dateSelect: '选择日期', + weekSelect: '选择周', + clear: '清除', + month: '月', + year: '年', + previousMonth: '上个月 (翻页上键)', + nextMonth: '下个月 (翻页下键)', + monthSelect: '选择月份', + yearSelect: '选择年份', + decadeSelect: '选择年代', + yearFormat: 'YYYY年', + dayFormat: 'D日', + dateFormat: 'YYYY年M月D日', + dateTimeFormat: 'YYYY年M月D日 HH时mm分ss秒', + previousYear: '上一年 (Control键加左方向键)', + nextYear: '下一年 (Control键加右方向键)', + previousDecade: '上一年代', + nextDecade: '下一年代', + previousCentury: '上一世纪', + nextCentury: '下一世纪', +}; + +export default locale; diff --git a/components/vc-picker/locale/zh_TW.ts b/components/vc-picker/locale/zh_TW.ts new file mode 100644 index 000000000..47bd3b03a --- /dev/null +++ b/components/vc-picker/locale/zh_TW.ts @@ -0,0 +1,33 @@ +import type { Locale } from '../interface'; + +const locale: Locale = { + locale: 'zh_TW', + + today: '今天', + now: '此刻', + backToToday: '返回今天', + ok: '確定', + timeSelect: '選擇時間', + dateSelect: '選擇日期', + weekSelect: '選擇周', + clear: '清除', + month: '月', + year: '年', + previousMonth: '上個月 (翻頁上鍵)', + nextMonth: '下個月 (翻頁下鍵)', + monthSelect: '選擇月份', + yearSelect: '選擇年份', + decadeSelect: '選擇年代', + yearFormat: 'YYYY年', + dayFormat: 'D日', + dateFormat: 'YYYY年M月D日', + dateTimeFormat: 'YYYY年M月D日 HH時mm分ss秒', + previousYear: '上一年 (Control鍵加左方向鍵)', + nextYear: '下一年 (Control鍵加右方向鍵)', + previousDecade: '上一年代', + nextDecade: '下一年代', + previousCentury: '上一世紀', + nextCentury: '下一世紀', +}; + +export default locale; diff --git a/components/vc-picker/panels/DatePanel/DateBody.tsx b/components/vc-picker/panels/DatePanel/DateBody.tsx new file mode 100644 index 000000000..800d2ea06 --- /dev/null +++ b/components/vc-picker/panels/DatePanel/DateBody.tsx @@ -0,0 +1,111 @@ + +import type { GenerateConfig } from '../../generate'; +import { + WEEK_DAY_COUNT, + getWeekStartDate, + isSameDate, + isSameMonth, + formatValue, +} from '../../utils/dateUtil'; +import type { Locale } from '../../interface'; +import useCellClassName from '../../hooks/useCellClassName'; +import PanelBody from '../PanelBody'; +import { VueNode } from '../../../_util/type'; +import { useInjectRange } from '../../RangeContext'; + +export type DateRender = (currentDate: DateType, today: DateType) => VueNode; + +export type DateBodyPassProps = { + dateRender?: DateRender; + disabledDate?: (date: DateType) => boolean; + + // Used for week panel + prefixColumn?: (date: DateType) => VueNode; + rowClassName?: (date: DateType) => string; +}; + +export type DateBodyProps = { + prefixCls: string; + generateConfig: GenerateConfig; + value?: DateType | null; + viewDate: DateType; + locale: Locale; + rowCount: number; + onSelect: (value: DateType) => void; +} & DateBodyPassProps; + +function DateBody(props: DateBodyProps) { + const { + prefixCls, + generateConfig, + prefixColumn, + locale, + rowCount, + viewDate, + value, + dateRender, + } = props; + + const { rangedValue, hoverRangedValue } =useInjectRange() + + const baseDate = getWeekStartDate(locale.locale, generateConfig, viewDate); + const cellPrefixCls = `${prefixCls}-cell`; + const weekFirstDay = generateConfig.locale.getWeekFirstDay(locale.locale); + const today = generateConfig.getNow(); + + // ============================== Header ============================== + const headerCells: VueNode[] = []; + const weekDaysLocale: string[] = + locale.shortWeekDays || + (generateConfig.locale.getShortWeekDays + ? generateConfig.locale.getShortWeekDays(locale.locale) + : []); + + if (prefixColumn) { + headerCells.push(); + } + for (let i = 0; i < WEEK_DAY_COUNT; i += 1) { + headerCells.push({weekDaysLocale[(i + weekFirstDay) % WEEK_DAY_COUNT]}); + } + + // =============================== Body =============================== + const getCellClassName = useCellClassName({ + cellPrefixCls, + today, + value, + generateConfig, + rangedValue: prefixColumn ? null : rangedValue, + hoverRangedValue: prefixColumn ? null : hoverRangedValue, + isSameCell: (current, target) => isSameDate(generateConfig, current, target), + isInView: date => isSameMonth(generateConfig, date, viewDate), + offsetCell: (date, offset) => generateConfig.addDate(date, offset), + }); + + const getCellNode = dateRender ? (date: DateType) => dateRender(date, today) : undefined; + + return ( + + formatValue(date, { + locale, + format: 'YYYY-MM-DD', + generateConfig, + }) + } + headerCells={headerCells} + /> + ); +} + +DateBody.displayName = 'DateBody' +DateBody.inheritAttrs = false; + +export default DateBody; diff --git a/components/vc-picker/panels/DatePanel/DateHeader.tsx b/components/vc-picker/panels/DatePanel/DateHeader.tsx new file mode 100644 index 000000000..e74b5581f --- /dev/null +++ b/components/vc-picker/panels/DatePanel/DateHeader.tsx @@ -0,0 +1,105 @@ + +import Header from '../Header'; +import type { Locale } from '../../interface'; +import type { GenerateConfig } from '../../generate'; +import { useInjectPanel } from '../../PanelContext'; +import { formatValue } from '../../utils/dateUtil'; +import { VueNode } from '../../../_util/type'; + +export type DateHeaderProps = { + prefixCls: string; + viewDate: DateType; + value?: DateType | null; + locale: Locale; + generateConfig: GenerateConfig; + + onPrevYear: () => void; + onNextYear: () => void; + onPrevMonth: () => void; + onNextMonth: () => void; + onYearClick: () => void; + onMonthClick: () => void; +}; + +function DateHeader(props: DateHeaderProps) { + const { + prefixCls, + generateConfig, + locale, + viewDate, + onNextMonth, + onPrevMonth, + onNextYear, + onPrevYear, + onYearClick, + onMonthClick, + } = props; + + const { hideHeader } = useInjectPanel() + if (hideHeader) { + return null; + } + + const headerPrefixCls = `${prefixCls}-header`; + + const monthsLocale: string[] = + locale.shortMonths || + (generateConfig.locale.getShortMonths + ? generateConfig.locale.getShortMonths(locale.locale) + : []); + + const month = generateConfig.getMonth(viewDate); + + // =================== Month & Year =================== + const yearNode: VueNode = ( + + ); + const monthNode: VueNode = ( + + ); + + const monthYearNodes = locale.monthBeforeYear ? [monthNode, yearNode] : [yearNode, monthNode]; + + return ( +
+ {monthYearNodes} +
+ ); +} + +DateHeader.displayName = 'DateHeader' +DateHeader.inheritAttrs = false; +export default DateHeader; diff --git a/components/vc-picker/panels/DatePanel/index.tsx b/components/vc-picker/panels/DatePanel/index.tsx new file mode 100644 index 000000000..819dd5ae3 --- /dev/null +++ b/components/vc-picker/panels/DatePanel/index.tsx @@ -0,0 +1,116 @@ + +import type { DateBodyPassProps, DateRender } from './DateBody'; +import DateBody from './DateBody'; +import DateHeader from './DateHeader'; +import type { PanelSharedProps } from '../../interface'; +import { WEEK_DAY_COUNT } from '../../utils/dateUtil'; +import type { KeyboardConfig } from '../../utils/uiUtil'; +import { createKeyDownHandler } from '../../utils/uiUtil'; +import classNames from '../../../_util/classNames'; +import { ref } from '@vue/reactivity'; + +const DATE_ROW_COUNT = 6; + +export type DatePanelProps = { + active?: boolean; + dateRender?: DateRender; + + // Used for week panel + panelName?: string; + keyboardConfig?: KeyboardConfig; +} & PanelSharedProps & DateBodyPassProps; + +function DatePanel(props: DatePanelProps) { + const { + prefixCls, + panelName = 'date', + keyboardConfig, + active, + generateConfig, + value, + viewDate, + onViewDateChange, + onPanelChange, + onSelect, + } = props; + const panelPrefixCls = `${prefixCls}-${panelName}-panel`; + const operationRef = ref() + // ======================= Keyboard ======================= + operationRef.value = { + onKeyDown: event => + createKeyDownHandler(event, { + onLeftRight: diff => { + onSelect(generateConfig.addDate(value || viewDate, diff), 'key'); + }, + onCtrlLeftRight: diff => { + onSelect(generateConfig.addYear(value || viewDate, diff), 'key'); + }, + onUpDown: diff => { + onSelect(generateConfig.addDate(value || viewDate, diff * WEEK_DAY_COUNT), 'key'); + }, + onPageUpDown: diff => { + onSelect(generateConfig.addMonth(value || viewDate, diff), 'key'); + }, + ...keyboardConfig, + }), + }; + + // ==================== View Operation ==================== + const onYearChange = (diff: number) => { + const newDate = generateConfig.addYear(viewDate, diff); + onViewDateChange(newDate); + onPanelChange(null, newDate); + }; + const onMonthChange = (diff: number) => { + const newDate = generateConfig.addMonth(viewDate, diff); + onViewDateChange(newDate); + onPanelChange(null, newDate); + }; + + return ( +
+ { + onYearChange(-1); + }} + onNextYear={() => { + onYearChange(1); + }} + onPrevMonth={() => { + onMonthChange(-1); + }} + onNextMonth={() => { + onMonthChange(1); + }} + onMonthClick={() => { + onPanelChange('month', viewDate); + }} + onYearClick={() => { + onPanelChange('year', viewDate); + }} + /> + onSelect(date, 'mouse')} + prefixCls={prefixCls} + value={value} + viewDate={viewDate} + rowCount={DATE_ROW_COUNT} + /> +
+ ); +} + +DatePanel.displayName ='DatePanel' +DatePanel.inheritAttrs = false; + +export default DatePanel; diff --git a/components/vc-picker/panels/DatetimePanel/index.tsx b/components/vc-picker/panels/DatetimePanel/index.tsx new file mode 100644 index 000000000..98d3a9e7e --- /dev/null +++ b/components/vc-picker/panels/DatetimePanel/index.tsx @@ -0,0 +1,186 @@ + +import type { DatePanelProps } from '../DatePanel'; +import DatePanel from '../DatePanel'; +import type { SharedTimeProps } from '../TimePanel'; +import TimePanel from '../TimePanel'; +import { tuple } from '../../utils/miscUtil'; +import { setDateTime as setTime } from '../../utils/timeUtil'; +import type { PanelRefProps, DisabledTime } from '../../interface'; +import KeyCode from '../../../_util/KeyCode'; +import classNames from '../../../_util/classNames'; +import { ref } from '@vue/reactivity'; + +export type DatetimePanelProps = { + disabledTime?: DisabledTime; + showTime?: boolean | SharedTimeProps; + defaultValue?: DateType; +} & Omit< + DatePanelProps, + 'disabledHours' | 'disabledMinutes' | 'disabledSeconds' + >; + +const ACTIVE_PANEL = tuple('date', 'time'); +type ActivePanelType = typeof ACTIVE_PANEL[number]; + +function DatetimePanel(props: DatetimePanelProps) { + const { + prefixCls, + operationRef, + generateConfig, + value, + defaultValue, + disabledTime, + showTime, + onSelect, + } = props; + const panelPrefixCls = `${prefixCls}-datetime-panel`; + const activePanel = ref( + null, + ); + + const dateOperationRef = ref({}); + const timeOperationRef = ref({}); + + const timeProps = typeof showTime === 'object' ? { ...showTime } : {}; + + // ======================= Keyboard ======================= + function getNextActive(offset: number) { + const activeIndex = ACTIVE_PANEL.indexOf(activePanel.value!) + offset; + const nextActivePanel = ACTIVE_PANEL[activeIndex] || null; + return nextActivePanel; + } + + const onBlur = (e?: FocusEvent) => { + if (timeOperationRef.value.onBlur) { + timeOperationRef.value.onBlur(e!); + } + activePanel.value = null; + }; + + operationRef.current = { + onKeyDown: event => { + // Switch active panel + if (event.which === KeyCode.TAB) { + const nextActivePanel = getNextActive(event.shiftKey ? -1 : 1); + activePanel.value = nextActivePanel + + if (nextActivePanel) { + event.preventDefault(); + } + + return true; + } + + // Operate on current active panel + if (activePanel.value) { + const ref = + activePanel.value === 'date' ? dateOperationRef : timeOperationRef; + + if (ref.value && ref.value.onKeyDown) { + ref.value.onKeyDown(event); + } + + return true; + } + + // Switch first active panel if operate without panel + if ( + [KeyCode.LEFT, KeyCode.RIGHT, KeyCode.UP, KeyCode.DOWN].includes( + event.which, + ) + ) { + activePanel.value = 'date' + return true; + } + + return false; + }, + onBlur, + onClose: onBlur, + }; + + // ======================== Events ======================== + const onInternalSelect = (date: DateType, source: 'date' | 'time') => { + let selectedDate = date; + + if (source === 'date' && !value && timeProps.defaultValue) { + // Date with time defaultValue + selectedDate = generateConfig.setHour( + selectedDate, + generateConfig.getHour(timeProps.defaultValue), + ); + selectedDate = generateConfig.setMinute( + selectedDate, + generateConfig.getMinute(timeProps.defaultValue), + ); + selectedDate = generateConfig.setSecond( + selectedDate, + generateConfig.getSecond(timeProps.defaultValue), + ); + } else if (source === 'time' && !value && defaultValue) { + selectedDate = generateConfig.setYear( + selectedDate, + generateConfig.getYear(defaultValue), + ); + selectedDate = generateConfig.setMonth( + selectedDate, + generateConfig.getMonth(defaultValue), + ); + selectedDate = generateConfig.setDate( + selectedDate, + generateConfig.getDate(defaultValue), + ); + } + + if (onSelect) { + onSelect(selectedDate, 'mouse'); + } + }; + + // ======================== Render ======================== + const disabledTimes = disabledTime ? disabledTime(value || null) : {}; + + return ( +
+ { + onInternalSelect( + setTime( + generateConfig, + date, + showTime && typeof showTime === 'object' + ? showTime.defaultValue + : null, + ), + 'date', + ); + }} + /> + { + onInternalSelect(date, 'time'); + }} + /> +
+ ); +} + + +DatetimePanel.displayName ='DatetimePanel' +DatetimePanel.inheritAttrs = false; + +export default DatetimePanel; diff --git a/components/vc-picker/panels/DecadePanel/DecadeBody.tsx b/components/vc-picker/panels/DecadePanel/DecadeBody.tsx new file mode 100644 index 000000000..2b216abf1 --- /dev/null +++ b/components/vc-picker/panels/DecadePanel/DecadeBody.tsx @@ -0,0 +1,68 @@ + +import type { GenerateConfig } from '../../generate'; +import { DECADE_DISTANCE_COUNT, DECADE_UNIT_DIFF } from '.'; +import PanelBody from '../PanelBody'; + +export const DECADE_COL_COUNT = 3; +const DECADE_ROW_COUNT = 4; + +export type YearBodyProps = { + prefixCls: string; + generateConfig: GenerateConfig; + viewDate: DateType; + disabledDate?: (date: DateType) => boolean; + onSelect: (value: DateType) => void; +}; + +function DecadeBody(props: YearBodyProps) { + const DECADE_UNIT_DIFF_DES = DECADE_UNIT_DIFF - 1; + const { prefixCls, viewDate, generateConfig } = props; + + const cellPrefixCls = `${prefixCls}-cell`; + + const yearNumber = generateConfig.getYear(viewDate); + const decadeYearNumber = Math.floor(yearNumber / DECADE_UNIT_DIFF) * DECADE_UNIT_DIFF; + + const startDecadeYear = Math.floor(yearNumber / DECADE_DISTANCE_COUNT) * DECADE_DISTANCE_COUNT; + const endDecadeYear = startDecadeYear + DECADE_DISTANCE_COUNT - 1; + + const baseDecadeYear = generateConfig.setYear( + viewDate, + startDecadeYear - + Math.ceil( + (DECADE_COL_COUNT * DECADE_ROW_COUNT * DECADE_UNIT_DIFF - DECADE_DISTANCE_COUNT) / 2, + ), + ); + + const getCellClassName = (date: DateType) => { + const startDecadeNumber = generateConfig.getYear(date); + const endDecadeNumber = startDecadeNumber + DECADE_UNIT_DIFF_DES; + + return { + [`${cellPrefixCls}-in-view`]: + startDecadeYear <= startDecadeNumber && endDecadeNumber <= endDecadeYear, + [`${cellPrefixCls}-selected`]: startDecadeNumber === decadeYearNumber, + }; + }; + + return ( + { + const startDecadeNumber = generateConfig.getYear(date); + return `${startDecadeNumber}-${startDecadeNumber + DECADE_UNIT_DIFF_DES}`; + }} + getCellClassName={getCellClassName} + getCellDate={(date, offset) => generateConfig.addYear(date, offset * DECADE_UNIT_DIFF)} + /> + ); +} + + +DecadeBody.displayName ='DecadeBody' +DecadeBody.inheritAttrs = false; + +export default DecadeBody; diff --git a/components/vc-picker/panels/DecadePanel/DecadeHeader.tsx b/components/vc-picker/panels/DecadePanel/DecadeHeader.tsx new file mode 100644 index 000000000..0f23113e6 --- /dev/null +++ b/components/vc-picker/panels/DecadePanel/DecadeHeader.tsx @@ -0,0 +1,51 @@ + +import Header from '../Header'; +import type { GenerateConfig } from '../../generate'; +import { DECADE_DISTANCE_COUNT } from '.'; +import { useInjectPanel } from '../../PanelContext'; + +export type YearHeaderProps = { + prefixCls: string; + viewDate: DateType; + generateConfig: GenerateConfig; + + onPrevDecades: () => void; + onNextDecades: () => void; +}; + +function DecadeHeader(props: YearHeaderProps) { + const { + prefixCls, + generateConfig, + viewDate, + onPrevDecades, + onNextDecades, + } = props; + const { hideHeader } =useInjectPanel() + if (hideHeader) { + return null; + } + + const headerPrefixCls = `${prefixCls}-header`; + + const yearNumber = generateConfig.getYear(viewDate); + const startYear = + Math.floor(yearNumber / DECADE_DISTANCE_COUNT) * DECADE_DISTANCE_COUNT; + const endYear = startYear + DECADE_DISTANCE_COUNT - 1; + + return ( +
+ {startYear}-{endYear} +
+ ); +} + +DecadeHeader.displayName ='DecadeHeader' +DecadeHeader.inheritAttrs = false; + +export default DecadeHeader; diff --git a/components/vc-picker/panels/DecadePanel/index.tsx b/components/vc-picker/panels/DecadePanel/index.tsx new file mode 100644 index 000000000..fa4f0f3e4 --- /dev/null +++ b/components/vc-picker/panels/DecadePanel/index.tsx @@ -0,0 +1,96 @@ + +import DecadeHeader from './DecadeHeader'; +import DecadeBody, { DECADE_COL_COUNT } from './DecadeBody'; +import type { PanelSharedProps } from '../../interface'; +import { createKeyDownHandler } from '../../utils/uiUtil'; + +export type DecadePanelProps = PanelSharedProps; + +export const DECADE_UNIT_DIFF = 10; +export const DECADE_DISTANCE_COUNT = DECADE_UNIT_DIFF * 10; + +function DecadePanel(props: DecadePanelProps) { + const { + prefixCls, + onViewDateChange, + generateConfig, + viewDate, + operationRef, + onSelect, + onPanelChange, + } = props; + + const panelPrefixCls = `${prefixCls}-decade-panel`; + + // ======================= Keyboard ======================= + operationRef.current = { + onKeyDown: event => + createKeyDownHandler(event, { + onLeftRight: diff => { + onSelect( + generateConfig.addYear(viewDate, diff * DECADE_UNIT_DIFF), + 'key', + ); + }, + onCtrlLeftRight: diff => { + onSelect( + generateConfig.addYear(viewDate, diff * DECADE_DISTANCE_COUNT), + 'key', + ); + }, + onUpDown: diff => { + onSelect( + generateConfig.addYear( + viewDate, + diff * DECADE_UNIT_DIFF * DECADE_COL_COUNT, + ), + 'key', + ); + }, + onEnter: () => { + onPanelChange('year', viewDate); + }, + }), + }; + + // ==================== View Operation ==================== + const onDecadesChange = (diff: number) => { + const newDate = generateConfig.addYear( + viewDate, + diff * DECADE_DISTANCE_COUNT, + ); + onViewDateChange(newDate); + onPanelChange(null, newDate); + }; + + const onInternalSelect = (date: DateType) => { + onSelect(date, 'mouse'); + onPanelChange('year', date); + }; + + return ( +
+ { + onDecadesChange(-1); + }} + onNextDecades={() => { + onDecadesChange(1); + }} + /> + +
+ ); +} + + +DecadePanel.displayName ='DecadePanel' +DecadePanel.inheritAttrs = false; + +export default DecadePanel; diff --git a/components/vc-picker/panels/Header.tsx b/components/vc-picker/panels/Header.tsx new file mode 100644 index 000000000..6b8102738 --- /dev/null +++ b/components/vc-picker/panels/Header.tsx @@ -0,0 +1,100 @@ +import { CSSProperties } from '@vue/runtime-dom'; +import { VueNode } from '../../_util/type'; +import { useInjectPanel } from '../PanelContext'; + +const HIDDEN_STYLE: CSSProperties = { + visibility: 'hidden', +}; + +export type HeaderProps = { + prefixCls: string; + + // Icons + prevIcon?: VueNode; + nextIcon?: VueNode; + superPrevIcon?: VueNode; + superNextIcon?: VueNode; + + /** Last one step */ + onPrev?: () => void; + /** Next one step */ + onNext?: () => void; + /** Last multiple steps */ + onSuperPrev?: () => void; + /** Next multiple steps */ + onSuperNext?: () => void; + + children?: VueNode; +}; + +function Header( + { + prefixCls, + prevIcon = '\u2039', + nextIcon = '\u203A', + superPrevIcon = '\u00AB', + superNextIcon = '\u00BB', + onSuperPrev, + onSuperNext, + onPrev, + onNext, + }: HeaderProps, + { slots }, +) { + const { hideNextBtn, hidePrevBtn } = useInjectPanel(); + + return ( +
+ {onSuperPrev && ( + + )} + {onPrev && ( + + )} +
{slots.default?.()}
+ {onNext && ( + + )} + {onSuperNext && ( + + )} +
+ ); +} + +Header.displayName = 'Header'; +Header.inheritAttrs = false; + +export default Header; diff --git a/components/vc-picker/panels/MonthPanel/MonthBody.tsx b/components/vc-picker/panels/MonthPanel/MonthBody.tsx new file mode 100644 index 000000000..c553d6c93 --- /dev/null +++ b/components/vc-picker/panels/MonthPanel/MonthBody.tsx @@ -0,0 +1,89 @@ + +import type { GenerateConfig } from '../../generate'; +import type { Locale } from '../../interface'; +import { formatValue, isSameMonth } from '../../utils/dateUtil'; +import { useInjectRange } from '../../RangeContext'; +import useCellClassName from '../../hooks/useCellClassName'; +import PanelBody from '../PanelBody'; +import { VueNode } from '../../../_util/type'; + +export const MONTH_COL_COUNT = 3; +const MONTH_ROW_COUNT = 4; + +export type MonthCellRender = (currentDate: DateType, locale: Locale) => VueNode; + +export type MonthBodyProps = { + prefixCls: string; + locale: Locale; + generateConfig: GenerateConfig; + value?: DateType | null; + viewDate: DateType; + disabledDate?: (date: DateType) => boolean; + monthCellRender?: MonthCellRender; + onSelect: (value: DateType) => void; +}; + +function MonthBody(props: MonthBodyProps) { + const { prefixCls, locale, value, viewDate, generateConfig, monthCellRender } = props; + + const { rangedValue, hoverRangedValue } = useInjectRange() + + const cellPrefixCls = `${prefixCls}-cell`; + + const getCellClassName = useCellClassName({ + cellPrefixCls, + value, + generateConfig, + rangedValue, + hoverRangedValue, + isSameCell: (current, target) => isSameMonth(generateConfig, current, target), + isInView: () => true, + offsetCell: (date, offset) => generateConfig.addMonth(date, offset), + }); + + const monthsLocale: string[] = + locale.shortMonths || + (generateConfig.locale.getShortMonths + ? generateConfig.locale.getShortMonths(locale.locale) + : []); + + const baseMonth = generateConfig.setMonth(viewDate, 0); + + const getCellNode = monthCellRender + ? (date: DateType) => monthCellRender(date, locale) + : undefined; + + return ( + + locale.monthFormat + ? formatValue(date, { + locale, + format: locale.monthFormat, + generateConfig, + }) + : monthsLocale[generateConfig.getMonth(date)] + } + getCellClassName={getCellClassName} + getCellDate={generateConfig.addMonth} + titleCell={date => + formatValue(date, { + locale, + format: 'YYYY-MM', + generateConfig, + }) + } + /> + ); +} + + +MonthBody.displayName ='MonthBody' +MonthBody.inheritAttrs = false; + +export default MonthBody; diff --git a/components/vc-picker/panels/MonthPanel/MonthHeader.tsx b/components/vc-picker/panels/MonthPanel/MonthHeader.tsx new file mode 100644 index 000000000..e4ad37239 --- /dev/null +++ b/components/vc-picker/panels/MonthPanel/MonthHeader.tsx @@ -0,0 +1,58 @@ + +import Header from '../Header'; +import type { Locale } from '../../interface'; +import type { GenerateConfig } from '../../generate'; +import { useInjectPanel } from '../../PanelContext'; +import { formatValue } from '../../utils/dateUtil'; + +export type MonthHeaderProps = { + prefixCls: string; + viewDate: DateType; + locale: Locale; + generateConfig: GenerateConfig; + + onPrevYear: () => void; + onNextYear: () => void; + onYearClick: () => void; +}; + +function MonthHeader(props: MonthHeaderProps) { + const { + prefixCls, + generateConfig, + locale, + viewDate, + onNextYear, + onPrevYear, + onYearClick, + } = props; + const { hideHeader } = useInjectPanel() + if (hideHeader) { + return null; + } + + const headerPrefixCls = `${prefixCls}-header`; + + return ( +
+ +
+ ); +} + + +MonthHeader.displayName ='MonthHeader' +MonthHeader.inheritAttrs = false; + +export default MonthHeader; diff --git a/components/vc-picker/panels/MonthPanel/index.tsx b/components/vc-picker/panels/MonthPanel/index.tsx new file mode 100644 index 000000000..dd57ef9c7 --- /dev/null +++ b/components/vc-picker/panels/MonthPanel/index.tsx @@ -0,0 +1,86 @@ + +import MonthHeader from './MonthHeader'; +import type { MonthCellRender } from './MonthBody'; +import MonthBody, { MONTH_COL_COUNT } from './MonthBody'; +import type { PanelSharedProps } from '../../interface'; +import { createKeyDownHandler } from '../../utils/uiUtil'; + +export type MonthPanelProps = { + monthCellContentRender?: MonthCellRender; +} & PanelSharedProps; + +function MonthPanel(props: MonthPanelProps) { + const { + prefixCls, + operationRef, + onViewDateChange, + generateConfig, + value, + viewDate, + onPanelChange, + onSelect, + } = props; + + const panelPrefixCls = `${prefixCls}-month-panel`; + + // ======================= Keyboard ======================= + operationRef.current = { + onKeyDown: event => + createKeyDownHandler(event, { + onLeftRight: diff => { + onSelect(generateConfig.addMonth(value || viewDate, diff), 'key'); + }, + onCtrlLeftRight: diff => { + onSelect(generateConfig.addYear(value || viewDate, diff), 'key'); + }, + onUpDown: diff => { + onSelect( + generateConfig.addMonth(value || viewDate, diff * MONTH_COL_COUNT), + 'key', + ); + }, + onEnter: () => { + onPanelChange('date', value || viewDate); + }, + }), + }; + + // ==================== View Operation ==================== + const onYearChange = (diff: number) => { + const newDate = generateConfig.addYear(viewDate, diff); + onViewDateChange(newDate); + onPanelChange(null, newDate); + }; + + return ( +
+ { + onYearChange(-1); + }} + onNextYear={() => { + onYearChange(1); + }} + onYearClick={() => { + onPanelChange('year', viewDate); + }} + /> + + {...props} + prefixCls={prefixCls} + onSelect={date => { + onSelect(date, 'mouse'); + onPanelChange('date', date); + }} + /> +
+ ); +} + + +MonthPanel.displayName ='MonthPanel' +MonthPanel.inheritAttrs = false; + +export default MonthPanel; diff --git a/components/vc-picker/panels/PanelBody.tsx b/components/vc-picker/panels/PanelBody.tsx new file mode 100644 index 000000000..e4a95d669 --- /dev/null +++ b/components/vc-picker/panels/PanelBody.tsx @@ -0,0 +1,144 @@ + +import { useInjectPanel } from '../PanelContext'; +import type { GenerateConfig } from '../generate'; +import { getLastDay } from '../utils/timeUtil'; +import type { PanelMode } from '../interface'; +import { getCellDateDisabled } from '../utils/dateUtil'; +import { VueNode } from '../../_util/type'; +import classNames from '../../_util/classNames'; + +export type PanelBodyProps = { + prefixCls: string; + disabledDate?: (date: DateType) => boolean; + onSelect: (value: DateType) => void; + picker?: PanelMode; + + // By panel + headerCells?: VueNode; + rowNum: number; + colNum: number; + baseDate: DateType; + getCellClassName: (date: DateType) => Record; + getCellDate: (date: DateType, offset: number) => DateType; + getCellText: (date: DateType) => VueNode; + getCellNode?: (date: DateType) => VueNode; + titleCell?: (date: DateType) => string; + generateConfig: GenerateConfig; + + // Used for week panel + prefixColumn?: (date: DateType) => VueNode; + rowClassName?: (date: DateType) => string; +}; + +function PanelBody({ + prefixCls, + disabledDate, + onSelect, + picker, + rowNum, + colNum, + prefixColumn, + rowClassName, + baseDate, + getCellClassName, + getCellText, + getCellNode, + getCellDate, + generateConfig, + titleCell, + headerCells, +}: PanelBodyProps) { + const { onDateMouseEnter, onDateMouseLeave, mode } = useInjectPanel() + + const cellPrefixCls = `${prefixCls}-cell`; + + // =============================== Body =============================== + const rows: VueNode[] = []; + + for (let i = 0; i < rowNum; i += 1) { + const row: VueNode[] = []; + let rowStartDate: DateType; + + for (let j = 0; j < colNum; j += 1) { + const offset = i * colNum + j; + const currentDate = getCellDate(baseDate, offset); + const disabled = getCellDateDisabled({ + cellDate: currentDate, + mode, + disabledDate, + generateConfig, + }); + + if (j === 0) { + rowStartDate = currentDate; + + if (prefixColumn) { + row.push(prefixColumn(rowStartDate)); + } + } + + const title = titleCell && titleCell(currentDate); + + row.push( + { + if (!disabled) { + onSelect(currentDate); + } + }} + onMouseenter={() => { + if (!disabled && onDateMouseEnter) { + onDateMouseEnter(currentDate); + } + }} + onMouseleave={() => { + if (!disabled && onDateMouseLeave) { + onDateMouseLeave(currentDate); + } + }} + > + {getCellNode ? ( + getCellNode(currentDate) + ) : ( +
{getCellText(currentDate)}
+ )} + , + ); + } + + rows.push( + + {row} + , + ); + } + + return ( +
+ + {headerCells && ( + + {headerCells} + + )} + {rows} +
+
+ ); +} + +PanelBody.displayName = 'PanelBody'; +PanelBody.inheritAttrs = false; + +export default PanelBody; diff --git a/components/vc-picker/panels/QuarterPanel/QuarterBody.tsx b/components/vc-picker/panels/QuarterPanel/QuarterBody.tsx new file mode 100644 index 000000000..1bc6d23be --- /dev/null +++ b/components/vc-picker/panels/QuarterPanel/QuarterBody.tsx @@ -0,0 +1,71 @@ + +import type { GenerateConfig } from '../../generate'; +import type { Locale } from '../../interface'; +import { formatValue, isSameQuarter } from '../../utils/dateUtil'; +import RangeContext, { useInjectRange } from '../../RangeContext'; +import useCellClassName from '../../hooks/useCellClassName'; +import PanelBody from '../PanelBody'; + +export const QUARTER_COL_COUNT = 4; +const QUARTER_ROW_COUNT = 1; + +export type QuarterBodyProps = { + prefixCls: string; + locale: Locale; + generateConfig: GenerateConfig; + value?: DateType | null; + viewDate: DateType; + disabledDate?: (date: DateType) => boolean; + onSelect: (value: DateType) => void; +}; + +function QuarterBody(props: QuarterBodyProps) { + const { prefixCls, locale, value, viewDate, generateConfig } = props; + + const { rangedValue, hoverRangedValue } = useInjectRange() + + const cellPrefixCls = `${prefixCls}-cell`; + + const getCellClassName = useCellClassName({ + cellPrefixCls, + value, + generateConfig, + rangedValue, + hoverRangedValue, + isSameCell: (current, target) => isSameQuarter(generateConfig, current, target), + isInView: () => true, + offsetCell: (date, offset) => generateConfig.addMonth(date, offset * 3), + }); + + const baseQuarter = generateConfig.setDate(generateConfig.setMonth(viewDate, 0), 1); + + return ( + + formatValue(date, { + locale, + format: locale.quarterFormat || '[Q]Q', + generateConfig, + }) + } + getCellClassName={getCellClassName} + getCellDate={(date, offset) => generateConfig.addMonth(date, offset * 3)} + titleCell={date => + formatValue(date, { + locale, + format: 'YYYY-[Q]Q', + generateConfig, + }) + } + /> + ); +} + + +QuarterBody.displayName ='QuarterBody' +QuarterBody.inheritAttrs = false; +export default QuarterBody; diff --git a/components/vc-picker/panels/QuarterPanel/QuarterHeader.tsx b/components/vc-picker/panels/QuarterPanel/QuarterHeader.tsx new file mode 100644 index 000000000..d2727a0d5 --- /dev/null +++ b/components/vc-picker/panels/QuarterPanel/QuarterHeader.tsx @@ -0,0 +1,57 @@ + +import Header from '../Header'; +import type { Locale } from '../../interface'; +import type { GenerateConfig } from '../../generate'; +import { useInjectPanel } from '../../PanelContext'; +import { formatValue } from '../../utils/dateUtil'; + +export type QuarterHeaderProps = { + prefixCls: string; + viewDate: DateType; + locale: Locale; + generateConfig: GenerateConfig; + + onPrevYear: () => void; + onNextYear: () => void; + onYearClick: () => void; +}; + +function QuarterHeader(props: QuarterHeaderProps) { + const { + prefixCls, + generateConfig, + locale, + viewDate, + onNextYear, + onPrevYear, + onYearClick, + } = props; + const { hideHeader } =useInjectPanel() + if (hideHeader) { + return null; + } + + const headerPrefixCls = `${prefixCls}-header`; + return ( +
+ +
+ ); +} + + +QuarterHeader.displayName ='QuarterHeader' +QuarterHeader.inheritAttrs = false; + +export default QuarterHeader; diff --git a/components/vc-picker/panels/QuarterPanel/index.tsx b/components/vc-picker/panels/QuarterPanel/index.tsx new file mode 100644 index 000000000..320b1b968 --- /dev/null +++ b/components/vc-picker/panels/QuarterPanel/index.tsx @@ -0,0 +1,76 @@ + +import QuarterHeader from './QuarterHeader'; +import QuarterBody from './QuarterBody'; +import type { PanelSharedProps } from '../../interface'; +import { createKeyDownHandler } from '../../utils/uiUtil'; + +export type QuarterPanelProps = {} & PanelSharedProps; + +function QuarterPanel(props: QuarterPanelProps) { + const { + prefixCls, + operationRef, + onViewDateChange, + generateConfig, + value, + viewDate, + onPanelChange, + onSelect, + } = props; + + const panelPrefixCls = `${prefixCls}-quarter-panel`; + + // ======================= Keyboard ======================= + operationRef.current = { + onKeyDown: event => + createKeyDownHandler(event, { + onLeftRight: diff => { + onSelect(generateConfig.addMonth(value || viewDate, diff * 3), 'key'); + }, + onCtrlLeftRight: diff => { + onSelect(generateConfig.addYear(value || viewDate, diff), 'key'); + }, + onUpDown: diff => { + onSelect(generateConfig.addYear(value || viewDate, diff), 'key'); + }, + }), + }; + + // ==================== View Operation ==================== + const onYearChange = (diff: number) => { + const newDate = generateConfig.addYear(viewDate, diff); + onViewDateChange(newDate); + onPanelChange(null, newDate); + }; + + return ( +
+ { + onYearChange(-1); + }} + onNextYear={() => { + onYearChange(1); + }} + onYearClick={() => { + onPanelChange('year', viewDate); + }} + /> + + {...props} + prefixCls={prefixCls} + onSelect={date => { + onSelect(date, 'mouse'); + }} + /> +
+ ); +} + + +QuarterPanel.displayName ='QuarterPanel' +QuarterPanel.inheritAttrs = false; + +export default QuarterPanel; diff --git a/components/vc-picker/panels/TimePanel/TimeBody.tsx b/components/vc-picker/panels/TimePanel/TimeBody.tsx new file mode 100644 index 000000000..1cc84b938 --- /dev/null +++ b/components/vc-picker/panels/TimePanel/TimeBody.tsx @@ -0,0 +1,249 @@ + +import type { GenerateConfig } from '../../generate'; +import type { Locale, OnSelect } from '../../interface'; +import type { Unit } from './TimeUnitColumn'; +import TimeUnitColumn from './TimeUnitColumn'; +import { leftPad } from '../../utils/miscUtil'; +import type { SharedTimeProps } from '.'; +import { setTime as utilSetTime } from '../../utils/timeUtil'; +import { cloneElement } from '../../../_util/vnode'; +import { VueNode } from '../../../_util/type'; +import { Ref } from '@vue/reactivity'; + +function shouldUnitsUpdate(prevUnits: Unit[], nextUnits: Unit[]) { + if (prevUnits.length !== nextUnits.length) return true; + // if any unit's disabled status is different, the units should be re-evaluted + for (let i = 0; i < prevUnits.length; i += 1) { + if (prevUnits[i].disabled !== nextUnits[i].disabled) return true; + } + return false; +} + +function generateUnits( + start: number, + end: number, + step: number, + disabledUnits: number[] | undefined, +) { + const units: Unit[] = []; + for (let i = start; i <= end; i += step) { + units.push({ + label: leftPad(i, 2), + value: i, + disabled: (disabledUnits || []).includes(i), + }); + } + return units; +} + +export type BodyOperationRef = { + onUpDown: (diff: number) => void; +}; + +export type TimeBodyProps = { + prefixCls: string; + locale: Locale; + generateConfig: GenerateConfig; + value?: DateType | null; + onSelect: OnSelect; + activeColumnIndex: number; + operationRef: Ref; +} & SharedTimeProps; + +function TimeBody(props: TimeBodyProps) { + const { + generateConfig, + prefixCls, + operationRef, + activeColumnIndex, + value, + showHour, + showMinute, + showSecond, + use12Hours, + hourStep = 1, + minuteStep = 1, + secondStep = 1, + disabledHours, + disabledMinutes, + disabledSeconds, + hideDisabledOptions, + onSelect, + } = props; + + const columns: { + node: VueNode; + value: number; + units: Unit[]; + onSelect: (diff: number) => void; + }[] = []; + const contentPrefixCls = `${prefixCls}-content`; + const columnPrefixCls = `${prefixCls}-time-panel`; + + let isPM: boolean | undefined; + const originHour = value ? generateConfig.getHour(value) : -1; + let hour = originHour; + const minute = value ? generateConfig.getMinute(value) : -1; + const second = value ? generateConfig.getSecond(value) : -1; + + const setTime = ( + isNewPM: boolean | undefined, + newHour: number, + newMinute: number, + newSecond: number, + ) => { + let newDate = value || generateConfig.getNow(); + + const mergedHour = Math.max(0, newHour); + const mergedMinute = Math.max(0, newMinute); + const mergedSecond = Math.max(0, newSecond); + + newDate = utilSetTime( + generateConfig, + newDate, + !use12Hours || !isNewPM ? mergedHour : mergedHour + 12, + mergedMinute, + mergedSecond, + ); + + return newDate; + }; + + // ========================= Unit ========================= + const rawHours = generateUnits(0, 23, hourStep, disabledHours && disabledHours()); + + const memorizedRawHours = useMemo(() => rawHours, rawHours, shouldUnitsUpdate); + + // Should additional logic to handle 12 hours + if (use12Hours) { + isPM = hour >= 12; // -1 means should display AM + hour %= 12; + } + + const [AMDisabled, PMDisabled] = React.useMemo(() => { + if (!use12Hours) { + return [false, false]; + } + const AMPMDisabled = [true, true]; + memorizedRawHours.forEach(({ disabled, value: hourValue }) => { + if (disabled) return; + if (hourValue >= 12) { + AMPMDisabled[1] = false; + } else { + AMPMDisabled[0] = false; + } + }); + return AMPMDisabled; + }, [use12Hours, memorizedRawHours]); + + const hours = React.useMemo(() => { + if (!use12Hours) return memorizedRawHours; + return memorizedRawHours + .filter(isPM ? hourMeta => hourMeta.value >= 12 : hourMeta => hourMeta.value < 12) + .map(hourMeta => { + const hourValue = hourMeta.value % 12; + const hourLabel = hourValue === 0 ? '12' : leftPad(hourValue, 2); + return { + ...hourMeta, + label: hourLabel, + value: hourValue, + }; + }); + }, [use12Hours, isPM, memorizedRawHours]); + + const minutes = generateUnits(0, 59, minuteStep, disabledMinutes && disabledMinutes(originHour)); + + const seconds = generateUnits( + 0, + 59, + secondStep, + disabledSeconds && disabledSeconds(originHour, minute), + ); + + // ====================== Operations ====================== + operationRef.value = { + onUpDown: diff => { + const column = columns[activeColumnIndex]; + if (column) { + const valueIndex = column.units.findIndex(unit => unit.value === column.value); + + const unitLen = column.units.length; + for (let i = 1; i < unitLen; i += 1) { + const nextUnit = column.units[(valueIndex + diff * i + unitLen) % unitLen]; + + if (nextUnit.disabled !== true) { + column.onSelect(nextUnit.value); + break; + } + } + } + }, + }; + + // ======================== Render ======================== + function addColumnNode( + condition: boolean | undefined, + node: VueNode, + columnValue: number, + units: Unit[], + onColumnSelect: (diff: number) => void, + ) { + if (condition !== false) { + columns.push({ + node: cloneElement(node, { + prefixCls: columnPrefixCls, + value: columnValue, + active: activeColumnIndex === columns.length, + onSelect: onColumnSelect, + units, + hideDisabledOptions, + }), + onSelect: onColumnSelect, + value: columnValue, + units, + }); + } + } + + // Hour + addColumnNode(showHour, , hour, hours, num => { + onSelect(setTime(isPM, num, minute, second), 'mouse'); + }); + + // Minute + addColumnNode(showMinute, , minute, minutes, num => { + onSelect(setTime(isPM, hour, num, second), 'mouse'); + }); + + // Second + addColumnNode(showSecond, , second, seconds, num => { + onSelect(setTime(isPM, hour, minute, num), 'mouse'); + }); + + // 12 Hours + let PMIndex = -1; + if (typeof isPM === 'boolean') { + PMIndex = isPM ? 1 : 0; + } + + addColumnNode( + use12Hours === true, + , + PMIndex, + [ + { label: 'AM', value: 0, disabled: AMDisabled }, + { label: 'PM', value: 1, disabled: PMDisabled }, + ], + num => { + onSelect(setTime(!!num, hour, minute, second), 'mouse'); + }, + ); + + return
{columns.map(({ node }) => node)}
; +} + + +TimeBody.displayName ='TimeBody' +TimeBody.inheritAttrs = false; + +export default TimeBody; diff --git a/components/vc-picker/panels/TimePanel/TimeHeader.tsx b/components/vc-picker/panels/TimePanel/TimeHeader.tsx new file mode 100644 index 000000000..1e9fe954f --- /dev/null +++ b/components/vc-picker/panels/TimePanel/TimeHeader.tsx @@ -0,0 +1,42 @@ + +import Header from '../Header'; +import type { Locale } from '../../interface'; +import type { GenerateConfig } from '../../generate'; +import { useInjectPanel } from '../../PanelContext'; +import { formatValue } from '../../utils/dateUtil'; + +export type TimeHeaderProps = { + prefixCls: string; + value?: DateType | null; + locale: Locale; + generateConfig: GenerateConfig; + format: string; +}; + +function TimeHeader(props: TimeHeaderProps) { + const { hideHeader } = useInjectPanel() + if (hideHeader) { + return null; + } + + const { prefixCls, generateConfig, locale, value, format } = props; + const headerPrefixCls = `${prefixCls}-header`; + + return ( +
+ {value + ? formatValue(value, { + locale, + format, + generateConfig, + }) + : '\u00A0'} +
+ ); +} + + +TimeHeader.displayName ='TimeHeader' +TimeHeader.inheritAttrs = false; + +export default TimeHeader; diff --git a/components/vc-picker/panels/TimePanel/TimeUnitColumn.tsx b/components/vc-picker/panels/TimePanel/TimeUnitColumn.tsx new file mode 100644 index 000000000..18327abb8 --- /dev/null +++ b/components/vc-picker/panels/TimePanel/TimeUnitColumn.tsx @@ -0,0 +1,97 @@ +import { scrollTo, waitElementReady } from '../../utils/uiUtil'; +import { useInjectPanel } from '../../PanelContext'; +import classNames from '../../../_util/classNames'; +import { ref } from '@vue/reactivity'; +import { onBeforeUnmount, watch } from '@vue/runtime-core'; + +export type Unit = { + label: any; + value: number; + disabled: boolean; +}; + +export type TimeUnitColumnProps = { + prefixCls?: string; + units?: Unit[]; + value?: number; + active?: boolean; + hideDisabledOptions?: boolean; + onSelect?: (value: number) => void; +}; + +function TimeUnitColumn(props: TimeUnitColumnProps) { + const { prefixCls, units, onSelect, value, active, hideDisabledOptions } = props; + const cellPrefixCls = `${prefixCls}-cell`; + const { open } = useInjectPanel(); + + const ulRef = ref(null); + const liRefs = ref>(new Map()); + const scrollRef = ref(); + + watch( + () => props.value, + () => { + const li = liRefs.value.get(value!); + if (li && open !== false) { + scrollTo(ulRef.value!, li.offsetTop, 120); + } + }, + ); + onBeforeUnmount(() => { + scrollRef.value?.(); + }); + + watch(open, () => { + scrollRef.value?.(); + if (open) { + const li = liRefs.value.get(value!); + if (li) { + scrollRef.value = waitElementReady(li, () => { + scrollTo(ulRef.value!, li.offsetTop, 0); + }); + } + } + }); + + return ( +
    + {units!.map(unit => { + if (hideDisabledOptions && unit.disabled) { + return null; + } + + return ( +
  • { + liRefs.value.set(unit.value, element as HTMLElement); + }} + class={classNames(cellPrefixCls, { + [`${cellPrefixCls}-disabled`]: unit.disabled, + [`${cellPrefixCls}-selected`]: value === unit.value, + })} + onClick={() => { + if (unit.disabled) { + return; + } + onSelect!(unit.value); + }} + > +
    {unit.label}
    +
  • + ); + })} +
+ ); +} + +TimeUnitColumn.displayName = 'TimeUnitColumn'; +TimeUnitColumn.inheritAttrs = false; + +export default TimeUnitColumn; diff --git a/components/vc-picker/panels/TimePanel/index.tsx b/components/vc-picker/panels/TimePanel/index.tsx new file mode 100644 index 000000000..92c86c428 --- /dev/null +++ b/components/vc-picker/panels/TimePanel/index.tsx @@ -0,0 +1,98 @@ + +import TimeHeader from './TimeHeader'; +import type { BodyOperationRef } from './TimeBody'; +import TimeBody from './TimeBody'; +import type { PanelSharedProps, DisabledTimes } from '../../interface'; +import { createKeyDownHandler } from '../../utils/uiUtil'; +import classNames from '../../../_util/classNames'; +import { ref } from '@vue/reactivity'; + +export type SharedTimeProps = { + format?: string; + showNow?: boolean; + showHour?: boolean; + showMinute?: boolean; + showSecond?: boolean; + use12Hours?: boolean; + hourStep?: number; + minuteStep?: number; + secondStep?: number; + hideDisabledOptions?: boolean; + defaultValue?: DateType; +} & DisabledTimes; + +export type TimePanelProps = { + format?: string; + active?: boolean; +} & PanelSharedProps & SharedTimeProps; + +const countBoolean = (boolList: (boolean | undefined)[]) => + boolList.filter(bool => bool !== false).length; + +function TimePanel(props: TimePanelProps) { + const { + generateConfig, + format = 'HH:mm:ss', + prefixCls, + active, + operationRef, + showHour, + showMinute, + showSecond, + use12Hours = false, + onSelect, + value, + } = props; + const panelPrefixCls = `${prefixCls}-time-panel`; + const bodyOperationRef = ref(); + + // ======================= Keyboard ======================= + const activeColumnIndex = ref(-1); + const columnsCount = countBoolean([showHour, showMinute, showSecond, use12Hours]); + + operationRef.current = { + onKeyDown: event => + createKeyDownHandler(event, { + onLeftRight: diff => { + activeColumnIndex.value = (activeColumnIndex.value + diff + columnsCount) % columnsCount; + }, + onUpDown: diff => { + if (activeColumnIndex.value === -1) { + activeColumnIndex.value = 0 + } else if (bodyOperationRef.value) { + bodyOperationRef.value.onUpDown(diff); + } + }, + onEnter: () => { + onSelect(value || generateConfig.getNow(), 'key'); + activeColumnIndex.value = -1 + }, + }), + + onBlur: () => { + activeColumnIndex.value = -1 + }, + }; + + return ( +
+ + +
+ ); +} + + +TimePanel.displayName ='TimePanel' +TimePanel.inheritAttrs = false; + +export default TimePanel; diff --git a/components/vc-picker/panels/WeekPanel/index.tsx b/components/vc-picker/panels/WeekPanel/index.tsx new file mode 100644 index 000000000..c48bbb3f2 --- /dev/null +++ b/components/vc-picker/panels/WeekPanel/index.tsx @@ -0,0 +1,52 @@ + +import DatePanel from '../DatePanel'; +import type { PanelSharedProps } from '../../interface'; +import { isSameWeek } from '../../utils/dateUtil'; +import classNames from '../../../_util/classNames'; + +export type WeekPanelProps = PanelSharedProps; + +function WeekPanel(props: WeekPanelProps) { + const { prefixCls, generateConfig, locale, value } = props; + + // Render additional column + const cellPrefixCls = `${prefixCls}-cell`; + const prefixColumn = (date: DateType) => ( + + {generateConfig.locale.getWeek(locale.locale, date)} + + ); + + // Add row className + const rowPrefixCls = `${prefixCls}-week-panel-row`; + const rowClassName = (date: DateType) => + classNames(rowPrefixCls, { + [`${rowPrefixCls}-selected`]: isSameWeek( + generateConfig, + locale.locale, + value, + date, + ), + }); + + return ( + + ); +} + + +WeekPanel.displayName = 'WeekPanel'; +WeekPanel.inheritAttrs = false; + +export default WeekPanel; diff --git a/components/vc-picker/panels/YearPanel/YearBody.tsx b/components/vc-picker/panels/YearPanel/YearBody.tsx new file mode 100644 index 000000000..56b68d319 --- /dev/null +++ b/components/vc-picker/panels/YearPanel/YearBody.tsx @@ -0,0 +1,79 @@ + +import type { GenerateConfig } from '../../generate'; +import { YEAR_DECADE_COUNT } from '.'; +import type { Locale, NullableDateType } from '../../interface'; +import useCellClassName from '../../hooks/useCellClassName'; +import { formatValue, isSameYear } from '../../utils/dateUtil'; +import RangeContext, { useInjectRange } from '../../RangeContext'; +import PanelBody from '../PanelBody'; + +export const YEAR_COL_COUNT = 3; +const YEAR_ROW_COUNT = 4; + +export type YearBodyProps = { + prefixCls: string; + locale: Locale; + generateConfig: GenerateConfig; + value?: NullableDateType; + viewDate: DateType; + disabledDate?: (date: DateType) => boolean; + onSelect: (value: DateType) => void; +}; + +function YearBody(props: YearBodyProps) { + const { prefixCls, value, viewDate, locale, generateConfig } = props; + const { rangedValue, hoverRangedValue } = useInjectRange() + + const yearPrefixCls = `${prefixCls}-cell`; + + // =============================== Year =============================== + const yearNumber = generateConfig.getYear(viewDate); + const startYear = Math.floor(yearNumber / YEAR_DECADE_COUNT) * YEAR_DECADE_COUNT; + const endYear = startYear + YEAR_DECADE_COUNT - 1; + const baseYear = generateConfig.setYear( + viewDate, + startYear - Math.ceil((YEAR_COL_COUNT * YEAR_ROW_COUNT - YEAR_DECADE_COUNT) / 2), + ); + + const isInView = (date: DateType) => { + const currentYearNumber = generateConfig.getYear(date); + return startYear <= currentYearNumber && currentYearNumber <= endYear; + }; + + const getCellClassName = useCellClassName({ + cellPrefixCls: yearPrefixCls, + value, + generateConfig, + rangedValue, + hoverRangedValue, + isSameCell: (current, target) => isSameYear(generateConfig, current, target), + isInView, + offsetCell: (date, offset) => generateConfig.addYear(date, offset), + }); + + return ( + + formatValue(date, { + locale, + format: 'YYYY', + generateConfig, + }) + } + /> + ); +} + + + +YearBody.displayName = 'YearBody'; +YearBody.inheritAttrs = false; + +export default YearBody; diff --git a/components/vc-picker/panels/YearPanel/YearHeader.tsx b/components/vc-picker/panels/YearPanel/YearHeader.tsx new file mode 100644 index 000000000..d7d3d52e0 --- /dev/null +++ b/components/vc-picker/panels/YearPanel/YearHeader.tsx @@ -0,0 +1,50 @@ + +import Header from '../Header'; +import type { GenerateConfig } from '../../generate'; +import { YEAR_DECADE_COUNT } from '.'; +import { useInjectPanel } from '../../PanelContext'; + +export type YearHeaderProps = { + prefixCls: string; + viewDate: DateType; + value?: DateType | null; + generateConfig: GenerateConfig; + + onPrevDecade: () => void; + onNextDecade: () => void; + onDecadeClick: () => void; +}; + +function YearHeader(props: YearHeaderProps) { + const { prefixCls, generateConfig, viewDate, onPrevDecade, onNextDecade, onDecadeClick } = props; + const { hideHeader } = useInjectPanel() + if (hideHeader) { + return null; + } + + const headerPrefixCls = `${prefixCls}-header`; + + const yearNumber = generateConfig.getYear(viewDate); + const startYear = Math.floor(yearNumber / YEAR_DECADE_COUNT) * YEAR_DECADE_COUNT; + const endYear = startYear + YEAR_DECADE_COUNT - 1; + + return ( +
+ +
+ ); +} + + + +YearHeader.displayName = 'YearHeader'; +YearHeader.inheritAttrs = false; + +export default YearHeader; diff --git a/components/vc-picker/panels/YearPanel/index.tsx b/components/vc-picker/panels/YearPanel/index.tsx new file mode 100644 index 000000000..b8191cf41 --- /dev/null +++ b/components/vc-picker/panels/YearPanel/index.tsx @@ -0,0 +1,95 @@ + +import YearHeader from './YearHeader'; +import YearBody, { YEAR_COL_COUNT } from './YearBody'; +import type { PanelSharedProps, PanelMode } from '../../interface'; +import { createKeyDownHandler } from '../../utils/uiUtil'; + +export type YearPanelProps = { + sourceMode: PanelMode; +} & PanelSharedProps; + +export const YEAR_DECADE_COUNT = 10; + +function YearPanel(props: YearPanelProps) { + const { + prefixCls, + operationRef, + onViewDateChange, + generateConfig, + value, + viewDate, + sourceMode, + onSelect, + onPanelChange, + } = props; + + const panelPrefixCls = `${prefixCls}-year-panel`; + + // ======================= Keyboard ======================= + operationRef.current = { + onKeyDown: event => + createKeyDownHandler(event, { + onLeftRight: diff => { + onSelect(generateConfig.addYear(value || viewDate, diff), 'key'); + }, + onCtrlLeftRight: diff => { + onSelect( + generateConfig.addYear(value || viewDate, diff * YEAR_DECADE_COUNT), + 'key', + ); + }, + onUpDown: diff => { + onSelect( + generateConfig.addYear(value || viewDate, diff * YEAR_COL_COUNT), + 'key', + ); + }, + onEnter: () => { + onPanelChange( + sourceMode === 'date' ? 'date' : 'month', + value || viewDate, + ); + }, + }), + }; + + // ==================== View Operation ==================== + const onDecadeChange = (diff: number) => { + const newDate = generateConfig.addYear(viewDate, diff * 10); + onViewDateChange(newDate); + onPanelChange(null, newDate); + }; + + return ( +
+ { + onDecadeChange(-1); + }} + onNextDecade={() => { + onDecadeChange(1); + }} + onDecadeClick={() => { + onPanelChange('decade', viewDate); + }} + /> + { + onPanelChange(sourceMode === 'date' ? 'date' : 'month', date); + onSelect(date, 'mouse'); + }} + /> +
+ ); +} + + + +YearPanel.displayName = 'YearPanel'; +YearPanel.inheritAttrs = false; + +export default YearPanel; diff --git a/components/vc-picker/utils/dateUtil.ts b/components/vc-picker/utils/dateUtil.ts new file mode 100644 index 000000000..8b6a542b4 --- /dev/null +++ b/components/vc-picker/utils/dateUtil.ts @@ -0,0 +1,321 @@ +import { DECADE_UNIT_DIFF } from '../panels/DecadePanel/index'; +import type { PanelMode, NullableDateType, PickerMode, Locale, CustomFormat } from '../interface'; +import type { GenerateConfig } from '../generate'; + +export const WEEK_DAY_COUNT = 7; + +export function isNullEqual(value1: T, value2: T): boolean | undefined { + if (!value1 && !value2) { + return true; + } + if (!value1 || !value2) { + return false; + } + return undefined; +} + +export function isSameDecade( + generateConfig: GenerateConfig, + decade1: NullableDateType, + decade2: NullableDateType, +) { + const equal = isNullEqual(decade1, decade2); + if (typeof equal === 'boolean') { + return equal; + } + + const num1 = Math.floor(generateConfig.getYear(decade1!) / 10); + const num2 = Math.floor(generateConfig.getYear(decade2!) / 10); + return num1 === num2; +} + +export function isSameYear( + generateConfig: GenerateConfig, + year1: NullableDateType, + year2: NullableDateType, +) { + const equal = isNullEqual(year1, year2); + if (typeof equal === 'boolean') { + return equal; + } + + return generateConfig.getYear(year1!) === generateConfig.getYear(year2!); +} + +export function getQuarter(generateConfig: GenerateConfig, date: DateType) { + const quota = Math.floor(generateConfig.getMonth(date) / 3); + return quota + 1; +} + +export function isSameQuarter( + generateConfig: GenerateConfig, + quarter1: NullableDateType, + quarter2: NullableDateType, +) { + const equal = isNullEqual(quarter1, quarter2); + if (typeof equal === 'boolean') { + return equal; + } + + return ( + isSameYear(generateConfig, quarter1, quarter2) && + getQuarter(generateConfig, quarter1!) === getQuarter(generateConfig, quarter2!) + ); +} + +export function isSameMonth( + generateConfig: GenerateConfig, + month1: NullableDateType, + month2: NullableDateType, +) { + const equal = isNullEqual(month1, month2); + if (typeof equal === 'boolean') { + return equal; + } + + return ( + isSameYear(generateConfig, month1, month2) && + generateConfig.getMonth(month1!) === generateConfig.getMonth(month2!) + ); +} + +export function isSameDate( + generateConfig: GenerateConfig, + date1: NullableDateType, + date2: NullableDateType, +) { + const equal = isNullEqual(date1, date2); + if (typeof equal === 'boolean') { + return equal; + } + + return ( + generateConfig.getYear(date1!) === generateConfig.getYear(date2!) && + generateConfig.getMonth(date1!) === generateConfig.getMonth(date2!) && + generateConfig.getDate(date1!) === generateConfig.getDate(date2!) + ); +} + +export function isSameTime( + generateConfig: GenerateConfig, + time1: NullableDateType, + time2: NullableDateType, +) { + const equal = isNullEqual(time1, time2); + if (typeof equal === 'boolean') { + return equal; + } + + return ( + generateConfig.getHour(time1!) === generateConfig.getHour(time2!) && + generateConfig.getMinute(time1!) === generateConfig.getMinute(time2!) && + generateConfig.getSecond(time1!) === generateConfig.getSecond(time2!) + ); +} + +export function isSameWeek( + generateConfig: GenerateConfig, + locale: string, + date1: NullableDateType, + date2: NullableDateType, +) { + const equal = isNullEqual(date1, date2); + if (typeof equal === 'boolean') { + return equal; + } + + return ( + generateConfig.locale.getWeek(locale, date1!) === generateConfig.locale.getWeek(locale, date2!) + ); +} + +export function isEqual( + generateConfig: GenerateConfig, + value1: NullableDateType, + value2: NullableDateType, +) { + return isSameDate(generateConfig, value1, value2) && isSameTime(generateConfig, value1, value2); +} + +/** Between in date but not equal of date */ +export function isInRange( + generateConfig: GenerateConfig, + startDate: NullableDateType, + endDate: NullableDateType, + current: NullableDateType, +) { + if (!startDate || !endDate || !current) { + return false; + } + + return ( + !isSameDate(generateConfig, startDate, current) && + !isSameDate(generateConfig, endDate, current) && + generateConfig.isAfter(current, startDate) && + generateConfig.isAfter(endDate, current) + ); +} + +export function getWeekStartDate( + locale: string, + generateConfig: GenerateConfig, + value: DateType, +) { + const weekFirstDay = generateConfig.locale.getWeekFirstDay(locale); + const monthStartDate = generateConfig.setDate(value, 1); + const startDateWeekDay = generateConfig.getWeekDay(monthStartDate); + + let alignStartDate = generateConfig.addDate(monthStartDate, weekFirstDay - startDateWeekDay); + + if ( + generateConfig.getMonth(alignStartDate) === generateConfig.getMonth(value) && + generateConfig.getDate(alignStartDate) > 1 + ) { + alignStartDate = generateConfig.addDate(alignStartDate, -7); + } + + return alignStartDate; +} + +export function getClosingViewDate( + viewDate: DateType, + picker: PickerMode, + generateConfig: GenerateConfig, + offset = 1, +): DateType { + switch (picker) { + case 'year': + return generateConfig.addYear(viewDate, offset * 10); + case 'quarter': + case 'month': + return generateConfig.addYear(viewDate, offset); + default: + return generateConfig.addMonth(viewDate, offset); + } +} + +export function formatValue( + value: DateType, + { + generateConfig, + locale, + format, + }: { + generateConfig: GenerateConfig; + locale: Locale; + format: string | CustomFormat; + }, +) { + return typeof format === 'function' + ? format(value) + : generateConfig.locale.format(locale.locale, value, format); +} + +export function parseValue( + value: string, + { + generateConfig, + locale, + formatList, + }: { + generateConfig: GenerateConfig; + locale: Locale; + formatList: (string | CustomFormat)[]; + }, +) { + if (!value || typeof formatList[0] === 'function') { + return null; + } + + return generateConfig.locale.parse(locale.locale, value, formatList as string[]); +} + +// eslint-disable-next-line consistent-return +export function getCellDateDisabled({ + cellDate, + mode, + disabledDate, + generateConfig, +}: { + cellDate: DateType; + mode: Omit; + generateConfig: GenerateConfig; + disabledDate?: (date: DateType) => boolean; +}): boolean { + if (!disabledDate) return false; + // Whether cellDate is disabled in range + const getDisabledFromRange = ( + currentMode: 'date' | 'month' | 'year', + start: number, + end: number, + ) => { + let current = start; + while (current <= end) { + let date: DateType; + switch (currentMode) { + case 'date': { + date = generateConfig.setDate(cellDate, current); + if (!disabledDate(date)) { + return false; + } + break; + } + case 'month': { + date = generateConfig.setMonth(cellDate, current); + if ( + !getCellDateDisabled({ + cellDate: date, + mode: 'month', + generateConfig, + disabledDate, + }) + ) { + return false; + } + break; + } + case 'year': { + date = generateConfig.setYear(cellDate, current); + if ( + !getCellDateDisabled({ + cellDate: date, + mode: 'year', + generateConfig, + disabledDate, + }) + ) { + return false; + } + break; + } + } + current += 1; + } + return true; + }; + switch (mode) { + case 'date': + case 'week': { + return disabledDate(cellDate); + } + case 'month': { + const startDate = 1; + const endDate = generateConfig.getDate(generateConfig.getEndDate(cellDate)); + return getDisabledFromRange('date', startDate, endDate); + } + case 'quarter': { + const startMonth = Math.floor(generateConfig.getMonth(cellDate) / 3) * 3; + const endMonth = startMonth + 2; + return getDisabledFromRange('month', startMonth, endMonth); + } + case 'year': { + return getDisabledFromRange('month', 0, 11); + } + case 'decade': { + const year = generateConfig.getYear(cellDate); + const startYear = Math.floor(year / DECADE_UNIT_DIFF) * DECADE_UNIT_DIFF; + const endYear = startYear + DECADE_UNIT_DIFF - 1; + return getDisabledFromRange('year', startYear, endYear); + } + } +} diff --git a/components/vc-picker/utils/getExtraFooter.tsx b/components/vc-picker/utils/getExtraFooter.tsx new file mode 100644 index 000000000..2a03db7ee --- /dev/null +++ b/components/vc-picker/utils/getExtraFooter.tsx @@ -0,0 +1,16 @@ + +import type { PanelMode } from '../interface'; + +export default function getExtraFooter( + prefixCls: string, + mode: PanelMode, + renderExtraFooter?: (mode: PanelMode) => any, +) { + if (!renderExtraFooter) { + return null; + } + + return ( + + ); +} diff --git a/components/vc-picker/utils/getRanges.tsx b/components/vc-picker/utils/getRanges.tsx new file mode 100644 index 000000000..20fcb140f --- /dev/null +++ b/components/vc-picker/utils/getRanges.tsx @@ -0,0 +1,79 @@ + +import { VueNode } from '../../_util/type'; +import type { Components, RangeList, Locale } from '../interface'; + +export type RangesProps = { + prefixCls: string; + rangeList?: RangeList; + components?: Components; + needConfirmButton: boolean; + onNow?: null | (() => void) | false; + onOk?: null | (() => void) | false; + okDisabled?: boolean; + showNow?: boolean; + locale: Locale; +}; + +export default function getRanges({ + prefixCls, + rangeList = [], + components = {}, + needConfirmButton, + onNow, + onOk, + okDisabled, + showNow, + locale, +}: RangesProps) { + let presetNode: VueNode; + let okNode: VueNode; + + if (rangeList.length) { + const Item = (components.rangeItem || 'span') as any; + + presetNode = ( + <> + {rangeList.map(({ label, onClick, onMouseEnter, onMouseLeave }) => ( +
  • + + {label} + +
  • + ))} + + ); + } + + if (needConfirmButton) { + const Button = (components.button || 'button') as any; + + if (onNow && !presetNode && showNow !== false) { + presetNode = ( +
  • + + {locale.now} + +
  • + ); + } + + okNode = needConfirmButton && ( +
  • + +
  • + ); + } + + if (!presetNode && !okNode) { + return null; + } + + return ( +
      + {presetNode} + {okNode} +
    + ); +} diff --git a/components/vc-picker/utils/miscUtil.ts b/components/vc-picker/utils/miscUtil.ts new file mode 100644 index 000000000..ed62cb73b --- /dev/null +++ b/components/vc-picker/utils/miscUtil.ts @@ -0,0 +1,58 @@ +export function leftPad(str: string | number, length: number, fill = '0') { + let current = String(str); + while (current.length < length) { + current = `${fill}${str}`; + } + return current; +} + +export const tuple = (...args: T) => args; + +export function toArray(val: T | T[]): T[] { + if (val === null || val === undefined) { + return []; + } + + return Array.isArray(val) ? val : [val]; +} + +export default function getDataOrAriaProps(props: any) { + const retProps: any = {}; + + Object.keys(props).forEach(key => { + if ( + (key.substr(0, 5) === 'data-' || + key.substr(0, 5) === 'aria-' || + key === 'role' || + key === 'name') && + key.substr(0, 7) !== 'data-__' + ) { + retProps[key] = props[key]; + } + }); + + return retProps; +} + +export function getValue(values: null | undefined | (T | null)[], index: number): T | null { + return values ? values[index] : null; +} + +type UpdateValue = (prev: T) => T; + +export function updateValues( + values: [T | null, T | null] | null, + value: T | UpdateValue, + index: number, +): R { + const newValues: [T | null, T | null] = [getValue(values, 0), getValue(values, 1)]; + + newValues[index] = + typeof value === 'function' ? (value as UpdateValue)(newValues[index]) : value; + + if (!newValues[0] && !newValues[1]) { + return (null as unknown) as R; + } + + return (newValues as unknown) as R; +} diff --git a/components/vc-picker/utils/timeUtil.ts b/components/vc-picker/utils/timeUtil.ts new file mode 100644 index 000000000..3b4051824 --- /dev/null +++ b/components/vc-picker/utils/timeUtil.ts @@ -0,0 +1,70 @@ +import type { NullableDateType } from '../interface'; +import type { GenerateConfig } from '../generate'; + +export function setTime( + generateConfig: GenerateConfig, + date: DateType, + hour: number, + minute: number, + second: number, +): DateType { + let nextTime = generateConfig.setHour(date, hour); + nextTime = generateConfig.setMinute(nextTime, minute); + nextTime = generateConfig.setSecond(nextTime, second); + return nextTime; +} + +export function setDateTime( + generateConfig: GenerateConfig, + date: DateType, + defaultDate: NullableDateType, +) { + if (!defaultDate) { + return date; + } + + let newDate = date; + newDate = generateConfig.setHour( + newDate, + generateConfig.getHour(defaultDate), + ); + newDate = generateConfig.setMinute( + newDate, + generateConfig.getMinute(defaultDate), + ); + newDate = generateConfig.setSecond( + newDate, + generateConfig.getSecond(defaultDate), + ); + return newDate; +} + + +export function getLowerBoundTime( + hour: number, + minute: number, + second: number, + hourStep: number, + minuteStep: number, + secondStep: number, +): [number, number, number] { + const lowerBoundHour = Math.floor(hour / hourStep) * hourStep; + if (lowerBoundHour < hour) { + return [lowerBoundHour, 60 - minuteStep, 60 - secondStep]; + } + const lowerBoundMinute = Math.floor(minute / minuteStep) * minuteStep; + if (lowerBoundMinute < minute) { + return [lowerBoundHour, lowerBoundMinute, 60 - secondStep]; + } + const lowerBoundSecond = Math.floor(second / secondStep) * secondStep; + return [lowerBoundHour, lowerBoundMinute, lowerBoundSecond]; +} + +export function getLastDay(generateConfig: GenerateConfig, date: DateType) { + const year = generateConfig.getYear(date); + const month = generateConfig.getMonth(date) + 1; + const endDate = generateConfig.getEndDate(generateConfig.getFixedDate(`${year}-${month}-01`)); + const lastDay = generateConfig.getDate(endDate); + const monthShow = month < 10 ? `0${month}` : `${month}`; + return `${year}-${monthShow}-${lastDay}`; +} diff --git a/components/vc-picker/utils/uiUtil.ts b/components/vc-picker/utils/uiUtil.ts new file mode 100644 index 000000000..ae41a2874 --- /dev/null +++ b/components/vc-picker/utils/uiUtil.ts @@ -0,0 +1,276 @@ +import isVisible from '../../vc-util/Dom/isVisible'; +import KeyCode from '../../_util/KeyCode'; +import raf from '../../_util/raf'; +import type { GenerateConfig } from '../generate'; +import type { CustomFormat, PanelMode, PickerMode } from '../interface'; + +const scrollIds = new Map(); + +/** Trigger when element is visible in view */ +export function waitElementReady(element: HTMLElement, callback: () => void): () => void { + let id: number; + + function tryOrNextFrame() { + if (isVisible(element)) { + callback(); + } else { + id = raf(() => { + tryOrNextFrame(); + }); + } + } + + tryOrNextFrame(); + + return () => { + raf.cancel(id); + }; +} + +/* eslint-disable no-param-reassign */ +export function scrollTo(element: HTMLElement, to: number, duration: number) { + if (scrollIds.get(element)) { + cancelAnimationFrame(scrollIds.get(element)!); + } + + // jump to target if duration zero + if (duration <= 0) { + scrollIds.set( + element, + requestAnimationFrame(() => { + element.scrollTop = to; + }), + ); + + return; + } + const difference = to - element.scrollTop; + const perTick = (difference / duration) * 10; + + scrollIds.set( + element, + requestAnimationFrame(() => { + element.scrollTop += perTick; + if (element.scrollTop !== to) { + scrollTo(element, to, duration - 10); + } + }), + ); +} +/* eslint-enable */ + +export type KeyboardConfig = { + onLeftRight?: ((diff: number) => void) | null; + onCtrlLeftRight?: ((diff: number) => void) | null; + onUpDown?: ((diff: number) => void) | null; + onPageUpDown?: ((diff: number) => void) | null; + onEnter?: (() => void) | null; +}; +export function createKeyDownHandler( + event: KeyboardEvent, + { onLeftRight, onCtrlLeftRight, onUpDown, onPageUpDown, onEnter }: KeyboardConfig, +): boolean { + const { which, ctrlKey, metaKey } = event; + + switch (which) { + case KeyCode.LEFT: + if (ctrlKey || metaKey) { + if (onCtrlLeftRight) { + onCtrlLeftRight(-1); + return true; + } + } else if (onLeftRight) { + onLeftRight(-1); + return true; + } + /* istanbul ignore next */ + break; + + case KeyCode.RIGHT: + if (ctrlKey || metaKey) { + if (onCtrlLeftRight) { + onCtrlLeftRight(1); + return true; + } + } else if (onLeftRight) { + onLeftRight(1); + return true; + } + /* istanbul ignore next */ + break; + + case KeyCode.UP: + if (onUpDown) { + onUpDown(-1); + return true; + } + /* istanbul ignore next */ + break; + + case KeyCode.DOWN: + if (onUpDown) { + onUpDown(1); + return true; + } + /* istanbul ignore next */ + break; + + case KeyCode.PAGE_UP: + if (onPageUpDown) { + onPageUpDown(-1); + return true; + } + /* istanbul ignore next */ + break; + + case KeyCode.PAGE_DOWN: + if (onPageUpDown) { + onPageUpDown(1); + return true; + } + /* istanbul ignore next */ + break; + + case KeyCode.ENTER: + if (onEnter) { + onEnter(); + return true; + } + /* istanbul ignore next */ + break; + } + + return false; +} + +// ===================== Format ===================== +export function getDefaultFormat( + format: string | CustomFormat | (string | CustomFormat)[] | undefined, + picker: PickerMode | undefined, + showTime: boolean | object | undefined, + use12Hours: boolean | undefined, +) { + let mergedFormat = format; + if (!mergedFormat) { + switch (picker) { + case 'time': + mergedFormat = use12Hours ? 'hh:mm:ss a' : 'HH:mm:ss'; + break; + + case 'week': + mergedFormat = 'gggg-wo'; + break; + + case 'month': + mergedFormat = 'YYYY-MM'; + break; + + case 'quarter': + mergedFormat = 'YYYY-[Q]Q'; + break; + + case 'year': + mergedFormat = 'YYYY'; + break; + + default: + mergedFormat = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; + } + } + + return mergedFormat; +} + +export function getInputSize( + picker: PickerMode | undefined, + format: string | CustomFormat, + generateConfig: GenerateConfig, +) { + const defaultSize = picker === 'time' ? 8 : 10; + const length = + typeof format === 'function' ? format(generateConfig.getNow()).length : format.length; + return Math.max(defaultSize, length) + 2; +} + +// ===================== Window ===================== +type ClickEventHandler = (event: MouseEvent) => void; +let globalClickFunc: ClickEventHandler | null = null; +const clickCallbacks = new Set(); + +export function addGlobalMouseDownEvent(callback: ClickEventHandler) { + if (!globalClickFunc && typeof window !== 'undefined' && window.addEventListener) { + globalClickFunc = (e: MouseEvent) => { + // Clone a new list to avoid repeat trigger events + [...clickCallbacks].forEach((queueFunc) => { + queueFunc(e); + }); + }; + window.addEventListener('mousedown', globalClickFunc); + } + + clickCallbacks.add(callback); + + return () => { + clickCallbacks.delete(callback); + if (clickCallbacks.size === 0) { + window.removeEventListener('mousedown', globalClickFunc!); + globalClickFunc = null; + } + }; +} + +export function getTargetFromEvent(e: Event) { + const target = e.target as HTMLElement; + + // get target if in shadow dom + if (e.composed && target.shadowRoot) { + return (e.composedPath?.()[0] || target) as HTMLElement; + } + + return target; +} + +// ====================== Mode ====================== +const getYearNextMode = (next: PanelMode): PanelMode => { + if (next === 'month' || next === 'date') { + return 'year'; + } + return next; +}; + +const getMonthNextMode = (next: PanelMode): PanelMode => { + if (next === 'date') { + return 'month'; + } + return next; +}; + +const getQuarterNextMode = (next: PanelMode): PanelMode => { + if (next === 'month' || next === 'date') { + return 'quarter'; + } + return next; +}; + +const getWeekNextMode = (next: PanelMode): PanelMode => { + if (next === 'date') { + return 'week'; + } + return next; +}; + +export const PickerModeMap: Record PanelMode) | null> = { + year: getYearNextMode, + month: getMonthNextMode, + quarter: getQuarterNextMode, + week: getWeekNextMode, + time: null, + date: null, +}; + +export function elementsContains( + elements: (HTMLElement | undefined | null)[], + target: HTMLElement, +) { + return elements.some((ele) => ele && ele.contains(target)); +} diff --git a/tsconfig.json b/tsconfig.json index 14c4d2a04..299711fc8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,5 @@ { "compilerOptions": { - "baseUrl": "./", - "paths": { - "ant-design-vue": ["components/index.tsx"], - "ant-design-vue/es/*": ["components/*"] - }, "strictNullChecks": false, "strict": true, "moduleResolution": "node", @@ -19,5 +14,15 @@ "skipLibCheck": true, "allowJs": true }, - "exclude": ["node_modules", "lib", "es", "antd-tools", "dist", "v2-doc"] + "include": ["components/**/*"], + "exclude": [ + "node_modules", + "lib", + "es", + "antd-tools", + "dist", + "v2-doc", + "scripts", + "**/__tests__/**/*" + ] }