ant-design-vue/components/vc-picker/Picker.tsx
tangjinzhou 3aeeeb2aed
3.0 ready (#4523)
* refactor: transfer、tooltip (#4306)

* refactor(transfer): use composition api (#4135)

* refactor(transfer): use composition api

* fix: remove console

* refactor(tooltip): use composition api (#4059)

* refactor(tooltip): use composition api

* chore: useConfigInject

* fix: remove useless

* style: format code

* refactor: transfer

* refactor: tooltip

Co-authored-by: ajuner <106791576@qq.com>

* Refactor mentions (#4341)

* refactor(mentions): use compositionAPI (#4313)

* refactor: mentions

* refactor: mentions

Co-authored-by: ajuner <106791576@qq.com>

* Refactor progress (#4358)

* fix: timepicker error border not show #4331

* fix(UploadDragger): fix UploadDrager no export (#4334)

* refactor(switch): support customize checked value #4329 (#4332)

* refactor(switch): support customize checked value #4329

* test: add test case

* refactor: update props name

* refactor: update ts

* refactor: optimize

* style: uncheckedValue to unCheckedValue

* test: update snap

* feat: udpate switch ts

* docs: remove ie11

* fix: tree-select throw error when use slot title

* fix: TypeScript definition of Table interface for typescript 4.3.5 (#4353)

* fix type for typescript 4.3.5

* Update interface.ts

close #4296

* fix: dropdown submenu style error #4351
close #4351

* fix(notification): 完善notification类型 (#4346)

* refactor(progress): use composition API (#4355)

* refactor(progress): use composition API

* refactor(vc-progress): update

* refactor: progress

* refactor: progress

* fix: timepicker error border not show #4331

* fix(UploadDragger): fix UploadDrager no export (#4334)

* refactor(switch): support customize checked value #4329 (#4332)

* refactor(switch): support customize checked value #4329

* test: add test case

* refactor: update props name

* refactor: update ts

* refactor: optimize

* style: uncheckedValue to unCheckedValue

* test: update snap

* feat: udpate switch ts

* docs: remove ie11

* fix: tree-select throw error when use slot title

* fix: TypeScript definition of Table interface for typescript 4.3.5 (#4353)

* fix type for typescript 4.3.5

* Update interface.ts

close #4296

* fix: dropdown submenu style error #4351
close #4351

* fix(notification): 完善notification类型 (#4346)

* refactor(progress): use composition API (#4355)

* refactor(progress): use composition API

* refactor(vc-progress): update

* refactor: progress

* refactor: progress

Co-authored-by: Jarvis <35361626+fanhaoyuan@users.noreply.github.com>
Co-authored-by: John <John60676@qq.com>
Co-authored-by: 艾斯特洛 <axetroy.dev@gmail.com>
Co-authored-by: zanllp <qc@zanllp.cn>

* docs: add changelog

* refactor: tree

* refactor: tree

* style: lint

* refactor: tree

* 热factor: tree

* refactor: tree

* refactor: tree

* refactor: tree

* refactor: directory tree

* refactor: tree

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* style: lint format

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* fix: upload ts error

* fix: update tree title render & switchIcon

* test: update tree test

* feat: add VirtualScroll tree

* refactor: datePicker & calendar & trigger (#4522)

* style: update

* test: update calendar test

* test: update test

* test: update test

* refactor: slider

* feat: update slider css

* refactor: slider to ts

* refactor: slider to ts

* perf: update default itemHeight

* test: update

* fix: uddate ts type

* fix: update skeleton

* fix: update skeleton

* refactor: update vc-pagination

* refactor: pagination

* refactor: timeline

* refactor: steps

* refactor: collapse

* refactor: collapse

* refactor: popconfirm

* refactor: popover

* refactor: dropdown

* doc: merge doc

* chore: vite for dev (#4602)

* style: js to jsx

* doc: add site

* style: lint

* style: format ts type

* doc: update

* style: format code

* style: format site

* doc: update

* style: dmeo

* style: format scripts

* chore: remove sub-modules

* chore: update vite

* site: add site build

* test: update snap

* doc(select): add tip (#4606)

* refactor: table (#4641)

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* fix: column not pass to cell

* doc: uppate table

* fix: update bodyCell headerCell

* doc: remove examples

* refactor: table

* fix: table title not work

* fix: table selection

* fix: table checkStrictly

* refactor: table

* fix: table template error

* feat: table support summary

* test: update snap

* perf: table

* docs(table): fix ajax demo (#4639)

* test: update table

* refactor: remove old table

* doc: update  table doc

* doc: update doc

* doc: update select

* doc: update summary

Co-authored-by: John <John60676@qq.com>

* doc: update doc

* fix: menu arrow not work

* test: update

* doc: add next site

* style: format

* doc: update

* doc: update site script

* fix: expand icon not fixed

* feat: use renderSlot

* test: update table snap

* feat: confirm support reactively

* feat: configProvider.config

* feat: message support configprovider.config

* feat: notification support configprovider.config

* doc: update doc

* fix: typescript compile error

* style: add import eslint

* doc: update demo

* chore: set transpileOnly true

* style: fix eslint error

* test: update snap

* doc: update

* test: mock date

* test: update snap

* chore: remove gulp-typescript (#4675)

* feat: V3 form (#4678)

* chore: update husky

* perf: update formItem

* perf: useInjectFormItemContext

* fix: table ts error

* doc: add Customized Form Controls demo

* feat: export useInjectFormItemContext

* doc: update form doc

* doc: update doc

* doc: update doc

* feat: autocomplete support option slot

* doc: update

* feat: add form item rest

* style: remove omit.js

* refactor: autocomplete

* doc: add changelog to site

* doc: update site anchor

* doc: update doc layout

* test: update table test

* doc: update

* chore: udpate gulp script

* chore: udpate gulp script

* doc: add changelog

* doc: update

* test: ignore some test wait vue-test-utils

* fix: form id error #4582
close #4582

* doc: add select Responsive demo

* doc: remove temp doc

Co-authored-by: ajuner <106791576@qq.com>
Co-authored-by: Jarvis <35361626+fanhaoyuan@users.noreply.github.com>
Co-authored-by: John <John60676@qq.com>
Co-authored-by: 艾斯特洛 <axetroy.dev@gmail.com>
Co-authored-by: zanllp <qc@zanllp.cn>
Co-authored-by: Amour1688 <lcz_1996@foxmail.com>
2021-09-25 16:51:32 +08:00

593 lines
18 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 { useProvidePanel } 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 type { CSSProperties, Ref } from 'vue';
import { computed, defineComponent, ref, toRef, watch } from 'vue';
import type { ChangeEvent, FocusEventHandler, MouseEventHandler } from '../_util/EventInterface';
import type { VueNode } from '../_util/type';
import type { AlignType } from '../vc-align/interface';
import useMergedState from '../_util/hooks/useMergedState';
import { warning } from '../vc-util/warning';
import classNames from '../_util/classNames';
import type { SharedTimeProps } from './panels/TimePanel';
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;
// WAI-ARIA
role?: string;
name?: string;
autocomplete?: string;
direction?: 'ltr' | 'rtl';
showToday?: boolean;
showTime?: boolean | SharedTimeProps<DateType>;
};
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',
inheritAttrs: false,
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',
'panelRender',
'onChange',
'onOpenChange',
'onFocus',
'onBlur',
'onMousedown',
'onMouseup',
'onMouseenter',
'onMouseleave',
'onContextmenu',
'onClick',
'onKeydown',
'onSelect',
'direction',
'autocomplete',
'showToday',
'renderExtraFooter',
'dateRender',
] as any,
// slots: [
// 'suffixIcon',
// 'clearIcon',
// 'prevIcon',
// 'nextIcon',
// 'superPrevIcon',
// 'superNextIcon',
// 'panelRender',
// ],
setup(props, { attrs, expose }) {
const inputRef = ref(null);
const picker = computed(() => props.picker ?? 'date');
const needConfirmButton = computed(
() => (picker.value === 'date' && !!props.showTime) || picker.value === 'time',
);
// ============================= State =============================
const formatList = computed(() =>
toArray(getDefaultFormat(props.format, picker.value, 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 [valueTexts, firstValueText] = useValueTexts(selectedValue, {
formatList,
generateConfig: toRef(props, 'generateConfig'),
locale: toRef(props, 'locale'),
});
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.value[0] })
: '',
);
}
};
const triggerOpen = (newOpen: boolean) => {
if (props.disabled && newOpen) {
return;
}
triggerInnerOpen(newOpen);
};
const forwardKeydown = (e: KeyboardEvent) => {
if (mergedOpen.value && 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.value, inputDivRef.value], 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);
},
});
// ============================= 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(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(() => picker.value === '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 () => {
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'>),
...attrs,
class: classNames({
[`${prefixCls}-panel-focused`]: !typing.value,
}),
style: undefined,
pickerValue: undefined,
onPickerValueChange: undefined,
onChange: null,
};
let panelNode: VueNode = (
<PickerPanel
{...panelProps}
generateConfig={generateConfig}
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}
popupStyle={popupStyle}
prefixCls={prefixCls}
dropdownClassName={dropdownClassName}
dropdownAlign={dropdownAlign}
getPopupContainer={getPopupContainer}
transitionName={transitionName}
popupPlacement={popupPlacement}
direction={direction}
v-slots={{
popupElement: () => panel,
}}
>
<div
class={classNames(prefixCls, attrs.class, {
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-focused`]: focused.value,
[`${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.value || text.value}
onInput={(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<any>();