vexip-ui
Version:
A Vue 3 UI library, Highly customizability, full TypeScript, performance pretty good
1 lines • 77.5 kB
Source Map (JSON)
{"version":3,"file":"select.vue2.cjs","sources":["../../../components/select/select.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { Icon } from '@/components/icon'\nimport { Input } from '@/components/input'\nimport { NativeScroll } from '@/components/native-scroll'\nimport { Option } from '@/components/option'\nimport { Overflow } from '@/components/overflow'\nimport { Popper } from '@/components/popper'\nimport { Renderer } from '@/components/renderer'\nimport { Tag } from '@/components/tag'\nimport { Tooltip } from '@/components/tooltip'\nimport { VirtualList } from '@/components/virtual-list'\nimport { useFieldStore } from '@/components/form'\n\nimport { computed, nextTick, onMounted, reactive, ref, toRef, watch, watchEffect } from 'vue'\n\nimport {\n placementWhileList,\n useClickOutside,\n useHover,\n useModifier,\n useMounted,\n usePopper,\n} from '@vexip-ui/hooks'\nimport {\n createIconProp,\n createSizeProp,\n createStateProp,\n emitEvent,\n useIcons,\n useLocale,\n useNameHelper,\n useProps,\n} from '@vexip-ui/config'\nimport {\n decide,\n getLast,\n getRangeWidth,\n isNull,\n removeArrayItem,\n toAttrValue,\n} from '@vexip-ui/utils'\nimport { selectProps } from './props'\nimport { defaultKeyConfig } from './symbol'\n\nimport type { InputExposed } from '@/components/input'\nimport type { PopperExposed } from '@/components/popper'\nimport type { TooltipExposed } from '@/components/tooltip'\nimport type { VirtualListExposed } from '@/components/virtual-list'\nimport type {\n ChangeEvent,\n SelectBaseValue,\n SelectEvent,\n SelectOptionState,\n SelectSlots,\n SelectValue,\n} from './symbol'\n\nfunction isSameValue(newValue: SelectValue, oldValue: SelectValue) {\n const isNewArray = Array.isArray(newValue)\n const isOldArray = Array.isArray(oldValue)\n\n if (isNewArray !== isOldArray) return false\n\n if (isNewArray && isOldArray) {\n if (newValue.length !== oldValue.length) return false\n\n for (let i = 0, len = newValue.length; i < len; ++i) {\n if (newValue[i] !== oldValue[i]) return false\n }\n\n return true\n }\n\n if (isNull(newValue)) return isNull(oldValue)\n\n return newValue === oldValue\n}\n\ndefineOptions({ name: 'Select' })\n\nconst {\n idFor,\n labelId,\n state,\n disabled,\n loading,\n size,\n validateField,\n clearField,\n getFieldValue,\n setFieldValue,\n} = useFieldStore<SelectValue>(focus)\n\nconst nh = useNameHelper('select')\n\nconst _props = defineProps(selectProps)\nconst props = useProps('select', _props, {\n size: createSizeProp(size),\n state: createStateProp(state),\n locale: null,\n visible: {\n default: false,\n static: true,\n },\n options: {\n default: () => [],\n static: true,\n },\n disabled: () => disabled.value,\n transitionName: () => nh.ns('drop'),\n outsideClose: true,\n placeholder: null,\n prefix: createIconProp(),\n prefixColor: '',\n suffix: createIconProp(),\n suffixColor: '',\n noSuffix: false,\n value: {\n default: () => getFieldValue()!,\n static: true,\n },\n multiple: false,\n clearable: false,\n maxListHeight: 300,\n listClass: null,\n placement: {\n default: 'bottom',\n validator: value => placementWhileList.includes(value),\n },\n transfer: false,\n optionCheck: false,\n emptyText: null,\n staticSuffix: false,\n loading: () => loading.value,\n loadingIcon: createIconProp(),\n loadingLock: false,\n loadingEffect: null,\n keyConfig: () => ({}),\n filter: false,\n ignoreCase: false,\n creatable: false,\n transparent: false,\n maxTagCount: 0,\n noRestTip: false,\n tagType: null,\n noPreview: false,\n remote: false,\n fitPopper: false,\n name: {\n default: '',\n static: true,\n },\n popperAlive: null,\n countLimit: 0,\n filterPosition: 'in-control',\n slots: () => ({}),\n shift: true,\n})\n\nconst emit = defineEmits(['update:value', 'update:visible', 'update:label'])\n\nconst slots = defineSlots<SelectSlots>()\n\nconst locale = useLocale('select', toRef(props, 'locale'))\nconst icons = useIcons()\n\nconst currentVisible = ref(props.visible)\nconst currentLabels = ref<string[]>([])\nconst currentValues = ref<SelectBaseValue[]>([])\nconst currentIndex = ref(-1)\nconst placement = toRef(props, 'placement')\nconst transfer = toRef(props, 'transfer')\n// const listHeight = ref<string>()\nconst baseOptions = ref<SelectOptionState[]>([])\nconst currentFilter = ref('')\nconst anchorWidth = ref(0)\nconst userOptions = ref<SelectOptionState[]>([])\nconst restTagCount = ref(0)\nconst restTipShow = ref(false)\nconst composing = ref(false)\n\nconst { isMounted } = useMounted()\n\nconst dynamicOption = reactive<SelectOptionState>({\n disabled: false,\n divided: false,\n title: '',\n value: '',\n label: '',\n group: false,\n depth: 0,\n parent: null,\n hidden: false,\n hitting: true,\n data: '',\n})\n\nconst optionValues = reactive(new Set<string | number>())\nconst hittingOption = ref<SelectOptionState>()\nconst optionStates = computed(() => userOptions.value.concat(baseOptions.value))\nconst visibleOptions = computed(() => optionStates.value.filter(state => !state.hidden))\n\nconst keyConfig = computed(() => ({ ...defaultKeyConfig, ...props.keyConfig }))\n\nconst wrapper = useClickOutside(handleClickOutside)\nconst nativeInput = ref<HTMLInputElement>()\nconst filterInput = ref<InputExposed>()\nconst device = ref<HTMLElement>()\nconst virtualList = ref<VirtualListExposed>()\nconst popper = ref<PopperExposed>()\nconst restTip = ref<TooltipExposed>()\n\nconst input = computed(() => filterInput.value?.input ?? nativeInput.value)\n\nconst { reference, transferTo, updatePopper } = usePopper({\n placement,\n transfer,\n wrapper,\n popper: computed(() => popper.value?.wrapper),\n isDrop: true,\n shift: toRef(props, 'shift'),\n})\nconst { isHover } = useHover(reference)\n\nconst cachedSelected = reactive(new Map<SelectBaseValue, SelectOptionState>())\nconst optionValueMap = ref(new Map<SelectBaseValue, SelectOptionState>())\n\nlet emittedValue: typeof props.value | null = props.value\n\nconst updateTrigger = ref(0)\n\nwatchEffect(() => {\n /* eslint-disable @typescript-eslint/no-unused-expressions */\n props.keyConfig.value\n props.keyConfig.label\n props.keyConfig.disabled\n props.keyConfig.divided\n props.keyConfig.title\n props.keyConfig.group\n props.keyConfig.children\n\n // If we only read the `props.options`, when user use Array native methods to\n // change options, Vue will not trigger the watch callback\n for (let i = 0, len = props.options.length; i < len; ++i) {\n props.options[i]\n }\n /* eslint-enable */\n\n updateTrigger.value++\n})\n\nwatch(updateTrigger, initOptionState, { immediate: true })\n\nfunction initOptionState() {\n const {\n value: valueKey,\n label: labelKey,\n disabled: disabledKey,\n divided: dividedKey,\n title: titleKey,\n group: groupKey,\n children: childrenKey,\n } = keyConfig.value\n const oldMap = optionValueMap.value\n const map = new Map<string | number, SelectOptionState>()\n const states: SelectOptionState[] = []\n const loop = props.options\n .map(option => ({ option, depth: 0, parent: null as SelectOptionState | null }))\n .reverse()\n\n optionValues.clear()\n\n for (const option of userOptions.value) {\n map.set(option.value, option)\n optionValues.add(option.value)\n }\n\n while (loop.length) {\n const { option, depth, parent } = loop.pop()!\n const rawOption = typeof option === 'string' ? { [valueKey]: option } : option\n const group = !!rawOption[groupKey]\n const value = rawOption[valueKey]\n\n if (!group && isNull(value)) continue\n\n const label = rawOption[labelKey] || String(value)\n const {\n [disabledKey]: disabled = false,\n [dividedKey]: divided = false,\n [titleKey]: title = '',\n [childrenKey]: children = null,\n } = rawOption\n const oldState = oldMap.get(rawOption.value)\n const optionState = reactive({\n disabled,\n divided,\n title,\n value,\n label,\n group,\n depth,\n parent,\n hidden: oldState?.hidden ?? false,\n hitting: oldState?.hitting ?? false,\n data: option,\n }) as SelectOptionState\n\n states.push(optionState)\n\n if (!group) {\n map.set(value, optionState)\n optionValues.add(String(value))\n }\n\n if (Array.isArray(children) && children.length) {\n loop.push(\n ...children\n .map(child => {\n return { option: child, depth: depth + 1, parent: optionState }\n })\n .reverse(),\n )\n }\n }\n\n optionValueMap.value = map\n baseOptions.value = states\n\n initValueAndLabel(emittedValue)\n}\n\nuseModifier({\n target: wrapper,\n passive: false,\n onKeyDown: (event, modifier) => {\n if (composing.value) {\n event.stopPropagation()\n return\n }\n\n if (!currentVisible.value) {\n if (modifier.space || modifier.enter) {\n event.preventDefault()\n event.stopPropagation()\n toggleVisible()\n }\n\n return\n }\n\n if (modifier.tab || modifier.escape) {\n setVisible(false)\n modifier.resetAll()\n\n return\n }\n\n decide(\n [\n [\n () => modifier.up || modifier.down,\n () => {\n const options = visibleOptions.value\n const length = options.length\n\n if (!length) return\n\n const step = modifier.down ? 1 : -1\n\n let index = (Math.max(-1, currentIndex.value + step) + length) % length\n let option = options[index]\n\n for (let i = 0; (option.disabled || option.group) && i < length; ++i) {\n index += step\n index = (index + length) % length\n option = options[index]\n }\n\n updateHitting(index)\n },\n ],\n [\n () => modifier.enter || (!props.filter && modifier.space),\n () => {\n if (currentIndex.value >= 0) {\n handleSelect(totalOptions.value[currentIndex.value])\n } else if (showDynamic.value) {\n handleSelect(dynamicOption)\n } else {\n setVisible(false)\n }\n },\n ],\n ],\n {\n beforeMatchAny: () => {\n event.preventDefault()\n event.stopPropagation()\n },\n afterMatchAny: modifier.resetAll,\n },\n )\n },\n})\n\nconst className = computed(() => {\n return {\n [nh.b()]: true,\n [nh.ns('input-vars')]: true,\n [nh.bs('vars')]: true,\n [nh.bm('inherit')]: props.inherit,\n [nh.bm('multiple')]: props.multiple,\n [nh.bm('filter')]: props.filter,\n [nh.bm('responsive')]: props.multiple && props.maxTagCount <= 0,\n [nh.bm('disabled')]: props.disabled,\n }\n})\nconst readonly = computed(() => props.loading && props.loadingLock)\nconst selectorClass = computed(() => {\n const baseCls = nh.be('selector')\n\n return {\n [baseCls]: true,\n [`${baseCls}--focused`]: !props.disabled && currentVisible.value,\n [`${baseCls}--disabled`]: props.disabled,\n [`${baseCls}--readonly`]: readonly.value,\n [`${baseCls}--loading`]: props.loading,\n [`${baseCls}--${props.size}`]: props.size !== 'default',\n [`${baseCls}--${props.state}`]: props.state !== 'default',\n [`${baseCls}--has-prefix`]: hasPrefix.value,\n [`${baseCls}--has-suffix`]: !props.noSuffix,\n [`${baseCls}--transparent`]: props.transparent,\n }\n})\nconst hasValue = computed(\n () => !isNull(currentValues.value[0]) && (props.multiple || currentValues.value[0] !== ''),\n)\nconst hasPrefix = computed(() => !!(slots.prefix || props.prefix || props.slots.prefix))\nconst showDynamic = computed(() => {\n return !!(\n props.filter &&\n props.creatable &&\n dynamicOption.value &&\n !optionValues.has(dynamicOption.value)\n )\n})\nconst totalOptions = computed(() => {\n return showDynamic.value ? [dynamicOption].concat(visibleOptions.value) : visibleOptions.value\n})\nconst normalOptions = computed(() => optionStates.value.filter(option => !option.group))\nconst optionParentMap = computed(() => {\n const options = normalOptions.value\n const map = new Map<string | number, SelectOptionState>()\n\n for (let i = 0, len = options.length; i < len; ++i) {\n const option = options[i]\n\n if (option.parent) {\n map.set(option.value, option.parent)\n }\n }\n\n return map\n})\nconst showClear = computed(() => {\n return !props.disabled && !readonly.value && props.clearable && isHover.value && hasValue.value\n})\nconst previewOption = computed(() => {\n return !props.noPreview && currentVisible.value ? hittingOption.value : undefined\n})\nconst limited = computed(() => {\n return props.multiple && props.countLimit > 0 && currentValues.value.length >= props.countLimit\n})\nconst showPlaceholder = computed(() => {\n if (props.filterPosition !== 'in-control') {\n return (\n !hasValue.value && !previewOption.value && !!(props.placeholder ?? locale.value.placeholder)\n )\n }\n\n // 采用反推,出现下列情况时不显示:\n // 1. 开始组合(如输入了任意拼音)\n // 2. 有值且 未开预览/多选模式/未打开列表\n // 3. 没有预览选项且没有合法的占位值\n // 4. 打开列表且输入了过滤值\n return (\n !composing.value &&\n !(hasValue.value && (props.noPreview || props.multiple || !currentVisible.value)) &&\n !(!previewOption.value && !(props.placeholder ?? locale.value.placeholder)) &&\n !(currentVisible.value && currentFilter.value)\n )\n})\n\nfunction getOptionFromMap(value?: SelectBaseValue | null) {\n if (isNull(value)) return null\n\n return optionValueMap.value.get(value) ?? cachedSelected.get(value) ?? null\n}\n\nfunction fitPopperWidth() {\n requestAnimationFrame(() => {\n updatePopper()\n\n if (wrapper.value && popper.value?.wrapper) {\n if (typeof props.fitPopper === 'number') {\n popper.value.wrapper.style.width = `${props.fitPopper}px`\n } else if (props.fitPopper) {\n popper.value.wrapper.style.width = `${wrapper.value.offsetWidth}px`\n } else {\n popper.value.wrapper.style.minWidth = `${wrapper.value.offsetWidth}px`\n }\n }\n })\n}\n\nwatch(\n () => props.visible,\n value => {\n currentVisible.value = value\n },\n)\nwatch(currentVisible, value => {\n if (value) {\n restTipShow.value = false\n initHittingIndex()\n fitPopperWidth()\n }\n\n if (props.filterPosition !== 'in-control') {\n requestAnimationFrame(syncInputValue)\n } else {\n syncInputValue()\n }\n})\nwatch(\n () => props.value,\n value => {\n if (!emittedValue || !isSameValue(value, emittedValue)) {\n emittedValue = value\n initValueAndLabel(value)\n syncInputValue()\n }\n },\n)\nwatch(\n () => props.disabled,\n value => {\n if (value) {\n setVisible(false)\n }\n },\n)\nwatch(readonly, value => {\n if (value) {\n setVisible(false)\n }\n})\nwatch(currentFilter, value => {\n dynamicOption.value = value\n dynamicOption.label = value\n dynamicOption.data = value\n\n filterOptions(value)\n})\n\ndefineExpose({\n idFor,\n labelId,\n currentVisible,\n currentValues,\n currentLabels,\n optionStates,\n isHover,\n currentFilter,\n composing,\n visibleOptions,\n totalOptions,\n\n wrapper,\n reference,\n popper,\n input,\n device,\n virtualList,\n restTip,\n\n updatePopper,\n isSelected,\n getOptionFromMap,\n updateHitting,\n handleClear,\n focus,\n blur: () => {\n input.value?.blur()\n reference.value?.blur()\n },\n})\n\nonMounted(() => {\n syncInputValue()\n\n if (props.visible) {\n restTipShow.value = false\n initHittingIndex()\n fitPopperWidth()\n }\n})\n\nfunction initValueAndLabel(value: SelectValue | null) {\n if (isNull(value)) {\n currentValues.value = []\n currentLabels.value = []\n return\n }\n\n const normalizedValue = !Array.isArray(value) ? [value] : value\n\n const valueSet = new Set(normalizedValue)\n const selectedValues: SelectBaseValue[] = []\n const selectedLabels: string[] = []\n\n valueSet.forEach(value => {\n let option = getOptionFromMap(value)\n\n if (option) {\n selectedValues.push(option.value)\n selectedLabels.push(option.label)\n\n if (!cachedSelected.has(option.value)) {\n cachedSelected.set(option.value, option)\n }\n } else if (props.remote) {\n option = reactive({\n value,\n disabled: false,\n divided: false,\n title: '',\n label: String(value),\n group: false,\n depth: -1,\n parent: null,\n hidden: true,\n hitting: false,\n data: value,\n }) as SelectOptionState\n\n cachedSelected.set(value, option)\n selectedValues.push(value)\n selectedLabels.push(option.label)\n }\n })\n\n for (const cachedValue of Array.from(cachedSelected.keys())) {\n if (!valueSet.has(cachedValue)) {\n cachedSelected.delete(cachedValue)\n }\n }\n\n currentValues.value = selectedValues\n currentLabels.value = selectedLabels\n\n initHittingIndex()\n filterOptions(currentFilter.value)\n}\n\nfunction initHittingIndex() {\n const value = currentValues.value[0]\n\n if (isNull(value)) {\n updateHitting(-1)\n } else {\n if (!isMounted.value) return\n\n updateHitting(visibleOptions.value.findIndex(option => option.value === value))\n }\n}\n\nfunction setVisible(visible: boolean) {\n if (currentVisible.value === visible) return\n\n currentVisible.value = visible\n\n emit('update:visible', visible)\n emitEvent(props.onToggle, visible)\n}\n\nfunction updateHitting(hitting: number, ensureInView = true) {\n currentIndex.value = hitting\n hittingOption.value = undefined\n\n let index = -1\n\n optionStates.value.forEach(option => {\n if (!option.hidden) {\n index += 1\n option.hitting = hitting === index\n\n if (option.hitting) {\n hittingOption.value = option\n }\n } else {\n option.hitting = false\n }\n })\n\n if (ensureInView && currentVisible.value && virtualList.value) {\n virtualList.value.ensureIndexInView(hitting)\n }\n}\n\nfunction isSelected(option: SelectOptionState) {\n if (props.multiple) {\n return currentValues.value.includes(option.value)\n }\n\n return currentValues.value[0] === option.value\n}\n\nfunction filterOptions(inputValue: string) {\n const filter = props.filter\n\n if (!filter || props.remote) return\n\n if (!inputValue) {\n optionStates.value.forEach(state => {\n state.hidden = false\n })\n } else {\n optionStates.value.forEach(state => {\n state.hidden = true\n })\n\n if (typeof filter === 'function') {\n normalOptions.value.forEach(state => {\n state.hidden = !filter(inputValue, state)\n })\n } else {\n if (props.ignoreCase) {\n const ignoreCaseValue = inputValue.toString().toLocaleLowerCase()\n\n normalOptions.value.forEach(state => {\n state.hidden = !state.label?.toString().toLocaleLowerCase().includes(ignoreCaseValue)\n })\n } else {\n normalOptions.value.forEach(state => {\n state.hidden = !state.label?.toString().includes(inputValue?.toString())\n })\n }\n }\n\n const parentMap = optionParentMap.value\n\n normalOptions.value.forEach(option => {\n if (!option.hidden && option.parent) {\n let parent = parentMap.get(option.value) || null\n\n while (parent && parent.hidden) {\n parent.hidden = false\n parent = parent.parent\n }\n }\n })\n }\n\n updateHitting(currentIndex.value)\n}\n\nfunction handleTagClose(value?: SelectBaseValue | null) {\n if (props.disabled || readonly.value) return\n\n !isNull(value) && handleSelect(getOptionFromMap(value))\n}\n\nfunction handleRestTagClose(value?: SelectBaseValue | null) {\n handleTagClose(value)\n\n if (restTipShow.value) {\n restTip.value?.updatePopper()\n }\n}\n\nfunction handleSelect(option?: SelectOptionState | null) {\n if (!option) return\n\n const selected = isSelected(option)\n const value = option.value\n\n if (selected) {\n if (userOptions.value.find(item => item.value === value)) {\n removeArrayItem(userOptions.value, item => item.value === value)\n optionValueMap.value.delete(value)\n }\n\n cachedSelected.delete(value)\n } else {\n if (!props.multiple) {\n userOptions.value.length = 0\n }\n\n if (limited.value) return\n\n if (dynamicOption.value && value === dynamicOption.value) {\n const newOption = { ...dynamicOption }\n\n userOptions.value.push(newOption)\n optionValueMap.value.set(value, newOption)\n }\n\n cachedSelected.set(option.value, option)\n }\n\n emitEvent(\n props[props.multiple && selected ? 'onCancel' : 'onSelect'] as SelectEvent,\n value,\n option.data,\n )\n handleChange(option)\n\n if (props.multiple) {\n if (props.filterPosition === 'in-control') {\n currentFilter.value = ''\n syncInputValue()\n }\n\n requestAnimationFrame(updatePopper)\n } else {\n setVisible(false)\n }\n\n anchorWidth.value = 0\n}\n\nfunction handleChange(option: SelectOptionState) {\n if (props.multiple) {\n if (isSelected(option)) {\n const index = currentValues.value.findIndex(v => v === option.value)\n\n if (~index) {\n currentValues.value.splice(index, 1)\n currentLabels.value.splice(index, 1)\n }\n } else {\n currentValues.value.push(option.value)\n currentLabels.value.push(option.label)\n }\n\n emittedValue = Array.from(currentValues.value)\n\n emit('update:value', emittedValue)\n emit('update:label', currentLabels.value)\n setFieldValue(emittedValue)\n emitEvent(\n props.onChange as ChangeEvent,\n emittedValue,\n emittedValue.map(value => getOptionFromMap(value)?.data ?? value),\n )\n validateField()\n } else {\n const prevValue = currentValues.value[0]\n\n currentValues.value.length = 0\n currentLabels.value.length = 0\n currentValues.value.push(option.value)\n currentLabels.value.push(option.label)\n\n if (prevValue !== option.value) {\n emittedValue = option.value\n\n emit('update:value', emittedValue)\n emit('update:label', currentLabels.value[0])\n setFieldValue(emittedValue)\n emitEvent(props.onChange as ChangeEvent, emittedValue, option.data)\n validateField()\n }\n }\n}\n\nfunction toggleVisible() {\n if (props.disabled || readonly.value) return\n\n setVisible(!currentVisible.value)\n}\n\nfunction handleClickOutside() {\n restTipShow.value = false\n emitEvent(props.onClickOutside)\n\n if (props.outsideClose && currentVisible.value) {\n setVisible(false)\n emitEvent(props.onOutsideClose)\n }\n}\n\nfunction handleClear() {\n if (props.disabled || readonly.value) return\n\n if (props.clearable) {\n for (const option of userOptions.value) {\n optionValueMap.value.delete(option.value)\n }\n\n cachedSelected.clear()\n\n userOptions.value.length = 0\n currentValues.value.length = 0\n currentLabels.value.length = 0\n restTipShow.value = false\n\n emittedValue = props.multiple ? [] : ''\n\n syncInputValue()\n emit('update:value', emittedValue)\n emitEvent(props.onChange as ChangeEvent, emittedValue, props.multiple ? [] : '')\n emitEvent(props.onClear)\n clearField(emittedValue!)\n updatePopper()\n }\n}\n\nlet focused = false\n\nfunction handleFocus(event: FocusEvent) {\n if (!focused) {\n focused = true\n emitEvent(props.onFocus, event)\n }\n}\n\nfunction handleBlur(event: FocusEvent) {\n if (focused) {\n focused = false\n\n setTimeout(() => {\n if (!focused) {\n emitEvent(props.onBlur, event)\n }\n }, 120)\n }\n}\n\nfunction syncInputValue() {\n if (!input.value) return\n\n const visible = currentVisible.value\n\n if (props.multiple) {\n input.value.value = ''\n } else {\n input.value.value = visible ? '' : currentLabels.value[0] || ''\n }\n\n visible ? input.value.focus() : input.value.blur()\n}\n\nfunction handleFilterInput() {\n if (!input.value || composing.value) return\n\n let hittingIndex: number\n\n currentFilter.value = input.value.value\n\n if (!currentFilter.value) {\n hittingIndex = -1\n } else if (showDynamic.value || currentIndex.value !== -1) {\n hittingIndex = 0\n } else {\n hittingIndex = visibleOptions.value.findIndex(\n option => String(option.label) === currentFilter.value,\n )\n hittingIndex = hittingIndex === -1 ? 0 : hittingIndex\n }\n\n requestAnimationFrame(() => {\n if (!hittingIndex) {\n hittingIndex = visibleOptions.value.findIndex(\n option => !currentValues.value.includes(option.value),\n )\n }\n\n if (hittingIndex !== currentIndex.value) {\n updateHitting(hittingIndex)\n }\n\n if (props.multiple && device.value) {\n anchorWidth.value = getRangeWidth(device.value)\n }\n\n updatePopper()\n })\n\n emitEvent(props.onFilterInput, currentFilter.value)\n}\n\nfunction handleCompositionEnd() {\n if (!composing.value) return\n\n composing.value = false\n\n if (input.value) {\n input.value.dispatchEvent(new Event('input'))\n }\n}\n\nfunction handleFilterKeyDown(event: KeyboardEvent) {\n if (!input.value) return\n\n if (\n props.filterPosition !== 'in-list' &&\n event.key === 'Backspace' &&\n !input.value.value &&\n !isNull(getLast(currentValues.value))\n ) {\n event.stopPropagation()\n handleTagClose(getLast(currentValues.value))\n }\n}\n\nfunction toggleShowRestTip() {\n if (!currentVisible.value) {\n restTipShow.value = !restTipShow.value\n\n if (restTipShow.value) {\n nextTick(() => {\n restTip.value?.updatePopper()\n })\n }\n } else {\n toggleVisible()\n restTipShow.value = false\n }\n}\n\nfunction focus(options?: FocusOptions) {\n if (currentVisible.value) {\n ;(input.value || reference.value)?.focus(options)\n } else {\n reference.value?.focus(options)\n }\n}\n</script>\n\n<template>\n <div\n :id=\"idFor\"\n ref=\"wrapper\"\n :class=\"className\"\n role=\"group\"\n :aria-disabled=\"toAttrValue(props.disabled)\"\n :aria-expanded=\"toAttrValue(currentVisible)\"\n aria-haspopup=\"listbox\"\n :aria-labelledby=\"labelId\"\n @click=\"toggleVisible\"\n >\n <div\n ref=\"reference\"\n :class=\"selectorClass\"\n tabindex=\"0\"\n @focus=\"handleFocus\"\n @blur=\";(!props.filter || !currentVisible) && handleBlur($event)\"\n >\n <div\n v-if=\"hasPrefix\"\n :class=\"[nh.be('icon'), nh.be('prefix')]\"\n :style=\"{ color: props.prefixColor }\"\n >\n <slot name=\"prefix\">\n <Renderer :renderer=\"props.slots.prefix\">\n <Icon :icon=\"props.prefix\"></Icon>\n </Renderer>\n </slot>\n </div>\n <div :class=\"nh.be('control')\">\n <slot name=\"control\">\n <Renderer :renderer=\"props.slots.control\">\n <template v-if=\"props.multiple\">\n <Overflow\n inherit\n :class=\"[nh.be('tags')]\"\n :items=\"currentValues\"\n :max-count=\"props.maxTagCount\"\n :style=\"{\n maxWidth: props.maxTagCount <= 0 && `calc(100% - ${anchorWidth}px)`,\n }\"\n @rest-change=\"restTagCount = $event\"\n @click.stop=\"toggleVisible\"\n >\n <template #default=\"{ item: value, index }\">\n <slot\n name=\"tag\"\n :value=\"value\"\n :option=\"getOptionFromMap(value)\"\n :handle-close=\"handleTagClose.bind(null, value)\"\n >\n <Renderer\n :renderer=\"props.slots.tag\"\n :data=\"{\n value,\n option: getOptionFromMap(value),\n handleClose: handleTagClose.bind(null, value),\n }\"\n >\n <Tag\n inherit\n :class=\"nh.be('tag')\"\n :type=\"props.tagType\"\n closable\n :disabled=\"props.disabled\"\n @close=\"handleTagClose(value)\"\n >\n <span :class=\"nh.be('label')\">\n <slot name=\"selected\" :value=\"value\" :option=\"getOptionFromMap(value)\">\n <Renderer\n :renderer=\"props.slots.selected\"\n :data=\"{ value, option: getOptionFromMap(value) }\"\n >\n {{ currentLabels[index] }}\n </Renderer>\n </slot>\n </span>\n </Tag>\n </Renderer>\n </slot>\n </template>\n <template #counter=\"{ count }\">\n <slot v-if=\"props.noRestTip\" name=\"restTag\" :rest-count=\"count\">\n <Renderer :renderer=\"props.slots.restTag\" :data=\"{ restCount: count }\">\n <Tag\n inherit\n :class=\"[nh.be('tag'), nh.be('counter')]\"\n :type=\"props.tagType\"\n :disabled=\"props.disabled\"\n >\n {{ `+${count}` }}\n </Tag>\n </Renderer>\n </slot>\n <template v-else>\n <Tooltip\n ref=\"restTip\"\n inherit\n :transfer=\"false\"\n :visible=\"restTipShow\"\n trigger=\"custom\"\n placement=\"top\"\n :tip-class=\"nh.be('rest-tip')\"\n @click.stop=\"toggleShowRestTip\"\n >\n <template #trigger>\n <slot name=\"restTag\" :rest-count=\"count\">\n <Renderer :renderer=\"props.slots.restTag\" :data=\"{ restCount: count }\">\n <Tag\n inherit\n :class=\"[nh.be('tag'), nh.be('counter')]\"\n :type=\"props.tagType\"\n :disabled=\"props.disabled\"\n >\n {{ `+${count}` }}\n </Tag>\n </Renderer>\n </slot>\n </template>\n <NativeScroll inherit use-y-bar>\n <template v-for=\"(value, index) in currentValues\" :key=\"index\">\n <slot\n v-if=\"index >= currentValues.length - restTagCount\"\n name=\"tag\"\n :value=\"value\"\n :option=\"getOptionFromMap(value)\"\n :handle-close=\"handleRestTagClose.bind(null, value)\"\n >\n <Renderer\n :renderer=\"props.slots.tag\"\n :data=\"{\n value,\n option: getOptionFromMap(value),\n handleClose: handleRestTagClose.bind(null, value),\n }\"\n >\n <Tag\n inherit\n :class=\"nh.be('tag')\"\n closable\n :type=\"props.tagType\"\n :disabled=\"props.disabled\"\n @close=\"handleRestTagClose(value)\"\n >\n <span :class=\"nh.be('label')\">\n <slot\n name=\"selected\"\n :value=\"value\"\n :option=\"getOptionFromMap(value)\"\n >\n <Renderer\n :renderer=\"props.slots.selected\"\n :data=\"{ value, option: getOptionFromMap(value) }\"\n >\n {{ currentLabels[index] }}\n </Renderer>\n </slot>\n </span>\n </Tag>\n </Renderer>\n </slot>\n </template>\n </NativeScroll>\n </Tooltip>\n </template>\n </template>\n </Overflow>\n <div\n v-if=\"props.filter && props.filterPosition === 'in-control'\"\n :class=\"nh.be('anchor')\"\n :style=\"{\n width: `${anchorWidth}px`,\n }\"\n >\n <input\n ref=\"nativeInput\"\n :class=\"[\n nh.be('input'),\n nh.bem('input', 'multiple'),\n currentVisible && nh.bem('input', 'visible'),\n ]\"\n :disabled=\"props.disabled\"\n autocomplete=\"off\"\n tabindex=\"-1\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :name=\"props.name\"\n @submit.prevent\n @input=\"handleFilterInput\"\n @keydown=\"handleFilterKeyDown\"\n @focus=\"handleFocus($event)\"\n @blur=\"handleBlur($event)\"\n @compositionstart=\"composing = true\"\n @compositionend=\"handleCompositionEnd\"\n @change=\"handleCompositionEnd\"\n />\n <span ref=\"device\" :class=\"nh.be('device')\" aria-hidden=\"true\">\n {{ currentFilter }}\n </span>\n </div>\n </template>\n <template v-else>\n <input\n v-if=\"props.filter && props.filterPosition === 'in-control'\"\n ref=\"nativeInput\"\n :class=\"[nh.be('input'), currentVisible && nh.bem('input', 'visible')]\"\n :disabled=\"props.disabled\"\n autocomplete=\"off\"\n tabindex=\"-1\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :name=\"props.name\"\n :style=\"{\n opacity: currentVisible ? undefined : '0%',\n }\"\n @submit.prevent\n @input=\"handleFilterInput\"\n @focus=\"handleFocus($event)\"\n @blur=\"handleBlur($event)\"\n @compositionstart=\"composing = true\"\n @compositionend=\"handleCompositionEnd\"\n @change=\"handleCompositionEnd\"\n />\n <span\n v-if=\"\n (props.noPreview || !currentVisible) &&\n hasValue &&\n (props.filterPosition !== 'in-control' || !currentFilter)\n \"\n :class=\"{\n [nh.be('selected')]: true,\n [nh.bem('selected', 'placeholder')]: props.filter && currentVisible && hasValue,\n }\"\n >\n <slot\n v-if=\"getOptionFromMap(currentValues[0])\"\n name=\"selected\"\n :value=\"currentValues[0]\"\n :option=\"getOptionFromMap(currentValues[0])\"\n >\n <Renderer\n :renderer=\"props.slots.selected\"\n :data=\"{ value: currentValues[0], option: getOptionFromMap(currentValues[0]) }\"\n >\n {{ currentLabels[0] }}\n </Renderer>\n </slot>\n <template v-else>\n {{ currentLabels[0] }}\n </template>\n </span>\n </template>\n <span v-if=\"showPlaceholder\" :class=\"nh.be('placeholder')\">\n <slot\n v-if=\"previewOption\"\n name=\"selected\"\n :preview=\"true\"\n :value=\"previewOption.value\"\n :option=\"previewOption\"\n >\n <Renderer\n :renderer=\"props.slots.selected\"\n :data=\"{ value: previewOption.value, preview: true, option: previewOption }\"\n >\n {{ previewOption.label }}\n </Renderer>\n </slot>\n <template v-else>\n {{ props.placeholder ?? locale.placeholder }}\n </template>\n </span>\n </Renderer>\n </slot>\n </div>\n <div\n v-if=\"!props.noSuffix\"\n :class=\"[nh.be('icon'), nh.be('suffix')]\"\n :style=\"{\n color: props.suffixColor,\n opacity: showClear || props.loading ? '0%' : '',\n }\"\n >\n <slot name=\"suffix\">\n <Renderer :renderer=\"props.slots.suffix\">\n <Icon\n v-if=\"props.suffix\"\n :icon=\"props.suffix\"\n :class=\"{\n [nh.be('arrow')]: !props.staticSuffix,\n }\"\n ></Icon>\n <Icon v-else v-bind=\"icons.angleDown\" :class=\"nh.be('arrow')\"></Icon>\n </Renderer>\n </slot>\n </div>\n <div\n v-else-if=\"props.clearable || props.loading\"\n :class=\"[nh.be('icon'), nh.bem('icon', 'placeholder'), nh.be('suffix')]\"\n ></div>\n <Transition :name=\"nh.ns('fade')\" appear>\n <button\n v-if=\"showClear\"\n :class=\"[nh.be('icon'), nh.be('clear')]\"\n type=\"button\"\n tabindex=\"-1\"\n :aria-label=\"locale.ariaLabel.clear\"\n @click.stop=\"handleClear\"\n >\n <Icon v-bind=\"icons.clear\" label=\"clear\"></Icon>\n </button>\n <div v-else-if=\"props.loading\" :class=\"[nh.be('icon'), nh.be('loading')]\">\n <Icon\n v-bind=\"icons.loading\"\n :effect=\"props.loadingEffect || icons.loading.effect\"\n :icon=\"props.loadingIcon || icons.loading.icon\"\n label=\"loading\"\n ></Icon>\n </div>\n </Transition>\n </div>\n <Popper\n ref=\"popper\"\n :class=\"[nh.be('popper'), nh.bs('vars')]\"\n :visible=\"currentVisible\"\n :to=\"transferTo\"\n :transition=\"props.transitionName\"\n :alive=\"props.popperAlive ?? !transferTo\"\n @click.stop=\"focus\"\n @after-leave=\"currentFilter = ''\"\n >\n <slot\n name=\"list\"\n :options=\"totalOptions\"\n :is-selected=\"isSelected\"\n :handle-select=\"handleSelect\"\n >\n <Renderer\n :renderer=\"props.slots.list\"\n :data=\"{ options: totalOptions, isSelected, handleSelect }\"\n >\n <div\n :class=\"[\n nh.be('list'),\n (slots.prepend || slots.append) && nh.bem('list', 'with-extra'),\n props.listClass,\n ]\"\n >\n <div v-if=\"props.filter && props.filterPosition === 'in-list'\" :class=\"nh.be('filter')\">\n <Input\n ref=\"filterInput\"\n :class=\"nh.be('filter-input')\"\n transparent\n :disabled=\"props.disabled\"\n :placeholder=\"locale.search\"\n :autocomplete=\"false\"\n :tabindex=\"-1\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n @input=\"handleFilterInput\"\n @keydown=\"handleFilterKeyDown\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n @compositionstart=\"composing = true\"\n @compositionend=\"handleCompositionEnd\"\n @change=\"handleCompositionEnd\"\n >\n <template #suffix>\n <Icon v-bind=\"icons.search\"></Icon>\n </template>\n </Input>\n </div>\n <slot v-if=\"slots.prepend || props.slots.prepend\" name=\"prepend\">\n <Renderer :renderer=\"props.slots.prepend\"></Renderer>\n </slot>\n <VirtualList\n ref=\"virtualList\"\n inherit\n :style=\"{\n height: undefined,\n maxHeight: `${props.maxListHeight}px`,\n }\"\n :items=\"totalOptions\"\n :item-size=\"32\"\n use-y-bar\n :height=\"'100%'\"\n id-key=\"value\"\n :items-attrs=\"{\n class: [nh.be('options'), props.optionCheck ? nh.bem('options', 'has-check') : ''],\n role: 'listbox',\n ariaLabel: 'options',\n ariaMultiselectable: props.multiple,\n }\"\n >\n <template #default=\"{ item: option, index }\">\n <li\n v-if=\"option.group\"\n :class=\"[nh.ns('option-vars'), nh.be('group')]\"\n :title=\"option.label\"\n >\n <slot name=\"group\" :option=\"option\" :index=\"index\">\n <Renderer :renderer=\"props.slots.group\" :data=\"{ option, index }\">\n <div\n :class=\"[nh.be('label'), nh.bem('label', 'group')]\"\n :style=\"{ paddingInlineStart: `${option.depth * 6}px` }\"\n >\n {{ option.label }}\n </div>\n </Renderer>\n </slot>\n </li>\n <Option\n v-else\n :label=\"option.label\"\n :value=\"option.value\"\n :disabled=\"option.disabled || (limited && !isSelected(option))\"\n :divided=\"option.divided\"\n :no-title=\"option.title\"\n :hitting=\"option.hitting\"\n :selected=\"isSelected(option)\"\n no-hover\n @select=\"handleSelect(option)\"\n @mousemove=\"updateHitting(index, false)\"\n >\n <slot :option=\"option\" :index=\"index\" :selected=\"isSelected(option)\">\n <Renderer\n :renderer=\"props.slots.default\"\n :data=\"{ option, index, selected: isSelected(option) }\"\n >\n <span\n :class=\"nh.be('label')\"\n :style=\"{ paddingInlineStart: `${option.depth * 6}px` }\"\n >\n {{ option.label }}\n </span>\n <Transition v-if=\"props.optionCheck\" :name=\"nh.ns('fade')\" appear>\n <Icon\n v-if=\"isSelected(option)\"\n v-bind=\"icons.check\"\n :class=\"nh.be('check')\"\n ></Icon>\n </Transition>\n </Renderer>\n </slot>\n </Option>\n </template>\n <template #empty>\n <div :class=\"nh.be('empty')\">\n <slot name=\"empty\">\n <Renderer :renderer=\"props.slots.empty\">\n {{ props.emptyText ?? locale.empty }}\n </Renderer>\n </slot>\n </div>\n </template>\n </VirtualList>\n <slot v-if=\"slots.append || props.slots.append\" name=\"append\">\n <Renderer :renderer=\"props.slots.append\"></Renderer>\n </slot>\n </div>\n </Renderer>\n </slot>\n </Popper>\n </div>\n</template>\n"],"names":["isSameValue","newValue","oldValue","isNewArray","isOldArray","len","isNull","idFor","labelId","state","disabled","loading","size","validateField","clearField","getFieldValue","setFieldValue","useFieldStore","focus","nh","useNameHelper","_props","__props","props","useProps","createSizeProp","createStateProp","createIconProp","value","placementWhileList","emit","__emit","slots","_useSlots","locale","useLocale","toRef","icons","useIcons","currentVisible","ref","currentLabels","currentValues","currentIndex","placement","transfer","baseOptions","currentFilter","anchorWidth","userOptions","restTagCount","restTipShow","composing","isMounted","useMounted","dynamicOption","reactive","optionValues","hittingOption","optionStates","computed","visibleOptions","keyConfig","defaultKeyConfig","wrapper","useClickOutside","handleClickOutside","nativeInput","filterInput","device","virtualList","popper","restTip","input","_a","reference","transferTo","updatePopper","usePopper","isHover","useHover","cachedSelected","optionValueMap","emittedValue","updateTrigger","watchEffect","i","watch","initOptionState","valueKey","labelKey","disabledKey","dividedKey","titleKey","groupKey","childrenKey","oldMap","map","states","loop","option","depth","parent","rawOption","group","label","divided","title","children","oldState","optionState","child","initValueAndLabel","useModifier","event","modifier","toggleVisible","setVisible","decide","options","length","step","index","updateHitting","handleSelect","totalOptions","showDynamic","className","readonly","selectorClass","baseCls","hasPrefix","hasValue","normalOptions","optionParentMap","showClear","previewOption","limited","showPlaceholder","getOptionFromMap","fitPopperWidth","initHittingIndex","syncInputValue","filterOptions","__expose","isSelected","handleClear","_b","onMounted","normalizedValue","valueSet","selectedValues","selectedLabels","cachedValue","visible","emitEvent","hitting","ensureInView","inputValue","filter","ignoreCaseValue","parentMap","handleTagClose","handleRestTagClose","selected","item","removeArrayItem","newOption","handleChange","v","prevValue","focused","handleFocus","handleBlur","handleFilterInput","hittingIndex","getRangeWidth","handleCompositionEnd","handleFilterKeyDown","getLast","toggleShowRestTip","nextTick","_createElementBlock","_unref","toAttrValue","_createElementVNode","_cache","$event","_normalizeStyle","_renderSlot","_ctx","_createVNode","Renderer","Icon","_normalizeClass","_Fragment","Overflow","_withCtx","Tag","_createTextVNode","_toDisplayString","count","_createBlock","Tooltip","NativeScroll","_openBlock","_renderList","_mergeProps","_Transition","Popper","Input","_normalizeProps","_guardReactiveProps","VirtualList","Option"],"mappings":"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