Feat/select (#381)

* fix: resove conflict

* feat: select basic usage

* feat: select basic usage

* feat: select feature create item

* fix: fix option data insert

* refactor: select

* fix: fix parse error

* feat: select test

* fix: select add popper

* fix: update select option

* fix: add select dependency

* fix: add index.ts file

* fix(select): clean up

* fix(select): some refactor

* fix(select): some update

* fix(select): fix all test cases

Co-authored-by: helen <yinhelen.hlj@qq.com>
This commit is contained in:
Herrington Darkholme 2020-10-03 19:07:02 +08:00 committed by GitHub
parent 62f1135768
commit ff4d4d89da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2045 additions and 4 deletions

View File

@ -40,6 +40,7 @@ import ElDrawer from '@element-plus/drawer'
import ElForm from '@element-plus/form'
import ElUpload from '@element-plus/upload'
import ElTree from '@element-plus/tree'
import ElSelect from '@element-plus/select'
export {
ElAlert,
@ -82,6 +83,7 @@ export {
ElForm,
ElUpload,
ElTree,
ElSelect,
}
const install = (app: App): void => {
@ -125,6 +127,7 @@ const install = (app: App): void => {
ElDrawer(app)
ElUpload(app)
ElTree(app)
ElSelect(app)
}
const elementUI = {

View File

@ -39,6 +39,7 @@
"@element-plus/time-picker": "^0.0.0",
"@element-plus/tabs": "^0.0.0",
"@element-plus/form": "^0.0.0",
"@element-plus/tree": "^0.0.0"
"@element-plus/tree": "^0.0.0",
"@element-plus/select": "^0.0.0"
}
}

View File

@ -245,6 +245,9 @@ export default function (props: IPopperOptions, { emit }: SetupContext<string[]>
})
return {
update() {
popperInstance.update()
},
doDestroy,
show,
hide,

View File

@ -0,0 +1,493 @@
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import Select from '../src/select.vue'
import Option from '../src/option.vue'
interface SelectProps {
filterMethod?: any
remoteMethod?: any
multiple?: boolean
clearable?: boolean
filterable?: boolean
allowCreate?: boolean
remote?: boolean
collapseTags?: boolean
automaticDropdown?: boolean
multipleLimit?: number
popperClass?: string
}
const _mount = (template: string, data: any = () => ({}), otherObj?): any => mount({
components: {
'el-select': Select,
'el-option': Option,
},
template,
data,
...otherObj,
})
function getOptions(): HTMLElement[] {
return document.querySelectorAll('.el-popper__mask:last-child .el-select-dropdown__item') as any
}
const getSelectVm = (configs: SelectProps = {}, options?) => {
['multiple', 'clearable', 'filterable', 'allowCreate', 'remote', 'collapseTags', 'automaticDropdown'].forEach(config => {
configs[config] = configs[config] || false
})
configs.multipleLimit = configs.multipleLimit || 0
if (!options) {
options = [{
value: '选项1',
label: '黄金糕',
disabled: false,
}, {
value: '选项2',
label: '双皮奶',
disabled: false,
}, {
value: '选项3',
label: '蚵仔煎',
disabled: false,
}, {
value: '选项4',
label: '龙须面',
disabled: false,
}, {
value: '选项5',
label: '北京烤鸭',
disabled: false,
}]
}
return _mount(`
<el-select
ref="select"
v-model="value"
:multiple="multiple"
:multiple-limit="multipleLimit"
:popper-class="popperClass"
:clearable="clearable"
:filterable="filterable"
:collapse-tags="collapseTags"
:allow-create="allowCreate"
:filterMethod="filterMethod"
:remote="remote"
:loading="loading"
:remoteMethod="remoteMethod"
:automatic-dropdown="automaticDropdown">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:disabled="item.disabled"
:value="item.value">
</el-option>
</el-select>
`, () => ({
options,
multiple: configs.multiple,
multipleLimit: configs.multipleLimit,
clearable: configs.clearable,
filterable: configs.filterable,
collapseTags: configs.collapseTags,
allowCreate: configs.allowCreate,
popperClass: configs.popperClass,
automaticDropdown: configs.automaticDropdown,
loading: false,
filterMethod: configs.filterMethod,
remote: configs.remote,
remoteMethod: configs.remoteMethod,
value: configs.multiple ? [] : '',
}))
}
describe('Select', () => {
test('create', async () => {
const wrapper = _mount(`<el-select v-model="value"></el-select>`, () => ({ value: '' }))
expect(wrapper.classes()).toContain('el-select')
expect(wrapper.find('.el-input__inner').element.placeholder).toBe('Select')
const select = wrapper.findComponent({ name: 'ElSelect' })
wrapper.trigger('click')
expect((select.vm as any).visible).toBe(true)
})
test('options rendered correctly', () => {
const wrapper = getSelectVm()
const options = wrapper.element.querySelectorAll('.el-select-dropdown__item')
const result = [].every.call(options, (option, index) => {
const text = option.querySelector('span').textContent
const vm = wrapper.vm as any
return text === vm.options[index].label
})
expect(result).toBe(true)
})
test('custom dropdown class', () => {
const wrapper = getSelectVm({ popperClass: 'custom-dropdown' })
const dropdown = wrapper.findComponent({ name: 'ElSelectDropdown' })
expect(dropdown.classes()).toContain('custom-dropdown')
})
test('default value', async() => {
const wrapper = _mount(`
<el-select v-model="value">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
</el-option>
</el-select>
`,
() => ({
options: [{
value: '选项1',
label: '黄金糕',
}, {
value: '选项2',
label: '双皮奶',
}],
value: '选项2',
}))
await wrapper.vm.$nextTick()
expect(wrapper.find('.el-input__inner').element.value).toBe('双皮奶')
})
test('single select', async () => {
const wrapper = _mount(`
<el-select v-model="value" @change="handleChange">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
<p>{{item.label}} {{item.value}}</p>
</el-option>
</el-select>
`,
() => ({
options: [{
value: '选项1',
label: '黄金糕',
}, {
value: '选项2',
label: '双皮奶',
}, {
value: '选项3',
label: '蚵仔煎',
}, {
value: '选项4',
label: '龙须面',
}, {
value: '选项5',
label: '北京烤鸭',
}],
value: '',
count: 0,
}),
{
methods: {
handleChange() {
this.count++
},
},
})
await wrapper.find('.select-trigger').trigger('click')
const options = getOptions()
const vm = wrapper.vm as any
expect(vm.value).toBe('')
options[2].click()
await nextTick()
expect(vm.value).toBe('选项3')
expect(vm.count).toBe(1)
await nextTick()
options[4].click()
expect(vm.value).toBe('选项5')
expect(vm.count).toBe(2)
})
test('disabled option', async() => {
const wrapper = getSelectVm()
const vm = wrapper.vm as any
wrapper.find('.select-trigger').trigger('click')
vm.options[1].disabled = true
await nextTick()
const options = getOptions()
expect(options[1].className).toContain('is-disabled')
options[1].click()
await nextTick()
expect(vm.value).toBe('')
})
test('disabled select', () => {
const wrapper = _mount(`<el-select disabled></el-select>`)
expect(wrapper.find('.el-input').classes()).toContain('is-disabled')
})
test('visible event', async() => {
const wrapper = _mount(`
<el-select v-model="value" @visible-change="handleVisibleChange">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
</el-option>
</el-select>`,
() => ({
options: [],
value: '',
visible: '',
}),
{
methods: {
handleVisibleChange(val) {
this.visible = val
},
},
})
const select = wrapper.findComponent({ name: 'ElSelect' })
const vm = wrapper.vm as any
const selectVm = select.vm as any
selectVm.visible = true
await selectVm.$nextTick()
expect(vm.visible).toBe(true)
})
test('keyboard operations', async() => {
const wrapper = getSelectVm()
const select = wrapper.findComponent({ name: 'ElSelect' })
const vm = select.vm as any
let i = 8
while (i--) {
vm.navigateOptions('next')
}
vm.navigateOptions('prev')
await vm.$nextTick()
expect(vm.hoverIndex).toBe(0)
vm.selectOption()
await vm.$nextTick()
expect((wrapper.vm as any).value).toBe('选项1')
})
test('clearable', async () => {
const wrapper = getSelectVm({ clearable: true })
const select = wrapper.findComponent({ name: 'ElSelect' })
const vm = wrapper.vm as any
const selectVm = select.vm as any
vm.value = '选项1'
await vm.$nextTick()
selectVm.inputHovering = true
await selectVm.$nextTick()
const iconClear = wrapper.find('.el-input__icon.el-icon-circle-close')
expect(iconClear.exists()).toBe(true)
await iconClear.trigger('click')
expect(vm.value).toBe('')
})
test('allow create', async () => {
const wrapper = getSelectVm({ filterable: true, allowCreate: true })
const select = wrapper.findComponent({ name: 'ElSelect' })
const selectVm = select.vm as any
const input = wrapper.find('input')
await input.trigger('focus')
selectVm.selectedLabel = 'new'
selectVm.debouncedOnInputChange()
await selectVm.$nextTick()
const options = wrapper.findComponent({ name: 'ElSelect' }).findComponent({ ref: 'popper' }).findAll('.el-select-dropdown__item span')
const target = options.filter(option => option.text() === 'new')
await target[0].trigger('click')
expect((wrapper.vm as any).value).toBe('new')
})
test('multiple select', async () => {
const wrapper = getSelectVm({ multiple: true })
await wrapper.find('.select-trigger').trigger('click')
const options = getOptions()
const vm = wrapper.vm as any
vm.value = ['选项1']
vm.$nextTick()
options[1].click()
await nextTick()
options[3].click()
await nextTick()
expect(vm.value.indexOf('选项2') > -1 && vm.value.indexOf('选项4') > -1).toBe(true)
const tagCloseIcons = wrapper.findAll('.el-tag__close')
await tagCloseIcons[0].trigger('click')
expect(vm.value.indexOf('选项1')).toBe(-1)
})
test('multiple remove-tag', async () => {
const wrapper = _mount(`
<el-select v-model="value" multiple @remove-tag="handleRemoveTag">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
<p>{{item.label}} {{item.value}}</p>
</el-option>
</el-select>
`,
() => ({
options: [{
value: '选项1',
label: '黄金糕',
}, {
value: '选项2',
label: '双皮奶',
}, {
value: '选项3',
label: '蚵仔煎',
}, {
value: '选项4',
label: '龙须面',
}, {
value: '选项5',
label: '北京烤鸭',
}],
value: ['选项1', '选项2'],
}),
{
methods: {
handleRemoveTag() {
// pass
},
},
})
const vm = wrapper.vm as any
await vm.$nextTick()
expect(vm.value.length).toBe(2)
const tagCloseIcons = wrapper.findAll('.el-tag__close')
await tagCloseIcons[1].trigger('click')
expect(vm.value.length).toBe(1)
await tagCloseIcons[0].trigger('click')
expect(vm.value.length).toBe(0)
})
test('multiple limit', async () => {
const wrapper = getSelectVm({ multiple: true, multipleLimit: 1 })
const vm = wrapper.vm as any
await wrapper.find('.select-trigger').trigger('click')
const options = getOptions()
options[1].click()
await nextTick()
expect(vm.value.indexOf('选项2') > -1).toBe(true)
options[3].click()
await nextTick()
expect(vm.value.indexOf('选项4')).toBe(-1)
})
test('event:focus & blur', async () => {
const handleFocus = jest.fn()
const handleBlur = jest.fn()
const wrapper = _mount(`<el-select
@focus="handleFocus"
@blur="handleBlur" />`, () => ({
handleFocus,
handleBlur,
}))
const select = wrapper.findComponent(({ name: 'ElSelect' }))
const input = select.find('input')
expect(input.exists()).toBe(true)
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalled()
await input.trigger('blur')
expect(handleBlur).toHaveBeenCalled()
})
test('should not open popper when automatic-dropdown not set', async () => {
const wrapper = getSelectVm()
const select = wrapper.findComponent({ name: 'ElSelect' })
await select.findComponent({ ref: 'reference' })
.find('input')
.element.focus()
expect((select.vm as any).visible).toBe(false)
})
test('should open popper when automatic-dropdown is set', async () => {
const wrapper = getSelectVm({ automaticDropdown: true })
const select = wrapper.findComponent({ name: 'ElSelect' })
await select.findComponent({ ref: 'reference' }).find('input').trigger('focus')
expect((select.vm as any).visible).toBe(true)
})
test('only emit change on user input', async () => {
let callCount = 0
const wrapper = _mount(`
<el-select v-model="value" @change="change" ref="select">
<el-option label="1" value="1" />
<el-option label="2" value="2" />
<el-option label="3" value="3" />
</el-select>`,
() => ({
value: '1',
change: () => ++callCount,
}))
expect(callCount).toBe(0)
await wrapper.find('.select-trigger').trigger('click')
const options = getOptions()
options[2].click()
expect(callCount).toBe(1)
})
test('render slot `empty`', async () => {
const wrapper = _mount(`
<el-select v-model="value">
<div class="empty-slot" slot="empty">EmptySlot</div>
</el-select>`,
() => ({
value: '1',
}))
await wrapper.find('.select-trigger').trigger('click')
expect(document.querySelector('.empty-slot').textContent).toBe('EmptySlot')
})
test('should set placeholder to label of selected option when filterable is true and multiple is false', async() => {
const wrapper = _mount(`
<el-select ref="select" v-model="value" filterable>
<el-option label="test" value="test" />
</el-select>`,
() => ({ value: 'test' }))
const vm = wrapper.vm as any
await wrapper.trigger('click')
const selectVm = wrapper.findComponent({ name: 'ElSelect' }).vm as any
expect(selectVm.visible).toBe(true)
expect(wrapper.find('.el-input__inner').element.placeholder).toBe('test')
expect(vm.value).toBe('test')
})
test('default value is null or undefined', async() => {
const wrapper = _mount(`
<el-select v-model="value">
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
</el-option>
</el-select>`,
() => ({
options: [{
value: '选项1',
label: '黄金糕',
}, {
value: '选项2',
label: '双皮奶',
}],
value: undefined,
}))
const vm = wrapper.vm as any
vm.value = null
await vm.$nextTick()
expect(wrapper.find('.el-input__inner').element.value).toBe('')
vm.value = '选项1'
await vm.$nextTick()
expect(wrapper.find('.el-input__inner').element.value).toBe('黄金糕')
})
})

10
packages/select/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { App } from 'vue'
import Select from './src/select.vue'
import OptionGroup from './src/option-group.vue'
import Option from './src/option.vue'
export default (app: App): void => {
app.component(Select.name, Select)
app.component(OptionGroup.name, OptionGroup)
app.component(Option.name, Option)
}

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/select",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.3"
}
}

View File

@ -0,0 +1,56 @@
<template>
<ul v-show="visible" class="el-select-group__wrap">
<li class="el-select-group__title">{{ label }}</li>
<li>
<ul class="el-select-group">
<slot></slot>
</ul>
</li>
</ul>
</template>
<script lang="ts">
import {
defineComponent,
provide,
inject,
ref,
reactive, toRefs,
} from 'vue'
import {
selectGroupKey, selectKey,
selectEvents,
} from './token'
export default defineComponent({
name: 'ElOptionGroup',
componentName: 'ElOptionGroup',
props: {
label: String,
disabled: {
type: Boolean,
default: false,
},
},
setup(props) {
const visible = ref(true)
provide(selectGroupKey, reactive({
...toRefs(props),
}))
const select = inject(selectKey)
const queryChange = () => {
visible.value = select?.options?.some(option => option.visible === true )
}
select.selectEmitter.on(selectEvents.queryChange, queryChange)
return {
visible,
}
},
})
</script>

View File

@ -0,0 +1,104 @@
<template>
<li
v-show="visible"
class="el-select-dropdown__item"
:class="{
'selected': itemSelected,
'is-disabled': isDisabled,
'hover': hover}"
@mouseenter="hoverItem"
@click.stop="selectOptionClick"
>
<slot>
<span>{{ currentLabel }}</span>
</slot>
</li>
</template>
<script lang="ts">
import {
toRefs,
defineComponent,
getCurrentInstance,
onBeforeUnmount,
reactive,
} from 'vue'
import { useOption } from './useOption'
export default defineComponent({
name: 'ElOption',
componentName: 'ElOption',
props: {
value: {
required: true,
},
label: [String, Number],
created: Boolean,
disabled: {
type: Boolean,
default: false,
},
},
setup(props, ctx) {
const states = reactive({
index: -1,
groupDisabled: false,
visible: true,
hitState: false,
hover: false,
})
const {
currentLabel,
itemSelected,
isDisabled,
select,
hoverItem,
} = useOption(props, states, ctx)
const {
visible,
hover,
} = toRefs(states)
const vm = getCurrentInstance().proxy
onBeforeUnmount(() => {
const { selected, multiple } = select
let selectedOptions = multiple ? selected : [selected]
let index = select.cachedOptions.indexOf(vm)
let selectedIndex = selectedOptions?.indexOf(vm)
// if option is not selected, remove it from cache
if (index > -1 && selectedIndex < 0) {
select.cachedOptions.splice(index, 1)
}
select.onOptionDestroy(select.options.map(item => item.value).indexOf(props.value))
})
select.options.push(vm)
select.cachedOptions.push(vm)
function selectOptionClick() {
if (props.disabled !== true && states.groupDisabled !== true) {
select.handleOptionSelect(vm, true)
}
}
return {
currentLabel,
itemSelected,
isDisabled,
select,
hoverItem,
visible,
hover,
selectOptionClick,
}
},
})
</script>

View File

@ -0,0 +1,46 @@
<template>
<div
class="el-select-dropdown"
:class="[{ 'is-multiple': isMultiple }, popperClass]"
:style="{ minWidth: minWidth }"
>
<slot></slot>
</div>
</template>
<script lang="ts">
import {
computed,
onMounted,
inject,
} from 'vue'
import {
selectKey,
} from './token'
export default {
name: 'ElSelectDropdown',
componentName: 'ElSelectDropdown',
setup() {
const select = inject(selectKey)
// computed
const popperClass = computed(() => select.props.popperClass)
const isMultiple = computed(() => select.props.multiple)
const minWidth = computed(() => select.selectWrapper?.getBoundingClientRect().width + 'px')
onMounted(() => {
// TODO: updatePopper
// popper.value.update()
})
return {
minWidth,
popperClass,
isMultiple,
}
},
}
</script>

View File

@ -0,0 +1,433 @@
<template>
<div
ref="selectWrapper"
class="el-select"
:class="[selectSize ? 'el-select--' + selectSize : '']"
@click.stop="toggleMenu"
>
<el-popper
ref="popper"
v-model:visible="dropMenuVisible"
placement="bottom-start"
:show-arrow="true"
:append-to-body="popperAppendToBody"
pure
manual-mode
effect="light"
trigger="click"
:offset="6"
>
<template #trigger>
<div class="select-trigger">
<div
v-if="multiple"
ref="tags"
class="el-select__tags"
:style="{ 'max-width': inputWidth - 32 + 'px', width: '100%' }"
>
<span v-if="collapseTags && selected.length">
<el-tag
:closable="!selectDisabled"
:size="collapseTagSize"
:hit="selected[0].hitState"
type="info"
disable-transitions
@close="deleteTag($event, selected[0])"
>
<span class="el-select__tags-text">{{ selected[0].currentLabel }}</span>
</el-tag>
<el-tag
v-if="selected.length > 1"
:closable="false"
:size="collapseTagSize"
type="info"
disable-transitions
>
<span class="el-select__tags-text">+ {{ selected.length - 1 }}</span>
</el-tag>
</span>
<!-- <div> -->
<transition v-if="!collapseTags" @after-leave="resetInputHeight">
<span>
<el-tag
v-for="item in selected"
:key="getValueKey(item)"
:closable="!selectDisabled"
:size="collapseTagSize"
:hit="item.hitState"
type="info"
disable-transitions
@close="deleteTag($event, item)"
>
<span class="el-select__tags-text">{{ item.currentLabel }}</span>
</el-tag>
</span>
</transition>
<!-- </div> -->
<input
v-if="filterable"
ref="input"
v-model="query"
type="text"
class="el-select__input"
:class="[selectSize ? `is-${ selectSize }` : '']"
:disabled="selectDisabled"
:autocomplete="autocomplete"
:style="{ 'flex-grow': '1', width: inputLength / (inputWidth - 32) + '%', 'max-width': inputWidth - 42 + 'px' }"
@focus="handleFocus"
@blur="softFocus = false"
@keyup="managePlaceholder"
@keydown="resetInputState"
@keydown.down.prevent="navigateOptions('next')"
@keydown.up.prevent="navigateOptions('prev')"
@keydown.esc.stop.prevent="visible = false"
@keydown.enter.stop.prevent="selectOption"
@keydown.delete="deletePrevTag"
@keydown.tab="visible = false"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="debouncedQueryChange"
>
</div>
<el-input
:id="id"
ref="reference"
v-model="selectedLabel"
type="text"
:placeholder="currentPlaceholder"
:name="name"
:autocomplete="autocomplete"
:size="selectSize"
:disabled="selectDisabled"
:readonly="readonly"
:validate-event="false"
:class="{ 'is-focus': visible }"
:tabindex="(multiple && filterable) ? '-1' : null"
@focus="handleFocus"
@blur="handleBlur"
@input="debouncedOnInputChange"
@paste="debouncedOnInputChange"
@keydown.down.stop.prevent="navigateOptions('next')"
@keydown.up.stop.prevent="navigateOptions('prev')"
@keydown.enter.stop.prevent="selectOption"
@keydown.esc.stop.prevent="visible = false"
@keydown.tab="visible = false"
@mouseenter="inputHovering = true"
@mouseleave="inputHovering = false"
>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix"></slot>
</template>
<template #suffix>
<i v-show="!showClose" :class="['el-select__caret', 'el-input__icon', 'el-icon-' + iconClass]"></i>
<i v-if="showClose" class="el-select__caret el-input__icon el-icon-circle-close" @click="handleClearClick"></i>
</template>
</el-input>
</div>
</template>
<template #default>
<transition
name="el-zoom-in-top"
@before-enter="handleMenuEnter"
@after-leave="doDestroy"
>
<el-select-menu
v-show="visible && emptyText !== false"
ref="popper"
v-clickOutside="handleClose"
>
<el-scrollbar
v-show="options.length > 0 && !loading"
ref="scrollbar"
tag="ul"
wrap-class="el-select-dropdown__wrap"
view-class="el-select-dropdown__list"
:class="{ 'is-empty': !allowCreate && query && filteredOptionsCount === 0 }"
>
<el-option
v-if="showNewOption"
:value="query"
:created="true"
/>
<slot></slot>
</el-scrollbar>
<template v-if="emptyText && (!allowCreate || loading || (allowCreate && options.length === 0 ))">
<slot v-if="$slots.empty" name="empty"></slot>
<p v-else class="el-select-dropdown__empty">
{{ emptyText }}
</p>
</template>
</el-select-menu>
</transition>
</template>
</el-popper>
</div>
</template>
<script lang="ts">
import ElInput from '@element-plus/input/src/index.vue'
import ElOption from './option.vue'
import ElSelectMenu from './select-dropdown.vue'
import ElTag from '@element-plus/tag/src/index.vue'
import { Popper as ElPopper } from '@element-plus/popper'
import ElScrollbar from '@element-plus/scrollbar/src/index'
import ClickOutside from '@element-plus/directives/click-outside'
import { addResizeListener, removeResizeListener } from '@element-plus/utils/resize-event'
import { t } from '@element-plus/locale'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import { useSelect, useSelectStates } from './useSelect'
import { selectKey } from './token'
import {
toRefs,
defineComponent,
onMounted,
onBeforeUnmount,
nextTick,
reactive,
provide,
} from 'vue'
export default defineComponent({
name: 'ElSelect',
componentName: 'ElSelect',
components: {
ElInput,
ElSelectMenu,
ElOption,
ElTag,
ElScrollbar,
ElPopper,
},
directives: { ClickOutside },
props: {
name: String,
id: String,
modelValue: {
type: [Array, String],
},
autocomplete: {
type: String,
default: 'off',
},
automaticDropdown: Boolean,
size: String,
disabled: Boolean,
clearable: Boolean,
filterable: Boolean,
allowCreate: Boolean,
loading: Boolean,
popperClass: String,
remote: Boolean,
loadingText: String,
noMatchText: String,
noDataText: String,
remoteMethod: Function,
filterMethod: Function,
multiple: Boolean,
multipleLimit: {
type: Number,
default: 0,
},
placeholder: {
type: String,
default: t('el.select.placeholder'),
},
defaultFirstOption: Boolean,
reserveKeyword: Boolean,
valueKey: {
type: String,
default: 'value',
},
collapseTags: Boolean,
popperAppendToBody: {
type: Boolean,
default: true,
},
},
emits: ['remove-tag', 'clear', 'change', 'visible-change', 'focus', 'blur', UPDATE_MODEL_EVENT],
setup(props, ctx) {
const states = useSelectStates(props)
const {
selectSize,
readonly,
handleResize,
collapseTagSize,
debouncedOnInputChange,
debouncedQueryChange,
deletePrevTag,
deleteTag,
deleteSelected,
handleOptionSelect,
scrollToOption,
setSelected,
resetInputHeight,
managePlaceholder,
showClose,
selectDisabled,
iconClass,
showNewOption,
emptyText,
toggleLastOptionHitState,
resetInputState,
handleComposition,
onOptionDestroy,
handleMenuEnter,
handleFocus,
blur,
handleBlur,
handleClearClick,
doDestroy,
handleClose,
toggleMenu,
selectOption,
getValueKey,
navigateOptions,
dropMenuVisible,
reference,
input,
popper,
tags,
selectWrapper,
scrollbar,
} = useSelect(props, states, ctx)
const {
inputWidth,
selected,
inputLength,
filteredOptionsCount,
visible,
softFocus,
selectedLabel,
hoverIndex,
query,
inputHovering,
currentPlaceholder,
menuVisibleOnFocus,
isOnComposition,
isSilentBlur,
options,
cachedOptions,
optionsCount,
} = toRefs(states)
provide(selectKey, reactive({
options,
cachedOptions,
optionsCount,
filteredOptionsCount,
hoverIndex,
handleOptionSelect,
selectEmitter: states.selectEmitter,
onOptionDestroy,
props,
inputWidth,
selectWrapper,
popper,
}))
onMounted(() => {
states.cachedPlaceHolder = currentPlaceholder.value = props.placeholder
if (props.multiple && Array.isArray(props.modelValue) && props.modelValue.length > 0) {
currentPlaceholder.value = ''
}
addResizeListener(selectWrapper.value, handleResize)
if (reference.value && reference.value.$el) {
const sizeMap = {
medium: 36,
small: 32,
mini: 28,
}
const input = reference.value.$el
states.initialInputHeight = input.getBoundingClientRect().height || sizeMap[selectSize.value]
}
if (props.remote && props.multiple) {
resetInputHeight()
}
nextTick(() => {
if (reference.value.$el) {
inputWidth.value = reference.value.$el.getBoundingClientRect().width
}
})
setSelected()
})
onBeforeUnmount(() => {
if (selectWrapper.value && handleResize) removeResizeListener(selectWrapper.value, handleResize)
})
if (props.multiple && !Array.isArray(props.modelValue)) {
ctx.emit(UPDATE_MODEL_EVENT, [])
}
if (!props.multiple && Array.isArray(props.modelValue)) {
ctx.emit(UPDATE_MODEL_EVENT, '')
}
return {
selectSize,
readonly,
handleResize,
collapseTagSize,
debouncedOnInputChange,
debouncedQueryChange,
deletePrevTag,
deleteTag,
deleteSelected,
handleOptionSelect,
scrollToOption,
inputWidth,
selected,
inputLength,
filteredOptionsCount,
visible,
softFocus,
selectedLabel,
hoverIndex,
query,
inputHovering,
currentPlaceholder,
menuVisibleOnFocus,
isOnComposition,
isSilentBlur,
options,
resetInputHeight,
managePlaceholder,
showClose,
selectDisabled,
iconClass,
showNewOption,
emptyText,
toggleLastOptionHitState,
resetInputState,
handleComposition,
handleMenuEnter,
handleFocus,
blur,
handleBlur,
handleClearClick,
doDestroy,
handleClose,
toggleMenu,
selectOption,
getValueKey,
navigateOptions,
dropMenuVisible,
reference,
input,
popper,
tags,
selectWrapper,
scrollbar,
}
},
})
</script>
<style lang="scss" scoped>
.el-popper {
padding: 0;
}
</style>

View File

@ -0,0 +1,39 @@
import type { InjectionKey } from 'vue'
import type { Emitter } from 'mitt'
interface SelectGroupContext {
disabled: boolean
}
export interface SelectContext {
props: {
multiple: boolean
value: unknown[]
multipleLimit: number
valueKey: string
modelValue: unknown[]
popperClass: string
}
selectWrapper: HTMLElement
cachedOptions: any[]
selected: any | any[]
multiple: boolean
hoverIndex: number
setSelected(): void
valueKey: string
remote: boolean
optionsCount: number
filteredOptionsCount: number
options: unknown[]
selectEmitter: Emitter
onOptionDestroy(i: number)
handleOptionSelect(vm: unknown, byClick: boolean)
}
export const selectGroupKey: InjectionKey<SelectGroupContext> = Symbol('SelectGroup')
export const selectKey: InjectionKey<SelectContext> = Symbol('Select')
export const selectEvents = {
queryChange: 'elOptionGroupQueryChange',
}

View File

@ -0,0 +1,117 @@
import {
inject,
computed,
getCurrentInstance,
watch,
} from 'vue'
import { getValueByPath, escapeRegexpString } from '@element-plus/utils/util'
import {
selectKey, selectGroupKey,
selectEvents,
} from './token'
export function useOption(props, states, ctx) {
// inject
const select = inject(selectKey)
const selectGroup = inject(selectGroupKey, {})
// computed
const isObject = computed(() => {
return Object.prototype.toString.call(props.value).toLowerCase() === '[object object]'
})
const itemSelected = computed(() => {
if (!select.props.multiple) {
return isEqual(props.value, select.props.modelValue)
} else {
return contains(select.props.modelValue, props.value)
}
})
const limitReached = computed(() => {
if (select.props.multiple) {
return !itemSelected.value &&
(select.props.value || []).length >= select.props.multipleLimit &&
select.props.multipleLimit > 0
} else {
return false
}
})
const currentLabel = computed(() => {
return props.label || (isObject.value ? '' : props.value)
})
const currentValue = computed(() => {
return props.value || props.label || ''
})
const isDisabled = computed(() => {
return props.disabled || states.groupDisabled || limitReached.value
})
const instance = getCurrentInstance()
select.optionsCount++
select.filteredOptionsCount++
const contains = (arr = [], target) => {
if (!isObject.value) {
return arr && arr.indexOf(target) > -1
} else {
const valueKey = select.props.valueKey
return arr && arr.some(item => {
return getValueByPath(item, valueKey) === getValueByPath(target, valueKey)
})
}
}
const isEqual = (a: unknown, b: unknown) => {
if (!isObject.value) {
return a === b
} else {
const valueKey = select.valueKey
return getValueByPath(a, valueKey) === getValueByPath(b, valueKey)
}
}
const hoverItem = () => {
if (!props.disabled && !selectGroup.disabled) {
select.hoverIndex = select.options.indexOf(instance)
}
}
const queryChange = (query: string) => {
const regexp = new RegExp(escapeRegexpString(query), 'i')
states.visible = regexp.test(currentLabel.value) || props.created
if (!states.visible) {
select.filteredOptionsCount--
}
}
watch(() => currentLabel.value, () => {
if (!props.created && !select.remote) select.setSelected()
})
watch(() => props.value, (val, oldVal) => {
const { remote, valueKey } = select
if (!props.created && !remote) {
if (valueKey && typeof val === 'object' && typeof oldVal === 'object' && val[valueKey] === oldVal[valueKey]) {
return
}
select.setSelected()
}
})
// Emitter
select.selectEmitter.on(selectEvents.queryChange, queryChange)
return {
select,
currentLabel,
currentValue,
itemSelected,
isDisabled,
hoverItem,
}
}

View File

@ -0,0 +1,725 @@
import mitt from 'mitt'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import { t } from '@element-plus/locale'
import isServer from '@element-plus/utils/isServer'
import scrollIntoView from '@element-plus/utils/scroll-into-view'
import { debounce as lodashDebounce } from 'lodash'
import { isKorean } from '@element-plus/utils/isDef'
import {
inject,
nextTick,
computed,
watch,
ref,
reactive,
} from 'vue'
import {
getValueByPath,
isEqual,
isIE,
isEdge,
} from '@element-plus/utils/util'
const ELEMENT = { size: 'medium' }
export function useSelectStates(props) {
const selectEmitter = mitt()
return reactive({
options: [],
cachedOptions: [],
createdLabel: null,
createdSelected: false,
selected: props.multiple ? [] : {} as any,
inputLength: 20,
inputWidth: 0,
initialInputHeight: 0,
optionsCount: 0,
filteredOptionsCount: 0,
visible: false,
softFocus: false,
selectedLabel: '',
hoverIndex: -1,
query: '',
previousQuery: null,
inputHovering: false,
cachedPlaceHolder: '',
currentPlaceholder: t('el.select.placeholder'),
menuVisibleOnFocus: false,
isOnComposition: false,
isSilentBlur: false,
selectEmitter,
})
}
type States = ReturnType<typeof useSelectStates>
export const useSelect = (props, states: States, ctx) => {
// template refs
const reference = ref(null)
const input = ref(null)
const popper = ref(null)
const tags = ref(null)
const selectWrapper = ref<HTMLElement|null>(null)
const scrollbar = ref(null)
const hoverOption = ref(-1)
// inject
const elForm = inject<any>('elForm', {})
const elFormItem = inject<any>('elFormItem', {})
// computed
const _elFormItemSize = computed(() => (elFormItem || {}).elFormItemSize)
const readonly = computed(() => !props.filterable || props.multiple || (!isIE() && !isEdge() && !states.visible))
const selectDisabled = computed(() => props.disabled || (elForm || {}).disabled)
const showClose = computed(() => {
const hasValue = props.multiple
? Array.isArray(props.modelValue) && props.modelValue.length > 0
: props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== ''
const criteria =
props.clearable &&
!selectDisabled.value &&
states.inputHovering &&
hasValue
return criteria
})
const iconClass = computed(() => props.remote && props.filterable ? '' : (states.visible ? 'arrow-up is-reverse' : 'arrow-up'))
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.query === '' && states.options.length === 0) return false
if (props.filterable && states.query && states.options.length > 0 && states.filteredOptionsCount === 0) {
return props.noMatchText || t('el.select.noMatch')
}
if (states.options.length === 0) {
return props.noDataText || t('el.select.noData')
}
}
return null
})
const showNewOption = computed(() => {
const hasExistingOption = states.options.filter(option => {
return !option.created
}).some(option => {
return option.currentLabel === states.query
})
return props.filterable && props.allowCreate && states.query !== '' && !hasExistingOption
})
// TODO: ELEMENT
const selectSize = computed(() => props.size || _elFormItemSize.value || (ELEMENT || {}).size)
const collapseTagSize = computed(() => ['small', 'mini'].indexOf(selectSize.value) > -1 ? 'mini' : 'small')
const dropMenuVisible = computed(() => states.visible && emptyText.value !== false)
// watch
watch(() => selectDisabled.value, () => {
nextTick(() => {
resetInputHeight()
})
})
watch(() => props.placeholder, val => {
states.cachedPlaceHolder = states.currentPlaceholder = val
})
watch(() => props.modelValue, (val, oldVal) => {
if (props.multiple) {
resetInputHeight()
if ((val && val.length > 0) || (input.value && states.query !== '')) {
states.currentPlaceholder = ''
} else {
states.currentPlaceholder = states.cachedPlaceHolder
}
if (props.filterable && !props.reserveKeyword) {
states.query = ''
handleQueryChange(states.query)
}
}
setSelected()
if (props.filterable && !props.multiple) {
states.inputLength = 20
}
if (!isEqual(val, oldVal)) {
elFormItem?.changeEvent?.(val)
}
})
watch(() => states.visible, val => {
if (!val) {
doDestroy()
input.value && input.value.blur()
states.query = ''
states.previousQuery = null
states.selectedLabel = ''
states.inputLength = 20
states.menuVisibleOnFocus = false
resetHoverIndex()
nextTick(() => {
if (input.value && input.value.value === '' && states.selected.length === 0) {
states.currentPlaceholder = states.cachedPlaceHolder
}
})
if (!props.multiple) {
if (states.selected) {
if (props.filterable && props.allowCreate && states.createdSelected && states.createdLabel) {
states.selectedLabel = states.createdLabel
} else {
states.selectedLabel = states.selected.currentLabel
}
if (props.filterable) states.query = states.selectedLabel
}
if (props.filterable) {
states.currentPlaceholder = states.cachedPlaceHolder
}
}
} else {
popper.value?.update?.()
if (props.filterable) {
states.query = props.remote ? '' : states.selectedLabel
handleQueryChange(states.query)
if (props.multiple) {
input.value.focus()
} else {
if (!props.remote) {
states.selectEmitter.emit('elOptionQueryChange', '')
states.selectEmitter.emit('elOptionGroupQueryChange')
}
if (states.selectedLabel) {
states.currentPlaceholder = states.selectedLabel
states.selectedLabel = ''
}
}
}
}
ctx.emit('visible-change', val)
})
watch(() => states.options, () => {
if (isServer) return
popper.value?.update?.()
if (props.multiple) {
resetInputHeight()
}
const inputs = selectWrapper.value.querySelectorAll('input')
if ([].indexOf.call(inputs, document.activeElement) === -1) {
setSelected()
}
if (props.defaultFirstOption && (props.filterable || props.remote) && states.filteredOptionsCount) {
checkDefaultFirstOption()
}
})
watch(() => states.hoverIndex, val => {
if (typeof val === 'number' && val > -1) {
hoverOption.value = states.options[val] || {}
}
states.options.forEach(option => {
option.hover = hoverOption.value === option
})
})
// methods
const resetInputHeight = () => {
if (props.collapseTags && !props.filterable) return
nextTick(() => {
if (!reference.value) return
const inputChildNodes = reference.value.$el.childNodes
const input = [].filter.call(inputChildNodes, item => item.tagName === 'INPUT')[0]
const _tags = tags.value
const sizeInMap = states.initialInputHeight || 40
input.style.height = states.selected.length === 0
? sizeInMap + 'px'
: Math.max(
_tags ? (_tags.clientHeight + (_tags.clientHeight > sizeInMap ? 6 : 0)) : 0,
sizeInMap) + 'px'
if (states.visible && emptyText.value !== false) {
popper.value?.update?.()
}
})
}
const handleQueryChange = val => {
if (states.previousQuery === val || states.isOnComposition) return
if (
states.previousQuery === null &&
(typeof props.filterMethod === 'function' || typeof props.remoteMethod === 'function')
) {
states.previousQuery = val
return
}
states.previousQuery = val
nextTick(() => {
if (states.visible) popper.value?.update?.()
})
states.hoverIndex = -1
if (props.multiple && props.filterable) {
nextTick(() => {
const length = input.value.length * 15 + 20
states.inputLength = props.collapseTags ? Math.min(50, length) : length
managePlaceholder()
resetInputHeight()
})
}
if (props.remote && typeof props.remoteMethod === 'function') {
states.hoverIndex = -1
props.remoteMethod(val)
} else if (typeof props.filterMethod === 'function') {
props.filterMethod(val)
states.selectEmitter.emit('elOptionGroupQueryChange')
} else {
states.filteredOptionsCount = states.optionsCount
states.selectEmitter.emit('elOptionQueryChange', val)
states.selectEmitter.emit('elOptionGroupQueryChange')
}
if (props.defaultFirstOption && (props.filterable || props.remote) && states.filteredOptionsCount) {
checkDefaultFirstOption()
}
}
const managePlaceholder = () => {
if (states.currentPlaceholder !== '') {
states.currentPlaceholder = input.value ? '' : states.cachedPlaceHolder
}
}
const checkDefaultFirstOption = () => {
states.hoverIndex = -1
// highlight the created option
let hasCreated = false
for (let i = states.options.length - 1; i >= 0; i--) {
if (states.options[i].created) {
hasCreated = true
states.hoverIndex = i
break
}
}
if (hasCreated) return
for (let i = 0; i !== states.options.length; ++i) {
const option = states.options[i]
if (states.query) {
// highlight first options that passes the filter
if (!option.props.disabled && !option.props.groupDisabled && option.visible) {
states.hoverIndex = i
break
}
} else {
// highlight currently selected option
if (option.itemSelected) {
states.hoverIndex = i
break
}
}
}
}
const setSelected = () => {
if (!props.multiple) {
const option = getOption(props.modelValue)
if (option.props?.created) {
states.createdLabel = option.props.value
states.createdSelected = true
} else {
states.createdSelected = false
}
states.selectedLabel = option.currentLabel
states.selected = option
if (props.filterable) states.query = states.selectedLabel
return
}
const result = []
if (Array.isArray(props.modelValue)) {
props.modelValue.forEach(value => {
result.push(getOption(value))
})
}
states.selected = result
nextTick(() => {
resetInputHeight()
})
}
const getOption = value => {
let option
const isObject = Object.prototype.toString.call(props.modelValue).toLowerCase() === '[object object]'
const isNull = Object.prototype.toString.call(props.modelValue).toLowerCase() === '[object null]'
const isUndefined = Object.prototype.toString.call(props.modelValue).toLowerCase() === '[object undefined]'
for (let i = states.cachedOptions.length - 1; i >= 0; i--) {
const cachedOption = states.cachedOptions[i]
const isEqual = isObject
? getValueByPath(cachedOption.value, props.valueKey) === getValueByPath(props.modelValue, props.valueKey)
: cachedOption.value === value
if (isEqual) {
option = {
value,
currentLabel: cachedOption.currentLabel,
}
break
}
}
if (option) return option
const label = (!isObject && !isNull && !isUndefined) ? value : ''
const newOption = {
value: value,
currentLabel: label,
}
if (props.multiple) {
(newOption as any).hitState = false
}
return newOption
}
const resetHoverIndex = () => {
setTimeout(() => {
if (!props.multiple) {
states.hoverIndex = states.options.indexOf(states.selected)
} else {
if (states.selected.length > 0) {
states.hoverIndex = Math.min.apply(null, states.selected.map(item => states.options.indexOf(item)))
} else {
states.hoverIndex = -1
}
}
}, 300)
}
const handleResize = () => {
resetInputWidth()
if (props.multiple) resetInputHeight()
}
const resetInputWidth = () => {
states.inputWidth = reference.value?.$el.getBoundingClientRect().width
}
const onInputChange = () => {
if (props.filterable && states.query !== states.selectedLabel) {
states.query = states.selectedLabel
handleQueryChange(states.query)
}
}
const debouncedOnInputChange = lodashDebounce(() => {
onInputChange()
}, debounce.value)
const debouncedQueryChange = lodashDebounce(e => {
handleQueryChange(e.target.value)
}, debounce.value)
const emitChange = val => {
if (!isEqual(props.modelValue, val)) {
ctx.emit('change', val)
}
}
const deletePrevTag = e => {
if (e.target.value.length <= 0 && !toggleLastOptionHitState()) {
const value = props.modelValue.slice()
value.pop()
ctx.emit(UPDATE_MODEL_EVENT, value)
emitChange(value)
}
}
const deleteTag = (event, tag) => {
const index = states.selected.indexOf(tag)
if (index > -1 && !selectDisabled.value) {
const value = props.modelValue.slice()
value.splice(index, 1)
ctx.emit(UPDATE_MODEL_EVENT, value)
emitChange(value)
ctx.emit('remove-tag', tag.value)
}
event.stopPropagation()
}
const deleteSelected = event => {
event.stopPropagation()
const value = props.multiple ? [] : ''
ctx.emit(UPDATE_MODEL_EVENT, value)
emitChange(value)
states.visible = false
ctx.emit('clear')
}
const handleOptionSelect = (option, byClick) => {
if (props.multiple) {
const value = (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)
}
ctx.emit(UPDATE_MODEL_EVENT, value)
emitChange(value)
if (option.created) {
states.query = ''
handleQueryChange('')
states.inputLength = 20
}
if (props.filterable) input.value.focus()
} else {
ctx.emit(UPDATE_MODEL_EVENT, option.value)
emitChange(option.value)
states.visible = false
}
states.isSilentBlur = byClick
setSoftFocus()
if (states.visible) return
nextTick(() => {
scrollToOption(option)
})
}
const getValueIndex = (arr = [], value) => {
const isObject = Object.prototype.toString.call(value).toLowerCase() === '[object object]'
if (!isObject) {
return arr.indexOf(value)
} else {
const valueKey = props.valueKey
let index = -1
arr.some((item, i) => {
if (getValueByPath(item, valueKey) === getValueByPath(value, valueKey)) {
index = i
return true
}
return false
})
return index
}
}
const setSoftFocus = () => {
states.softFocus = true
const _input = input.value || reference.value
if (_input) {
_input.focus()
}
}
const scrollToOption = option => {
const target = Array.isArray(option) ? option[0]?.$el : option.$el
if (popper.value && target) {
const menu = popper.value?.$el?.querySelector?.('.el-select-dropdown__wrap')
if (menu) {
scrollIntoView(menu, target)
}
}
// TODO: handleScroll
// scrollbar.value?.handleScroll()
}
const onOptionDestroy = index => {
if (index > -1) {
states.optionsCount--
states.filteredOptionsCount--
states.options.splice(index, 1)
}
}
const resetInputState = e => {
if (e.keyCode !== 8) toggleLastOptionHitState(false)
states.inputLength = input.value.length * 15 + 20
resetInputHeight()
}
const toggleLastOptionHitState = (hit?: boolean) => {
if (!Array.isArray(states.selected)) return
const option = states.selected[states.selected.length - 1]
if (!option) return
if (hit === true || hit === false) {
option.hitState = hit
return hit
}
option.hitState = !option.hitState
return option.hitState
}
const handleComposition = event => {
const text = event.target.value
if (event.type === 'compositionend') {
states.isOnComposition = false
nextTick(() => handleQueryChange(text))
} else {
const lastCharacter = text[text.length - 1] || ''
states.isOnComposition = !isKorean(lastCharacter)
}
}
const handleMenuEnter = () => {
nextTick(() => scrollToOption(states.selected))
}
const handleFocus = event => {
if (!states.softFocus) {
if (props.automaticDropdown || props.filterable) {
states.visible = true
if (props.filterable) {
states.menuVisibleOnFocus = true
}
}
ctx.emit('focus', event)
} else {
states.softFocus = false
}
}
const blur = () => {
states.visible = false
reference.value.blur()
}
const handleBlur = event => {
// https://github.com/ElemeFE/element/pull/10822
nextTick(() => {
if (states.isSilentBlur) {
states.isSilentBlur = false
} else {
ctx.emit('blur', event)
}
})
states.softFocus = false
}
const handleClearClick = event => {
deleteSelected(event)
}
const doDestroy = () => {
popper.value?.doDestroy?.()
}
const handleClose = () => {
states.visible = false
}
const toggleMenu = () => {
if (props.automaticDropdown) return
if (!selectDisabled.value) {
if (states.menuVisibleOnFocus) {
states.menuVisibleOnFocus = false
} else {
states.visible = !states.visible
}
if (states.visible) {
(input.value || reference.value).focus()
}
}
}
const selectOption = () => {
if (!states.visible) {
toggleMenu()
} else {
if (states.options[states.hoverIndex]) {
handleOptionSelect(states.options[states.hoverIndex], undefined)
}
}
}
const getValueKey = item => {
if (Object.prototype.toString.call(item.value).toLowerCase() !== '[object object]') {
return item.value
} else {
return getValueByPath(item.value, this.valueKey)
}
}
const optionsAllDisabled = computed(() => states.options.filter(option => option.visible).every(option => option.disabled))
const navigateOptions = direction => {
if (!states.visible) {
states.visible = true
return
}
if (states.options.length === 0 || states.filteredOptionsCount === 0) return
if (!optionsAllDisabled.value) {
if (direction === 'next') {
states.hoverIndex++
if (states.hoverIndex === states.options.length) {
states.hoverIndex = 0
}
} else if (direction === 'prev') {
states.hoverIndex--
if (states.hoverIndex < 0) {
states.hoverIndex = states.options.length - 1
}
}
const option = states.options[states.hoverIndex]
if (option.disabled === true ||
option.groupDisabled === true ||
!option.visible) {
navigateOptions(direction)
}
nextTick(() => scrollToOption(hoverOption.value))
}
}
return {
selectSize,
handleResize,
debouncedOnInputChange,
debouncedQueryChange,
deletePrevTag,
deleteTag,
deleteSelected,
handleOptionSelect,
scrollToOption,
readonly,
resetInputHeight,
showClose,
iconClass,
showNewOption,
collapseTagSize,
setSelected,
managePlaceholder,
selectDisabled,
emptyText,
toggleLastOptionHitState,
resetInputState,
handleComposition,
onOptionDestroy,
handleMenuEnter,
handleFocus,
blur,
handleBlur,
handleClearClick,
doDestroy,
handleClose,
toggleMenu,
selectOption,
getValueKey,
navigateOptions,
dropMenuVisible,
// DOM ref
reference,
input,
popper,
tags,
selectWrapper,
scrollbar,
}
}

View File

@ -3,7 +3,6 @@
@import "./popper";
@include b(select-dropdown) {
position: absolute;
z-index: #{$--index-top + 1};
border: $--select-dropdown-border;
border-radius: $--border-radius-base;

View File

@ -14,7 +14,7 @@
.el-select__tags
>span {
display: contents;
display: inline-block;
}
&:hover {

View File

@ -107,7 +107,7 @@
</el-option>
</el-select>
</template>
<script>
export default {
data() {