/** * Removed: * - getCalendarContainer: use `getPopupContainer` instead * - onOk * * New Feature: * - picker * - allowEmpty * - selectable * * Tips: Should add faq about `datetime` mode with `defaultValue` */ 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'; import { computed, CSSProperties, defineComponent, HtmlHTMLAttributes, ref, Ref, toRef, toRefs, } from 'vue'; import { 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'; export type PickerRefConfig = { focus: () => void; blur: () => void; }; export type PickerSharedProps = { dropdownClassName?: string; dropdownAlign?: AlignType; popupStyle?: 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?: VueNode; clearIcon?: VueNode; prevIcon?: VueNode; nextIcon?: VueNode; superPrevIcon?: VueNode; superNextIcon?: VueNode; getPopupContainer?: (node: HTMLElement) => HTMLElement; panelRender?: (originPanel: VueNode) => VueNode; // Events onChange?: (value: DateType | null, dateString: string) => void; onOpenChange?: (open: boolean) => void; onFocus?: FocusEventHandler; onBlur?: FocusEventHandler; onMouseDown?: MouseEventHandler; onMouseUp?: MouseEventHandler; onMouseEnter?: MouseEventHandler; onMouseLeave?: MouseEventHandler; onClick?: MouseEventHandler; onContextMenu?: MouseEventHandler; onKeyDown?: (event: KeyboardEvent, preventDefault: () => void) => void; // Internal /** @private Internal usage, do not use in production mode!!! */ pickerRef?: Ref; // WAI-ARIA role?: string; name?: string; autocomplete?: string; direction?: 'ltr' | 'rtl'; } & HtmlHTMLAttributes; 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 Picker() { return defineComponent>({ name: 'Picker', props: [ 'prefixCls', 'id', 'tabindex', 'dropdownClassName', 'dropdownAlign', 'popupStyle', 'transitionName', 'generateConfig', 'locale', 'inputReadOnly', 'allowClear', 'autofocus', 'showTime', 'picker', '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', ] as any, inheritAttrs: false, slots: [ 'suffixIcon', 'clearIcon', 'prevIcon', 'nextIcon', 'superPrevIcon', 'superNextIcon', 'panelRender', ], setup(props, { slots, attrs, expose }) { const inputRef = ref(null); const needConfirmButton = computed( () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', ); // ============================= State ============================= const formatList = computed(() => toArray(getDefaultFormat(props.format, props.picker, props.showTime, props.use12Hours)), ); // Panel ref const panelDivRef = ref(null); const inputDivRef = ref(null); // Real value const [mergedValue, setInnerValue] = useMergedState(null, { value: toRef(props, 'value'), defaultValue: props.defaultValue, }); const selectedValue = ref(mergedValue.value) as Ref; const setSelectedValue = (val: DateType) => { selectedValue.value = val; }; // Operation ref const operationRef = ref(null); // Open const [mergedOpen, triggerInnerOpen] = useMergedState(false, { value: toRef(props, 'open'), defaultValue: props.defaultOpen, postState: postOpen => (props.disabled ? false : postOpen), onChange: newOpen => { if (props.onOpenChange) { props.onOpenChange(newOpen); } if (!newOpen && operationRef.value && operationRef.value.onClose) { operationRef.value.onClose(); } }, }); // ============================= Text ============================== const texts = 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, onTextChange: newText => { const inputDate = parseValue(newText, { locale: props.locale, formatList: formatList.value, generateConfig: props.generateConfig, }); if (inputDate && (!props.disabledDate || !props.disabledDate(inputDate))) { setSelectedValue(inputDate); } }, }); // ============================ Trigger ============================ const triggerChange = (newValue: DateType | null) => { const { onChange, generateConfig, locale } = props; setSelectedValue(newValue); setInnerValue(newValue); if (onChange && !isEqual(generateConfig, mergedValue.value, newValue)) { onChange( newValue, newValue ? formatValue(newValue, { generateConfig, locale, format: formatList[0] }) : '', ); } }; const triggerOpen = (newOpen: boolean) => { if (props.disabled && newOpen) { return; } triggerInnerOpen(newOpen); }; 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; } }; const onInternalMouseUp: MouseEventHandler = (...args) => { if (props.onMouseUp) { props.onMouseUp(...args); } if (inputRef.value) { inputRef.value.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 (props.disabledDate && props.disabledDate(selectedValue.value)) { return false; } triggerChange(selectedValue.value); triggerOpen(false); resetText(); return true; }, onCancel: () => { triggerOpen(false); setSelectedValue(mergedValue.value); resetText(); }, onKeyDown: (e, preventDefault) => { props.onKeyDown?.(e, preventDefault); }, onFocus: (e: FocusEvent) => { props.onFocus?.(e); }, onBlur: (e: FocusEvent) => { props.onBlur?.(e); }, }); return () => { return null; }; }, }); } export default Picker();