mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
* refactor(hooks): determine the focus by event listening * test: skip the focus test * test: fix test * feat(hooks): add beforeFocus * fix: optimize blur * chore(components): [mention] remove focus & blur
887 lines
23 KiB
TypeScript
887 lines
23 KiB
TypeScript
// @ts-nocheck
|
|
import {
|
|
computed,
|
|
nextTick,
|
|
onMounted,
|
|
reactive,
|
|
ref,
|
|
toRaw,
|
|
watch,
|
|
watchEffect,
|
|
} from 'vue'
|
|
import { isArray, isObject, toRawType } from '@vue/shared'
|
|
import {
|
|
findLastIndex,
|
|
get,
|
|
isEqual,
|
|
debounce as lodashDebounce,
|
|
} from 'lodash-unified'
|
|
import { useResizeObserver } from '@vueuse/core'
|
|
import {
|
|
CHANGE_EVENT,
|
|
EVENT_CODE,
|
|
UPDATE_MODEL_EVENT,
|
|
} from '@element-plus/constants'
|
|
import {
|
|
ValidateComponentsMap,
|
|
debugWarn,
|
|
ensureArray,
|
|
isClient,
|
|
isFunction,
|
|
isIOS,
|
|
isNumber,
|
|
isUndefined,
|
|
scrollIntoView,
|
|
} from '@element-plus/utils'
|
|
import {
|
|
useComposition,
|
|
useEmptyValues,
|
|
useFocusController,
|
|
useId,
|
|
useLocale,
|
|
useNamespace,
|
|
} from '@element-plus/hooks'
|
|
import {
|
|
useFormItem,
|
|
useFormItemInputId,
|
|
useFormSize,
|
|
} from '@element-plus/components/form'
|
|
|
|
import type ElTooltip from '@element-plus/components/tooltip'
|
|
import type { ISelectProps, SelectOptionProxy } from './token'
|
|
|
|
const MINIMUM_INPUT_WIDTH = 11
|
|
|
|
export const useSelect = (props: ISelectProps, emit) => {
|
|
const { t } = useLocale()
|
|
const contentId = useId()
|
|
const nsSelect = useNamespace('select')
|
|
const nsInput = useNamespace('input')
|
|
|
|
const states = reactive({
|
|
inputValue: '',
|
|
options: new Map(),
|
|
cachedOptions: new Map(),
|
|
disabledOptions: new Map(),
|
|
optionValues: [] as any[], // sorted value of options
|
|
selected: props.multiple ? [] : ({} as any),
|
|
selectionWidth: 0,
|
|
calculatorWidth: 0,
|
|
collapseItemWidth: 0,
|
|
selectedLabel: '',
|
|
hoveringIndex: -1,
|
|
previousQuery: null,
|
|
inputHovering: false,
|
|
menuVisibleOnFocus: false,
|
|
isBeforeHide: false,
|
|
})
|
|
|
|
// template refs
|
|
const selectRef = ref<HTMLElement>(null)
|
|
const selectionRef = ref<HTMLElement>(null)
|
|
const tooltipRef = ref<InstanceType<typeof ElTooltip> | null>(null)
|
|
const tagTooltipRef = ref<InstanceType<typeof ElTooltip> | null>(null)
|
|
const inputRef = ref<HTMLInputElement | null>(null)
|
|
const calculatorRef = ref<HTMLElement>(null)
|
|
const prefixRef = ref<HTMLElement>(null)
|
|
const suffixRef = ref<HTMLElement>(null)
|
|
const menuRef = ref<HTMLElement>(null)
|
|
const tagMenuRef = ref<HTMLElement>(null)
|
|
const collapseItemRef = ref<HTMLElement>(null)
|
|
const scrollbarRef = ref<{
|
|
handleScroll: () => void
|
|
} | null>(null)
|
|
|
|
const {
|
|
isComposing,
|
|
handleCompositionStart,
|
|
handleCompositionUpdate,
|
|
handleCompositionEnd,
|
|
} = useComposition({
|
|
afterComposition: (e) => onInput(e),
|
|
})
|
|
|
|
const { wrapperRef, isFocused, handleBlur } = useFocusController(inputRef, {
|
|
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
|
|
},
|
|
})
|
|
|
|
// the controller of the expanded popup
|
|
const expanded = ref(false)
|
|
const hoverOption = ref()
|
|
|
|
const { form, formItem } = useFormItem()
|
|
const { inputId } = useFormItemInputId(props, {
|
|
formItemContext: formItem,
|
|
})
|
|
const { valueOnClear, isEmptyValue } = useEmptyValues(props)
|
|
|
|
const selectDisabled = computed(() => props.disabled || form?.disabled)
|
|
|
|
const hasModelValue = computed(() => {
|
|
return isArray(props.modelValue)
|
|
? props.modelValue.length > 0
|
|
: !isEmptyValue(props.modelValue)
|
|
})
|
|
|
|
const showClose = computed(() => {
|
|
return (
|
|
props.clearable &&
|
|
!selectDisabled.value &&
|
|
states.inputHovering &&
|
|
hasModelValue.value
|
|
)
|
|
})
|
|
const iconComponent = computed(() =>
|
|
props.remote && props.filterable && !props.remoteShowSuffix
|
|
? ''
|
|
: props.suffixIcon
|
|
)
|
|
const iconReverse = computed(() =>
|
|
nsSelect.is('reverse', iconComponent.value && expanded.value)
|
|
)
|
|
|
|
const validateState = computed(() => formItem?.validateState || '')
|
|
const validateIcon = computed(
|
|
() => ValidateComponentsMap[validateState.value]
|
|
)
|
|
|
|
const debounce = computed(() => (props.remote ? 300 : 0))
|
|
|
|
const emptyText = computed(() => {
|
|
if (props.loading) {
|
|
return props.loadingText || t('el.select.loading')
|
|
} else {
|
|
if (props.remote && !states.inputValue && states.options.size === 0)
|
|
return false
|
|
if (
|
|
props.filterable &&
|
|
states.inputValue &&
|
|
states.options.size > 0 &&
|
|
filteredOptionsCount.value === 0
|
|
) {
|
|
return props.noMatchText || t('el.select.noMatch')
|
|
}
|
|
if (states.options.size === 0) {
|
|
return props.noDataText || t('el.select.noData')
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
|
|
const filteredOptionsCount = computed(
|
|
() => optionsArray.value.filter((option) => option.visible).length
|
|
)
|
|
|
|
const optionsArray = computed(() => {
|
|
const list = Array.from(states.options.values())
|
|
const newList = []
|
|
states.optionValues.forEach((item) => {
|
|
const index = list.findIndex((i) => i.value === item)
|
|
if (index > -1) {
|
|
newList.push(list[index])
|
|
}
|
|
})
|
|
return newList.length >= list.length ? newList : list
|
|
})
|
|
|
|
const cachedOptionsArray = computed(() =>
|
|
Array.from(states.cachedOptions.values())
|
|
)
|
|
|
|
const showNewOption = computed(() => {
|
|
const hasExistingOption = optionsArray.value
|
|
.filter((option) => {
|
|
return !option.created
|
|
})
|
|
.some((option) => {
|
|
return option.currentLabel === states.inputValue
|
|
})
|
|
return (
|
|
props.filterable &&
|
|
props.allowCreate &&
|
|
states.inputValue !== '' &&
|
|
!hasExistingOption
|
|
)
|
|
})
|
|
|
|
const updateOptions = () => {
|
|
if (props.filterable && isFunction(props.filterMethod)) return
|
|
if (props.filterable && props.remote && isFunction(props.remoteMethod))
|
|
return
|
|
optionsArray.value.forEach((option) => {
|
|
option.updateOption?.(states.inputValue)
|
|
})
|
|
}
|
|
|
|
const selectSize = useFormSize()
|
|
|
|
const collapseTagSize = computed(() =>
|
|
['small'].includes(selectSize.value) ? 'small' : 'default'
|
|
)
|
|
|
|
const dropdownMenuVisible = computed({
|
|
get() {
|
|
return expanded.value && emptyText.value !== false
|
|
},
|
|
set(val: boolean) {
|
|
expanded.value = val
|
|
},
|
|
})
|
|
|
|
const shouldShowPlaceholder = computed(() => {
|
|
if (props.multiple && !isUndefined(props.modelValue)) {
|
|
return ensureArray(props.modelValue).length === 0 && !states.inputValue
|
|
}
|
|
const value = isArray(props.modelValue)
|
|
? props.modelValue[0]
|
|
: props.modelValue
|
|
return props.filterable || isUndefined(value) ? !states.inputValue : true
|
|
})
|
|
|
|
const currentPlaceholder = computed(() => {
|
|
const _placeholder = props.placeholder ?? t('el.select.placeholder')
|
|
return props.multiple || !hasModelValue.value
|
|
? _placeholder
|
|
: states.selectedLabel
|
|
})
|
|
|
|
// iOS Safari does not handle click events when a mouseenter event is registered and a DOM-change happens in a child
|
|
// We use a Vue custom event binding to only register the event on non-iOS devices
|
|
// ref.: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html
|
|
// Github Issue: https://github.com/vuejs/vue/issues/9859
|
|
const mouseEnterEventName = computed(() => (isIOS ? null : 'mouseenter'))
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(val, oldVal) => {
|
|
if (props.multiple) {
|
|
if (props.filterable && !props.reserveKeyword) {
|
|
states.inputValue = ''
|
|
handleQueryChange('')
|
|
}
|
|
}
|
|
setSelected()
|
|
if (!isEqual(val, oldVal) && props.validateEvent) {
|
|
formItem?.validate('change').catch((err) => debugWarn(err))
|
|
}
|
|
},
|
|
{
|
|
flush: 'post',
|
|
deep: true,
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => expanded.value,
|
|
(val) => {
|
|
if (val) {
|
|
handleQueryChange(states.inputValue)
|
|
} else {
|
|
states.inputValue = ''
|
|
states.previousQuery = null
|
|
states.isBeforeHide = true
|
|
}
|
|
emit('visible-change', val)
|
|
}
|
|
)
|
|
|
|
watch(
|
|
// fix `Array.prototype.push/splice/..` cannot trigger non-deep watcher
|
|
// https://github.com/vuejs/vue-next/issues/2116
|
|
() => states.options.entries(),
|
|
() => {
|
|
if (!isClient) return
|
|
// tooltipRef.value?.updatePopper?.()
|
|
const inputs = selectRef.value?.querySelectorAll('input') || []
|
|
if (
|
|
(!props.filterable &&
|
|
!props.defaultFirstOption &&
|
|
!isUndefined(props.modelValue)) ||
|
|
!Array.from(inputs).includes(document.activeElement as HTMLInputElement)
|
|
) {
|
|
setSelected()
|
|
}
|
|
if (
|
|
props.defaultFirstOption &&
|
|
(props.filterable || props.remote) &&
|
|
filteredOptionsCount.value
|
|
) {
|
|
checkDefaultFirstOption()
|
|
}
|
|
},
|
|
{
|
|
flush: 'post',
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => states.hoveringIndex,
|
|
(val) => {
|
|
if (isNumber(val) && val > -1) {
|
|
hoverOption.value = optionsArray.value[val] || {}
|
|
} else {
|
|
hoverOption.value = {}
|
|
}
|
|
optionsArray.value.forEach((option) => {
|
|
option.hover = hoverOption.value === option
|
|
})
|
|
}
|
|
)
|
|
|
|
watchEffect(() => {
|
|
// Anything could cause options changed, then update options
|
|
// If you want to control it by condition, write here
|
|
if (states.isBeforeHide) return
|
|
updateOptions()
|
|
})
|
|
|
|
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) &&
|
|
filteredOptionsCount.value
|
|
) {
|
|
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 = optionsArray.value.filter(
|
|
(n) => n.visible && !n.disabled && !n.states.groupDisabled
|
|
)
|
|
const userCreatedOption = optionsInDropdown.find((n) => n.created)
|
|
const firstOriginOption = optionsInDropdown[0]
|
|
states.hoveringIndex = getValueIndex(
|
|
optionsArray.value,
|
|
userCreatedOption || firstOriginOption
|
|
)
|
|
}
|
|
|
|
const setSelected = () => {
|
|
if (!props.multiple) {
|
|
const value = isArray(props.modelValue)
|
|
? props.modelValue[0]
|
|
: props.modelValue
|
|
const option = getOption(value)
|
|
states.selectedLabel = option.currentLabel
|
|
states.selected = option
|
|
return
|
|
} else {
|
|
states.selectedLabel = ''
|
|
}
|
|
const result: any[] = []
|
|
if (!isUndefined(props.modelValue)) {
|
|
ensureArray(props.modelValue).forEach((value) => {
|
|
result.push(getOption(value))
|
|
})
|
|
}
|
|
states.selected = result
|
|
}
|
|
|
|
const getOption = (value) => {
|
|
let option
|
|
const isObjectValue = toRawType(value).toLowerCase() === 'object'
|
|
const isNull = toRawType(value).toLowerCase() === 'null'
|
|
const isUndefined = toRawType(value).toLowerCase() === 'undefined'
|
|
|
|
for (let i = states.cachedOptions.size - 1; i >= 0; i--) {
|
|
const cachedOption = cachedOptionsArray.value[i]
|
|
const isEqualValue = isObjectValue
|
|
? get(cachedOption.value, props.valueKey) === get(value, props.valueKey)
|
|
: cachedOption.value === value
|
|
if (isEqualValue) {
|
|
option = {
|
|
value,
|
|
currentLabel: cachedOption.currentLabel,
|
|
get isDisabled() {
|
|
return cachedOption.isDisabled
|
|
},
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if (option) return option
|
|
const label = isObjectValue
|
|
? value.label
|
|
: !isNull && !isUndefined
|
|
? value
|
|
: ''
|
|
const newOption = {
|
|
value,
|
|
currentLabel: label,
|
|
}
|
|
return newOption
|
|
}
|
|
|
|
const updateHoveringIndex = () => {
|
|
if (!props.multiple) {
|
|
states.hoveringIndex = optionsArray.value.findIndex((item) => {
|
|
return getValueKey(item) === getValueKey(states.selected)
|
|
})
|
|
} else {
|
|
states.hoveringIndex = optionsArray.value.findIndex((item) =>
|
|
states.selected.some(
|
|
(selected) => getValueKey(selected) === getValueKey(item)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
const resetSelectionWidth = () => {
|
|
states.selectionWidth = selectionRef.value.getBoundingClientRect().width
|
|
}
|
|
|
|
const resetCalculatorWidth = () => {
|
|
states.calculatorWidth = calculatorRef.value.getBoundingClientRect().width
|
|
}
|
|
|
|
const resetCollapseItemWidth = () => {
|
|
states.collapseItemWidth =
|
|
collapseItemRef.value.getBoundingClientRect().width
|
|
}
|
|
|
|
const updateTooltip = () => {
|
|
tooltipRef.value?.updatePopper?.()
|
|
}
|
|
|
|
const updateTagTooltip = () => {
|
|
tagTooltipRef.value?.updatePopper?.()
|
|
}
|
|
|
|
const onInputChange = () => {
|
|
if (states.inputValue.length > 0 && !expanded.value) {
|
|
expanded.value = true
|
|
}
|
|
handleQueryChange(states.inputValue)
|
|
}
|
|
|
|
const onInput = (event) => {
|
|
states.inputValue = event.target.value
|
|
if (props.remote) {
|
|
debouncedOnInputChange()
|
|
} else {
|
|
return onInputChange()
|
|
}
|
|
}
|
|
|
|
const debouncedOnInputChange = lodashDebounce(() => {
|
|
onInputChange()
|
|
}, debounce.value)
|
|
|
|
const emitChange = (val) => {
|
|
if (!isEqual(props.modelValue, val)) {
|
|
emit(CHANGE_EVENT, val)
|
|
}
|
|
}
|
|
|
|
const getLastNotDisabledIndex = (value) =>
|
|
findLastIndex(value, (it) => !states.disabledOptions.has(it))
|
|
|
|
const deletePrevTag = (e) => {
|
|
if (!props.multiple) return
|
|
if (e.code === EVENT_CODE.delete) return
|
|
if (e.target.value.length <= 0) {
|
|
const value = ensureArray(props.modelValue).slice()
|
|
const lastNotDisabledIndex = getLastNotDisabledIndex(value)
|
|
if (lastNotDisabledIndex < 0) return
|
|
const removeTagValue = value[lastNotDisabledIndex]
|
|
value.splice(lastNotDisabledIndex, 1)
|
|
emit(UPDATE_MODEL_EVENT, value)
|
|
emitChange(value)
|
|
emit('remove-tag', removeTagValue)
|
|
}
|
|
}
|
|
|
|
const deleteTag = (event, tag) => {
|
|
const index = states.selected.indexOf(tag)
|
|
if (index > -1 && !selectDisabled.value) {
|
|
const value = ensureArray(props.modelValue).slice()
|
|
value.splice(index, 1)
|
|
emit(UPDATE_MODEL_EVENT, value)
|
|
emitChange(value)
|
|
emit('remove-tag', tag.value)
|
|
}
|
|
event.stopPropagation()
|
|
focus()
|
|
}
|
|
|
|
const deleteSelected = (event) => {
|
|
event.stopPropagation()
|
|
const value: string | any[] = props.multiple ? [] : valueOnClear.value
|
|
if (props.multiple) {
|
|
for (const item of states.selected) {
|
|
if (item.isDisabled) value.push(item.value)
|
|
}
|
|
}
|
|
emit(UPDATE_MODEL_EVENT, value)
|
|
emitChange(value)
|
|
states.hoveringIndex = -1
|
|
expanded.value = false
|
|
emit('clear')
|
|
focus()
|
|
}
|
|
|
|
const handleOptionSelect = (option) => {
|
|
if (props.multiple) {
|
|
const value = ensureArray(props.modelValue ?? []).slice()
|
|
const optionIndex = getValueIndex(value, option.value)
|
|
if (optionIndex > -1) {
|
|
value.splice(optionIndex, 1)
|
|
} else if (
|
|
props.multipleLimit <= 0 ||
|
|
value.length < props.multipleLimit
|
|
) {
|
|
value.push(option.value)
|
|
}
|
|
emit(UPDATE_MODEL_EVENT, value)
|
|
emitChange(value)
|
|
if (option.created) {
|
|
handleQueryChange('')
|
|
}
|
|
if (props.filterable && !props.reserveKeyword) {
|
|
states.inputValue = ''
|
|
}
|
|
} else {
|
|
emit(UPDATE_MODEL_EVENT, option.value)
|
|
emitChange(option.value)
|
|
expanded.value = false
|
|
}
|
|
focus()
|
|
if (expanded.value) return
|
|
nextTick(() => {
|
|
scrollToOption(option)
|
|
})
|
|
}
|
|
|
|
const getValueIndex = (arr: any[] = [], value) => {
|
|
if (!isObject(value)) return arr.indexOf(value)
|
|
|
|
const valueKey = props.valueKey
|
|
let index = -1
|
|
arr.some((item, i) => {
|
|
if (toRaw(get(item, valueKey)) === get(value, valueKey)) {
|
|
index = i
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
return index
|
|
}
|
|
|
|
const scrollToOption = (option) => {
|
|
const targetOption = isArray(option) ? option[0] : option
|
|
let target = null
|
|
|
|
if (targetOption?.value) {
|
|
const options = optionsArray.value.filter(
|
|
(item) => item.value === targetOption.value
|
|
)
|
|
if (options.length > 0) {
|
|
target = options[0].$el
|
|
}
|
|
}
|
|
|
|
if (tooltipRef.value && target) {
|
|
const menu = tooltipRef.value?.popperRef?.contentRef?.querySelector?.(
|
|
`.${nsSelect.be('dropdown', 'wrap')}`
|
|
)
|
|
if (menu) {
|
|
scrollIntoView(menu as HTMLElement, target)
|
|
}
|
|
}
|
|
scrollbarRef.value?.handleScroll()
|
|
}
|
|
|
|
const onOptionCreate = (vm: SelectOptionProxy) => {
|
|
states.options.set(vm.value, vm)
|
|
states.cachedOptions.set(vm.value, vm)
|
|
vm.disabled && states.disabledOptions.set(vm.value, vm)
|
|
}
|
|
|
|
const onOptionDestroy = (key, vm: SelectOptionProxy) => {
|
|
if (states.options.get(key) === vm) {
|
|
states.options.delete(key)
|
|
}
|
|
}
|
|
|
|
const popperRef = computed(() => {
|
|
return tooltipRef.value?.popperRef?.contentRef
|
|
})
|
|
|
|
const handleMenuEnter = () => {
|
|
states.isBeforeHide = false
|
|
nextTick(() => scrollToOption(states.selected))
|
|
}
|
|
|
|
const focus = () => {
|
|
inputRef.value?.focus()
|
|
}
|
|
|
|
const blur = () => {
|
|
handleClickOutside()
|
|
}
|
|
|
|
const handleClearClick = (event: Event) => {
|
|
deleteSelected(event)
|
|
}
|
|
|
|
const handleClickOutside = (event: Event) => {
|
|
expanded.value = false
|
|
|
|
if (isFocused.value) {
|
|
const _event = new FocusEvent('focus', event)
|
|
nextTick(() => handleBlur(_event))
|
|
}
|
|
}
|
|
|
|
const handleEsc = () => {
|
|
if (states.inputValue.length > 0) {
|
|
states.inputValue = ''
|
|
} else {
|
|
expanded.value = false
|
|
}
|
|
}
|
|
|
|
const toggleMenu = () => {
|
|
if (selectDisabled.value) return
|
|
|
|
// We only set the inputHovering state to true on mouseenter event on iOS devices
|
|
// To keep the state updated we set it here to true
|
|
if (isIOS) states.inputHovering = true
|
|
|
|
if (states.menuVisibleOnFocus) {
|
|
// controlled by automaticDropdown
|
|
states.menuVisibleOnFocus = false
|
|
} else {
|
|
expanded.value = !expanded.value
|
|
}
|
|
}
|
|
|
|
const selectOption = () => {
|
|
if (!expanded.value) {
|
|
toggleMenu()
|
|
} else {
|
|
if (optionsArray.value[states.hoveringIndex]) {
|
|
handleOptionSelect(optionsArray.value[states.hoveringIndex])
|
|
}
|
|
}
|
|
}
|
|
|
|
const getValueKey = (item) => {
|
|
return isObject(item.value) ? get(item.value, props.valueKey) : item.value
|
|
}
|
|
|
|
const optionsAllDisabled = computed(() =>
|
|
optionsArray.value
|
|
.filter((option) => option.visible)
|
|
.every((option) => option.disabled)
|
|
)
|
|
|
|
const showTagList = computed(() => {
|
|
if (!props.multiple) {
|
|
return []
|
|
}
|
|
return props.collapseTags
|
|
? states.selected.slice(0, props.maxCollapseTags)
|
|
: states.selected
|
|
})
|
|
|
|
const collapseTagList = computed(() => {
|
|
if (!props.multiple) {
|
|
return []
|
|
}
|
|
return props.collapseTags
|
|
? states.selected.slice(props.maxCollapseTags)
|
|
: []
|
|
})
|
|
|
|
const navigateOptions = (direction) => {
|
|
if (!expanded.value) {
|
|
expanded.value = true
|
|
return
|
|
}
|
|
if (
|
|
states.options.size === 0 ||
|
|
states.filteredOptionsCount === 0 ||
|
|
isComposing.value
|
|
)
|
|
return
|
|
|
|
if (!optionsAllDisabled.value) {
|
|
if (direction === 'next') {
|
|
states.hoveringIndex++
|
|
if (states.hoveringIndex === states.options.size) {
|
|
states.hoveringIndex = 0
|
|
}
|
|
} else if (direction === 'prev') {
|
|
states.hoveringIndex--
|
|
if (states.hoveringIndex < 0) {
|
|
states.hoveringIndex = states.options.size - 1
|
|
}
|
|
}
|
|
const option = optionsArray.value[states.hoveringIndex]
|
|
if (
|
|
option.disabled === true ||
|
|
option.states.groupDisabled === true ||
|
|
!option.visible
|
|
) {
|
|
navigateOptions(direction)
|
|
}
|
|
nextTick(() => scrollToOption(hoverOption.value))
|
|
}
|
|
}
|
|
|
|
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 maxWidth =
|
|
collapseItemRef.value && props.maxCollapseTags === 1
|
|
? states.selectionWidth - states.collapseItemWidth - gapWidth
|
|
: states.selectionWidth
|
|
return { maxWidth: `${maxWidth}px` }
|
|
})
|
|
|
|
const collapseTagStyle = computed(() => {
|
|
return { maxWidth: `${states.selectionWidth}px` }
|
|
})
|
|
|
|
const inputStyle = computed(() => ({
|
|
width: `${Math.max(states.calculatorWidth, MINIMUM_INPUT_WIDTH)}px`,
|
|
}))
|
|
|
|
useResizeObserver(selectionRef, resetSelectionWidth)
|
|
useResizeObserver(calculatorRef, resetCalculatorWidth)
|
|
useResizeObserver(menuRef, updateTooltip)
|
|
useResizeObserver(wrapperRef, updateTooltip)
|
|
useResizeObserver(tagMenuRef, updateTagTooltip)
|
|
useResizeObserver(collapseItemRef, resetCollapseItemWidth)
|
|
|
|
onMounted(() => {
|
|
setSelected()
|
|
})
|
|
|
|
return {
|
|
inputId,
|
|
contentId,
|
|
nsSelect,
|
|
nsInput,
|
|
states,
|
|
isFocused,
|
|
expanded,
|
|
optionsArray,
|
|
hoverOption,
|
|
selectSize,
|
|
filteredOptionsCount,
|
|
resetCalculatorWidth,
|
|
updateTooltip,
|
|
updateTagTooltip,
|
|
debouncedOnInputChange,
|
|
onInput,
|
|
deletePrevTag,
|
|
deleteTag,
|
|
deleteSelected,
|
|
handleOptionSelect,
|
|
scrollToOption,
|
|
hasModelValue,
|
|
shouldShowPlaceholder,
|
|
currentPlaceholder,
|
|
mouseEnterEventName,
|
|
showClose,
|
|
iconComponent,
|
|
iconReverse,
|
|
validateState,
|
|
validateIcon,
|
|
showNewOption,
|
|
updateOptions,
|
|
collapseTagSize,
|
|
setSelected,
|
|
selectDisabled,
|
|
emptyText,
|
|
handleCompositionStart,
|
|
handleCompositionUpdate,
|
|
handleCompositionEnd,
|
|
onOptionCreate,
|
|
onOptionDestroy,
|
|
handleMenuEnter,
|
|
focus,
|
|
blur,
|
|
handleClearClick,
|
|
handleClickOutside,
|
|
handleEsc,
|
|
toggleMenu,
|
|
selectOption,
|
|
getValueKey,
|
|
navigateOptions,
|
|
dropdownMenuVisible,
|
|
showTagList,
|
|
collapseTagList,
|
|
|
|
// computed style
|
|
tagStyle,
|
|
collapseTagStyle,
|
|
inputStyle,
|
|
|
|
// DOM ref
|
|
popperRef,
|
|
inputRef,
|
|
tooltipRef,
|
|
tagTooltipRef,
|
|
calculatorRef,
|
|
prefixRef,
|
|
suffixRef,
|
|
selectRef,
|
|
wrapperRef,
|
|
selectionRef,
|
|
scrollbarRef,
|
|
menuRef,
|
|
tagMenuRef,
|
|
collapseItemRef,
|
|
}
|
|
}
|