901 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Vue
		
	
	
			
		
		
	
	
			901 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Vue
		
	
	
| 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,
 | ||
|   getCurrentInstance,
 | ||
|   onBeforeUnmount,
 | ||
|   onMounted,
 | ||
|   provide,
 | ||
|   shallowRef,
 | ||
|   toRefs,
 | ||
|   watch,
 | ||
|   watchEffect,
 | ||
| } from 'vue';
 | ||
| import type { CSSProperties, ExtractPropTypes, PropType, VNode } 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);
 | ||
| 
 | ||
|     /** 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 (innerOpen.value !== nextOpen && !props.disabled) {
 | ||
|         setInnerOpen(nextOpen);
 | ||
|         if (props.onDropdownVisibleChange) {
 | ||
|           props.onDropdownVisibleChange(nextOpen);
 | ||
|         }
 | ||
|       }
 | ||
|     };
 | ||
| 
 | ||
|     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);
 | ||
|         }
 | ||
|       },
 | ||
|       { 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 onContainerBlur: FocusEventHandler = (...args) => {
 | ||
|       setMockFocused(false, () => {
 | ||
|         focusRef.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);
 | ||
|       }
 | ||
|     };
 | ||
|     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: VNode | JSX.Element;
 | ||
| 
 | ||
|       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: VNode | JSX.Element;
 | ||
|       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}
 | ||
|           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: VNode | JSX.Element;
 | ||
| 
 | ||
|       // 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;
 | ||
|     };
 | ||
|   },
 | ||
| });
 |