import type { PropType } from 'vue' ; import { computed , defineComponent , reactive , ref , watch , onMounted , TransitionGroup , inject , } from 'vue' ; import { getPrefixCls } from '../_utils/global-config' ; import { INPUT_EVENTS , Size , SIZES } from '../_utils/constant' ; import { Backspace , Enter } from '../_utils/keycode' ; import { getValueData } from './utils' ; import Tag from '../tag' ; import IconHover from '../_components/icon-hover.vue' ; import IconClose from '../icon/icon-close' ; import { TagData } from './interface' ; import { omit } from '../_utils/omit' ; import pick from '../_utils/pick' ; import { EmitType } from '../_utils/types' ; import ResizeObserver from '../_components/resize-observer' ; import { configProviderInjectionKey } from '../config-provider/context' ; export default defineComponent ( { name : 'InputTag' , inheritAttrs : false , props : { zh en vModel modelValue : { type : Array as PropType < Array < string | number | TagData > > } , zh en defaultValue : { type : Array as PropType < Array < string | number | TagData > > , default : ( ) => [ ] } , zh en vModel inputValue : String , zh en defaultInputValue : { type : String , default : '' } , zh en placeholder : String , zh en disabled : { type : Boolean , default : false } , zh en error : { type : Boolean , default : false } , zh en readonly : { type : Boolean , default : false } , zh en allowClear : { type : Boolean , default : false } , zh en values defaultValue size : { type : String as PropType < Size > , default : ( ) => inject ( configProviderInjectionKey , undefined ) ?. size ?? 'medium' } , zh en maxTagCount : { type : Number , default : 0 } , zh en retainInputValue : { type : Boolean , default : false } , zh en formatTag : { type : Function as PropType < ( data : TagData ) => string > } , zh en version uniqueValue : { type : Boolean , default : false } , baseCls : String , focused : Boolean , disabledInput : Boolean , onChange : { type : [ Function , Array ] as PropType < EmitType < ( value : string | number | TagData ) => void > > } , onInputValueChange : { type : [ Function , Array ] as PropType < ( inputValue : string ) => void > } , onPressEnter : { type : [ Function , Array ] as PropType < ( ) => void > } , onRemove : { type : [ Function , Array ] as PropType < ( removed : string | number ) => void > } , onClear : { type : [ Function , Array ] as PropType < ( ) => void > } , onFocus : { type : [ Function , Array ] as PropType < ( ) => void > } , onBlur : { type : [ Function , Array ] as PropType < ( ) => void > } } , emits : [ 'update:modelValue' , 'update:inputValue' , 'change' , 'inputValueChange' , 'pressEnter' , 'remove' , 'clear' , 'focus' , 'blur' , ] , setup ( props , { emit , slots , attrs } ) { const prefixCls = props . baseCls || getPrefixCls ( 'input-tag' ) ; const inputRef = ref < HTMLInputElement > ( ) ; const mirrorRef = ref < HTMLElement > ( ) ; const _focused = ref ( false ) ; const _value = ref ( props . defaultValue ) ; const _inputValue = ref ( props . defaultInputValue ) ; const isComposition = ref ( false ) ; const compositionValue = ref ( '' ) ; const inputStyle = reactive ( { width : '12px' } ) ; const mergedFocused = computed ( ( ) => props . focused || _focused . value ) ; const updateInputValue = ( value ) => { _inputValue . value = value ; emit ( 'update:inputValue' , value ) ; } ; const handleComposition = ( e ) => { const { value } = e . target as HTMLInputElement ; if ( e . type === 'compositionend' ) { isComposition . value = false ; compositionValue . value = '' ; emit ( 'inputValueChange' , value , e ) ; updateInputValue ( value ) ; } else { isComposition . value = true ; compositionValue . value = computedInputValue . value + ( e . data ?? '' ) ; } } ; const computedValue = computed ( ( ) => props . modelValue ?? _value . value ) ; const computedInputValue = computed ( ( ) => props . inputValue ?? _inputValue . value ) ; const handleMousedown = ( e ) => { if ( inputRef . value && e . target !== inputRef . value ) { e . preventDefault ( ) ; inputRef . value . focus ( ) ; } } ; const handleInput = ( e ) => { const { value } = e . target as HTMLInputElement ; if ( ! isComposition . value ) { emit ( 'inputValueChange' , value , e ) ; updateInputValue ( value ) ; } } ; const tags = computed ( ( ) => { const valueData = getValueData ( computedValue . value ) ; if ( props . maxTagCount > 0 ) { const invisibleTags = valueData . length - props . maxTagCount ; if ( invisibleTags > 0 ) { const result = valueData . slice ( 0 , props . maxTagCount ) ; result . push ( { value : 'more' , label : `+${ invisibleTags }...` , closable : false } ) ; return result ; } } return valueData ; } ) ; const handleRemove = ( value , index , e ) => { const newValue = computedValue . value ?. filter ( ( _ , i ) => i !== index ) ; _value . value = newValue ; emit ( 'remove' , value , e ) ; emit ( 'update:modelValue' , newValue ) ; emit ( 'change' , newValue , e ) ; } ; const handleClear = ( e ) => { const newValue = [ ] ; _value . value = newValue ; emit ( 'clear' , e ) ; emit ( 'update:modelValue' , newValue ) ; emit ( 'change' , newValue , e ) ; } ; const showClearBtn = computed ( ( ) => ! props . disabled && ! props . readonly && props . allowClear && Boolean ( computedValue . value . length ) ) ; const handlePressEnter = ( e ) => { if ( computedInputValue . value ) { e . preventDefault ( ) ; emit ( 'pressEnter' , computedInputValue . value , e ) ; if ( props . uniqueValue && computedValue . value ?. includes ( computedInputValue . value ) ) { return ; } const newValue = computedValue . value . concat ( computedInputValue . value ) ; _value . value = newValue ; emit ( 'update:modelValue' , newValue ) ; emit ( 'change' , newValue , e ) ; if ( ! props . retainInputValue ) { _inputValue . value = '' ; emit ( 'update:inputValue' , '' ) ; emit ( 'inputValueChange' , '' , e ) ; } } } ; const handleFocus = ( ev ) => { _focused . value = true ; emit ( 'focus' , ev ) ; } ; const handleBlur = ( ev ) => { _focused . value = false ; emit ( 'blur' , ev ) ; } ; const handleKeyDown = ( e ) => { const keyCode = e . key || e . code ; if ( ! isComposition . value && computedInputValue . value && keyCode === Enter . key ) { handlePressEnter ( e ) ; } if ( ! isComposition . value && tags . value . length > 0 && ! computedInputValue . value && keyCode === Backspace . key ) { const lastIndex = tags . value . length - 1 ; handleRemove ( tags . value [ lastIndex ] . value , lastIndex , e ) ; } } ; const setInputWidth = ( width ) => { if ( width > 12 ) { inputStyle . width = `${ width }px` ; } else { inputStyle . width = '12px' ; } } ; onMounted ( ( ) => { if ( mirrorRef . value ) { setInputWidth ( mirrorRef . value . offsetWidth ) ; } } ) ; const handleResize = ( ) => { if ( mirrorRef . value ) { setInputWidth ( mirrorRef . value . offsetWidth ) ; } } ; watch ( computedInputValue , ( value ) => { if ( inputRef . value && ! isComposition . value && value !== inputRef . value . value ) { inputRef . value . value = value ; } } ) ; const cls = computed ( ( ) => [ prefixCls , `${ prefixCls }-size-${ props . size }` , { [ `${ prefixCls }-disabled` ] : props . disabled , [ `${ prefixCls }-disabled-input` ] : props . disabledInput , [ `${ prefixCls }-error` ] : props . error , [ `${ prefixCls }-focus` ] : mergedFocused . value , [ `${ prefixCls }-readonly` ] : props . readonly , [ `${ prefixCls }-has-tag` ] : tags . value . length > 0 , [ `${ prefixCls }-has-prefix` ] : Boolean ( slots . prefix ) , [ `${ prefixCls }-has-suffix` ] : Boolean ( slots . suffix ) || showClearBtn . value , [ `${ prefixCls }-has-placeholder` ] : ! computedValue . value . length } , ] ) ; const wrapperAttrs = computed ( ( ) => omit ( attrs , INPUT_EVENTS ) ) ; const inputAttrs = computed ( ( ) => pick ( attrs , INPUT_EVENTS ) ) ; const render = ( ) => ( < span class = { cls . value } onMousedown = { handleMousedown } { ... wrapperAttrs . value } > < ResizeObserver onResize = { handleResize } > < span ref = { mirrorRef } class = { `${ prefixCls }-mirror` } > { tags . value . length > 0 ? compositionValue . value || computedInputValue . value : compositionValue . value || computedInputValue . value || props . placeholder } < /span> < /ResizeObserver> { slots . prefix && ( < span class = { `${ prefixCls }-prefix` } > { slots . prefix ( ) } < /span>)} < TransitionGroup tag = "span" name = "input-tag-zoom" class = { `${ prefixCls }-inner` } > { tags . value . map ( ( item , index ) => ( < Tag key = { `tag-${ item . value }` } class = { `${ prefixCls }-tag` } closable = { ! props . disabled && ! props . readonly && item . closable } visible onClose = { ( ev ) => handleRemove ( item . value , index , ev ) } { ... item . tagProps } > { slots . tag ?. ( { data : item } ) ?? props . formatTag ?. ( item ) ?? item . label } < /Tag>))} < input { ... inputAttrs . value } ref = { inputRef } key = "input-tag-input" class = { `${ prefixCls }-input` } style = { inputStyle } placeholder = { tags . value . length === 0 ? props . placeholder : undefined } disabled = { props . disabled } readonly = { props . readonly || props . disabledInput } onInput = { handleInput } onKeydown = { handleKeyDown } onFocus = { handleFocus } onBlur = { handleBlur } onCompositionstart = { handleComposition } onCompositionupdate = { handleComposition } onCompositionend = { handleComposition } / > < /TransitionGroup> { showClearBtn . value && ( < IconHover class = { `${ prefixCls }-clear-btn` } onClick = { handleClear } onMousedown = { ( e ) => e . stopPropagation ( ) } > < IconClose /> < /IconHover>)} { slots . suffix && ( < span class = { `${ prefixCls }-suffix` } > { slots . suffix ( ) } < /span>)} < /span>); return { inputRef , render } ; } , methods : { zh en public focus ( ) { ( this . inputRef as HTMLInputElement ) ?. focus ( ) ; } , zh en public blur ( ) { ( this . inputRef as HTMLInputElement ) ?. blur ( ) ; } } , render ( ) { return this . render ( ) ; } } ) ;