refactor: date

pull/4499/head
tangjinzhou 2021-07-20 17:32:49 +08:00
parent 16fc2a10a9
commit 53f3c6e1a5
10 changed files with 1506 additions and 1132 deletions

View File

@ -1,3 +1,8 @@
export type FocusEventHandler = (e: FocusEvent) => void;
export type MouseEventHandler = (e: MouseEvent) => void;
export type KeyboardEventHandler = (e: KeyboardEvent) => void;
export type ChangeEvent = Event & {
target: {
value?: string | undefined;
};
};

View File

@ -20,8 +20,7 @@ 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 { ContextOperationRefProps, useProvidePanel } from './PanelContext';
import type { CustomFormat, PickerMode } from './interface';
import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil';
import usePickerInput from './hooks/usePickerInput';
@ -30,20 +29,21 @@ import useValueTexts from './hooks/useValueTexts';
import useHoverValue from './hooks/useHoverValue';
import {
computed,
createVNode,
CSSProperties,
defineComponent,
HtmlHTMLAttributes,
ref,
Ref,
toRef,
toRefs,
watch,
} from 'vue';
import { FocusEventHandler, MouseEventHandler } from '../_util/EventInterface';
import { ChangeEvent, FocusEventHandler, MouseEventHandler } from '../_util/EventInterface';
import { VueNode } from '../_util/type';
import { AlignType } from '../vc-align/interface';
import useMergedState from '../_util/hooks/useMergedState';
import { locale } from 'dayjs';
import { warning } from '../vc-util/warning';
import classNames from '../_util/classNames';
export type PickerRefConfig = {
focus: () => void;
@ -92,10 +92,6 @@ export type PickerSharedProps<DateType> = {
onContextMenu?: MouseEventHandler;
onKeyDown?: (event: KeyboardEvent, preventDefault: () => void) => void;
// Internal
/** @private Internal usage, do not use in production mode!!! */
pickerRef?: Ref<PickerRefConfig>;
// WAI-ARIA
role?: string;
name?: string;
@ -169,7 +165,6 @@ function Picker<DateType>() {
'disabledDate',
'placeholder',
'getPopupContainer',
'pickerRef',
'panelRender',
'onChange',
'onOpenChange',
@ -196,7 +191,7 @@ function Picker<DateType>() {
'superNextIcon',
'panelRender',
],
setup(props, { slots, attrs, expose }) {
setup(props, { attrs, expose }) {
const inputRef = ref(null);
const needConfirmButton = computed(
() => (props.picker === 'date' && !!props.showTime) || props.picker === 'time',
@ -242,13 +237,11 @@ function Picker<DateType>() {
});
// ============================= Text ==============================
const texts = useValueTexts(selectedValue, {
const [valueTexts, firstValueText] = useValueTexts(selectedValue, {
formatList,
generateConfig: toRef(props, 'generateConfig'),
locale: toRef(props, 'locale'),
});
const valueTexts = computed(() => texts.value[0]);
const firstValueText = computed(() => texts.value[1]);
const [text, triggerTextChange, resetText] = useTextValueMapping({
valueTexts,
@ -351,11 +344,256 @@ function Picker<DateType>() {
},
});
// ============================= Sync ==============================
// Close should sync back with text value
watch([mergedOpen, valueTexts], () => {
if (!mergedOpen.value) {
setSelectedValue(mergedValue.value);
if (!valueTexts.value.length || valueTexts.value[0] === '') {
triggerTextChange('');
} else if (firstValueText.value !== text.value) {
resetText();
}
}
});
// Change picker should sync back with text value
watch(
() => props.picker,
() => {
if (!mergedOpen.value) {
resetText();
}
},
);
// Sync innerValue with control mode
watch(mergedValue, () => {
// Sync select value
setSelectedValue(mergedValue.value);
});
const [hoverValue, onEnter, onLeave] = useHoverValue(text, {
formatList,
generateConfig: toRef(props, 'generateConfig'),
locale: toRef(props, 'locale'),
});
const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => {
if (type === 'submit' || (type !== 'key' && !needConfirmButton.value)) {
// triggerChange will also update selected values
triggerChange(date);
triggerOpen(false);
}
};
useProvidePanel({
operationRef,
hideHeader: computed(() => props.picker === 'time'),
panelRef: panelDivRef,
onSelect: onContextSelect,
open: mergedOpen,
defaultOpenValue: toRef(props, 'defaultOpenValue'),
onDateMouseEnter: onEnter,
onDateMouseLeave: onLeave,
});
expose({
focus: () => {
if (inputRef.value) {
inputRef.value.focus();
}
},
blur: () => {
if (inputRef.value) {
inputRef.value.blur();
}
},
});
return () => {
return null;
const {
prefixCls = 'rc-picker',
id,
tabindex,
dropdownClassName,
dropdownAlign,
popupStyle,
transitionName,
generateConfig,
locale,
inputReadOnly,
allowClear,
autofocus,
picker = 'date',
defaultOpenValue,
suffixIcon,
clearIcon,
disabled,
placeholder,
getPopupContainer,
panelRender,
onMouseDown,
onMouseEnter,
onMouseLeave,
onContextMenu,
onClick,
onSelect,
direction,
autocomplete = 'off',
} = props;
// ============================= Panel =============================
const panelProps = {
// Remove `picker` & `format` here since TimePicker is little different with other panel
...(props as Omit<MergedPickerProps<DateType>, 'picker' | 'format'>),
pickerValue: undefined,
onPickerValueChange: undefined,
onChange: null,
};
let panelNode: VueNode = (
<PickerPanel
{...panelProps}
generateConfig={generateConfig}
class={classNames({
[`${prefixCls}-panel-focused`]: !typing.value,
})}
value={selectedValue.value}
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 = (
<div
class={`${prefixCls}-panel-container`}
onMousedown={e => {
e.preventDefault();
}}
>
{panelNode}
</div>
);
let suffixNode: VueNode;
if (suffixIcon) {
suffixNode = <span class={`${prefixCls}-suffix`}>{suffixIcon}</span>;
}
let clearNode: VueNode;
if (allowClear && mergedValue.value && !disabled) {
clearNode = (
<span
onMousedown={e => {
e.preventDefault();
e.stopPropagation();
}}
onMouseup={e => {
e.preventDefault();
e.stopPropagation();
triggerChange(null);
triggerOpen(false);
}}
class={`${prefixCls}-clear`}
role="button"
>
{clearIcon || <span class={`${prefixCls}-clear-btn`} />}
</span>
);
}
// ============================ Warning ============================
if (process.env.NODE_ENV !== 'production') {
warning(
!defaultOpenValue,
'`defaultOpenValue` may confuse user for the current value status. Please use `defaultValue` instead.',
);
}
// ============================ Return =============================
const popupPlacement = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
return (
<PickerTrigger
visible={mergedOpen.value}
popupElement={panel}
popupStyle={popupStyle}
prefixCls={prefixCls}
dropdownClassName={dropdownClassName}
dropdownAlign={dropdownAlign}
getPopupContainer={getPopupContainer}
transitionName={transitionName}
popupPlacement={popupPlacement}
direction={direction}
>
<div
class={classNames(prefixCls, attrs.class, {
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-focused`]: focused,
[`${prefixCls}-rtl`]: direction === 'rtl',
})}
style={attrs.style}
onMousedown={onMouseDown}
onMouseup={onInternalMouseUp}
onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave}
onContextmenu={onContextMenu}
onClick={onClick}
>
<div
class={classNames(`${prefixCls}-input`, {
[`${prefixCls}-input-placeholder`]: !!hoverValue.value,
})}
ref={inputDivRef}
>
<input
id={id}
tabindex={tabindex}
disabled={disabled}
readonly={
inputReadOnly || typeof formatList.value[0] === 'function' || !typing.value
}
value={hoverValue || text}
onChange={(e: ChangeEvent) => {
triggerTextChange(e.target.value);
}}
autofocus={autofocus}
placeholder={placeholder}
ref={inputRef}
title={text.value}
{...inputProps.value}
size={getInputSize(picker, formatList.value[0], generateConfig)}
{...getDataOrAriaProps(props)}
autocomplete={autocomplete}
/>
{suffixNode}
{clearNode}
</div>
</div>
</PickerTrigger>
);
};
},
});
}
export default Picker();
const InterPicker = Picker<any>();
export default <DateType extends any>(props: MergedPickerProps<DateType>, { slots }): JSX.Element =>
createVNode(InterPicker, props, slots);

View File

@ -33,7 +33,16 @@ import getExtraFooter from './utils/getExtraFooter';
import getRanges from './utils/getRanges';
import { getLowerBoundTime, setDateTime, setTime } from './utils/timeUtil';
import { VueNode } from '../_util/type';
import { computed, defineComponent, ref, toRef, watch, watchEffect } from 'vue';
import {
computed,
createVNode,
defineComponent,
HTMLAttributes,
ref,
toRef,
watch,
watchEffect,
} from 'vue';
import useMergedState from '../_util/hooks/useMergedState';
import { warning } from '../vc-util/warning';
import KeyCode from '../_util/KeyCode';
@ -83,7 +92,7 @@ export type PickerPanelSharedProps<DateType> = {
/** @private Internal usage. Do not use in your production env */
components?: Components;
};
} & HTMLAttributes;
export type PickerPanelBaseProps<DateType> = {
picker: Exclude<PickerMode, 'date' | 'time'>;
@ -598,5 +607,6 @@ function PickerPanel<DateType>() {
},
});
}
export default PickerPanel();
const InterPickerPanel = PickerPanel<any>();
export default <DateType extends any>(props: MergedPickerPanelProps<DateType>): JSX.Element =>
createVNode(InterPickerPanel, props);

View File

@ -47,7 +47,6 @@ export type PickerTriggerProps = {
visible: boolean;
popupElement: VueNode;
popupStyle?: CSSProperties;
children: VueNode;
dropdownClassName?: string;
transitionName?: string;
getPopupContainer?: (node: HTMLElement) => HTMLElement;

View File

@ -1,4 +1,14 @@
import { inject, InjectionKey, provide, Ref } from 'vue';
import {
defineComponent,
inject,
InjectionKey,
PropType,
provide,
ref,
Ref,
toRef,
watch,
} from 'vue';
import type { NullableDateType, RangeValue } from './interface';
export type RangeContextProps = {
@ -12,6 +22,17 @@ export type RangeContextProps = {
panelPosition?: Ref<'left' | 'right' | false>;
};
type RangeContextProviderValue = {
/**
* Set displayed range value style.
* Panel only has one value, this is only style effect.
*/
rangedValue?: [NullableDateType<any>, NullableDateType<any>] | null;
hoverRangedValue?: RangeValue<any>;
inRange?: boolean;
panelPosition?: 'left' | 'right' | false;
};
const RangeContextKey: InjectionKey<RangeContextProps> = Symbol('RangeContextProps');
export const useProvideRange = (props: RangeContextProps) => {
@ -22,4 +43,36 @@ export const useInjectRange = () => {
return inject(RangeContextKey);
};
export const RangeContextProvider = defineComponent({
name: 'PanelContextProvider',
inheritAttrs: false,
props: {
value: {
type: Object as PropType<RangeContextProviderValue>,
default: () => ({} as RangeContextProviderValue),
},
},
setup(props, { slots }) {
const value: RangeContextProps = {
rangedValue: ref(props.value.rangedValue),
hoverRangedValue: ref(props.value.hoverRangedValue),
inRange: ref(props.value.inRange),
panelPosition: ref(props.value.panelPosition),
};
useProvideRange(value);
toRef;
watch(
() => props.value,
() => {
Object.keys(props.value).forEach(key => {
if (value[key]) {
value[key].value = props.value[key];
}
});
},
);
return () => slots.default?.();
},
});
export default RangeContextKey;

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,31 @@
import { useState, useEffect, useRef } from 'react';
import type { ComputedRef, Ref, UnwrapRef } from 'vue';
import { ref, onBeforeUnmount, watch } from 'vue';
import type { ValueTextConfig } from './useValueTexts';
import useValueTexts from './useValueTexts';
export default function useHoverValue<DateType>(
valueText: string,
valueText: Ref<string>,
{ formatList, generateConfig, locale }: ValueTextConfig<DateType>,
): [string, (date: DateType) => void, (immediately?: boolean) => void] {
const [value, internalSetValue] = useState<DateType>(null);
const raf = useRef(null);
): [ComputedRef<string>, (date: DateType) => void, (immediately?: boolean) => void] {
const innerValue = ref<DateType>(null);
const raf = ref(null);
function setValue(val: DateType, immediately = false) {
cancelAnimationFrame(raf.current);
cancelAnimationFrame(raf.value);
if (immediately) {
internalSetValue(val);
innerValue.value = val as UnwrapRef<DateType>;
return;
}
raf.current = requestAnimationFrame(() => {
internalSetValue(val);
raf.value = requestAnimationFrame(() => {
innerValue.value = val as UnwrapRef<DateType>;
});
}
const [, firstText] = useValueTexts(value, {
const [, firstText] = useValueTexts(innerValue as Ref<DateType>, {
formatList,
generateConfig,
locale,
});
function onEnter(date: DateType) {
setValue(date);
}
@ -34,11 +34,12 @@ export default function useHoverValue<DateType>(
setValue(null, immediately);
}
useEffect(() => {
watch(valueText, () => {
onLeave(true);
}, [valueText]);
useEffect(() => () => cancelAnimationFrame(raf.current), []);
});
onBeforeUnmount(() => {
cancelAnimationFrame(raf.value);
});
return [firstText, onEnter, onLeave];
}

View File

@ -1,8 +1,9 @@
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';
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
export default function useRangeDisabled<DateType>(
{
@ -13,101 +14,101 @@ export default function useRangeDisabled<DateType>(
disabled,
generateConfig,
}: {
picker: PickerMode;
selectedValue: RangeValue<DateType>;
disabledDate?: (date: DateType) => boolean;
disabled: [boolean, boolean];
locale: Locale;
generateConfig: GenerateConfig<DateType>;
picker: Ref<PickerMode>;
selectedValue: Ref<RangeValue<DateType>>;
disabledDate?: Ref<(date: DateType) => boolean>;
disabled: ComputedRef<[boolean, boolean]>;
locale: Ref<Locale>;
generateConfig: Ref<GenerateConfig<DateType>>;
},
disabledStart: boolean,
disabledEnd: boolean,
openRecordsRef: Ref<{
[x: number]: boolean;
}>,
) {
const startDate = getValue(selectedValue, 0);
const endDate = getValue(selectedValue, 1);
const startDate = computed(() => getValue(selectedValue.value, 0));
const endDate = computed(() => getValue(selectedValue.value, 1));
function weekFirstDate(date: DateType) {
return generateConfig.locale.getWeekFirstDate(locale.locale, date);
return generateConfig.value.locale.getWeekFirstDate(locale.value.locale, date);
}
function monthNumber(date: DateType) {
const year = generateConfig.getYear(date);
const month = generateConfig.getMonth(date);
const year = generateConfig.value.getYear(date);
const month = generateConfig.value.getMonth(date);
return year * 100 + month;
}
function quarterNumber(date: DateType) {
const year = generateConfig.getYear(date);
const quarter = getQuarter(generateConfig, date);
const year = generateConfig.value.getYear(date);
const quarter = getQuarter(generateConfig.value, date);
return year * 10 + quarter;
}
const disabledStartDate = React.useCallback(
(date: DateType) => {
if (disabledDate && disabledDate(date)) {
return true;
const disabledStartDate = (date: DateType) => {
if (disabledDate && disabledDate.value(date)) {
return true;
}
// Disabled range
if (disabled[1] && endDate) {
return (
!isSameDate(generateConfig.value, date, endDate.value) &&
generateConfig.value.isAfter(date, endDate.value)
);
}
// Disabled part
if (openRecordsRef.value[1] && endDate.value) {
switch (picker.value) {
case 'quarter':
return quarterNumber(date) > quarterNumber(endDate.value);
case 'month':
return monthNumber(date) > monthNumber(endDate.value);
case 'week':
return weekFirstDate(date) > weekFirstDate(endDate.value);
default:
return (
!isSameDate(generateConfig.value, date, endDate.value) &&
generateConfig.value.isAfter(date, endDate.value)
);
}
}
// Disabled range
if (disabled[1] && endDate) {
return !isSameDate(generateConfig, date, endDate) && generateConfig.isAfter(date, endDate);
return false;
};
const disabledEndDate = (date: DateType) => {
if (disabledDate.value?.(date)) {
return true;
}
// Disabled range
if (disabled[0] && startDate) {
return (
!isSameDate(generateConfig.value, date, endDate.value) &&
generateConfig.value.isAfter(startDate.value, date)
);
}
// Disabled part
if (openRecordsRef.value[0] && startDate.value) {
switch (picker.value) {
case 'quarter':
return quarterNumber(date) < quarterNumber(startDate.value);
case 'month':
return monthNumber(date) < monthNumber(startDate.value);
case 'week':
return weekFirstDate(date) < weekFirstDate(startDate.value);
default:
return (
!isSameDate(generateConfig.value, date, startDate.value) &&
generateConfig.value.isAfter(startDate.value, date)
);
}
}
// 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 false;
};
return [disabledStartDate, disabledEndDate];
}

View File

@ -1,8 +1,10 @@
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';
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
import { ref } from 'vue';
function getStartEndDistance<DateType>(
startDate: DateType,
@ -67,55 +69,64 @@ export default function useRangeViewDates<DateType>({
defaultDates,
generateConfig,
}: {
values: RangeValue<DateType>;
picker: PickerMode;
values: Ref<RangeValue<DateType>>;
picker: Ref<PickerMode>;
defaultDates: RangeValue<DateType> | undefined;
generateConfig: GenerateConfig<DateType>;
}): [(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<RangeValue<DateType>>(null);
const startDate = getValue(values, 0);
const endDate = getValue(values, 1);
generateConfig: Ref<GenerateConfig<DateType>>;
}): [
ComputedRef<DateType>,
ComputedRef<DateType>,
(viewDate: DateType | null, index: 0 | 1) => void,
] {
const defaultViewDates = ref<[DateType | null, DateType | null]>([
getValue(defaultDates, 0),
getValue(defaultDates, 1),
]);
const viewDates = ref<RangeValue<DateType>>(null);
const startDate = computed(() => getValue(values.value, 0));
const endDate = computed(() => getValue(values.value, 1));
function getViewDate(index: 0 | 1): DateType {
// If set default view date, use it
if (defaultViewDates[index]) {
return defaultViewDates[index]!;
if (defaultViewDates.value[index]) {
return defaultViewDates.value[index]! as DateType;
}
return (
getValue(viewDates, index) ||
getRangeViewDate(values, index, picker, generateConfig) ||
startDate ||
endDate ||
generateConfig.getNow()
(getValue(viewDates.value, index) as any) ||
getRangeViewDate(values.value, index, picker.value, generateConfig.value) ||
startDate.value ||
endDate.value ||
generateConfig.value.getNow()
);
}
const startViewDate = computed(() => {
return getViewDate(0);
});
const endViewDate = computed(() => {
return getViewDate(1);
});
function setViewDate(viewDate: DateType | null, index: 0 | 1) {
if (viewDate) {
let newViewDates = updateValues(viewDates, viewDate, index);
let newViewDates = updateValues(viewDates.value, viewDate as any, index);
// Set view date will clean up default one
setDefaultViewDates(
// Should always be an array
updateValues(defaultViewDates, null, index) || [null, null],
);
// Should always be an array
defaultViewDates.value = updateValues(defaultViewDates.value, null, index) || [null, null];
// Reset another one when not have value
const anotherIndex = (index + 1) % 2;
if (!getValue(values, anotherIndex)) {
if (!getValue(values.value, anotherIndex)) {
newViewDates = updateValues(newViewDates, viewDate, anotherIndex);
}
setInternalViewDates(newViewDates);
} else if (startDate || endDate) {
viewDates.value = newViewDates;
} else if (startDate.value || endDate.value) {
// Reset all when has values when `viewDate` is `null` which means from open trigger
setInternalViewDates(null);
viewDates.value = null;
}
}
return [getViewDate, setViewDate];
return [startViewDate, endViewDate, setViewDate];
}

View File

@ -1,4 +1,5 @@
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
import useMemo from '../../_util/hooks/useMemo';
import shallowequal from '../../_util/shallowequal';
import type { GenerateConfig } from '../generate';
@ -14,8 +15,8 @@ export type ValueTextConfig<DateType> = {
export default function useValueTexts<DateType>(
value: Ref<DateType | null>,
{ formatList, generateConfig, locale }: ValueTextConfig<DateType>,
) {
return useMemo<[string[], string]>(
): [ComputedRef<string[]>, ComputedRef<string>] {
const texts = useMemo<[string[], string]>(
() => {
if (!value.value) {
return [[''], ''];
@ -44,4 +45,7 @@ export default function useValueTexts<DateType>(
[value, formatList],
(next, prev) => prev[0] !== next[0] || !shallowequal(prev[1], next[1]),
);
const fullValueTexts = computed(() => texts.value[0]);
const firstValueText = computed(() => texts.value[1]);
return [fullValueTexts, firstValueText];
}