parent
8773a560bd
commit
72ae76bd5f
@ -0,0 +1,884 @@ |
||||
<template> |
||||
<div |
||||
v-clickoutside="handleClose" |
||||
class="el-select" |
||||
:class="[selectSize ? 'el-select--' + selectSize : '']" |
||||
@click.stop="toggleMenu" |
||||
> |
||||
<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" |
||||
@close="deleteTag($event, selected[0])" |
||||
disable-transitions |
||||
> |
||||
<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> |
||||
<transition-group @after-leave="resetInputHeight" v-if="!collapseTags"> |
||||
<el-tag |
||||
v-for="item in selected" |
||||
:key="getValueKey(item)" |
||||
:closable="!selectDisabled" |
||||
:size="collapseTagSize" |
||||
:hit="item.hitState" |
||||
type="info" |
||||
@close="deleteTag($event, item)" |
||||
disable-transitions |
||||
> |
||||
<span class="el-select__tags-text">{{ item.currentLabel }}</span> |
||||
</el-tag> |
||||
</transition-group> |
||||
<input |
||||
type="text" |
||||
class="el-select__input" |
||||
:class="[selectSize ? `is-${ selectSize }` : '']" |
||||
:disabled="selectDisabled" |
||||
:autocomplete="autocomplete" |
||||
@focus="handleFocus" |
||||
@blur="softFocus = false" |
||||
@keyup="managePlaceholder" |
||||
@keydown="resetInputState" |
||||
@keydown.down.prevent="navigateOptions('next')" |
||||
@keydown.up.prevent="navigateOptions('prev')" |
||||
@keydown.enter.prevent="selectOption" |
||||
@keydown.esc.stop.prevent="visible = false" |
||||
@keydown.delete="deletePrevTag" |
||||
@keydown.tab="visible = false" |
||||
@compositionstart="handleComposition" |
||||
@compositionupdate="handleComposition" |
||||
@compositionend="handleComposition" |
||||
v-model="query" |
||||
@input="debouncedQueryChange" |
||||
v-if="filterable" |
||||
:style="{ 'flex-grow': '1', width: inputLength / (inputWidth - 32) + '%', 'max-width': inputWidth - 42 + 'px' }" |
||||
ref="input"> |
||||
</div> |
||||
<el-input |
||||
ref="reference" |
||||
v-model="selectedLabel" |
||||
type="text" |
||||
:placeholder="currentPlaceholder" |
||||
:name="name" |
||||
:id="id" |
||||
:autocomplete="autocomplete" |
||||
:size="selectSize" |
||||
:disabled="selectDisabled" |
||||
:readonly="readonly" |
||||
:validate-event="false" |
||||
:class="{ 'is-focus': visible }" |
||||
:tabindex="(multiple && filterable) ? '-1' : null" |
||||
@focus="handleFocus" |
||||
@blur="handleBlur" |
||||
@keyup.native="debouncedOnInputChange" |
||||
@keydown.native.down.stop.prevent="navigateOptions('next')" |
||||
@keydown.native.up.stop.prevent="navigateOptions('prev')" |
||||
@keydown.native.enter.prevent="selectOption" |
||||
@keydown.native.esc.stop.prevent="visible = false" |
||||
@keydown.native.tab="visible = false" |
||||
@paste.native="debouncedOnInputChange" |
||||
@mouseenter.native="inputHovering = true" |
||||
@mouseleave.native="inputHovering = false"> |
||||
<template slot="prefix" v-if="$slots.prefix"> |
||||
<slot name="prefix"/> |
||||
</template> |
||||
<template slot="suffix"> |
||||
<i v-show="!showClose" :class="['el-select__caret', 'el-input__icon', 'el-icon-' + iconClass]"/> |
||||
<i v-if="showClose" class="el-select__caret el-input__icon el-icon-circle-close" |
||||
@click="handleClearClick"/> |
||||
</template> |
||||
</el-input> |
||||
<transition name="el-zoom-in-top" @before-enter="handleMenuEnter" @after-leave="doDestroy"> |
||||
<el-select-menu |
||||
v-show="visible && emptyText !== false" |
||||
placement="bottom" |
||||
:append-to-body="popperAppendToBody" |
||||
> |
||||
<el-scrollbar |
||||
ref="scrollbar" |
||||
v-show="options.length > 0 && !loading" |
||||
:class="{ 'is-empty': !allowCreate && query && filteredOptionsCount === 0 }" |
||||
tag="ul" |
||||
wrap-class="el-select-dropdown__wrap" |
||||
view-class="el-select-dropdown__list" |
||||
> |
||||
<el-option v-if="showNewOption" :value="query" created/> |
||||
<slot/> |
||||
</el-scrollbar> |
||||
<template v-if="emptyText && (!allowCreate || loading || (allowCreate && options.length === 0 ))"> |
||||
<slot name="empty" v-if="$slots.empty"/> |
||||
<p class="el-select-dropdown__empty" v-else>{{ emptyText }}</p> |
||||
</template> |
||||
</el-select-menu> |
||||
</transition> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import Emitter from 'element-ui/lib/mixins/emitter' |
||||
import Focus from 'element-ui/lib/mixins/focus' |
||||
import Locale from 'element-ui/lib/mixins/locale' |
||||
import ElSelectMenu from './SelectDropdown.vue' |
||||
import {debounce} from '@/utils' |
||||
import Clickoutside from 'element-ui/lib/utils/clickoutside' |
||||
import {addResizeListener, removeResizeListener} from 'element-ui/lib/utils/resize-event' |
||||
import {t} from 'element-ui/lib/locale' |
||||
import scrollIntoView from 'element-ui/lib/utils/scroll-into-view' |
||||
import {getValueByPath, isEdge, isIE, valueEquals} from 'element-ui/lib/utils/util' |
||||
import NavigationMixin from 'element-ui/packages/select/src/navigation-mixin' |
||||
import {isKorean} from 'element-ui/lib/utils/shared' |
||||
|
||||
export default { |
||||
mixins: [Emitter, Locale, Focus('reference'), NavigationMixin], |
||||
|
||||
name: 'ElSelect', |
||||
|
||||
componentName: 'ElSelect', |
||||
|
||||
inject: { |
||||
elForm: { |
||||
default: '' |
||||
}, |
||||
|
||||
elFormItem: { |
||||
default: '' |
||||
} |
||||
}, |
||||
|
||||
provide() { |
||||
return { |
||||
'select': this |
||||
} |
||||
}, |
||||
|
||||
components: {ElSelectMenu}, |
||||
|
||||
directives: {Clickoutside}, |
||||
|
||||
props: { |
||||
name: String, |
||||
id: String, |
||||
value: { |
||||
required: true |
||||
}, |
||||
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() { |
||||
return t('el.select.placeholder') |
||||
} |
||||
}, |
||||
defaultFirstOption: Boolean, |
||||
reserveKeyword: Boolean, |
||||
valueKey: { |
||||
type: String, |
||||
default: 'value' |
||||
}, |
||||
collapseTags: Boolean, |
||||
popperAppendToBody: Boolean |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
options: [], |
||||
cachedOptions: [], |
||||
createdLabel: null, |
||||
createdSelected: false, |
||||
selected: this.multiple ? [] : {}, |
||||
inputLength: 20, |
||||
inputWidth: 0, |
||||
initialInputHeight: 0, |
||||
cachedPlaceHolder: '', |
||||
optionsCount: 0, |
||||
filteredOptionsCount: 0, |
||||
visible: false, |
||||
softFocus: false, |
||||
selectedLabel: '', |
||||
hoverIndex: -1, |
||||
query: '', |
||||
previousQuery: null, |
||||
inputHovering: false, |
||||
currentPlaceholder: '', |
||||
menuVisibleOnFocus: false, |
||||
isOnComposition: false, |
||||
isSilentBlur: false |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
_elFormItemSize() { |
||||
return (this.elFormItem || {}).elFormItemSize |
||||
}, |
||||
|
||||
readonly() { |
||||
return !this.filterable || this.multiple || (!isIE() && !isEdge() && !this.visible) |
||||
}, |
||||
|
||||
showClose() { |
||||
let hasValue = this.multiple |
||||
? Array.isArray(this.value) && this.value.length > 0 |
||||
: this.value !== undefined && this.value !== null && this.value !== '' |
||||
return this.clearable && |
||||
!this.selectDisabled && |
||||
this.inputHovering && |
||||
hasValue |
||||
}, |
||||
|
||||
iconClass() { |
||||
return this.remote && this.filterable ? '' : (this.visible ? 'arrow-up is-reverse' : 'arrow-up') |
||||
}, |
||||
|
||||
debounce() { |
||||
return this.remote ? 300 : 0 |
||||
}, |
||||
|
||||
emptyText() { |
||||
if (this.loading) { |
||||
return this.loadingText || this.t('el.select.loading') |
||||
} |
||||
else { |
||||
if (this.remote && this.query === '' && this.options.length === 0) return false |
||||
if (this.filterable && this.query && this.options.length > 0 && this.filteredOptionsCount === 0) { |
||||
return this.noMatchText || this.t('el.select.noMatch') |
||||
} |
||||
if (this.options.length === 0) { |
||||
return this.noDataText || this.t('el.select.noData') |
||||
} |
||||
} |
||||
return null |
||||
}, |
||||
|
||||
showNewOption() { |
||||
let hasExistingOption = this.options.filter(option => !option.created) |
||||
.some(option => option.currentLabel === this.query) |
||||
return this.filterable && this.allowCreate && this.query !== '' && !hasExistingOption |
||||
}, |
||||
|
||||
selectSize() { |
||||
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size |
||||
}, |
||||
|
||||
selectDisabled() { |
||||
return this.disabled || (this.elForm || {}).disabled |
||||
}, |
||||
|
||||
collapseTagSize() { |
||||
return ['small', 'mini'].indexOf(this.selectSize) > -1 |
||||
? 'mini' |
||||
: 'small' |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
selectDisabled() { |
||||
this.$nextTick(() => { |
||||
this.resetInputHeight() |
||||
}) |
||||
}, |
||||
|
||||
placeholder(val) { |
||||
this.cachedPlaceHolder = this.currentPlaceholder = val |
||||
}, |
||||
|
||||
value(val, oldVal) { |
||||
if (this.multiple) { |
||||
this.resetInputHeight() |
||||
if ((val && val.length > 0) || (this.$refs.input && this.query !== '')) { |
||||
this.currentPlaceholder = '' |
||||
} |
||||
else { |
||||
this.currentPlaceholder = this.cachedPlaceHolder |
||||
} |
||||
if (this.filterable && !this.reserveKeyword) { |
||||
this.query = '' |
||||
this.handleQueryChange(this.query) |
||||
} |
||||
} |
||||
this.setSelected() |
||||
if (this.filterable && !this.multiple) { |
||||
this.inputLength = 20 |
||||
} |
||||
if (!valueEquals(val, oldVal)) { |
||||
this.dispatch('ElFormItem', 'el.form.change', val) |
||||
} |
||||
}, |
||||
|
||||
visible(val) { |
||||
if (!val) { |
||||
this.broadcast('ElSelectDropdown', 'destroyPopper') |
||||
if (this.$refs.input) { |
||||
this.$refs.input.blur() |
||||
} |
||||
this.query = '' |
||||
this.previousQuery = null |
||||
this.selectedLabel = '' |
||||
this.inputLength = 20 |
||||
this.menuVisibleOnFocus = false |
||||
this.resetHoverIndex() |
||||
this.$nextTick(() => { |
||||
if (this.$refs.input && |
||||
this.$refs.input.value === '' && |
||||
this.selected.length === 0) { |
||||
this.currentPlaceholder = this.cachedPlaceHolder |
||||
} |
||||
}) |
||||
if (!this.multiple) { |
||||
if (this.selected) { |
||||
if (this.filterable && this.allowCreate && |
||||
this.createdSelected && this.createdLabel) { |
||||
this.selectedLabel = this.createdLabel |
||||
} |
||||
else { |
||||
this.selectedLabel = this.selected.currentLabel |
||||
} |
||||
if (this.filterable) this.query = this.selectedLabel |
||||
} |
||||
|
||||
if (this.filterable) { |
||||
this.currentPlaceholder = this.cachedPlaceHolder |
||||
} |
||||
} |
||||
} |
||||
else { |
||||
this.broadcast('ElSelectDropdown', 'updatePopper') |
||||
if (this.filterable) { |
||||
this.query = this.remote ? '' : this.selectedLabel |
||||
this.handleQueryChange(this.query) |
||||
if (this.multiple) { |
||||
this.$refs.input.focus() |
||||
} |
||||
else { |
||||
if (!this.remote) { |
||||
this.broadcast('ElOption', 'queryChange', '') |
||||
this.broadcast('ElOptionGroup', 'queryChange') |
||||
} |
||||
|
||||
if (this.selectedLabel) { |
||||
this.currentPlaceholder = this.selectedLabel |
||||
this.selectedLabel = '' |
||||
} |
||||
} |
||||
} |
||||
} |
||||
this.$emit('visible-change', val) |
||||
}, |
||||
|
||||
options() { |
||||
if (this.$isServer) return |
||||
this.$nextTick(() => { |
||||
this.broadcast('ElSelectDropdown', 'updatePopper') |
||||
}) |
||||
if (this.multiple) { |
||||
this.resetInputHeight() |
||||
} |
||||
let inputs = this.$el.querySelectorAll('input') |
||||
if ([].indexOf.call(inputs, document.activeElement) === -1) { |
||||
this.setSelected() |
||||
} |
||||
if (this.defaultFirstOption && (this.filterable || this.remote) && this.filteredOptionsCount) { |
||||
this.checkDefaultFirstOption() |
||||
} |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
handleComposition(event) { |
||||
const text = event.target.value |
||||
if (event.type === 'compositionend') { |
||||
this.isOnComposition = false |
||||
this.$nextTick(_ => this.handleQueryChange(text)) |
||||
} |
||||
else { |
||||
const lastCharacter = text[text.length - 1] || '' |
||||
this.isOnComposition = !isKorean(lastCharacter) |
||||
} |
||||
}, |
||||
handleQueryChange(val) { |
||||
if (this.previousQuery === val || this.isOnComposition) return |
||||
if ( |
||||
this.previousQuery === null && |
||||
(typeof this.filterMethod === 'function' || typeof this.remoteMethod === 'function') |
||||
) { |
||||
this.previousQuery = val |
||||
return |
||||
} |
||||
this.previousQuery = val |
||||
this.$nextTick(() => { |
||||
if (this.visible) this.broadcast('ElSelectDropdown', 'updatePopper') |
||||
}) |
||||
this.hoverIndex = -1 |
||||
if (this.multiple && this.filterable) { |
||||
this.$nextTick(() => { |
||||
const length = this.$refs.input.value.length * 15 + 20 |
||||
this.inputLength = this.collapseTags ? Math.min(50, length) : length |
||||
this.managePlaceholder() |
||||
this.resetInputHeight() |
||||
}) |
||||
} |
||||
if (this.remote && typeof this.remoteMethod === 'function') { |
||||
this.hoverIndex = -1 |
||||
this.remoteMethod(val) |
||||
} |
||||
else if (typeof this.filterMethod === 'function') { |
||||
this.filterMethod(val) |
||||
this.broadcast('ElOptionGroup', 'queryChange') |
||||
} |
||||
else { |
||||
this.filteredOptionsCount = this.optionsCount |
||||
this.broadcast('ElOption', 'queryChange', val) |
||||
this.broadcast('ElOptionGroup', 'queryChange') |
||||
} |
||||
if (this.defaultFirstOption && (this.filterable || this.remote) && this.filteredOptionsCount) { |
||||
this.checkDefaultFirstOption() |
||||
} |
||||
}, |
||||
|
||||
scrollToOption(option) { |
||||
const target = Array.isArray(option) && option[0] ? option[0].$el : option.$el |
||||
if (this.$refs.popper && target) { |
||||
const menu = this.$refs.popper.$el.querySelector('.el-select-dropdown__wrap') |
||||
scrollIntoView(menu, target) |
||||
} |
||||
this.$refs.scrollbar && this.$refs.scrollbar.handleScroll() |
||||
}, |
||||
|
||||
handleMenuEnter() { |
||||
this.$nextTick(() => this.scrollToOption(this.selected)) |
||||
}, |
||||
|
||||
emitChange(val) { |
||||
if (!valueEquals(this.value, val)) { |
||||
this.$emit('change', val) |
||||
} |
||||
}, |
||||
|
||||
getOption(value) { |
||||
let option |
||||
const isObject = Object.prototype.toString.call(value).toLowerCase() === '[object object]' |
||||
const isNull = Object.prototype.toString.call(value).toLowerCase() === '[object null]' |
||||
const isUndefined = Object.prototype.toString.call(value).toLowerCase() === '[object undefined]' |
||||
|
||||
for (let i = this.cachedOptions.length - 1; i >= 0; i--) { |
||||
const cachedOption = this.cachedOptions[i] |
||||
const isEqual = isObject |
||||
? getValueByPath(cachedOption.value, this.valueKey) === getValueByPath(value, this.valueKey) |
||||
: cachedOption.value === value |
||||
if (isEqual) { |
||||
option = cachedOption |
||||
break |
||||
} |
||||
} |
||||
if (option) return option |
||||
const label = (!isObject && !isNull && !isUndefined) |
||||
? value : '' |
||||
let newOption = { |
||||
value: value, |
||||
currentLabel: label |
||||
} |
||||
if (this.multiple) { |
||||
newOption.hitState = false |
||||
} |
||||
return newOption |
||||
}, |
||||
|
||||
setSelected() { |
||||
if (!this.multiple) { |
||||
let option = this.getOption(this.value) |
||||
if (option.created) { |
||||
this.createdLabel = option.currentLabel |
||||
this.createdSelected = true |
||||
} |
||||
else { |
||||
this.createdSelected = false |
||||
} |
||||
this.selectedLabel = option.currentLabel |
||||
this.selected = option |
||||
if (this.filterable) this.query = this.selectedLabel |
||||
return |
||||
} |
||||
let result = [] |
||||
if (Array.isArray(this.value)) { |
||||
this.value.forEach(value => { |
||||
result.push(this.getOption(value)) |
||||
}) |
||||
} |
||||
this.selected = result |
||||
this.$nextTick(() => { |
||||
this.resetInputHeight() |
||||
}) |
||||
}, |
||||
|
||||
handleFocus(event) { |
||||
if (!this.softFocus) { |
||||
if (this.automaticDropdown || this.filterable) { |
||||
this.visible = true |
||||
if (this.filterable) { |
||||
this.menuVisibleOnFocus = true |
||||
} |
||||
} |
||||
this.$emit('focus', event) |
||||
} |
||||
else { |
||||
this.softFocus = false |
||||
} |
||||
}, |
||||
|
||||
blur() { |
||||
this.visible = false |
||||
this.$refs.reference.blur() |
||||
}, |
||||
|
||||
handleBlur(event) { |
||||
setTimeout(() => { |
||||
if (this.isSilentBlur) { |
||||
this.isSilentBlur = false |
||||
} |
||||
else { |
||||
this.$emit('blur', event) |
||||
} |
||||
}, 50) |
||||
this.softFocus = false |
||||
}, |
||||
|
||||
handleClearClick(event) { |
||||
this.deleteSelected(event) |
||||
}, |
||||
|
||||
doDestroy() { |
||||
this.$refs.popper && this.$refs.popper.doDestroy() |
||||
}, |
||||
|
||||
handleClose() { |
||||
this.visible = false |
||||
}, |
||||
|
||||
toggleLastOptionHitState(hit) { |
||||
if (!Array.isArray(this.selected)) return |
||||
const option = this.selected[this.selected.length - 1] |
||||
if (!option) return |
||||
|
||||
if (hit === true || hit === false) { |
||||
option.hitState = hit |
||||
return hit |
||||
} |
||||
|
||||
option.hitState = !option.hitState |
||||
return option.hitState |
||||
}, |
||||
|
||||
deletePrevTag(e) { |
||||
if (e.target.value.length <= 0 && !this.toggleLastOptionHitState()) { |
||||
const value = this.value.slice() |
||||
value.pop() |
||||
this.$emit('input', value) |
||||
this.emitChange(value) |
||||
} |
||||
}, |
||||
|
||||
managePlaceholder() { |
||||
if (this.currentPlaceholder !== '') { |
||||
this.currentPlaceholder = this.$refs.input.value ? '' : this.cachedPlaceHolder |
||||
} |
||||
}, |
||||
|
||||
resetInputState(e) { |
||||
if (e.keyCode !== 8) this.toggleLastOptionHitState(false) |
||||
this.inputLength = this.$refs.input.value.length * 15 + 20 |
||||
this.resetInputHeight() |
||||
}, |
||||
|
||||
resetInputHeight() { |
||||
if (this.collapseTags && !this.filterable) return |
||||
this.$nextTick(() => { |
||||
if (!this.$refs.reference) return |
||||
let inputChildNodes = this.$refs.reference.$el.childNodes |
||||
let input = [].filter.call(inputChildNodes, item => item.tagName === 'INPUT')[0] |
||||
const tags = this.$refs.tags |
||||
const sizeInMap = this.initialInputHeight || 40 |
||||
input.style.height = this.selected.length === 0 |
||||
? sizeInMap + 'px' |
||||
: Math.max( |
||||
tags ? (tags.clientHeight + (tags.clientHeight > sizeInMap ? 6 : 0)) : 0, |
||||
sizeInMap |
||||
) + 'px' |
||||
if (this.visible && this.emptyText !== false) { |
||||
this.broadcast('ElSelectDropdown', 'updatePopper') |
||||
} |
||||
}) |
||||
}, |
||||
|
||||
resetHoverIndex() { |
||||
setTimeout(() => { |
||||
if (!this.multiple) { |
||||
this.hoverIndex = this.options.indexOf(this.selected) |
||||
} |
||||
else { |
||||
if (this.selected.length > 0) { |
||||
this.hoverIndex = Math.min.apply(null, this.selected.map(item => this.options.indexOf(item))) |
||||
} |
||||
else { |
||||
this.hoverIndex = -1 |
||||
} |
||||
} |
||||
}, 300) |
||||
}, |
||||
|
||||
handleOptionSelect(option, byClick) { |
||||
if (this.multiple) { |
||||
const value = (this.value || []).slice() |
||||
const optionIndex = this.getValueIndex(value, option.value) |
||||
if (optionIndex > -1) { |
||||
value.splice(optionIndex, 1) |
||||
} |
||||
else if (this.multipleLimit <= 0 || value.length < this.multipleLimit) { |
||||
value.push(option.value) |
||||
} |
||||
this.$emit('input', value) |
||||
this.emitChange(value) |
||||
if (option.created) { |
||||
this.query = '' |
||||
this.handleQueryChange('') |
||||
this.inputLength = 20 |
||||
} |
||||
if (this.filterable) this.$refs.input.focus() |
||||
} |
||||
else { |
||||
this.$emit('input', option.value) |
||||
this.emitChange(option.value) |
||||
this.visible = false |
||||
} |
||||
this.isSilentBlur = byClick |
||||
this.setSoftFocus() |
||||
if (this.visible) return |
||||
this.$nextTick(() => { |
||||
this.scrollToOption(option) |
||||
}) |
||||
}, |
||||
|
||||
setSoftFocus() { |
||||
this.softFocus = true |
||||
const input = this.$refs.input || this.$refs.reference |
||||
if (input) { |
||||
input.focus() |
||||
} |
||||
}, |
||||
|
||||
getValueIndex(arr = [], value) { |
||||
const isObject = Object.prototype.toString.call(value).toLowerCase() === '[object object]' |
||||
if (!isObject) { |
||||
return arr.indexOf(value) |
||||
} |
||||
else { |
||||
const valueKey = this.valueKey |
||||
let index = -1 |
||||
arr.some((item, i) => { |
||||
if (getValueByPath(item, valueKey) === getValueByPath(value, valueKey)) { |
||||
index = i |
||||
return true |
||||
} |
||||
return false |
||||
}) |
||||
return index |
||||
} |
||||
}, |
||||
|
||||
toggleMenu() { |
||||
if (!this.selectDisabled) { |
||||
if (this.menuVisibleOnFocus) { |
||||
this.menuVisibleOnFocus = false |
||||
} |
||||
else { |
||||
this.visible = !this.visible |
||||
} |
||||
if (this.visible) { |
||||
(this.$refs.input || this.$refs.reference).focus() |
||||
} |
||||
} |
||||
}, |
||||
|
||||
selectOption() { |
||||
if (!this.visible) { |
||||
this.toggleMenu() |
||||
} |
||||
else { |
||||
if (this.options[this.hoverIndex]) { |
||||
this.handleOptionSelect(this.options[this.hoverIndex]) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
deleteSelected(event) { |
||||
event.stopPropagation() |
||||
const value = this.multiple ? [] : '' |
||||
this.$emit('input', value) |
||||
this.emitChange(value) |
||||
this.visible = false |
||||
this.$emit('clear') |
||||
}, |
||||
|
||||
deleteTag(event, tag) { |
||||
let index = this.selected.indexOf(tag) |
||||
if (index > -1 && !this.selectDisabled) { |
||||
const value = this.value.slice() |
||||
value.splice(index, 1) |
||||
this.$emit('input', value) |
||||
this.emitChange(value) |
||||
this.$emit('remove-tag', tag.value) |
||||
} |
||||
event.stopPropagation() |
||||
}, |
||||
|
||||
onInputChange() { |
||||
if (this.filterable && this.query !== this.selectedLabel) { |
||||
this.query = this.selectedLabel |
||||
this.handleQueryChange(this.query) |
||||
} |
||||
}, |
||||
|
||||
onOptionDestroy(index) { |
||||
if (index > -1) { |
||||
this.optionsCount-- |
||||
this.filteredOptionsCount-- |
||||
this.options.splice(index, 1) |
||||
} |
||||
}, |
||||
|
||||
resetInputWidth() { |
||||
this.inputWidth = this.$refs.reference.$el.getBoundingClientRect().width |
||||
}, |
||||
|
||||
handleResize() { |
||||
this.resetInputWidth() |
||||
if (this.multiple) this.resetInputHeight() |
||||
}, |
||||
|
||||
checkDefaultFirstOption() { |
||||
this.hoverIndex = -1 |
||||
// highlight the created option |
||||
let hasCreated = false |
||||
for (let i = this.options.length - 1; i >= 0; i--) { |
||||
if (this.options[i].created) { |
||||
hasCreated = true |
||||
this.hoverIndex = i |
||||
break |
||||
} |
||||
} |
||||
if (hasCreated) return |
||||
for (let i = 0; i !== this.options.length; ++i) { |
||||
const option = this.options[i] |
||||
if (this.query) { |
||||
// highlight first options that passes the filter |
||||
if (!option.disabled && !option.groupDisabled && option.visible) { |
||||
this.hoverIndex = i |
||||
break |
||||
} |
||||
} |
||||
else { |
||||
// highlight currently selected option |
||||
if (option.itemSelected) { |
||||
this.hoverIndex = i |
||||
break |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
|
||||
getValueKey(item) { |
||||
if (Object.prototype.toString.call(item.value).toLowerCase() !== '[object object]') { |
||||
return item.value |
||||
} |
||||
else { |
||||
return getValueByPath(item.value, this.valueKey) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
created() { |
||||
this.cachedPlaceHolder = this.currentPlaceholder = this.placeholder |
||||
if (this.multiple && !Array.isArray(this.value)) { |
||||
this.$emit('input', []) |
||||
} |
||||
if (!this.multiple && Array.isArray(this.value)) { |
||||
this.$emit('input', '') |
||||
} |
||||
|
||||
this.debouncedOnInputChange = debounce(this.onInputChange, this.debounce) |
||||
|
||||
this.debouncedQueryChange = debounce(e => this.handleQueryChange(e.target.value), this.debounce) |
||||
|
||||
this.$on('handleOptionClick', this.handleOptionSelect) |
||||
this.$on('setSelected', this.setSelected) |
||||
}, |
||||
|
||||
mounted() { |
||||
if (this.multiple && Array.isArray(this.value) && this.value.length > 0) { |
||||
this.currentPlaceholder = '' |
||||
} |
||||
addResizeListener(this.$el, this.handleResize) |
||||
|
||||
const reference = this.$refs.reference |
||||
if (reference && reference.$el) { |
||||
const sizeMap = { |
||||
medium: 36, |
||||
small: 32, |
||||
mini: 28 |
||||
} |
||||
const input = reference.$el.querySelector('input') |
||||
this.initialInputHeight = input.getBoundingClientRect().height || sizeMap[this.selectSize] |
||||
} |
||||
if (this.remote && this.multiple) { |
||||
this.resetInputHeight() |
||||
} |
||||
this.$nextTick(() => { |
||||
if (reference && reference.$el) { |
||||
this.inputWidth = reference.$el.getBoundingClientRect().width |
||||
} |
||||
}) |
||||
this.setSelected() |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
if (this.$el && this.handleResize) removeResizeListener(this.$el, this.handleResize) |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,47 @@ |
||||
<template> |
||||
<div |
||||
ref="popper" |
||||
class="el-select-dropdown el-popper" |
||||
:class="[{ 'is-multiple': $parent.multiple }, popperClass]" |
||||
:style="{ minWidth }"> |
||||
<slot/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script type="text/babel"> |
||||
import Popper from '@ele/mixins/vue-popper' |
||||
|
||||
export default { |
||||
name: 'ElSelectDropdown', |
||||
|
||||
componentName: 'ElSelectDropdown', |
||||
|
||||
mixins: [Popper], |
||||
|
||||
data() { |
||||
return { |
||||
minWidth: '' |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
popperClass() { |
||||
return this.$parent.popperClass |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
'$parent.inputWidth'() { |
||||
this.minWidth = this.$parent.$el.getBoundingClientRect().width + 'px' |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
this.referenceElm = this.$parent.$refs.reference.$el |
||||
this.$parent.popperElm = this.popperElm = this.$el |
||||
this.$on('updatePopper', () => { |
||||
this.$parent.visible && this.updatePopper() |
||||
}) |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,163 @@ |
||||
import {PopupManager} from 'element-ui/lib/utils/popup' |
||||
|
||||
const PopperJS = require('@popperjs/core') |
||||
|
||||
const stop = e => e.stopPropagation() |
||||
|
||||
/** |
||||
* @param {HTMLElement} [reference=$refs.reference] - The reference element used to position the popper. |
||||
* @param {HTMLElement} [popper=$refs.popper] - The HTML element used as popper, or a configuration used to generate the popper. |
||||
* @param {String} [placement=button] - Placement of the popper accepted values: top(-start, -end), right(-start, -end), bottom(-start, -end), left(-start, -end) |
||||
* @param {Number} [offset=0] - Amount of pixels the popper will be shifted (can be negative). |
||||
* @param {Boolean} [visible=false] Visibility of the popup element. |
||||
* @param {Boolean} [visible-arrow=false] Visibility of the arrow, no style. |
||||
*/ |
||||
export default { |
||||
props: { |
||||
transformOrigin: { |
||||
type: [Boolean, String], |
||||
default: true |
||||
}, |
||||
placement: { |
||||
type: String, |
||||
default: 'bottom' |
||||
}, |
||||
boundariesPadding: { |
||||
type: Number, |
||||
default: 5 |
||||
}, |
||||
reference: {}, |
||||
popper: {}, |
||||
offset: { |
||||
type: Number, |
||||
default: 12 |
||||
}, |
||||
value: Boolean, |
||||
visibleArrow: { |
||||
type: Boolean, |
||||
default: true |
||||
}, |
||||
arrowOffset: { |
||||
type: Number, |
||||
default: 35 |
||||
}, |
||||
appendToBody: Boolean, |
||||
popperOptions: { |
||||
type: Object, |
||||
default: () => ({modifiers: []}) |
||||
} |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
showPopper: this.value |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
value: { |
||||
immediate: true, |
||||
handler(val) { |
||||
if (this.disabled) return |
||||
val && this.updatePopper() |
||||
this.showPopper = val |
||||
this.$emit('input', val) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
createPopper() { |
||||
if (!/^(top|bottom|left|right)(-start|-end)?$/g.test(this.placement)) { |
||||
return |
||||
} |
||||
|
||||
const options = this.popperOptions |
||||
const popper = this.popperElm = this.popperElm || this.popper || this.$refs.popper |
||||
let reference = this.referenceElm = this.referenceElm || this.reference || this.$refs.reference |
||||
|
||||
if (!reference && this.$slots.reference && this.$slots.reference[0]) { |
||||
reference = this.referenceElm = this.$slots.reference[0].elm |
||||
} |
||||
|
||||
if (!popper || !reference) return |
||||
|
||||
if (this.popperJS && this.popperJS.hasOwnProperty('destroy')) { |
||||
this.popperJS.destroy() |
||||
} |
||||
|
||||
if (this.appendToBody) document.body.appendChild(this.popperElm) |
||||
if (this.visibleArrow) this.setArrow() |
||||
this.setOffset() |
||||
|
||||
options.placement = this.placement |
||||
options.onFirstUpdate = () => { |
||||
this.$emit('created', this) |
||||
this.$nextTick(this.updatePopper) |
||||
} |
||||
|
||||
|
||||
this.popperJS = PopperJS.createPopper(reference, popper, options) |
||||
|
||||
popper.style.zIndex = PopupManager.nextZIndex() |
||||
this.popperElm.addEventListener('click', stop) |
||||
}, |
||||
|
||||
updatePopper() { |
||||
const popper = this.popperJS |
||||
|
||||
if (!popper) return this.createPopper() |
||||
|
||||
popper.update() |
||||
this.popperElm.style.zIndex = PopupManager.nextZIndex() |
||||
}, |
||||
|
||||
doDestroy(forceDestroy) { |
||||
if (!this.popperJS || (this.showPopper && !forceDestroy)) return |
||||
this.popperJS.destroy() |
||||
this.popperJS = null |
||||
}, |
||||
|
||||
setArrow() { |
||||
if (this.appended) return |
||||
this.appended = true |
||||
|
||||
const arrow = document.createElement('div') |
||||
arrow.className = 'popper__arrow' |
||||
arrow.setAttribute('data-popper-arrow', '') |
||||
|
||||
this.popperElm.appendChild(arrow) |
||||
|
||||
const modifier = this.popperOptions.modifiers.find(modifier => modifier.name === 'arrow'), |
||||
options = {element: '[data-popper-arrow]', padding: this.arrowOffset} |
||||
|
||||
if (!modifier) { |
||||
this.popperOptions.modifiers.push({name: 'arrow', options}) |
||||
} |
||||
else modifier.options = options |
||||
}, |
||||
|
||||
setOffset() { |
||||
const modifier = this.popperOptions.modifiers.find(modifier => modifier.name === 'offset'), |
||||
options = {offset: [0, this.offset]} |
||||
|
||||
if (!modifier) { |
||||
this.popperOptions.modifiers.push({name: 'offset', options}) |
||||
} |
||||
else modifier.options = options |
||||
} |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
this.doDestroy(true) |
||||
if (this.popperElm && this.popperElm.parentNode === document.body) { |
||||
this.popperElm.removeEventListener('click', stop) |
||||
document.body.removeChild(this.popperElm) |
||||
} |
||||
}, |
||||
|
||||
// call destroy in keep-alive mode
|
||||
deactivated() { |
||||
this.$options.beforeDestroy[0].call(this) |
||||
} |
||||
} |
||||
@ -0,0 +1,2 @@ |
||||
@import "./message.scss"; |
||||
@import "./popper.scss"; |
||||
@ -0,0 +1,58 @@ |
||||
.el-message { |
||||
overflow: visible; |
||||
|
||||
&__badge { |
||||
animation: el-message-badge .42s; |
||||
padding: 4px 8px; |
||||
position: absolute; |
||||
box-shadow: 0 1px 3px rgba(#000, .2), 0 1px 1px rgba(#000, .14), 0 2px 1px -1px rgba(#000, .12); |
||||
background-color: $--color-danger; |
||||
color: #fff; |
||||
border-radius: 4px; |
||||
top: -10px; |
||||
left: -10px; |
||||
font-size: 12px; |
||||
line-height: 12px; |
||||
} |
||||
|
||||
&__progress { |
||||
z-index: -1; |
||||
position: absolute; |
||||
height: 3px; |
||||
bottom: 0; |
||||
left: 10px; |
||||
right: 10px; |
||||
animation: el-message-progress linear; |
||||
background: currentColor; |
||||
opacity: .3; |
||||
transform-origin: 0 50%; |
||||
transform: scaleX(0); |
||||
} |
||||
} |
||||
|
||||
@keyframes el-message-badge { |
||||
15% { |
||||
transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg) |
||||
} |
||||
30% { |
||||
transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg) |
||||
} |
||||
45% { |
||||
transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg) |
||||
} |
||||
60% { |
||||
transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg) |
||||
} |
||||
75% { |
||||
transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg) |
||||
} |
||||
} |
||||
|
||||
@keyframes el-message-progress { |
||||
0% { |
||||
transform: scaleX(1) |
||||
} |
||||
100% { |
||||
transform: scaleX(0) |
||||
} |
||||
} |
||||
@ -0,0 +1,98 @@ |
||||
@include b(popper) { |
||||
.popper__arrow, |
||||
.popper__arrow::after { |
||||
position: absolute; |
||||
display: block; |
||||
width: 0; |
||||
height: 0; |
||||
border-color: transparent; |
||||
border-style: solid; |
||||
} |
||||
|
||||
.popper__arrow { |
||||
border-width: $--popover-arrow-size; |
||||
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03)) |
||||
} |
||||
|
||||
.popper__arrow::after { |
||||
content: " "; |
||||
border-width: $--popover-arrow-size; |
||||
} |
||||
|
||||
&[data-popper-placement^="top"] { |
||||
margin-bottom: #{$--popover-arrow-size + 6}; |
||||
} |
||||
|
||||
&[data-popper-placement^="top"] .popper__arrow { |
||||
bottom: -$--popover-arrow-size; |
||||
left: 50%; |
||||
margin-right: #{$--tooltip-arrow-size / 2}; |
||||
border-top-color: $--popover-border-color; |
||||
border-bottom-width: 0; |
||||
|
||||
&::after { |
||||
bottom: 1px; |
||||
margin-left: -$--popover-arrow-size; |
||||
border-top-color: $--popover-background-color; |
||||
border-bottom-width: 0; |
||||
} |
||||
} |
||||
|
||||
&[data-popper-placement^="bottom"] { |
||||
margin-top: #{$--popover-arrow-size + 6}; |
||||
} |
||||
|
||||
&[data-popper-placement^="bottom"] .popper__arrow { |
||||
top: -$--popover-arrow-size; |
||||
left: 50%; |
||||
margin-right: #{$--tooltip-arrow-size / 2}; |
||||
border-top-width: 0; |
||||
border-bottom-color: $--popover-border-color; |
||||
|
||||
&::after { |
||||
top: 1px; |
||||
margin-left: -$--popover-arrow-size; |
||||
border-top-width: 0; |
||||
border-bottom-color: $--popover-background-color; |
||||
} |
||||
} |
||||
|
||||
&[data-popper-placement^="right"] { |
||||
margin-left: #{$--popover-arrow-size + 6}; |
||||
} |
||||
|
||||
&[data-popper-placement^="right"] .popper__arrow { |
||||
top: 50%; |
||||
left: -$--popover-arrow-size; |
||||
margin-bottom: #{$--tooltip-arrow-size / 2}; |
||||
border-right-color: $--popover-border-color; |
||||
border-left-width: 0; |
||||
|
||||
&::after { |
||||
bottom: -$--popover-arrow-size; |
||||
left: 1px; |
||||
border-right-color: $--popover-background-color; |
||||
border-left-width: 0; |
||||
} |
||||
} |
||||
|
||||
&[data-popper-placement^="left"] { |
||||
margin-right: #{$--popover-arrow-size + 6}; |
||||
} |
||||
|
||||
&[data-popper-placement^="left"] .popper__arrow { |
||||
top: 50%; |
||||
right: -$--popover-arrow-size; |
||||
margin-bottom: #{$--tooltip-arrow-size / 2}; |
||||
border-right-width: 0; |
||||
border-left-color: $--popover-border-color; |
||||
|
||||
&::after { |
||||
right: 1px; |
||||
bottom: -$--popover-arrow-size; |
||||
margin-left: -$--popover-arrow-size; |
||||
border-right-width: 0; |
||||
border-left-color: $--popover-background-color; |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue