Files
element-plus/packages/components/select-v2/src/useSelect.ts

1089 lines
29 KiB
TypeScript

import {
computed,
nextTick,
onMounted,
reactive,
ref,
watch,
watchEffect,
} from 'vue'
import { findLastIndex, get, isEqual } from 'lodash-unified'
import { useDebounceFn, useResizeObserver } from '@vueuse/core'
import {
ValidateComponentsMap,
debugWarn,
escapeStringRegexp,
getEventCode,
isArray,
isEmpty,
isFunction,
isNumber,
isObject,
isUndefined,
} from '@element-plus/utils'
import {
useComposition,
useEmptyValues,
useFocusController,
useLocale,
useNamespace,
} from '@element-plus/hooks'
import {
CHANGE_EVENT,
EVENT_CODE,
MINIMUM_INPUT_WIDTH,
UPDATE_MODEL_EVENT,
} from '@element-plus/constants'
import {
useFormDisabled,
useFormItem,
useFormItemInputId,
useFormSize,
} from '@element-plus/components/form'
import { useAllowCreate } from './useAllowCreate'
import { useProps } from './useProps'
import type { Option, OptionType, SelectStates } from './select.types'
import type { SelectV2Props } from './token'
import type { SelectV2EmitFn } from './defaults'
import type { TooltipInstance } from '@element-plus/components/tooltip'
import type { SelectDropdownInstance } from './select-dropdown'
const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
// inject
const { t } = useLocale()
const nsSelect = useNamespace('select')
const nsInput = useNamespace('input')
const { form: elForm, formItem: elFormItem } = useFormItem()
const { inputId } = useFormItemInputId(props, {
formItemContext: elFormItem,
})
const { aliasProps, getLabel, getValue, getDisabled, getOptions } =
useProps(props)
const { valueOnClear, isEmptyValue } = useEmptyValues(props)
const states: SelectStates = reactive({
inputValue: '',
cachedOptions: [],
createdOptions: [],
hoveringIndex: -1,
inputHovering: false,
selectionWidth: 0,
collapseItemWidth: 0,
previousQuery: null,
previousValue: undefined,
selectedLabel: '',
menuVisibleOnFocus: false,
isBeforeHide: false,
})
// data refs
const popperSize = ref(-1)
const debouncing = ref(false)
// DOM & Component refs
const selectRef = ref<HTMLElement>()
const selectionRef = ref<HTMLElement>()
const tooltipRef = ref<TooltipInstance>()
const tagTooltipRef = ref<TooltipInstance>()
const inputRef = ref<HTMLElement>()
const prefixRef = ref<HTMLElement>()
const suffixRef = ref<HTMLElement>()
const menuRef = ref<SelectDropdownInstance>()
const tagMenuRef = ref<HTMLElement>()
const collapseItemRef = ref<HTMLElement>()
const {
isComposing,
handleCompositionStart,
handleCompositionEnd,
handleCompositionUpdate,
} = useComposition({
afterComposition: (e) => onInput(e),
})
const selectDisabled = useFormDisabled()
const { wrapperRef, isFocused, handleBlur } = useFocusController(inputRef, {
disabled: selectDisabled,
afterFocus() {
if (props.automaticDropdown && !expanded.value) {
expanded.value = true
states.menuVisibleOnFocus = true
}
},
beforeBlur(event) {
return (
tooltipRef.value?.isFocusInsideContent(event) ||
tagTooltipRef.value?.isFocusInsideContent(event)
)
},
afterBlur() {
expanded.value = false
states.menuVisibleOnFocus = false
if (props.validateEvent) {
elFormItem?.validate?.('blur').catch((err) => debugWarn(err))
}
},
})
const allOptions = computed(() => filterOptions(''))
const hasOptions = computed(() => {
if (props.loading) return false
return props.options.length > 0 || states.createdOptions.length > 0
})
const filteredOptions = ref<OptionType[]>([])
// the controller of the expanded popup
const expanded = ref(false)
const needStatusIcon = computed(() => elForm?.statusIcon ?? false)
const popupHeight = computed(() => {
const totalHeight = filteredOptions.value.length * props.itemHeight
return totalHeight > props.height ? props.height : totalHeight
})
const hasModelValue = computed(() => {
return props.multiple
? isArray(props.modelValue) && props.modelValue.length > 0
: !isEmptyValue(props.modelValue)
})
const showClearBtn = computed(() => {
return (
props.clearable &&
!selectDisabled.value &&
hasModelValue.value &&
(isFocused.value || states.inputHovering)
)
})
const iconComponent = computed(() =>
props.remote && props.filterable && !props.remoteShowSuffix
? ''
: props.suffixIcon
)
const iconReverse = computed(
() => iconComponent.value && nsSelect.is('reverse', expanded.value)
)
const validateState = computed(() => elFormItem?.validateState || '')
const validateIcon = computed(() => {
// When we use indexed access to get the type of an undeclared property,
// the unsafe type `any` will be inferred, which TypeScript throws an error to emphasize it.
// To avoid TypeScript complaining about it, we use truthiness narrowing to narrow the type of validateState.
if (!validateState.value) return
return ValidateComponentsMap[validateState.value]
})
const debounce = computed(() => (props.remote ? props.debounce : 0))
const isRemoteSearchEmpty = computed(
() => props.remote && !states.inputValue && !hasOptions.value
)
// filteredOptions includes flatten the data into one dimensional array.
const emptyText = computed(() => {
if (props.loading) {
return props.loadingText || t('el.select.loading')
} else {
if (
props.filterable &&
states.inputValue &&
hasOptions.value &&
filteredOptions.value.length === 0
) {
return props.noMatchText || t('el.select.noMatch')
}
if (!hasOptions.value) {
return props.noDataText || t('el.select.noData')
}
}
return null
})
const isFilterMethodValid = computed(
() => props.filterable && isFunction(props.filterMethod)
)
const isRemoteMethodValid = computed(
() => props.filterable && props.remote && isFunction(props.remoteMethod)
)
const filterOptions = (query: string) => {
const regexp = new RegExp(escapeStringRegexp(query), 'i')
const isValidOption = (o: Option): boolean => {
if (isFilterMethodValid.value || isRemoteMethodValid.value) return true
// when query was given, we should test on the label see whether the label contains the given query
return query ? regexp.test(getLabel(o) || '') : true
}
if (props.loading) {
return []
}
return [...states.createdOptions, ...props.options].reduce((all, item) => {
const options = getOptions(item)
if (isArray(options)) {
const filtered = options.filter(isValidOption)
if (filtered.length > 0) {
all.push(
{
label: getLabel(item),
type: 'Group',
},
...filtered
)
}
} else if (props.remote || isValidOption(item)) {
all.push(item)
}
return all
}, []) as OptionType[]
}
const updateOptions = () => {
filteredOptions.value = filterOptions(states.inputValue)
}
const allOptionsValueMap = computed(() => {
const valueMap = new Map()
allOptions.value.forEach((option, index) => {
valueMap.set(getValueKey(getValue(option)), { option, index })
})
return valueMap
})
const filteredOptionsValueMap = computed(() => {
const valueMap = new Map()
filteredOptions.value.forEach((option, index) => {
valueMap.set(getValueKey(getValue(option)), { option, index })
})
return valueMap
})
const optionsAllDisabled = computed(() =>
filteredOptions.value.every((option) => getDisabled(option))
)
const selectSize = useFormSize()
const collapseTagSize = computed(() =>
'small' === selectSize.value ? 'small' : 'default'
)
const calculatePopperSize = () => {
if (isNumber(props.fitInputWidth)) {
popperSize.value = props.fitInputWidth
return
}
const width = selectRef.value?.offsetWidth || 200
if (!props.fitInputWidth && hasOptions.value) {
nextTick(() => {
popperSize.value = Math.max(width, calculateLabelMaxWidth())
})
} else {
popperSize.value = width
}
}
// TODO Caching implementation
// 1. There is no need to calculate options that have already been calculated
// 2. Repeatedly expand and close when persistent is set to false, no need for repeated calculations
const calculateLabelMaxWidth = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const selector = nsSelect.be('dropdown', 'item')
const dom = menuRef.value?.listRef?.innerRef || document
const dropdownItemEl = dom.querySelector(`.${selector}`)
if (dropdownItemEl === null || ctx === null) return 0
const style = getComputedStyle(dropdownItemEl)
const padding =
Number.parseFloat(style.paddingLeft) +
Number.parseFloat(style.paddingRight)
ctx.font = `bold ${style.font.replace(
new RegExp(`\\b${style.fontWeight}\\b`),
''
)}`
const maxWidth = filteredOptions.value.reduce((max, option) => {
const metrics = ctx.measureText(getLabel(option))
return Math.max(metrics.width, max)
}, 0)
return maxWidth + padding
}
const getGapWidth = () => {
if (!selectionRef.value) return 0
const style = window.getComputedStyle(selectionRef.value)
return Number.parseFloat(style.gap || '6px')
}
// computed style
const tagStyle = computed(() => {
const gapWidth = getGapWidth()
const inputSlotWidth = props.filterable ? gapWidth + MINIMUM_INPUT_WIDTH : 0
const maxWidth =
collapseItemRef.value && props.maxCollapseTags === 1
? states.selectionWidth -
states.collapseItemWidth -
gapWidth -
inputSlotWidth
: states.selectionWidth - inputSlotWidth
return { maxWidth: `${maxWidth}px` }
})
const collapseTagStyle = computed(() => {
return { maxWidth: `${states.selectionWidth}px` }
})
const shouldShowPlaceholder = computed(() => {
if (isArray(props.modelValue)) {
return props.modelValue.length === 0 && !states.inputValue
}
// when it's not multiple mode, we only determine this flag based on filterable and expanded
// when filterable flag is true, which means we have input box on the screen
return props.filterable ? !states.inputValue : true
})
const currentPlaceholder = computed(() => {
const _placeholder = props.placeholder ?? t('el.select.placeholder')
return props.multiple || !hasModelValue.value
? _placeholder
: states.selectedLabel
})
// this obtains the actual popper DOM element.
const popperRef = computed(() => tooltipRef.value?.popperRef?.contentRef)
// the index with current value in options
const indexRef = computed<number>(() => {
if (props.multiple) {
const len = (props.modelValue as []).length
if (
len > 0 &&
filteredOptionsValueMap.value.has(props.modelValue[len - 1])
) {
const { index } = filteredOptionsValueMap.value.get(
props.modelValue[len - 1]
)
return index
}
} else {
if (
!isEmptyValue(props.modelValue) &&
filteredOptionsValueMap.value.has(props.modelValue)
) {
const { index } = filteredOptionsValueMap.value.get(props.modelValue)
return index
}
}
return -1
})
const dropdownMenuVisible = computed({
get() {
return (
expanded.value &&
(props.loading || !isRemoteSearchEmpty.value) &&
(!debouncing.value || !isEmpty(states.previousQuery))
)
},
set(val: boolean) {
expanded.value = val
},
})
const showTagList = computed(() => {
if (!props.multiple) {
return []
}
return props.collapseTags
? states.cachedOptions.slice(0, props.maxCollapseTags)
: states.cachedOptions
})
const collapseTagList = computed(() => {
if (!props.multiple) {
return []
}
return props.collapseTags
? states.cachedOptions.slice(props.maxCollapseTags)
: []
})
// hooks
const {
createNewOption,
removeNewOption,
selectNewOption,
clearAllNewOption,
} = useAllowCreate(props, states)
// methods
const toggleMenu = (event?: Event) => {
if (
selectDisabled.value ||
(props.filterable &&
expanded.value &&
event &&
!suffixRef.value?.contains(event.target as Node))
)
return
if (states.menuVisibleOnFocus) {
// controlled by automaticDropdown
states.menuVisibleOnFocus = false
} else {
expanded.value = !expanded.value
}
}
const onInputChange = () => {
if (states.inputValue.length > 0 && !expanded.value) {
expanded.value = true
}
createNewOption(states.inputValue)
nextTick(() => {
handleQueryChange(states.inputValue)
})
}
const debouncedOnInputChange = useDebounceFn(() => {
onInputChange()
debouncing.value = false
}, debounce)
const handleQueryChange = (val: string) => {
if (states.previousQuery === val || isComposing.value) {
return
}
states.previousQuery = val
if (props.filterable && isFunction(props.filterMethod)) {
props.filterMethod(val)
} else if (
props.filterable &&
props.remote &&
isFunction(props.remoteMethod)
) {
props.remoteMethod(val)
}
if (
props.defaultFirstOption &&
(props.filterable || props.remote) &&
filteredOptions.value.length
) {
nextTick(checkDefaultFirstOption)
} else {
nextTick(updateHoveringIndex)
}
}
/**
* find and highlight first option as default selected
* @remark
* - if the first option in dropdown list is user-created,
* it would be at the end of the optionsArray
* so find it and set hover.
* (NOTE: there must be only one user-created option in dropdown list with query)
* - if there's no user-created option in list, just find the first one as usual
* (NOTE: exclude options that are disabled or in disabled-group)
*/
const checkDefaultFirstOption = () => {
const optionsInDropdown = filteredOptions.value.filter(
(n) => !n.disabled && n.type !== 'Group'
)
const userCreatedOption = optionsInDropdown.find((n) => n.created)
const firstOriginOption = optionsInDropdown[0]
states.hoveringIndex = getValueIndex(
filteredOptions.value,
userCreatedOption || firstOriginOption
)
}
const emitChange = (val: any | any[]) => {
if (!isEqual(props.modelValue, val)) {
emit(CHANGE_EVENT, val)
}
}
const update = (val: any) => {
emit(UPDATE_MODEL_EVENT, val)
emitChange(val)
states.previousValue = props.multiple ? String(val) : val
nextTick(() => {
if (props.multiple && isArray(props.modelValue)) {
const cachedOptions = states.cachedOptions.slice()
const selectedOptions = props.modelValue.map((value) =>
getOption(value, cachedOptions)
)
if (!isEqual(states.cachedOptions, selectedOptions)) {
states.cachedOptions = selectedOptions
}
} else {
initStates(true)
}
})
}
const getValueIndex = (arr: unknown[] = [], value: unknown) => {
if (!isObject(value)) {
return arr.indexOf(value)
}
const valueKey = props.valueKey
let index = -1
arr.some((item, i) => {
if (get(item, valueKey) === get(value, valueKey)) {
index = i
return true
}
return false
})
return index
}
const getValueKey = (item: unknown) => {
return isObject(item) ? get(item, props.valueKey) : item
}
const handleResize = () => {
calculatePopperSize()
}
const resetSelectionWidth = () => {
states.selectionWidth = Number.parseFloat(
window.getComputedStyle(selectionRef.value!).width
)
}
const resetCollapseItemWidth = () => {
states.collapseItemWidth =
collapseItemRef.value!.getBoundingClientRect().width
}
const updateTooltip = () => {
tooltipRef.value?.updatePopper?.()
}
const updateTagTooltip = () => {
tagTooltipRef.value?.updatePopper?.()
}
const onSelect = (option: Option) => {
const optionValue = getValue(option)
if (props.multiple) {
let selectedOptions = (props.modelValue as any[]).slice()
const index = getValueIndex(selectedOptions, optionValue)
if (index > -1) {
selectedOptions = [
...selectedOptions.slice(0, index),
...selectedOptions.slice(index + 1),
]
states.cachedOptions.splice(index, 1)
removeNewOption(option)
} else if (
props.multipleLimit <= 0 ||
selectedOptions.length < props.multipleLimit
) {
selectedOptions = [...selectedOptions, optionValue]
states.cachedOptions.push(option)
selectNewOption(option)
}
update(selectedOptions)
if (option.created) {
handleQueryChange('')
}
if (props.filterable && !props.reserveKeyword) {
states.inputValue = ''
}
} else {
states.selectedLabel = getLabel(option)
!isEqual(props.modelValue, optionValue) && update(optionValue)
expanded.value = false
selectNewOption(option)
if (!option.created) {
clearAllNewOption()
}
}
focus()
}
const deleteTag = (event: MouseEvent, option: Option) => {
let selectedOptions = (props.modelValue as any[]).slice()
const index = getValueIndex(selectedOptions, getValue(option))
if (index > -1 && !selectDisabled.value) {
selectedOptions = [
...(props.modelValue as Array<unknown>).slice(0, index),
...(props.modelValue as Array<unknown>).slice(index + 1),
]
states.cachedOptions.splice(index, 1)
update(selectedOptions)
emit('remove-tag', getValue(option))
removeNewOption(option)
}
event.stopPropagation()
focus()
}
const focus = () => {
inputRef.value?.focus()
}
const blur = () => {
if (expanded.value) {
expanded.value = false
nextTick(() => inputRef.value?.blur())
return
}
inputRef.value?.blur()
}
// keyboard handlers
const handleEsc = () => {
if (states.inputValue.length > 0) {
states.inputValue = ''
} else {
expanded.value = false
}
}
const getLastNotDisabledIndex = (value: unknown[]) =>
findLastIndex(
value,
(it) =>
!states.cachedOptions.some(
(option) => getValue(option) === it && getDisabled(option)
)
)
const handleDel = (e: KeyboardEvent) => {
const code = getEventCode(e)
if (!props.multiple) return
if (code === EVENT_CODE.delete) return
if (states.inputValue.length === 0) {
e.preventDefault()
const selected = (props.modelValue as Array<any>).slice()
const lastNotDisabledIndex = getLastNotDisabledIndex(selected)
if (lastNotDisabledIndex < 0) return
const removeTagValue = selected[lastNotDisabledIndex]
selected.splice(lastNotDisabledIndex, 1)
const option = states.cachedOptions[lastNotDisabledIndex]
states.cachedOptions.splice(lastNotDisabledIndex, 1)
removeNewOption(option)
update(selected)
emit('remove-tag', removeTagValue)
}
}
const handleClear = () => {
let emptyValue: string | any[]
if (isArray(props.modelValue)) {
emptyValue = []
} else {
emptyValue = valueOnClear.value
}
states.selectedLabel = ''
expanded.value = false
update(emptyValue)
emit('clear')
clearAllNewOption()
focus()
}
const onKeyboardNavigate = (
direction: 'forward' | 'backward',
hoveringIndex: number | undefined = undefined
): void => {
const options = filteredOptions.value
if (
!['forward', 'backward'].includes(direction) ||
selectDisabled.value ||
options.length <= 0 ||
optionsAllDisabled.value ||
isComposing.value
) {
return
}
if (!expanded.value) {
return toggleMenu()
}
if (isUndefined(hoveringIndex)) {
hoveringIndex = states.hoveringIndex
}
let newIndex = -1
if (direction === 'forward') {
newIndex = hoveringIndex + 1
if (newIndex >= options.length) {
// return to the first option
newIndex = 0
}
} else if (direction === 'backward') {
newIndex = hoveringIndex - 1
if (newIndex < 0 || newIndex >= options.length) {
// navigate to the last one
newIndex = options.length - 1
}
}
const option = options[newIndex]
if (getDisabled(option) || option.type === 'Group') {
// prevent dispatching multiple nextTick callbacks.
return onKeyboardNavigate(direction, newIndex)
} else {
states.hoveringIndex = newIndex
scrollToItem(newIndex)
}
}
const onKeyboardSelect = () => {
if (!expanded.value) {
return toggleMenu()
} else if (
~states.hoveringIndex &&
filteredOptions.value[states.hoveringIndex]
) {
onSelect(filteredOptions.value[states.hoveringIndex])
}
}
const onHoverOption = (idx?: number) => {
states.hoveringIndex = idx ?? -1
}
const updateHoveringIndex = () => {
if (!props.multiple) {
states.hoveringIndex = filteredOptions.value.findIndex((item) => {
return getValueKey(getValue(item)) === getValueKey(props.modelValue)
})
} else {
const length = props.modelValue.length
if (length > 0) {
const lastValue = props.modelValue[length - 1]
states.hoveringIndex = filteredOptions.value.findIndex(
(item) => getValueKey(lastValue) === getValueKey(getValue(item))
)
} else {
states.hoveringIndex = -1
}
}
}
const onInput = (event: Event) => {
states.inputValue = (event.target as HTMLInputElement).value
if (props.remote) {
debouncing.value = true
debouncedOnInputChange()
} else {
return onInputChange()
}
}
const handleClickOutside = (event: Event) => {
expanded.value = false
if (isFocused.value) {
const _event = new FocusEvent('blur', event)
handleBlur(_event)
}
}
const handleMenuEnter = () => {
states.isBeforeHide = false
return nextTick(() => {
if (~indexRef.value) {
scrollToItem(indexRef.value)
}
})
}
const scrollToItem = (index: number) => {
menuRef.value!.scrollToItem(index)
}
const getOption = (value: unknown, cachedOptions?: Option[]) => {
// match the option with the given value, if not found, create a new option
const selectValue = getValueKey(value)
if (allOptionsValueMap.value.has(selectValue)) {
const { option } = allOptionsValueMap.value.get(selectValue)
return option
}
if (cachedOptions && cachedOptions.length) {
const option = cachedOptions.find(
(option) => getValueKey(getValue(option)) === selectValue
)
if (option) {
return option
}
}
return {
[aliasProps.value.value]: value,
[aliasProps.value.label]: value,
}
}
const getIndex = (option: Option) =>
allOptionsValueMap.value.get(getValue(option))?.index ?? -1
const initStates = (needUpdateSelectedLabel = false) => {
if (props.multiple) {
if ((props.modelValue as Array<any>).length > 0) {
const cachedOptions = states.cachedOptions.slice()
states.cachedOptions.length = 0
states.previousValue = props.modelValue.toString()
for (const value of props.modelValue) {
const option = getOption(value, cachedOptions)
states.cachedOptions.push(option)
}
} else {
states.cachedOptions = []
states.previousValue = undefined
}
} else {
if (hasModelValue.value) {
states.previousValue = props.modelValue
const options = filteredOptions.value
const selectedItemIndex = options.findIndex(
(option) =>
getValueKey(getValue(option)) === getValueKey(props.modelValue)
)
if (~selectedItemIndex) {
states.selectedLabel = getLabel(options[selectedItemIndex])
} else {
if (!states.selectedLabel || needUpdateSelectedLabel) {
states.selectedLabel = getValueKey(props.modelValue)
}
}
} else {
states.selectedLabel = ''
states.previousValue = undefined
}
}
clearAllNewOption()
calculatePopperSize()
}
watch(
() => props.fitInputWidth,
() => {
calculatePopperSize()
}
)
// in order to track these individually, we need to turn them into refs instead of watching the entire
// reactive object which could cause perf penalty when unnecessary field gets changed the watch method will
// be invoked.
watch(expanded, (val) => {
if (val) {
if (!props.persistent) {
calculatePopperSize()
}
handleQueryChange('')
} else {
states.inputValue = ''
states.previousQuery = null
states.isBeforeHide = true
createNewOption('')
}
})
watch(
() => props.modelValue,
(val, oldVal) => {
const isValEmpty = !val || (isArray(val) && val.length === 0)
if (
isValEmpty ||
(props.multiple && !isEqual(val.toString(), states.previousValue)) ||
(!props.multiple &&
getValueKey(val) !== getValueKey(states.previousValue))
) {
initStates(true)
}
if (!isEqual(val, oldVal) && props.validateEvent) {
elFormItem?.validate?.('change').catch((err) => debugWarn(err))
}
},
{
deep: true,
}
)
watch(
() => props.options,
() => {
const input = inputRef.value
// filter or remote-search scenarios are not initialized
if (!input || (input && document.activeElement !== input)) {
initStates()
}
},
{
deep: true,
flush: 'post',
}
)
// fix the problem that scrollTop is not reset in filterable mode
watch(
() => filteredOptions.value,
() => {
calculatePopperSize()
return menuRef.value && nextTick(menuRef.value.resetScrollTop)
}
)
watchEffect(() => {
// Anything could cause options changed, then update options
// If you want to control it by condition, write here
if (states.isBeforeHide) return
updateOptions()
})
watchEffect(() => {
const { valueKey, options } = props
const duplicateValue = new Map()
for (const item of options) {
const optionValue = getValue(item)
let v = optionValue
if (isObject(v)) {
v = get(optionValue, valueKey)
}
if (duplicateValue.get(v)) {
debugWarn(
'ElSelectV2',
`The option values you provided seem to be duplicated, which may cause some problems, please check.`
)
break
} else {
duplicateValue.set(v, true)
}
}
})
onMounted(() => {
initStates()
})
useResizeObserver(selectRef, handleResize)
useResizeObserver(selectionRef, resetSelectionWidth)
useResizeObserver(wrapperRef, updateTooltip)
useResizeObserver(tagMenuRef, updateTagTooltip)
useResizeObserver(collapseItemRef, resetCollapseItemWidth)
// #21498
let stop: (() => void) | undefined
watch(
() => dropdownMenuVisible.value,
(newVal) => {
if (newVal) {
stop = useResizeObserver(menuRef, updateTooltip).stop
} else {
stop?.()
stop = undefined
}
emit('visible-change', newVal)
}
)
return {
// data exports
inputId,
collapseTagSize,
currentPlaceholder,
expanded,
emptyText,
popupHeight,
debounce,
allOptions,
allOptionsValueMap,
filteredOptions,
iconComponent,
iconReverse,
tagStyle,
collapseTagStyle,
popperSize,
dropdownMenuVisible,
hasModelValue,
shouldShowPlaceholder,
selectDisabled,
selectSize,
needStatusIcon,
showClearBtn,
states,
isFocused,
nsSelect,
nsInput,
// refs items exports
inputRef,
menuRef,
tagMenuRef,
tooltipRef,
tagTooltipRef,
selectRef,
wrapperRef,
selectionRef,
prefixRef,
suffixRef,
collapseItemRef,
popperRef,
validateState,
validateIcon,
showTagList,
collapseTagList,
// methods exports
debouncedOnInputChange,
deleteTag,
getLabel,
getValue,
getDisabled,
getValueKey,
getIndex,
handleClear,
handleClickOutside,
handleDel,
handleEsc,
focus,
blur,
handleMenuEnter,
handleResize,
resetSelectionWidth,
updateTooltip,
updateTagTooltip,
updateOptions,
toggleMenu,
scrollTo: scrollToItem,
onInput,
onKeyboardNavigate,
onKeyboardSelect,
onSelect,
onHover: onHoverOption,
handleCompositionStart,
handleCompositionEnd,
handleCompositionUpdate,
}
}
export default useSelect