vuecssuiant-designantdreactantantd-vueenterprisefrontendui-designvue-antdvue-antd-uivue3vuecomponent
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1227 lines
41 KiB
1227 lines
41 KiB
import type { DisabledTimes, PanelMode, PickerMode, RangeValue, EventValue } from './interface'; |
|
import type { PickerBaseProps, PickerDateProps, PickerTimeProps } 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 { useProvidePanel } 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 { RangeContextProvider } 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'; |
|
import type { VueNode } from '../_util/type'; |
|
import type { ChangeEvent, FocusEventHandler, MouseEventHandler } from '../_util/EventInterface'; |
|
import { computed, defineComponent, ref, toRef, watch, watchEffect } from 'vue'; |
|
import useMergedState from '../_util/hooks/useMergedState'; |
|
import { warning } from '../vc-util/warning'; |
|
import useState from '../_util/hooks/useState'; |
|
import classNames from '../_util/classNames'; |
|
|
|
function reorderValues<DateType>( |
|
values: RangeValue<DateType>, |
|
generateConfig: GenerateConfig<DateType>, |
|
): RangeValue<DateType> { |
|
if (values && values[0] && values[1] && generateConfig.isAfter(values[0], values[1])) { |
|
return [values[1], values[0]]; |
|
} |
|
|
|
return values; |
|
} |
|
|
|
function canValueTrigger<DateType>( |
|
value: EventValue<DateType>, |
|
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<DateType> = (props: { |
|
current: DateType; |
|
today: DateType; |
|
info: RangeInfo; |
|
}) => VueNode; |
|
|
|
export type RangePickerSharedProps<DateType> = { |
|
id?: string; |
|
value?: RangeValue<DateType>; |
|
defaultValue?: RangeValue<DateType>; |
|
defaultPickerValue?: [DateType, DateType]; |
|
placeholder?: [string, string]; |
|
disabled?: boolean | [boolean, boolean]; |
|
disabledTime?: (date: EventValue<DateType>, type: RangeType) => DisabledTimes; |
|
ranges?: Record< |
|
string, |
|
Exclude<RangeValue<DateType>, null> | (() => Exclude<RangeValue<DateType>, null>) |
|
>; |
|
separator?: VueNode; |
|
allowEmpty?: [boolean, boolean]; |
|
mode?: [PanelMode, PanelMode]; |
|
onChange?: (values: RangeValue<DateType>, formatString: [string, string]) => void; |
|
onCalendarChange?: ( |
|
values: RangeValue<DateType>, |
|
formatString: [string, string], |
|
info: RangeInfo, |
|
) => void; |
|
onPanelChange?: (values: RangeValue<DateType>, modes: [PanelMode, PanelMode]) => void; |
|
onFocus?: FocusEventHandler; |
|
onBlur?: FocusEventHandler; |
|
onMouseenter?: MouseEventHandler; |
|
onMouseleave?: MouseEventHandler; |
|
onOk?: (dates: RangeValue<DateType>) => 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<DateType>; |
|
panelRender?: (originPanel: VueNode) => VueNode; |
|
}; |
|
|
|
type OmitPickerProps<Props> = Omit< |
|
Props, |
|
| 'value' |
|
| 'defaultValue' |
|
| 'defaultPickerValue' |
|
| 'placeholder' |
|
| 'disabled' |
|
| 'disabledTime' |
|
| 'showToday' |
|
| 'showTime' |
|
| 'mode' |
|
| 'onChange' |
|
| 'onSelect' |
|
| 'onPanelChange' |
|
| 'pickerValue' |
|
| 'onPickerValueChange' |
|
| 'onOk' |
|
| 'dateRender' |
|
>; |
|
|
|
type RangeShowTimeObject<DateType> = Omit<SharedTimeProps<DateType>, 'defaultValue'> & { |
|
defaultValue?: DateType[]; |
|
}; |
|
|
|
export type RangePickerBaseProps<DateType> = {} & RangePickerSharedProps<DateType> & |
|
OmitPickerProps<PickerBaseProps<DateType>>; |
|
|
|
export type RangePickerDateProps<DateType> = { |
|
showTime?: boolean | RangeShowTimeObject<DateType>; |
|
} & RangePickerSharedProps<DateType> & |
|
OmitPickerProps<PickerDateProps<DateType>>; |
|
|
|
export type RangePickerTimeProps<DateType> = { |
|
order?: boolean; |
|
} & RangePickerSharedProps<DateType> & |
|
OmitPickerProps<PickerTimeProps<DateType>>; |
|
|
|
export type RangePickerProps<DateType> = |
|
| RangePickerBaseProps<DateType> |
|
| RangePickerDateProps<DateType> |
|
| RangePickerTimeProps<DateType>; |
|
|
|
// TMP type to fit for ts 3.9.2 |
|
type OmitType<DateType> = Omit<RangePickerBaseProps<DateType>, 'picker'> & |
|
Omit<RangePickerDateProps<DateType>, 'picker'> & |
|
Omit<RangePickerTimeProps<DateType>, 'picker'>; |
|
|
|
type MergedRangePickerProps<DateType> = { |
|
picker?: PickerMode; |
|
} & OmitType<DateType>; |
|
function RangerPicker<DateType>() { |
|
return defineComponent<MergedRangePickerProps<DateType>>({ |
|
name: 'RangerPicker', |
|
inheritAttrs: false, |
|
props: [ |
|
'prefixCls', |
|
'id', |
|
'popupStyle', |
|
'dropdownClassName', |
|
'transitionName', |
|
'dropdownAlign', |
|
'getPopupContainer', |
|
'generateConfig', |
|
'locale', |
|
'placeholder', |
|
'autofocus', |
|
'disabled', |
|
'format', |
|
'picker', |
|
'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', |
|
] as any, |
|
setup(props, { attrs, expose }) { |
|
const needConfirmButton = computed( |
|
() => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', |
|
); |
|
|
|
// We record opened status here in case repeat open with picker |
|
const openRecordsRef = ref<Record<number, boolean>>({}); |
|
|
|
const containerRef = ref<HTMLDivElement>(null); |
|
const panelDivRef = ref<HTMLDivElement>(null); |
|
const startInputDivRef = ref<HTMLDivElement>(null); |
|
const endInputDivRef = ref<HTMLDivElement>(null); |
|
const separatorRef = ref<HTMLDivElement>(null); |
|
const startInputRef = ref<HTMLInputElement>(null); |
|
const endInputRef = ref<HTMLInputElement>(null); |
|
|
|
// ============================= Misc ============================== |
|
const formatList = computed(() => |
|
toArray( |
|
getDefaultFormat<DateType>(props.format, props.picker, props.showTime, props.use12Hours), |
|
), |
|
); |
|
|
|
// Active picker |
|
const [mergedActivePickerIndex, setMergedActivePickerIndex] = useMergedState<0 | 1>(0, { |
|
value: toRef(props, 'activePickerIndex'), |
|
}); |
|
|
|
// Operation ref |
|
const operationRef = ref<ContextOperationRefProps>(null); |
|
|
|
const mergedDisabled = computed<[boolean, boolean]>(() => { |
|
const { disabled } = props; |
|
if (Array.isArray(disabled)) { |
|
return disabled; |
|
} |
|
|
|
return [disabled || false, disabled || false]; |
|
}); |
|
|
|
// ============================= Value ============================= |
|
const [mergedValue, setInnerValue] = useMergedState<RangeValue<DateType>>(null, { |
|
value: toRef(props, 'value'), |
|
defaultValue: props.defaultValue, |
|
postState: values => |
|
props.picker === 'time' && !props.order |
|
? values |
|
: reorderValues(values, props.generateConfig), |
|
}); |
|
|
|
// =========================== View Date =========================== |
|
// Config view panel |
|
const [startViewDate, endViewDate, setViewDate] = useRangeViewDates({ |
|
values: mergedValue, |
|
picker: toRef(props, 'picker'), |
|
defaultDates: props.defaultPickerValue, |
|
generateConfig: toRef(props, 'generateConfig'), |
|
}); |
|
|
|
// ========================= Select Values ========================= |
|
const [selectedValue, setSelectedValue] = useMergedState(mergedValue.value, { |
|
postState: values => { |
|
let postValues = values; |
|
|
|
if (mergedDisabled.value[0] && mergedDisabled.value[1]) { |
|
return postValues; |
|
} |
|
|
|
// Fill disabled unit |
|
for (let i = 0; i < 2; i += 1) { |
|
if (mergedDisabled[i] && !getValue(postValues, i) && !getValue(props.allowEmpty, i)) { |
|
postValues = updateValues(postValues, props.generateConfig.getNow(), i); |
|
} |
|
} |
|
return postValues; |
|
}, |
|
}); |
|
|
|
// ============================= Modes ============================= |
|
const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>( |
|
[props.picker, props.picker], |
|
{ |
|
value: toRef(props, 'mode'), |
|
}, |
|
); |
|
|
|
watch( |
|
() => props.picker, |
|
() => { |
|
setInnerModes([props.picker, props.picker]); |
|
}, |
|
); |
|
|
|
const triggerModesChange = (modes: [PanelMode, PanelMode], values: RangeValue<DateType>) => { |
|
setInnerModes(modes); |
|
props.onPanelChange?.(values, modes); |
|
}; |
|
|
|
// ========================= Disable Date ========================== |
|
const [disabledStartDate, disabledEndDate] = useRangeDisabled( |
|
{ |
|
picker: toRef(props, 'picker'), |
|
selectedValue, |
|
locale: toRef(props, 'locale'), |
|
disabled: mergedDisabled, |
|
disabledDate: toRef(props, 'disabledDate'), |
|
generateConfig: toRef(props, 'generateConfig'), |
|
}, |
|
openRecordsRef, |
|
); |
|
|
|
// ============================= Open ============================== |
|
const [mergedOpen, triggerInnerOpen] = useMergedState(false, { |
|
value: toRef(props, 'open'), |
|
defaultValue: props.defaultOpen, |
|
postState: postOpen => |
|
mergedDisabled.value[mergedActivePickerIndex.value] ? false : postOpen, |
|
onChange: newOpen => { |
|
props.onOpenChange?.(newOpen); |
|
|
|
if (!newOpen && operationRef.value && operationRef.value.onClose) { |
|
operationRef.value.onClose(); |
|
} |
|
}, |
|
}); |
|
|
|
const startOpen = computed(() => mergedOpen.value && mergedActivePickerIndex.value === 0); |
|
const endOpen = computed(() => mergedOpen.value && mergedActivePickerIndex.value === 1); |
|
|
|
// ============================= Popup ============================= |
|
// Popup min width |
|
const popupMinWidth = ref(0); |
|
watch(mergedOpen, () => { |
|
if (!mergedOpen.value && containerRef.value) { |
|
popupMinWidth.value = containerRef.value.offsetWidth; |
|
} |
|
}); |
|
|
|
// ============================ Trigger ============================ |
|
const triggerRef = ref<any>(); |
|
|
|
function triggerOpen(newOpen: boolean, index: 0 | 1) { |
|
if (newOpen) { |
|
clearTimeout(triggerRef.value); |
|
openRecordsRef.value[index] = true; |
|
|
|
setMergedActivePickerIndex(index); |
|
triggerInnerOpen(newOpen); |
|
|
|
// Open to reset view date |
|
if (!mergedOpen.value) { |
|
setViewDate(null, index); |
|
} |
|
} else if (mergedActivePickerIndex.value === index) { |
|
triggerInnerOpen(newOpen); |
|
|
|
// Clean up async |
|
// This makes ref not quick refresh in case user open another input with blur trigger |
|
const openRecords = openRecordsRef.value; |
|
triggerRef.value = setTimeout(() => { |
|
if (openRecords === openRecordsRef.value) { |
|
openRecordsRef.value = {}; |
|
} |
|
}); |
|
} |
|
} |
|
|
|
function triggerOpenAndFocus(index: 0 | 1) { |
|
triggerOpen(true, index); |
|
// Use setTimeout to make sure panel DOM exists |
|
window.setTimeout(() => { |
|
const inputRef = [startInputRef, endInputRef][index]; |
|
if (inputRef.value) { |
|
inputRef.value.focus(); |
|
} |
|
}, 0); |
|
} |
|
|
|
function triggerChange(newValue: RangeValue<DateType>, sourceIndex: 0 | 1) { |
|
let values = newValue; |
|
let startValue = getValue(values, 0); |
|
let endValue = getValue(values, 1); |
|
const { generateConfig, locale, picker, order, onCalendarChange, allowEmpty, onChange } = |
|
props; |
|
|
|
// >>>>> 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.value = { |
|
[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.value[0] }) |
|
: ''; |
|
const endStr = |
|
values && values[1] |
|
? formatValue(values[1], { generateConfig, locale, format: formatList.value[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.value, |
|
allowEmpty, |
|
); |
|
const canEndValueTrigger = canValueTrigger(endValue, 1, mergedDisabled.value, allowEmpty); |
|
|
|
const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); |
|
|
|
if (canTrigger) { |
|
// Trigger onChange only when value is validate |
|
setInnerValue(values); |
|
|
|
if ( |
|
onChange && |
|
(!isEqual(generateConfig, getValue(mergedValue.value, 0), startValue) || |
|
!isEqual(generateConfig, getValue(mergedValue.value, 1), endValue)) |
|
) { |
|
onChange(values, [startStr, endStr]); |
|
} |
|
} |
|
|
|
// >>>>> Open picker when |
|
|
|
// Always open another picker if possible |
|
let nextOpenIndex: 0 | 1 = null; |
|
if (sourceIndex === 0 && !mergedDisabled.value[1]) { |
|
nextOpenIndex = 1; |
|
} else if (sourceIndex === 1 && !mergedDisabled.value[0]) { |
|
nextOpenIndex = 0; |
|
} |
|
|
|
if ( |
|
nextOpenIndex !== null && |
|
nextOpenIndex !== mergedActivePickerIndex.value && |
|
(!openRecordsRef.value[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: KeyboardEvent) => { |
|
if (mergedOpen && operationRef.value && operationRef.value.onKeydown) { |
|
// Let popup panel handle keyboard |
|
return operationRef.value.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: toRef(props, 'generateConfig'), |
|
locale: toRef(props, 'locale'), |
|
}; |
|
|
|
const [startValueTexts, firstStartValueText] = useValueTexts<DateType>( |
|
computed(() => getValue(selectedValue.value, 0)), |
|
sharedTextHooksProps, |
|
); |
|
|
|
const [endValueTexts, firstEndValueText] = useValueTexts<DateType>( |
|
computed(() => getValue(selectedValue.value, 1)), |
|
sharedTextHooksProps, |
|
); |
|
|
|
const onTextChange = (newText: string, index: 0 | 1) => { |
|
const inputDate = parseValue(newText, { |
|
locale: props.locale, |
|
formatList: formatList.value, |
|
generateConfig: props.generateConfig, |
|
}); |
|
|
|
const disabledFunc = index === 0 ? disabledStartDate : disabledEndDate; |
|
|
|
if (inputDate && !disabledFunc(inputDate)) { |
|
setSelectedValue(updateValues(selectedValue.value, 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<RangeValue<DateType>>(null); |
|
|
|
// ========================== Hover Range ========================== |
|
const [hoverRangedValue, setHoverRangedValue] = useState<RangeValue<DateType>>(null); |
|
|
|
const [startHoverValue, onStartEnter, onStartLeave] = useHoverValue( |
|
startText, |
|
sharedTextHooksProps, |
|
); |
|
|
|
const [endHoverValue, onEndEnter, onEndLeave] = useHoverValue(endText, sharedTextHooksProps); |
|
|
|
const onDateMouseenter = (date: DateType) => { |
|
setHoverRangedValue(updateValues(selectedValue.value, date, mergedActivePickerIndex.value)); |
|
if (mergedActivePickerIndex.value === 0) { |
|
onStartEnter(date); |
|
} else { |
|
onEndEnter(date); |
|
} |
|
}; |
|
|
|
const onDateMouseleave = () => { |
|
setHoverRangedValue(updateValues(selectedValue.value, null, mergedActivePickerIndex.value)); |
|
if (mergedActivePickerIndex.value === 0) { |
|
onStartLeave(); |
|
} else { |
|
onEndLeave(); |
|
} |
|
}; |
|
|
|
// ============================= Input ============================= |
|
const getSharedInputHookProps = (index: 0 | 1, resetText: () => void) => ({ |
|
forwardKeydown, |
|
onBlur: (e: FocusEvent) => { |
|
props.onBlur?.(e); |
|
}, |
|
isClickOutside: (target: EventTarget | null) => |
|
!elementsContains( |
|
[panelDivRef.value, startInputDivRef.value, endInputDivRef.value], |
|
target as HTMLElement, |
|
), |
|
onFocus: (e: FocusEvent) => { |
|
setMergedActivePickerIndex(index); |
|
props.onFocus?.(e); |
|
}, |
|
triggerOpen: (newOpen: boolean) => { |
|
triggerOpen(newOpen, index); |
|
}, |
|
onSubmit: () => { |
|
triggerChange(selectedValue.value, index); |
|
resetText(); |
|
}, |
|
onCancel: () => { |
|
triggerOpen(false, index); |
|
setSelectedValue(mergedValue.value); |
|
resetText(); |
|
}, |
|
}); |
|
|
|
const [startInputProps, { focused: startFocused, typing: startTyping }] = usePickerInput({ |
|
...getSharedInputHookProps(0, resetStartText), |
|
blurToCancel: needConfirmButton, |
|
open: startOpen, |
|
value: startText, |
|
onKeydown: (e, preventDefault) => { |
|
props.onKeydown?.(e, preventDefault); |
|
}, |
|
}); |
|
|
|
const [endInputProps, { focused: endFocused, typing: endTyping }] = usePickerInput({ |
|
...getSharedInputHookProps(1, resetEndText), |
|
blurToCancel: needConfirmButton, |
|
open: endOpen, |
|
value: endText, |
|
onKeydown: (e, preventDefault) => { |
|
props.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.value && |
|
!startInputRef.value.contains(e.target as Node) && |
|
!endInputRef.value.contains(e.target as Node) |
|
) { |
|
if (!mergedDisabled.value[0]) { |
|
triggerOpenAndFocus(0); |
|
} else if (!mergedDisabled.value[1]) { |
|
triggerOpenAndFocus(1); |
|
} |
|
} |
|
}; |
|
|
|
const onPickerMousedown = (e: MouseEvent) => { |
|
// shouldn't affect input elements if picker is active |
|
if ( |
|
mergedOpen.value && |
|
(startFocused.value || endFocused.value) && |
|
!startInputRef.value.contains(e.target as Node) && |
|
!endInputRef.value.contains(e.target as Node) |
|
) { |
|
e.preventDefault(); |
|
} |
|
}; |
|
|
|
// ============================= Sync ============================== |
|
// Close should sync back with text value |
|
const startStr = computed(() => |
|
mergedValue.value?.[0] |
|
? formatValue(mergedValue.value[0], { |
|
locale: props.locale, |
|
format: 'YYYYMMDDHHmmss', |
|
generateConfig: props.generateConfig, |
|
}) |
|
: '', |
|
); |
|
const endStr = computed(() => |
|
mergedValue.value?.[1] |
|
? formatValue(mergedValue.value[1], { |
|
locale: props.locale, |
|
format: 'YYYYMMDDHHmmss', |
|
generateConfig: props.generateConfig, |
|
}) |
|
: '', |
|
); |
|
|
|
watch([mergedOpen, startValueTexts, endValueTexts], () => { |
|
if (!mergedOpen.value) { |
|
setSelectedValue(mergedValue.value); |
|
|
|
if (!startValueTexts.value.length || startValueTexts.value[0] === '') { |
|
triggerStartTextChange(''); |
|
} else if (firstStartValueText.value !== startText.value) { |
|
resetStartText(); |
|
} |
|
if (!endValueTexts.value.length || endValueTexts.value[0] === '') { |
|
triggerEndTextChange(''); |
|
} else if (firstEndValueText.value !== endText.value) { |
|
resetEndText(); |
|
} |
|
} |
|
}); |
|
|
|
// Sync innerValue with control mode |
|
watch([startStr, endStr], () => { |
|
setSelectedValue(mergedValue.value); |
|
}); |
|
|
|
// ============================ Warning ============================ |
|
if (process.env.NODE_ENV !== 'production') { |
|
watchEffect(() => { |
|
const { value, disabled } = props; |
|
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.', |
|
); |
|
} |
|
}); |
|
} |
|
|
|
expose({ |
|
focus: () => { |
|
if (startInputRef.value) { |
|
startInputRef.value.focus(); |
|
} |
|
}, |
|
blur: () => { |
|
if (startInputRef.value) { |
|
startInputRef.value.blur(); |
|
} |
|
if (endInputRef.value) { |
|
endInputRef.value.blur(); |
|
} |
|
}, |
|
}); |
|
|
|
// ============================ Ranges ============================= |
|
|
|
const rangeList = computed(() => |
|
Object.keys(props.ranges || {}).map(label => { |
|
const range = props.ranges![label]; |
|
const newValues = typeof range === 'function' ? range() : range; |
|
|
|
return { |
|
label, |
|
onClick: () => { |
|
triggerChange(newValues, null); |
|
triggerOpen(false, mergedActivePickerIndex.value); |
|
}, |
|
onMouseenter: () => { |
|
setRangeHoverValue(newValues); |
|
}, |
|
onMouseleave: () => { |
|
setRangeHoverValue(null); |
|
}, |
|
}; |
|
}), |
|
); |
|
// ============================= Panel ============================= |
|
const panelHoverRangedValue = computed(() => { |
|
if ( |
|
mergedOpen.value && |
|
hoverRangedValue.value && |
|
hoverRangedValue.value[0] && |
|
hoverRangedValue.value[1] && |
|
props.generateConfig.isAfter(hoverRangedValue.value[1], hoverRangedValue.value[0]) |
|
) { |
|
return hoverRangedValue.value; |
|
} else { |
|
return null; |
|
} |
|
}); |
|
function renderPanel( |
|
panelPosition: 'left' | 'right' | false = false, |
|
panelProps: Partial<PickerPanelProps<DateType>> = {}, |
|
) { |
|
const { generateConfig, showTime, dateRender, direction, disabledTime, prefixCls, locale } = |
|
props; |
|
|
|
let panelShowTime: boolean | SharedTimeProps<DateType> | undefined = |
|
showTime as SharedTimeProps<DateType>; |
|
if (showTime && typeof showTime === 'object' && showTime.defaultValue) { |
|
const timeDefaultValues: DateType[] = showTime.defaultValue!; |
|
panelShowTime = { |
|
...showTime, |
|
defaultValue: getValue(timeDefaultValues, mergedActivePickerIndex.value) || undefined, |
|
}; |
|
} |
|
|
|
let panelDateRender: DateRender<DateType> | null = null; |
|
if (dateRender) { |
|
panelDateRender = ({ current: date, today }) => |
|
dateRender({ |
|
current: date, |
|
today, |
|
info: { |
|
range: mergedActivePickerIndex.value ? 'end' : 'start', |
|
}, |
|
}); |
|
} |
|
|
|
return ( |
|
<RangeContextProvider |
|
value={{ |
|
inRange: true, |
|
panelPosition, |
|
rangedValue: rangeHoverValue.value || selectedValue.value, |
|
hoverRangedValue: panelHoverRangedValue.value, |
|
}} |
|
> |
|
<PickerPanel<DateType> |
|
{...(props as any)} |
|
{...panelProps} |
|
dateRender={panelDateRender} |
|
showTime={panelShowTime} |
|
mode={mergedModes.value[mergedActivePickerIndex.value]} |
|
generateConfig={generateConfig} |
|
style={undefined} |
|
direction={direction} |
|
disabledDate={ |
|
mergedActivePickerIndex.value === 0 ? disabledStartDate : disabledEndDate |
|
} |
|
disabledTime={date => { |
|
if (disabledTime) { |
|
return disabledTime(date, mergedActivePickerIndex.value === 0 ? 'start' : 'end'); |
|
} |
|
return false; |
|
}} |
|
class={classNames({ |
|
[`${prefixCls}-panel-focused`]: |
|
mergedActivePickerIndex.value === 0 ? !startTyping.value : !endTyping.value, |
|
})} |
|
value={getValue(selectedValue.value, mergedActivePickerIndex.value)} |
|
locale={locale} |
|
tabIndex={-1} |
|
onPanelChange={(date, newMode) => { |
|
// clear hover value when panel change |
|
if (mergedActivePickerIndex.value === 0) { |
|
onStartLeave(true); |
|
} |
|
if (mergedActivePickerIndex.value === 1) { |
|
onEndLeave(true); |
|
} |
|
triggerModesChange( |
|
updateValues(mergedModes.value, newMode, mergedActivePickerIndex.value), |
|
updateValues(selectedValue.value, date, mergedActivePickerIndex.value), |
|
); |
|
|
|
let viewDate = date; |
|
if ( |
|
panelPosition === 'right' && |
|
mergedModes.value[mergedActivePickerIndex.value] === newMode |
|
) { |
|
viewDate = getClosingViewDate(viewDate, newMode as any, generateConfig, -1); |
|
} |
|
setViewDate(viewDate, mergedActivePickerIndex.value); |
|
}} |
|
onOk={null} |
|
onSelect={undefined} |
|
onChange={undefined} |
|
defaultValue={ |
|
mergedActivePickerIndex.value === 0 |
|
? getValue(selectedValue.value, 1) |
|
: getValue(selectedValue.value, 0) |
|
} |
|
defaultPickerValue={undefined} |
|
/> |
|
</RangeContextProvider> |
|
); |
|
} |
|
|
|
const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { |
|
const values = updateValues(selectedValue.value, date, mergedActivePickerIndex.value); |
|
|
|
if (type === 'submit' || (type !== 'key' && !needConfirmButton.value)) { |
|
// triggerChange will also update selected values |
|
triggerChange(values, mergedActivePickerIndex.value); |
|
// clear hover value style |
|
if (mergedActivePickerIndex.value === 0) { |
|
onStartLeave(); |
|
} else { |
|
onEndLeave(); |
|
} |
|
} else { |
|
setSelectedValue(values); |
|
} |
|
}; |
|
|
|
useProvidePanel({ |
|
operationRef, |
|
hideHeader: computed(() => props.picker === 'time'), |
|
onDateMouseenter, |
|
onDateMouseleave, |
|
hideRanges: computed(() => true), |
|
onSelect: onContextSelect, |
|
open: mergedOpen, |
|
}); |
|
|
|
return () => { |
|
const { |
|
prefixCls = 'rc-picker', |
|
id, |
|
popupStyle, |
|
dropdownClassName, |
|
transitionName, |
|
dropdownAlign, |
|
getPopupContainer, |
|
generateConfig, |
|
locale, |
|
placeholder, |
|
autofocus, |
|
picker = 'date', |
|
showTime, |
|
separator = '~', |
|
disabledDate, |
|
panelRender, |
|
allowClear, |
|
suffixIcon, |
|
clearIcon, |
|
inputReadOnly, |
|
renderExtraFooter, |
|
onMouseenter, |
|
onMouseleave, |
|
onOk, |
|
components, |
|
direction, |
|
autocomplete = 'off', |
|
} = props; |
|
let arrowLeft = 0; |
|
let panelLeft = 0; |
|
if ( |
|
mergedActivePickerIndex.value && |
|
startInputDivRef.value && |
|
separatorRef.value && |
|
panelDivRef.value |
|
) { |
|
// Arrow offset |
|
arrowLeft = startInputDivRef.value.offsetWidth + separatorRef.value.offsetWidth; |
|
|
|
if (panelDivRef.value.offsetWidth && arrowLeft > panelDivRef.value.offsetWidth) { |
|
panelLeft = arrowLeft; |
|
} |
|
} |
|
|
|
const arrowPositionStyle = direction === 'rtl' ? { right: arrowLeft } : { left: arrowLeft }; |
|
|
|
function renderPanels() { |
|
let panels: VueNode; |
|
const extraNode = getExtraFooter( |
|
prefixCls, |
|
mergedModes.value[mergedActivePickerIndex.value], |
|
renderExtraFooter, |
|
); |
|
|
|
const rangesNode = getRanges({ |
|
prefixCls, |
|
components, |
|
needConfirmButton: needConfirmButton.value, |
|
okDisabled: |
|
!getValue(selectedValue.value, mergedActivePickerIndex.value) || |
|
(disabledDate && disabledDate(selectedValue.value[mergedActivePickerIndex.value])), |
|
locale, |
|
rangeList: rangeList.value, |
|
onOk: () => { |
|
if (getValue(selectedValue.value, mergedActivePickerIndex.value)) { |
|
// triggerChangeOld(selectedValue.value); |
|
triggerChange(selectedValue.value, mergedActivePickerIndex.value); |
|
if (onOk) { |
|
onOk(selectedValue.value); |
|
} |
|
} |
|
}, |
|
}); |
|
|
|
if (picker !== 'time' && !showTime) { |
|
const viewDate = |
|
mergedActivePickerIndex.value === 0 ? startViewDate.value : endViewDate.value; |
|
const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig); |
|
const currentMode = mergedModes.value[mergedActivePickerIndex.value]; |
|
|
|
const showDoublePanel = currentMode === picker; |
|
const leftPanel = renderPanel(showDoublePanel ? 'left' : false, { |
|
pickerValue: viewDate, |
|
onPickerValueChange: newViewDate => { |
|
setViewDate(newViewDate, mergedActivePickerIndex.value); |
|
}, |
|
}); |
|
const rightPanel = renderPanel('right', { |
|
pickerValue: nextViewDate, |
|
onPickerValueChange: newViewDate => { |
|
setViewDate( |
|
getClosingViewDate(newViewDate, picker, generateConfig, -1), |
|
mergedActivePickerIndex.value, |
|
); |
|
}, |
|
}); |
|
|
|
if (direction === 'rtl') { |
|
panels = ( |
|
<> |
|
{rightPanel} |
|
{showDoublePanel && leftPanel} |
|
</> |
|
); |
|
} else { |
|
panels = ( |
|
<> |
|
{leftPanel} |
|
{showDoublePanel && rightPanel} |
|
</> |
|
); |
|
} |
|
} else { |
|
panels = renderPanel(); |
|
} |
|
|
|
let mergedNodes: VueNode = ( |
|
<> |
|
<div class={`${prefixCls}-panels`}>{panels}</div> |
|
{(extraNode || rangesNode) && ( |
|
<div class={`${prefixCls}-footer`}> |
|
{extraNode} |
|
{rangesNode} |
|
</div> |
|
)} |
|
</> |
|
); |
|
|
|
if (panelRender) { |
|
mergedNodes = panelRender(mergedNodes); |
|
} |
|
|
|
return ( |
|
<div |
|
class={`${prefixCls}-panel-container`} |
|
style={{ marginLeft: panelLeft }} |
|
ref={panelDivRef} |
|
onMousedown={e => { |
|
e.preventDefault(); |
|
}} |
|
> |
|
{mergedNodes} |
|
</div> |
|
); |
|
} |
|
|
|
const rangePanel = ( |
|
<div |
|
class={classNames(`${prefixCls}-range-wrapper`, `${prefixCls}-${picker}-range-wrapper`)} |
|
style={{ minWidth: `${popupMinWidth.value}px` }} |
|
> |
|
<div class={`${prefixCls}-range-arrow`} style={arrowPositionStyle} /> |
|
{renderPanels()} |
|
</div> |
|
); |
|
|
|
// ============================= Icons ============================= |
|
let suffixNode: VueNode; |
|
if (suffixIcon) { |
|
suffixNode = <span class={`${prefixCls}-suffix`}>{suffixIcon}</span>; |
|
} |
|
|
|
let clearNode: VueNode; |
|
if ( |
|
allowClear && |
|
((getValue(mergedValue.value, 0) && !mergedDisabled.value[0]) || |
|
(getValue(mergedValue.value, 1) && !mergedDisabled.value[1])) |
|
) { |
|
clearNode = ( |
|
<span |
|
onMousedown={e => { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
}} |
|
onMouseup={e => { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
let values = mergedValue.value; |
|
|
|
if (!mergedDisabled.value[0]) { |
|
values = updateValues(values, null, 0); |
|
} |
|
if (!mergedDisabled.value[1]) { |
|
values = updateValues(values, null, 1); |
|
} |
|
|
|
triggerChange(values, null); |
|
triggerOpen(false, mergedActivePickerIndex.value); |
|
}} |
|
class={`${prefixCls}-clear`} |
|
> |
|
{clearIcon || <span class={`${prefixCls}-clear-btn`} />} |
|
</span> |
|
); |
|
} |
|
|
|
const inputSharedProps = { |
|
size: getInputSize(picker, formatList.value[0], generateConfig), |
|
}; |
|
|
|
let activeBarLeft = 0; |
|
let activeBarWidth = 0; |
|
if (startInputDivRef.value && endInputDivRef.value && separatorRef.value) { |
|
if (mergedActivePickerIndex.value === 0) { |
|
activeBarWidth = startInputDivRef.value.offsetWidth; |
|
} else { |
|
activeBarLeft = arrowLeft; |
|
activeBarWidth = endInputDivRef.value.offsetWidth; |
|
} |
|
} |
|
const activeBarPositionStyle = |
|
direction === 'rtl' ? { right: `${activeBarLeft}px` } : { left: `${activeBarLeft}px` }; |
|
// ============================ Return ============================= |
|
|
|
return ( |
|
<PickerTrigger |
|
visible={mergedOpen.value} |
|
popupStyle={popupStyle} |
|
prefixCls={prefixCls} |
|
dropdownClassName={dropdownClassName} |
|
dropdownAlign={dropdownAlign} |
|
getPopupContainer={getPopupContainer} |
|
transitionName={transitionName} |
|
range |
|
direction={direction} |
|
v-slots={{ |
|
popupElement: () => rangePanel, |
|
}} |
|
> |
|
<div |
|
ref={containerRef} |
|
class={classNames(prefixCls, `${prefixCls}-range`, attrs.class, { |
|
[`${prefixCls}-disabled`]: mergedDisabled.value[0] && mergedDisabled.value[1], |
|
[`${prefixCls}-focused`]: |
|
mergedActivePickerIndex.value === 0 ? startFocused.value : endFocused.value, |
|
[`${prefixCls}-rtl`]: direction === 'rtl', |
|
})} |
|
style={attrs.style} |
|
onClick={onPickerClick} |
|
onMouseenter={onMouseenter} |
|
onMouseleave={onMouseleave} |
|
onMousedown={onPickerMousedown} |
|
{...getDataOrAriaProps(props)} |
|
> |
|
<div |
|
class={classNames(`${prefixCls}-input`, { |
|
[`${prefixCls}-input-active`]: mergedActivePickerIndex.value === 0, |
|
[`${prefixCls}-input-placeholder`]: !!startHoverValue.value, |
|
})} |
|
ref={startInputDivRef} |
|
> |
|
<input |
|
id={id} |
|
disabled={mergedDisabled.value[0]} |
|
readonly={ |
|
inputReadOnly || typeof formatList.value[0] === 'function' || !startTyping.value |
|
} |
|
value={startHoverValue.value || startText.value} |
|
onInput={(e: ChangeEvent) => { |
|
triggerStartTextChange(e.target.value); |
|
}} |
|
autofocus={autofocus} |
|
placeholder={getValue(placeholder, 0) || ''} |
|
ref={startInputRef} |
|
{...startInputProps.value} |
|
{...inputSharedProps} |
|
autocomplete={autocomplete} |
|
/> |
|
</div> |
|
<div class={`${prefixCls}-range-separator`} ref={separatorRef}> |
|
{separator} |
|
</div> |
|
<div |
|
class={classNames(`${prefixCls}-input`, { |
|
[`${prefixCls}-input-active`]: mergedActivePickerIndex.value === 1, |
|
[`${prefixCls}-input-placeholder`]: !!endHoverValue.value, |
|
})} |
|
ref={endInputDivRef} |
|
> |
|
<input |
|
disabled={mergedDisabled.value[1]} |
|
readonly={ |
|
inputReadOnly || typeof formatList.value[0] === 'function' || !endTyping.value |
|
} |
|
value={endHoverValue.value || endText.value} |
|
onInput={(e: ChangeEvent) => { |
|
triggerEndTextChange(e.target.value); |
|
}} |
|
placeholder={getValue(placeholder, 1) || ''} |
|
ref={endInputRef} |
|
{...endInputProps.value} |
|
{...inputSharedProps} |
|
autocomplete={autocomplete} |
|
/> |
|
</div> |
|
<div |
|
class={`${prefixCls}-active-bar`} |
|
style={{ |
|
...activeBarPositionStyle, |
|
width: `${activeBarWidth}px`, |
|
position: 'absolute', |
|
}} |
|
/> |
|
{suffixNode} |
|
{clearNode} |
|
</div> |
|
</PickerTrigger> |
|
); |
|
}; |
|
}, |
|
}); |
|
} |
|
|
|
const InterRangerPicker = RangerPicker<any>(); |
|
export default InterRangerPicker;
|
|
|