362 lines
10 KiB
Vue
362 lines
10 KiB
Vue
/**
|
|
* 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<DateType> = {
|
|
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<DateType> | (string | CustomFormat<DateType>)[];
|
|
|
|
// 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<PickerRefConfig>;
|
|
|
|
// WAI-ARIA
|
|
role?: string;
|
|
name?: string;
|
|
|
|
autocomplete?: string;
|
|
direction?: 'ltr' | 'rtl';
|
|
} & HtmlHTMLAttributes;
|
|
|
|
type OmitPanelProps<Props> = Omit<
|
|
Props,
|
|
'onChange' | 'hideHeader' | 'pickerValue' | 'onPickerValueChange'
|
|
>;
|
|
|
|
export type PickerBaseProps<DateType> = {} & PickerSharedProps<DateType> &
|
|
OmitPanelProps<PickerPanelBaseProps<DateType>>;
|
|
|
|
export type PickerDateProps<DateType> = {} & PickerSharedProps<DateType> &
|
|
OmitPanelProps<PickerPanelDateProps<DateType>>;
|
|
|
|
export type PickerTimeProps<DateType> = {
|
|
picker: 'time';
|
|
/**
|
|
* @deprecated Please use `defaultValue` directly instead
|
|
* since `defaultOpenValue` will confuse user of current value status
|
|
*/
|
|
defaultOpenValue?: DateType;
|
|
} & PickerSharedProps<DateType> &
|
|
Omit<OmitPanelProps<PickerPanelTimeProps<DateType>>, 'format'>;
|
|
|
|
export type PickerProps<DateType> =
|
|
| PickerBaseProps<DateType>
|
|
| PickerDateProps<DateType>
|
|
| PickerTimeProps<DateType>;
|
|
|
|
// TMP type to fit for ts 3.9.2
|
|
type OmitType<DateType> = Omit<PickerBaseProps<DateType>, 'picker'> &
|
|
Omit<PickerDateProps<DateType>, 'picker'> &
|
|
Omit<PickerTimeProps<DateType>, 'picker'>;
|
|
type MergedPickerProps<DateType> = {
|
|
picker?: PickerMode;
|
|
} & OmitType<DateType>;
|
|
|
|
function Picker<DateType>() {
|
|
return defineComponent<MergedPickerProps<DateType>>({
|
|
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<DateType>(null, {
|
|
value: toRef(props, 'value'),
|
|
defaultValue: props.defaultValue,
|
|
});
|
|
|
|
const selectedValue = ref(mergedValue.value) as Ref<DateType>;
|
|
const setSelectedValue = (val: DateType) => {
|
|
selectedValue.value = val;
|
|
};
|
|
|
|
// Operation ref
|
|
const operationRef = ref<ContextOperationRefProps>(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();
|