import type { ComponentPublicInstance , PropType } from 'vue' ; import { computed , defineComponent , inject , nextTick , ref , toRefs , watch , } from 'vue' ; import { getPrefixCls } from '../_utils/global-config' ; import { isArray , isFunction , isObject , isUndefined } from '../_utils/is' ; import { getTagDataFromModelValue } from './utils' ; import Trigger , { TriggerProps } from '../trigger' ; import SelectView from '../_components/select-view/select-view' ; import { Size } from '../_utils/constant' ; import { Data , EmitType } from '../_utils/types' ; import { CODE , getKeyDownHandler } from '../_utils/keyboard' ; import { DropdownPanel , DropDownOption , DropDownOptGroup , } from '../_components/dropdown' ; import { useOptions } from '../_hooks/use-options' ; import { Option , OptionData , OptionInfo , OptionNode } from './interface' ; import VirtualList from '../_components/virtual-list/virtual-list.vue' ; import { VirtualListProps } from '../_components/virtual-list/interface' ; import { configProviderInjectionKey } from '../config-provider/context' ; displayName export default defineComponent ( { name : 'BaseSelect' , inheritAttrs : false , props : { zh en multiple : { type : Boolean , default : false } , zh en modelValue : { type : [ String , Number , Array ] as PropType < string | number | ( string | number ) [ ] > } , zh en defaultValue defaultValue : { type : [ String , Number , Array ] as PropType < string | number | ( string | number ) [ ] > , default : ( props ) => ( isUndefined ( props . multiple ) ? '' : [ ] ) } , zh en vModel inputValue : { type : String } , zh en defaultInputValue : { type : String , default : '' } , zh en values defaultValue size : { type : String as PropType < Size > , default : ( ) => inject ( configProviderInjectionKey , undefined ) ?. size ?? 'medium' } , zh en placeholder : String , zh en loading : { type : Boolean , default : false } , zh en disabled : { type : Boolean , default : false } , zh en error : { type : Boolean , default : false } , zh en allowClear : { type : Boolean , default : false } , zh en defaultValue allowSearch : { type : [ Boolean , Object ] as PropType < boolean | { retainInputValue ? : boolean ; } > , default : ( props ) => Boolean ( props . multiple ) } , zh en allowCreate : { type : Boolean , default : false } , zh en maxTagCount : { type : Number , default : 0 } , zh en popupContainer : { type : [ String , Object ] as PropType < string | HTMLElement > } , zh en bordered : { type : Boolean , default : true } , defaultActiveFirstOption : { type : Boolean , default : true } , zh en vModel popupVisible : { type : Boolean , default : undefined } , zh en unmountOnClose : { type : Boolean , default : true } , zh en filterOption : { type : [ Boolean , Function ] as PropType < boolean | ( ( inputValue : string , optionInfo : OptionInfo ) => boolean ) > , default : true } , zh en options : { type : Array as PropType < Option [ ] > , default : ( ) => [ ] } , zh en type VirtualListProps virtualListProps : { type : Object as PropType < VirtualListProps > } , zh en type TriggerProps triggerProps : { type : Object as PropType < TriggerProps > } , zh en formatLabel : { type : Function as PropType < ( data : OptionInfo ) => string > } , zh en version fallbackOption : { type : [ Boolean , Function ] as PropType < boolean | ( ( value : string | number ) => OptionData ) > , default : false } , zh en version showExtraOptions : { type : Boolean , default : true } , onChange : { type : [ Function , Array ] as PropType < EmitType < ( value : string | number | Array < string | number > ) => void > > } , onInputValueChange : { type : [ Function , Array ] as PropType < EmitType < ( inputValue : string ) => void > > } , onPopupVisibleChange : { type : [ Function , Array ] as PropType < EmitType < ( popupVisible : boolean ) => void > > } , onClear : { type : [ Function , Array ] as PropType < EmitType < ( ) => void > > } , onRemove : { type : [ Function , Array ] as PropType < EmitType < ( removed : string ) => void > > } , onSearch : { type : [ Function , Array ] as PropType < EmitType < ( inputValue : string ) => void > > } } , emits : [ 'update:modelValue' , 'update:inputValue' , 'update:popupVisible' , 'change' , 'inputValueChange' , 'popupVisibleChange' , 'clear' , 'remove' , 'search' , 'dropdownScroll' , 'dropdownReachBottom' , ] , zh en slot zh en slot binding zh en slot binding zh en slot setup ( props , { slots , emit , attrs } ) { const prefixCls = getPrefixCls ( 'select' ) ; const { options , filterOption , showExtraOptions } = toRefs ( props ) ; const dropdownRef = ref < ComponentPublicInstance > ( ) ; const optionRefs = ref < Record < string | number , HTMLElement > > ( { } ) ; const virtualListRef = ref ( ) ; const _value = ref ( props . defaultValue ) ; const _inputValue = ref ( '' ) ; const _popupVisible = ref ( false ) ; const computedValue = computed ( ( ) => { const mergedValue = props . modelValue ?? _value . value ; if ( props . multiple ) { if ( ! isArray ( mergedValue ) ) { return mergedValue ? [ mergedValue ] : [ ] ; } } else if ( isArray ( mergedValue ) ) { return mergedValue [ 0 ] ?? '' ; } return mergedValue ; } ) ; const computedInputValue = computed ( ( ) => props . inputValue ?? _inputValue . value ) ; const computedPopupVisible = computed ( ( ) => props . popupVisible ?? _popupVisible . value ) ; const retainInputValue = computed ( ( ) => isObject ( props . allowSearch ) && Boolean ( props . allowSearch . retainInputValue ) ) ; const tagData = computed ( ( ) => getTagDataFromModelValue ( computedValue . value , optionInfoMap ) ) ; const getFallBackOption = ( value ) : OptionData => { if ( isFunction ( props . fallbackOption ) ) { return props . fallbackOption ( value ) ; } return { value , label : String ( value ) } ; } ; const createdOptions = computed ( ( ) => { const options = [ ] ; if ( props . allowCreate || Boolean ( props . fallbackOption ) ) { if ( isArray ( computedValue . value ) ) { options . push ( ... computedValue . value . map ( getFallBackOption ) ) ; } else if ( computedValue . value ) { options . push ( getFallBackOption ( computedValue . value ) ) ; } if ( props . allowCreate && computedInputValue . value ) { options . push ( getFallBackOption ( computedInputValue . value ) ) ; } } return options ; } ) ; const { nodes , optionInfoMap , enabledOptionSet , activeOption , getNextActiveOption , scrollIntoView , } = useOptions ( { options , extraOptions : createdOptions , inputValue : computedInputValue , filterOption , showExtraOptions , dropdownRef , optionRefs , virtualListRef } ) ; const updateValue = ( value ) => { _value . value = value ; emit ( 'update:modelValue' , value ) ; emit ( 'change' , value ) ; } ; const updateInputValue = ( inputValue ) => { _inputValue . value = inputValue ; emit ( 'update:inputValue' , inputValue ) ; emit ( 'inputValueChange' , inputValue ) ; } ; const handlePopupVisibleChange = ( visible ) : void => { if ( computedPopupVisible . value !== visible ) { _popupVisible . value = visible ; emit ( 'popupVisibleChange' , visible ) ; } } ; const checkOption = ( value ) => { if ( isArray ( computedValue . value ) && enabledOptionSet . has ( value ) ) { const newValue = computedValue . value . concat ( value ) ; updateValue ( newValue ) ; } } ; const uncheckOption = ( value ) => { if ( isArray ( computedValue . value ) ) { const newValue = computedValue . value . filter ( ( v ) => v !== value ) ; updateValue ( newValue ) ; } } ; const handleSelect = ( value , e ) => { if ( props . multiple ) { if ( isArray ( computedValue . value ) ) { if ( computedValue . value . includes ( value ) ) { uncheckOption ( value ) ; } else { checkOption ( value ) ; } } if ( ! retainInputValue . value ) { updateInputValue ( '' ) ; } } else { if ( value !== computedValue . value ) { updateValue ( value ) ; } if ( retainInputValue . value ) { const optionInfo = optionInfoMap . get ( value ) ; if ( optionInfo ) { updateInputValue ( optionInfo . label ) ; } } handlePopupVisibleChange ( false ) ; } } ; const handleMouseEnter = ( value , e ) => { const optionInfo = optionInfoMap . get ( value ) ; if ( optionInfo ) { activeOption . value = optionInfo ; } } ; const handleMouseLeave = ( value , e ) => { activeOption . value = undefined ; } ; const handleDropdownScroll = ( e ) => { emit ( 'dropdownScroll' , e ) ; } ; const handleDropdownReachBottom = ( e ) => { emit ( 'dropdownReachBottom' , e ) ; } ; const handleInputValueChange = ( inputValue ) => { if ( inputValue !== computedInputValue . value ) { if ( ! computedPopupVisible . value ) { _popupVisible . value = true ; emit ( 'popupVisibleChange' , true ) ; } updateInputValue ( inputValue ) ; if ( props . allowSearch ) { emit ( 'search' , inputValue ) ; } } } ; const handleKeyDown = getKeyDownHandler ( new Map ( [ [ CODE . ENTER , ( e ) => { if ( computedPopupVisible . value ) { if ( activeOption . value ) { handleSelect ( activeOption . value . value , e ) ; } } else { handlePopupVisibleChange ( true ) ; } e . preventDefault ( ) ; } , ] , [ CODE . ESC , ( e ) => { handlePopupVisibleChange ( false ) ; e . preventDefault ( ) ; } , ] , [ CODE . ARROW_DOWN , ( e ) => { const next = getNextActiveOption ( 'down' ) ; if ( next ) { activeOption . value = next ; scrollIntoView ( next . value ) ; } e . preventDefault ( ) ; } , ] , [ CODE . ARROW_UP , ( e ) => { const next = getNextActiveOption ( 'up' ) ; if ( next ) { activeOption . value = next ; scrollIntoView ( next . value ) ; } e . preventDefault ( ) ; } , ] , ] ) ) ; const handleRemove = ( tag ) => { if ( isArray ( computedValue . value ) ) { const newValue = computedValue . value . filter ( ( v ) => v !== tag ) ; updateValue ( newValue ) ; emit ( 'remove' , tag ) ; } } ; const handleClear = ( e ) => { e ?. stopPropagation ( ) ; if ( isArray ( computedValue . value ) ) { const newValue = computedValue . value . filter ( ( v ) => { const optionInfo = optionInfoMap . get ( v ) ; return Boolean ( optionInfo ?. disabled ) ; } ) ; updateValue ( newValue ) ; } else { updateValue ( '' ) ; } updateInputValue ( '' ) ; emit ( 'clear' ) ; } ; watch ( computedPopupVisible , ( visible ) => { if ( visible ) { const currentValue = isArray ( computedValue . value ) ? computedValue . value [ 0 ] : computedValue . value ; activeOption . value = enabledOptionSet . has ( currentValue ) ? optionInfoMap . get ( currentValue ) : optionInfoMap . get ( Array . from ( enabledOptionSet ) [ 0 ] ) ; nextTick ( ( ) => { if ( activeOption . value ?. value ) { scrollIntoView ( activeOption . value . value ) ; } } ) ; } else if ( ! retainInputValue . value ) { updateInputValue ( '' ) ; } } ) ; const getOptionContentFunc = ( item ) => { if ( isFunction ( slots . option ) && item . value ) { const optionInfo = optionInfoMap . get ( item . value ) ; const optionSlot = slots . option ; return ( ) => optionSlot ( { data : optionInfo } ) ; } if ( isFunction ( item . render ) ) { return item . render ; } return ( ) => item . label ; } ; const renderOption = ( item ) => { if ( item . isGroup ) { return ( < DropDownOptGroup v - slots = { { ... item . _slots , default : ( ) => item . options . map ( ( item ) => renderOption ( item ) ) } } { ... item . _props } key = { item . key } label = { item . label } / > ) ; } const { value = '' } = item ; const isSelected = isArray ( computedValue . value ) ? computedValue . value . includes ( value ) : value === computedValue . value ; return ( < DropDownOption v - slots = { { ... item . _slots , default : getOptionContentFunc ( item ) } } { ... item . _props } ref = { ( ref ) => { if ( ref ?. $el ) { optionRefs . value [ value ] = ref . $el ; } } } key = { item . key } value = { item . value } label = { item . label } disabled = { item . disabled } component = { props . virtualListProps ? 'div' : 'li' } isSelected = { isSelected } isActive = { activeOption . value && value === activeOption . value . value } multiple = { props . multiple } onClick = { handleSelect } onMouseenter = { handleMouseEnter } onMouseleave = { handleMouseLeave } / > ) ; } ; const renderDropDown = ( ) => { const vSlots = { empty : slots . empty , footer : slots . footer , ... ( props . virtualListProps ? { 'virtual-list' : ( ) => ( < VirtualList { ... props . virtualListProps } ref = { virtualListRef } data = { nodes . value } v - slots = { { item : ( { item } ) => renderOption ( item ) } } />) } : { default : ( ) => nodes . value . map ( ( item ) => renderOption ( item ) ) } ) } ; return ( < DropdownPanel ref = { dropdownRef } v - slots = { vSlots } class = { `${ prefixCls }-dropdown` } loading = { props . loading } isEmpty = { nodes . value . length === 0 } onScroll = { handleDropdownScroll } onReachBottom = { handleDropdownReachBottom } / > ) ; } ; return ( ) => ( < Trigger { ... props . triggerProps } v - slots = { { content : renderDropDown } } trigger = { 'click' } disabled = { props . disabled } position = "bl" popupOffset = { 4 } animationName = { 'slide-dynamic-origin' } popupVisible = { computedPopupVisible . value } unmountOnClose = { props . unmountOnClose } hideEmpty = { true } clickToClose = { ! ( props . allowSearch || props . allowCreate ) } preventFocus = { true } popupContainer = { props . popupContainer } onPopupVisibleChange = { handlePopupVisibleChange } autoFitPopupWidth autoFitTransformOrigin > < SelectView { ... attrs } v - slots = { { label : slots . label } } class = { prefixCls } modelValue = { getTagDataFromModelValue ( computedValue . value , optionInfoMap ) } inputValue = { computedInputValue . value } multiple = { props . multiple } disabled = { props . disabled } error = { props . error } loading = { props . loading } allowClear = { props . allowClear } allowCreate = { props . allowCreate } allowSearch = { Boolean ( props . allowSearch ) } opened = { computedPopupVisible . value } maxTagCount = { props . maxTagCount } placeholder = { props . placeholder } bordered = { props . bordered } size = { props . size } formatLabel = { props . formatLabel } onInputValueChange = { handleInputValueChange } onRemove = { handleRemove } onClear = { handleClear } onKeydown = { handleKeyDown } / > < /Trigger>); } } ) ;