mirror of
https://github.com/element-plus/element-plus.git
synced 2024-12-09 02:31:33 +08:00
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:
parent
62f1135768
commit
ff4d4d89da
@ -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 = {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -245,6 +245,9 @@ export default function (props: IPopperOptions, { emit }: SetupContext<string[]>
|
||||
})
|
||||
|
||||
return {
|
||||
update() {
|
||||
popperInstance.update()
|
||||
},
|
||||
doDestroy,
|
||||
show,
|
||||
hide,
|
||||
|
493
packages/select/__tests__/select.spec.ts
Normal file
493
packages/select/__tests__/select.spec.ts
Normal 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
10
packages/select/index.ts
Normal 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)
|
||||
}
|
12
packages/select/package.json
Normal file
12
packages/select/package.json
Normal 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"
|
||||
}
|
||||
}
|
56
packages/select/src/option-group.vue
Normal file
56
packages/select/src/option-group.vue
Normal 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>
|
104
packages/select/src/option.vue
Normal file
104
packages/select/src/option.vue
Normal 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>
|
46
packages/select/src/select-dropdown.vue
Normal file
46
packages/select/src/select-dropdown.vue
Normal 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>
|
433
packages/select/src/select.vue
Normal file
433
packages/select/src/select.vue
Normal 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>
|
39
packages/select/src/token.ts
Normal file
39
packages/select/src/token.ts
Normal 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',
|
||||
}
|
117
packages/select/src/useOption.ts
Normal file
117
packages/select/src/useOption.ts
Normal 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,
|
||||
}
|
||||
}
|
725
packages/select/src/useSelect.ts
Normal file
725
packages/select/src/useSelect.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
.el-select__tags
|
||||
>span {
|
||||
display: contents;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -107,7 +107,7 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
|
Loading…
Reference in New Issue
Block a user