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.
922 lines
28 KiB
922 lines
28 KiB
import { getSeparatedContent } from './utils/valueUtil'; |
|
import type { RefTriggerProps } from './SelectTrigger'; |
|
import SelectTrigger from './SelectTrigger'; |
|
import type { RefSelectorProps } from './Selector'; |
|
import Selector from './Selector'; |
|
import useSelectTriggerControl from './hooks/useSelectTriggerControl'; |
|
import useDelayReset from './hooks/useDelayReset'; |
|
import TransBtn from './TransBtn'; |
|
import useLock from './hooks/useLock'; |
|
import type { BaseSelectContextProps } from './hooks/useBaseProps'; |
|
import { useProvideBaseSelectProps } from './hooks/useBaseProps'; |
|
import type { Key, VueNode } from '../_util/type'; |
|
import type { |
|
FocusEventHandler, |
|
KeyboardEventHandler, |
|
MouseEventHandler, |
|
} from '../_util/EventInterface'; |
|
import type { ScrollConfig, ScrollTo } from '../vc-virtual-list/List'; |
|
import { |
|
computed, |
|
defineComponent, |
|
onBeforeUnmount, |
|
onMounted, |
|
provide, |
|
shallowRef, |
|
toRefs, |
|
watch, |
|
watchEffect, |
|
ref, |
|
} from 'vue'; |
|
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; |
|
import PropTypes from '../_util/vue-types'; |
|
import { initDefaultProps, isValidElement } from '../_util/props-util'; |
|
import isMobile from '../vc-util/isMobile'; |
|
import KeyCode from '../_util/KeyCode'; |
|
import { toReactive } from '../_util/toReactive'; |
|
import classNames from '../_util/classNames'; |
|
import createRef from '../_util/createRef'; |
|
import type { BaseOptionType } from './Select'; |
|
import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext'; |
|
import { cloneElement } from '../_util/vnode'; |
|
import type { AlignType } from '../vc-trigger/interface'; |
|
|
|
const DEFAULT_OMIT_PROPS = [ |
|
'value', |
|
'onChange', |
|
'removeIcon', |
|
'placeholder', |
|
'autofocus', |
|
'maxTagCount', |
|
'maxTagTextLength', |
|
'maxTagPlaceholder', |
|
'choiceTransitionName', |
|
'onInputKeyDown', |
|
'onPopupScroll', |
|
'tabindex', |
|
'OptionList', |
|
'notFoundContent', |
|
] as const; |
|
|
|
export type RenderNode = VueNode | ((props: any) => VueNode); |
|
|
|
export type RenderDOMFunc = (props: any) => HTMLElement; |
|
|
|
export type Mode = 'multiple' | 'tags' | 'combobox'; |
|
|
|
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight'; |
|
|
|
export type RawValueType = string | number; |
|
|
|
export interface RefOptionListProps { |
|
onKeydown: KeyboardEventHandler; |
|
onKeyup: KeyboardEventHandler; |
|
scrollTo?: (index: number | ScrollConfig) => void; |
|
} |
|
|
|
export type CustomTagProps = { |
|
label: any; |
|
value: any; |
|
disabled: boolean; |
|
onClose: (event?: MouseEvent) => void; |
|
closable: boolean; |
|
option: BaseOptionType; |
|
}; |
|
|
|
export interface DisplayValueType { |
|
key?: Key; |
|
value?: RawValueType; |
|
label?: any; |
|
disabled?: boolean; |
|
option?: BaseOptionType; |
|
} |
|
|
|
export type BaseSelectRef = { |
|
focus: () => void; |
|
blur: () => void; |
|
scrollTo: ScrollTo; |
|
}; |
|
|
|
const baseSelectPrivateProps = () => { |
|
return { |
|
prefixCls: String, |
|
id: String, |
|
omitDomProps: Array as PropType<string[]>, |
|
|
|
// >>> Value |
|
displayValues: Array as PropType<DisplayValueType[]>, |
|
onDisplayValuesChange: Function as PropType< |
|
( |
|
values: DisplayValueType[], |
|
info: { |
|
type: 'add' | 'remove' | 'clear'; |
|
values: DisplayValueType[]; |
|
}, |
|
) => void |
|
>, |
|
|
|
// >>> Active |
|
/** Current dropdown list active item string value */ |
|
activeValue: String, |
|
/** Link search input with target element */ |
|
activeDescendantId: String, |
|
onActiveValueChange: Function as PropType<(value: string | null) => void>, |
|
|
|
// >>> Search |
|
searchValue: String, |
|
/** Trigger onSearch, return false to prevent trigger open event */ |
|
onSearch: Function as PropType< |
|
( |
|
searchValue: string, |
|
info: { |
|
source: |
|
| 'typing' //User typing |
|
| 'effect' // Code logic trigger |
|
| 'submit' // tag mode only |
|
| 'blur'; // Not trigger event |
|
}, |
|
) => void |
|
>, |
|
/** Trigger when search text match the `tokenSeparators`. Will provide split content */ |
|
onSearchSplit: Function as PropType<(words: string[]) => void>, |
|
maxLength: Number, |
|
|
|
OptionList: PropTypes.any, |
|
|
|
/** Tell if provided `options` is empty */ |
|
emptyOptions: Boolean, |
|
}; |
|
}; |
|
|
|
export type DropdownObject = { |
|
menuNode?: VueNode; |
|
props?: Record<string, any>; |
|
}; |
|
|
|
export type DropdownRender = (opt?: DropdownObject) => VueNode; |
|
export const baseSelectPropsWithoutPrivate = () => { |
|
return { |
|
showSearch: { type: Boolean, default: undefined }, |
|
tagRender: { type: Function as PropType<(props: CustomTagProps) => any> }, |
|
optionLabelRender: { type: Function as PropType<(option: Record<string, any>) => any> }, |
|
direction: { type: String as PropType<'ltr' | 'rtl'> }, |
|
|
|
// MISC |
|
tabindex: Number, |
|
autofocus: Boolean, |
|
notFoundContent: PropTypes.any, |
|
placeholder: PropTypes.any, |
|
onClear: Function as PropType<() => void>, |
|
|
|
choiceTransitionName: String, |
|
|
|
// >>> Mode |
|
mode: String as PropType<Mode>, |
|
|
|
// >>> Status |
|
disabled: { type: Boolean, default: undefined }, |
|
loading: { type: Boolean, default: undefined }, |
|
|
|
// >>> Open |
|
open: { type: Boolean, default: undefined }, |
|
defaultOpen: { type: Boolean, default: undefined }, |
|
onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> }, |
|
|
|
// >>> Customize Input |
|
/** @private Internal usage. Do not use in your production. */ |
|
getInputElement: { type: Function as PropType<() => any> }, |
|
/** @private Internal usage. Do not use in your production. */ |
|
getRawInputElement: { type: Function as PropType<() => any> }, |
|
|
|
// >>> Selector |
|
maxTagTextLength: Number, |
|
maxTagCount: { type: [String, Number] as PropType<number | 'responsive'> }, |
|
maxTagPlaceholder: PropTypes.any, |
|
|
|
// >>> Search |
|
tokenSeparators: { type: Array as PropType<string[]> }, |
|
|
|
// >>> Icons |
|
allowClear: { type: Boolean, default: undefined }, |
|
showArrow: { type: Boolean, default: undefined }, |
|
inputIcon: PropTypes.any, |
|
/** Clear all icon */ |
|
clearIcon: PropTypes.any, |
|
/** Selector remove icon */ |
|
removeIcon: PropTypes.any, |
|
|
|
// >>> Dropdown |
|
animation: String, |
|
transitionName: String, |
|
dropdownStyle: { type: Object as PropType<CSSProperties> }, |
|
dropdownClassName: String, |
|
dropdownMatchSelectWidth: { |
|
type: [Boolean, Number] as PropType<boolean | number>, |
|
default: undefined, |
|
}, |
|
dropdownRender: { type: Function as PropType<DropdownRender> }, |
|
dropdownAlign: Object as PropType<AlignType>, |
|
placement: { |
|
type: String as PropType<Placement>, |
|
}, |
|
getPopupContainer: { type: Function as PropType<RenderDOMFunc> }, |
|
|
|
// >>> Focus |
|
showAction: { type: Array as PropType<('focus' | 'click')[]> }, |
|
onBlur: { type: Function as PropType<(e: FocusEvent) => void> }, |
|
onFocus: { type: Function as PropType<(e: FocusEvent) => void> }, |
|
|
|
// >>> Rest Events |
|
onKeyup: Function as PropType<(e: KeyboardEvent) => void>, |
|
onKeydown: Function as PropType<(e: KeyboardEvent) => void>, |
|
onMousedown: Function as PropType<(e: MouseEvent) => void>, |
|
onPopupScroll: Function as PropType<(e: UIEvent) => void>, |
|
onInputKeyDown: Function as PropType<(e: KeyboardEvent) => void>, |
|
onMouseenter: Function as PropType<(e: MouseEvent) => void>, |
|
onMouseleave: Function as PropType<(e: MouseEvent) => void>, |
|
onClick: Function as PropType<(e: MouseEvent) => void>, |
|
}; |
|
}; |
|
const baseSelectProps = () => { |
|
return { |
|
...baseSelectPrivateProps(), |
|
...baseSelectPropsWithoutPrivate(), |
|
}; |
|
}; |
|
|
|
export type BaseSelectPrivateProps = Partial< |
|
ExtractPropTypes<ReturnType<typeof baseSelectPrivateProps>> |
|
>; |
|
|
|
export type BaseSelectProps = Partial<ExtractPropTypes<ReturnType<typeof baseSelectProps>>>; |
|
|
|
export type BaseSelectPropsWithoutPrivate = Omit<BaseSelectProps, keyof BaseSelectPrivateProps>; |
|
|
|
export function isMultiple(mode: Mode) { |
|
return mode === 'tags' || mode === 'multiple'; |
|
} |
|
|
|
export default defineComponent({ |
|
compatConfig: { MODE: 3 }, |
|
name: 'BaseSelect', |
|
inheritAttrs: false, |
|
props: initDefaultProps(baseSelectProps(), { showAction: [], notFoundContent: 'Not Found' }), |
|
setup(props, { attrs, expose, slots }) { |
|
const multiple = computed(() => isMultiple(props.mode)); |
|
|
|
const mergedShowSearch = computed(() => |
|
props.showSearch !== undefined |
|
? props.showSearch |
|
: multiple.value || props.mode === 'combobox', |
|
); |
|
const mobile = shallowRef(false); |
|
onMounted(() => { |
|
mobile.value = isMobile(); |
|
}); |
|
const legacyTreeSelectContext = useInjectLegacySelectContext(); |
|
// ============================== Refs ============================== |
|
const containerRef = shallowRef<HTMLDivElement>(null); |
|
const selectorDomRef = createRef(); |
|
const triggerRef = shallowRef<RefTriggerProps>(null); |
|
const selectorRef = shallowRef<RefSelectorProps>(null); |
|
const listRef = shallowRef<RefOptionListProps>(null); |
|
const blurRef = ref<boolean>(false); |
|
|
|
/** Used for component focused management */ |
|
const [mockFocused, setMockFocused, cancelSetMockFocused] = useDelayReset(); |
|
|
|
const focus = () => { |
|
selectorRef.value?.focus(); |
|
}; |
|
const blur = () => { |
|
selectorRef.value?.blur(); |
|
}; |
|
expose({ |
|
focus, |
|
blur, |
|
scrollTo: arg => listRef.value?.scrollTo(arg), |
|
}); |
|
|
|
const mergedSearchValue = computed(() => { |
|
if (props.mode !== 'combobox') { |
|
return props.searchValue; |
|
} |
|
|
|
const val = props.displayValues[0]?.value; |
|
|
|
return typeof val === 'string' || typeof val === 'number' ? String(val) : ''; |
|
}); |
|
|
|
// ============================== Open ============================== |
|
const initOpen = props.open !== undefined ? props.open : props.defaultOpen; |
|
const innerOpen = shallowRef(initOpen); |
|
const mergedOpen = shallowRef(initOpen); |
|
const setInnerOpen = (val: boolean) => { |
|
innerOpen.value = props.open !== undefined ? props.open : val; |
|
mergedOpen.value = innerOpen.value; |
|
}; |
|
watch( |
|
() => props.open, |
|
() => { |
|
setInnerOpen(props.open); |
|
}, |
|
); |
|
|
|
// Not trigger `open` in `combobox` when `notFoundContent` is empty |
|
const emptyListContent = computed(() => !props.notFoundContent && props.emptyOptions); |
|
|
|
watchEffect(() => { |
|
mergedOpen.value = innerOpen.value; |
|
if ( |
|
props.disabled || |
|
(emptyListContent.value && mergedOpen.value && props.mode === 'combobox') |
|
) { |
|
mergedOpen.value = false; |
|
} |
|
}); |
|
|
|
const triggerOpen = computed(() => (emptyListContent.value ? false : mergedOpen.value)); |
|
|
|
const onToggleOpen = (newOpen?: boolean) => { |
|
const nextOpen = newOpen !== undefined ? newOpen : !mergedOpen.value; |
|
|
|
if (mergedOpen.value !== nextOpen && !props.disabled) { |
|
setInnerOpen(nextOpen); |
|
props.onDropdownVisibleChange && props.onDropdownVisibleChange(nextOpen); |
|
|
|
if (!nextOpen && popupFocused.value) { |
|
popupFocused.value = false; |
|
setMockFocused(false, () => { |
|
focusRef.value = false; |
|
blurRef.value = false; |
|
}); |
|
} |
|
} |
|
}; |
|
|
|
const tokenWithEnter = computed(() => |
|
(props.tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)), |
|
); |
|
|
|
const onInternalSearch = (searchText: string, fromTyping: boolean, isCompositing: boolean) => { |
|
let ret = true; |
|
let newSearchText = searchText; |
|
props.onActiveValueChange?.(null); |
|
|
|
// Check if match the `tokenSeparators` |
|
const patchLabels: string[] = isCompositing |
|
? null |
|
: getSeparatedContent(searchText, props.tokenSeparators); |
|
|
|
// Ignore combobox since it's not split-able |
|
if (props.mode !== 'combobox' && patchLabels) { |
|
newSearchText = ''; |
|
|
|
props.onSearchSplit?.(patchLabels); |
|
|
|
// Should close when paste finish |
|
onToggleOpen(false); |
|
|
|
// Tell Selector that break next actions |
|
ret = false; |
|
} |
|
|
|
if (props.onSearch && mergedSearchValue.value !== newSearchText) { |
|
props.onSearch(newSearchText, { |
|
source: fromTyping ? 'typing' : 'effect', |
|
}); |
|
} |
|
|
|
return ret; |
|
}; |
|
|
|
// Only triggered when menu is closed & mode is tags |
|
// If menu is open, OptionList will take charge |
|
// If mode isn't tags, press enter is not meaningful when you can't see any option |
|
const onInternalSearchSubmit = (searchText: string) => { |
|
// prevent empty tags from appearing when you click the Enter button |
|
if (!searchText || !searchText.trim()) { |
|
return; |
|
} |
|
props.onSearch?.(searchText, { source: 'submit' }); |
|
}; |
|
|
|
// Close will clean up single mode search text |
|
watch( |
|
mergedOpen, |
|
() => { |
|
if (!mergedOpen.value && !multiple.value && props.mode !== 'combobox') { |
|
onInternalSearch('', false, false); |
|
} |
|
}, |
|
{ immediate: true, flush: 'post' }, |
|
); |
|
|
|
// ============================ Disabled ============================ |
|
// Close dropdown & remove focus state when disabled change |
|
watch( |
|
() => props.disabled, |
|
() => { |
|
if (innerOpen.value && !!props.disabled) { |
|
setInnerOpen(false); |
|
} |
|
if (props.disabled && !blurRef.value) { |
|
setMockFocused(false); |
|
} |
|
}, |
|
{ immediate: true }, |
|
); |
|
|
|
// ============================ Keyboard ============================ |
|
/** |
|
* We record input value here to check if can press to clean up by backspace |
|
* - null: Key is not down, this is reset by key up |
|
* - true: Search text is empty when first time backspace down |
|
* - false: Search text is not empty when first time backspace down |
|
*/ |
|
const [getClearLock, setClearLock] = useLock(); |
|
|
|
// KeyDown |
|
const onInternalKeyDown: KeyboardEventHandler = (event, ...rest) => { |
|
const clearLock = getClearLock(); |
|
const { which } = event; |
|
|
|
if (which === KeyCode.ENTER) { |
|
// Do not submit form when type in the input |
|
if (props.mode !== 'combobox') { |
|
event.preventDefault(); |
|
} |
|
|
|
// We only manage open state here, close logic should handle by list component |
|
if (!mergedOpen.value) { |
|
onToggleOpen(true); |
|
} |
|
} |
|
|
|
setClearLock(!!mergedSearchValue.value); |
|
|
|
// Remove value by `backspace` |
|
if ( |
|
which === KeyCode.BACKSPACE && |
|
!clearLock && |
|
multiple.value && |
|
!mergedSearchValue.value && |
|
props.displayValues.length |
|
) { |
|
const cloneDisplayValues = [...props.displayValues]; |
|
let removedDisplayValue = null; |
|
|
|
for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) { |
|
const current = cloneDisplayValues[i]; |
|
|
|
if (!current.disabled) { |
|
cloneDisplayValues.splice(i, 1); |
|
removedDisplayValue = current; |
|
break; |
|
} |
|
} |
|
|
|
if (removedDisplayValue) { |
|
props.onDisplayValuesChange(cloneDisplayValues, { |
|
type: 'remove', |
|
values: [removedDisplayValue], |
|
}); |
|
} |
|
} |
|
|
|
if (mergedOpen.value && listRef.value) { |
|
listRef.value.onKeydown(event, ...rest); |
|
} |
|
|
|
props.onKeydown?.(event, ...rest); |
|
}; |
|
|
|
// KeyUp |
|
const onInternalKeyUp: KeyboardEventHandler = (event: KeyboardEvent, ...rest) => { |
|
if (mergedOpen.value && listRef.value) { |
|
listRef.value.onKeyup(event, ...rest); |
|
} |
|
|
|
if (props.onKeyup) { |
|
props.onKeyup(event, ...rest); |
|
} |
|
}; |
|
|
|
// ============================ Selector ============================ |
|
const onSelectorRemove = (val: DisplayValueType) => { |
|
const newValues = props.displayValues.filter(i => i !== val); |
|
|
|
props.onDisplayValuesChange(newValues, { |
|
type: 'remove', |
|
values: [val], |
|
}); |
|
}; |
|
|
|
// ========================== Focus / Blur ========================== |
|
/** Record real focus status */ |
|
const focusRef = shallowRef(false); |
|
const onContainerFocus: FocusEventHandler = (...args) => { |
|
setMockFocused(true); |
|
|
|
if (!props.disabled) { |
|
if (props.onFocus && !focusRef.value) { |
|
props.onFocus(...args); |
|
} |
|
|
|
// `showAction` should handle `focus` if set |
|
if (props.showAction && props.showAction.includes('focus')) { |
|
onToggleOpen(true); |
|
} |
|
} |
|
|
|
focusRef.value = true; |
|
}; |
|
const popupFocused = ref(false); |
|
const onContainerBlur: FocusEventHandler = (...args) => { |
|
if (popupFocused.value) { |
|
return; |
|
} |
|
blurRef.value = true; |
|
setMockFocused(false, () => { |
|
focusRef.value = false; |
|
blurRef.value = false; |
|
onToggleOpen(false); |
|
}); |
|
|
|
if (props.disabled) { |
|
return; |
|
} |
|
const searchVal = mergedSearchValue.value; |
|
if (searchVal) { |
|
// `tags` mode should move `searchValue` into values |
|
if (props.mode === 'tags') { |
|
props.onSearch(searchVal, { source: 'submit' }); |
|
} else if (props.mode === 'multiple') { |
|
// `multiple` mode only clean the search value but not trigger event |
|
props.onSearch('', { |
|
source: 'blur', |
|
}); |
|
} |
|
} |
|
|
|
if (props.onBlur) { |
|
props.onBlur(...args); |
|
} |
|
}; |
|
const onPopupFocusin = () => { |
|
popupFocused.value = true; |
|
}; |
|
const onPopupFocusout = () => { |
|
popupFocused.value = false; |
|
}; |
|
provide('VCSelectContainerEvent', { |
|
focus: onContainerFocus, |
|
blur: onContainerBlur, |
|
}); |
|
|
|
// Give focus back of Select |
|
const activeTimeoutIds: any[] = []; |
|
|
|
onMounted(() => { |
|
activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId)); |
|
activeTimeoutIds.splice(0, activeTimeoutIds.length); |
|
}); |
|
onBeforeUnmount(() => { |
|
activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId)); |
|
activeTimeoutIds.splice(0, activeTimeoutIds.length); |
|
}); |
|
|
|
const onInternalMouseDown: MouseEventHandler = (event, ...restArgs) => { |
|
const { target } = event; |
|
const popupElement: HTMLDivElement = triggerRef.value?.getPopupElement(); |
|
|
|
// We should give focus back to selector if clicked item is not focusable |
|
if (popupElement && popupElement.contains(target as HTMLElement)) { |
|
const timeoutId: any = setTimeout(() => { |
|
const index = activeTimeoutIds.indexOf(timeoutId); |
|
if (index !== -1) { |
|
activeTimeoutIds.splice(index, 1); |
|
} |
|
|
|
cancelSetMockFocused(); |
|
|
|
if (!mobile.value && !popupElement.contains(document.activeElement)) { |
|
selectorRef.value?.focus(); |
|
} |
|
}); |
|
|
|
activeTimeoutIds.push(timeoutId); |
|
} |
|
|
|
props.onMousedown?.(event, ...restArgs); |
|
}; |
|
|
|
// ============================= Dropdown ============================== |
|
const containerWidth = shallowRef<number>(null); |
|
// const instance = getCurrentInstance(); |
|
const onPopupMouseEnter = () => { |
|
// We need force update here since popup dom is render async |
|
// instance.update(); |
|
}; |
|
onMounted(() => { |
|
watch( |
|
triggerOpen, |
|
() => { |
|
if (triggerOpen.value) { |
|
const newWidth = Math.ceil(containerRef.value?.offsetWidth); |
|
if (containerWidth.value !== newWidth && !Number.isNaN(newWidth)) { |
|
containerWidth.value = newWidth; |
|
} |
|
} |
|
}, |
|
{ immediate: true, flush: 'post' }, |
|
); |
|
}); |
|
|
|
// Close when click on non-select element |
|
useSelectTriggerControl([containerRef, triggerRef], triggerOpen, onToggleOpen); |
|
useProvideBaseSelectProps( |
|
toReactive({ |
|
...toRefs(props), |
|
open: mergedOpen, |
|
triggerOpen, |
|
showSearch: mergedShowSearch, |
|
multiple, |
|
toggleOpen: onToggleOpen, |
|
} as unknown as BaseSelectContextProps), |
|
); |
|
return () => { |
|
const { |
|
prefixCls, |
|
id, |
|
|
|
open, |
|
defaultOpen, |
|
|
|
mode, |
|
|
|
// Search related |
|
showSearch, |
|
searchValue, |
|
onSearch, |
|
|
|
// Icons |
|
allowClear, |
|
clearIcon, |
|
showArrow, |
|
inputIcon, |
|
|
|
// Others |
|
disabled, |
|
loading, |
|
getInputElement, |
|
getPopupContainer, |
|
placement, |
|
|
|
// Dropdown |
|
animation, |
|
transitionName, |
|
dropdownStyle, |
|
dropdownClassName, |
|
dropdownMatchSelectWidth, |
|
dropdownRender, |
|
dropdownAlign, |
|
showAction, |
|
direction, |
|
|
|
// Tags |
|
tokenSeparators, |
|
tagRender, |
|
optionLabelRender, |
|
|
|
// Events |
|
onPopupScroll, |
|
onDropdownVisibleChange, |
|
onFocus, |
|
onBlur, |
|
onKeyup, |
|
onKeydown, |
|
onMousedown, |
|
|
|
onClear, |
|
omitDomProps, |
|
getRawInputElement, |
|
displayValues, |
|
onDisplayValuesChange, |
|
emptyOptions, |
|
activeDescendantId, |
|
activeValue, |
|
OptionList, |
|
|
|
...restProps |
|
} = { ...props, ...attrs } as BaseSelectProps; |
|
// ============================= Input ============================== |
|
// Only works in `combobox` |
|
const customizeInputElement: any = |
|
(mode === 'combobox' && getInputElement && getInputElement()) || null; |
|
|
|
// Used for customize replacement for `vc-cascader` |
|
const customizeRawInputElement: any = |
|
typeof getRawInputElement === 'function' && getRawInputElement(); |
|
const domProps = { |
|
...restProps, |
|
} as Omit<keyof typeof restProps, (typeof DEFAULT_OMIT_PROPS)[number]>; |
|
|
|
// Used for raw custom input trigger |
|
let onTriggerVisibleChange: null | ((newOpen: boolean) => void); |
|
if (customizeRawInputElement) { |
|
onTriggerVisibleChange = (newOpen: boolean) => { |
|
onToggleOpen(newOpen); |
|
}; |
|
} |
|
|
|
DEFAULT_OMIT_PROPS.forEach(propName => { |
|
delete domProps[propName]; |
|
}); |
|
|
|
omitDomProps?.forEach(propName => { |
|
delete domProps[propName]; |
|
}); |
|
|
|
// ============================= Arrow ============================== |
|
const mergedShowArrow = |
|
showArrow !== undefined ? showArrow : loading || (!multiple.value && mode !== 'combobox'); |
|
let arrowNode: VueNode; |
|
|
|
if (mergedShowArrow) { |
|
arrowNode = ( |
|
<TransBtn |
|
class={classNames(`${prefixCls}-arrow`, { |
|
[`${prefixCls}-arrow-loading`]: loading, |
|
})} |
|
customizeIcon={inputIcon} |
|
customizeIconProps={{ |
|
loading, |
|
searchValue: mergedSearchValue.value, |
|
open: mergedOpen.value, |
|
focused: mockFocused.value, |
|
showSearch: mergedShowSearch.value, |
|
}} |
|
/> |
|
); |
|
} |
|
|
|
// ============================= Clear ============================== |
|
let clearNode: VueNode; |
|
const onClearMouseDown: MouseEventHandler = () => { |
|
onClear?.(); |
|
|
|
onDisplayValuesChange([], { |
|
type: 'clear', |
|
values: displayValues, |
|
}); |
|
onInternalSearch('', false, false); |
|
}; |
|
|
|
if (!disabled && allowClear && (displayValues.length || mergedSearchValue.value)) { |
|
clearNode = ( |
|
<TransBtn |
|
class={`${prefixCls}-clear`} |
|
onMousedown={onClearMouseDown} |
|
customizeIcon={clearIcon} |
|
> |
|
× |
|
</TransBtn> |
|
); |
|
} |
|
|
|
// =========================== OptionList =========================== |
|
const optionList = ( |
|
<OptionList |
|
ref={listRef} |
|
v-slots={{ ...legacyTreeSelectContext.customSlots, option: slots.option }} |
|
/> |
|
); |
|
|
|
// ============================= Select ============================= |
|
const mergedClassName = classNames(prefixCls, attrs.class, { |
|
[`${prefixCls}-focused`]: mockFocused.value, |
|
[`${prefixCls}-multiple`]: multiple.value, |
|
[`${prefixCls}-single`]: !multiple.value, |
|
[`${prefixCls}-allow-clear`]: allowClear, |
|
[`${prefixCls}-show-arrow`]: mergedShowArrow, |
|
[`${prefixCls}-disabled`]: disabled, |
|
[`${prefixCls}-loading`]: loading, |
|
[`${prefixCls}-open`]: mergedOpen.value, |
|
[`${prefixCls}-customize-input`]: customizeInputElement, |
|
[`${prefixCls}-show-search`]: mergedShowSearch.value, |
|
}); |
|
|
|
// >>> Selector |
|
const selectorNode = ( |
|
<SelectTrigger |
|
ref={triggerRef} |
|
disabled={disabled} |
|
prefixCls={prefixCls} |
|
visible={triggerOpen.value} |
|
popupElement={optionList} |
|
containerWidth={containerWidth.value} |
|
animation={animation} |
|
transitionName={transitionName} |
|
dropdownStyle={dropdownStyle} |
|
dropdownClassName={dropdownClassName} |
|
direction={direction} |
|
dropdownMatchSelectWidth={dropdownMatchSelectWidth} |
|
dropdownRender={dropdownRender} |
|
dropdownAlign={dropdownAlign} |
|
placement={placement} |
|
getPopupContainer={getPopupContainer} |
|
empty={emptyOptions} |
|
getTriggerDOMNode={() => selectorDomRef.current} |
|
onPopupVisibleChange={onTriggerVisibleChange} |
|
onPopupMouseEnter={onPopupMouseEnter} |
|
onPopupFocusin={onPopupFocusin} |
|
onPopupFocusout={onPopupFocusout} |
|
v-slots={{ |
|
default: () => { |
|
return customizeRawInputElement ? ( |
|
isValidElement(customizeRawInputElement) && |
|
cloneElement( |
|
customizeRawInputElement, |
|
{ |
|
ref: selectorDomRef, |
|
}, |
|
false, |
|
true, |
|
) |
|
) : ( |
|
<Selector |
|
{...props} |
|
domRef={selectorDomRef} |
|
prefixCls={prefixCls} |
|
inputElement={customizeInputElement} |
|
ref={selectorRef} |
|
id={id} |
|
showSearch={mergedShowSearch.value} |
|
mode={mode} |
|
activeDescendantId={activeDescendantId} |
|
tagRender={tagRender} |
|
optionLabelRender={optionLabelRender} |
|
values={displayValues} |
|
open={mergedOpen.value} |
|
onToggleOpen={onToggleOpen} |
|
activeValue={activeValue} |
|
searchValue={mergedSearchValue.value} |
|
onSearch={onInternalSearch} |
|
onSearchSubmit={onInternalSearchSubmit} |
|
onRemove={onSelectorRemove} |
|
tokenWithEnter={tokenWithEnter.value} |
|
/> |
|
); |
|
}, |
|
}} |
|
></SelectTrigger> |
|
); |
|
// >>> Render |
|
let renderNode: VueNode; |
|
|
|
// Render raw |
|
if (customizeRawInputElement) { |
|
renderNode = selectorNode; |
|
} else { |
|
renderNode = ( |
|
<div |
|
{...domProps} |
|
class={mergedClassName} |
|
ref={containerRef} |
|
onMousedown={onInternalMouseDown} |
|
onKeydown={onInternalKeyDown} |
|
onKeyup={onInternalKeyUp} |
|
// onFocus={onContainerFocus} |
|
// onBlur={onContainerBlur} |
|
> |
|
{mockFocused.value && !mergedOpen.value && ( |
|
<span |
|
style={{ |
|
width: 0, |
|
height: 0, |
|
position: 'absolute', |
|
overflow: 'hidden', |
|
opacity: 0, |
|
}} |
|
aria-live="polite" |
|
> |
|
{/* Merge into one string to make screen reader work as expect */} |
|
{`${displayValues |
|
.map(({ label, value }) => |
|
['number', 'string'].includes(typeof label) ? label : value, |
|
) |
|
.join(', ')}`} |
|
</span> |
|
)} |
|
{selectorNode} |
|
|
|
{arrowNode} |
|
{clearNode} |
|
</div> |
|
); |
|
} |
|
return renderNode; |
|
}; |
|
}, |
|
});
|
|
|