2020-08-27 17:18:57 +08:00

620 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
v-clickoutside="close"
:class="{
'ui-select-visible': showList,
'ui-select-disabled': disabled,
'ui-select-readonly': readonly,
'ui-select-multiple': multiple
}"
class="ui-select"
>
<div
ref="selection"
:tabindex="getTabindex"
@click="toggle"
@mouseenter="focus"
@focus="focus"
@mouseleave="blur"
@blur="blur"
@keydown="keydown"
class="ui-select-selection"
>
<template v-if="multiple">
<span
v-if="currentValue === '' || currentValue.length == 0"
class="ui-select-placeholder"
>
{{placeholder || localPlaceholder}}
</span>
<template v-else>
<div class="ui-select-multiple-selected">
<span
v-for="(option, index) in selectedOption"
:key="index"
class="ui-select-selected-value"
>
{{option.currentLabel}}
<Icon
@click.stop="removeSelectedOption(option)"
type="md-close"
/>
</span>
</div>
</template>
</template>
<template v-else>
<span :class="getUISelectClass" @mouseenter="handleTitleAttr">
{{(selectedOption && selectedOption.currentLabel) ||
placeholder ||
localPlaceholder}}
</span>
</template>
<div class="ui-select-selection-icons">
<Icon v-show="!showClear" type="md-arrow-dropdown" />
<Icon
v-if="clearable"
v-show="showClear"
@click.stop="clear"
type="md-close-circle"
/>
</div>
</div>
<div
ref="list"
v-show="showList"
:class="{ 'no-option-children': noChildren }"
class="ui-select-dropdown"
transition="slide"
>
<template v-if="filterable">
<input
ref="searchInput"
v-model="searchValue"
:placeholder="t('el.select.searchPlaceholder')"
@input="changeSearchValue"
@keydown="keydown"
class="ui-select-selected-input"
/>
</template>
<ul @click="selectOption" class="ui-select-dropdown-list">
<slot />
<template v-if="noChildren">
<li
v-for="(item, index) in currentOptions"
:key="index"
:data-key="item.value"
:class="{
'ui-select-item-selected': item.selected,
'ui-select-item-disabled':
item.disabled ||
(disableLimit && !item.selected),
focus: item.isFocus
}"
@mouseenter="handleTitleAttr"
@mouseover="doFocus(item)"
class="ui-select-item"
>
{{item.text}}
</li>
<li class="ui-select-pagination">
<wb-button
:disabled="pagination.current == 0"
@click.stop="prev"
icon="md-arrow-back"
type="text"
/>
<wb-button
:disabled="
pagination.current ==
Math.ceil(
searchOptions.length / pagination.size
) -
1
"
@click.stop="next"
icon="md-arrow-forward"
type="text"
/>
</li>
</template>
</ul>
</div>
</div>
</template>
<script>
import clickoutside from '../../directives/clickoutside';
import emitter from '../../mixins/emitter';
import * as util from '../../utils/util';
import keyCode from '../../utils/keyCode';
import locale from '../../i18n/mixin';
import Icon from '../icon';
export default {
name: 'WbSelect',
components: {
Icon
},
directives: {
clickoutside
},
mixins: [emitter, locale],
props: {
value: {
type: [String, Number, Array],
default: ''
},
placeholder: {
type: String,
default() {
return '';
}
},
disabled: {
type: Boolean,
default: false
},
filterable: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: true
},
readonly: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
},
multipleLimit: {
type: Number,
default: 0
},
options: {
type: Array,
default() {
return [];
}
},
optionsValueName: {
type: String,
default: 'value'
},
optionsTextName: {
type: String,
default: 'text'
}
},
data() {
const myOptions = this.makeData();
return {
focusOption: null,
currentValue: this.value,
searchValue: '',
showClear: false,
showList: false,
myOptions,
noChildren: myOptions.length,
pagination: {
size: 100,
current: 0
},
searching: false
};
},
computed: {
getTabindex() {
if (this.disabled || this.readonly) {
return -1;
}
return 0;
},
getUISelectClass() {
if (this.selectedOption && this.selectedOption.currentLabel) {
return 'ui-select-selected-value';
}
return 'ui-select-placeholder';
},
localPlaceholder() {
return this.t('el.select.placeholder');
},
selectedOption() {
const arr = [];
this.myOptions.forEach((option) => {
if (option.selected) {
arr.push(option);
}
});
if (this.multiple) {
return arr;
}
if (arr.length > 0) {
return arr[0];
}
return null;
},
disableLimit() {
const { currentValue, multiple, multipleLimit } = this;
let disableLimit = false;
if (multiple) {
disableLimit = (currentValue || []).length >= multipleLimit
&& multipleLimit > 0;
}
return disableLimit;
},
searchOptions() {
const searchOptions = [];
this.myOptions.forEach((option) => {
if (
this.searchValue
&& option.text.indexOf(this.searchValue) != -1
) {
searchOptions.push(option);
} else if (!this.searchValue) {
searchOptions.push(option);
}
});
return searchOptions;
},
currentOptions() {
const start = this.pagination.size * this.pagination.current;
const arr = this.searchOptions.slice(
start,
start + this.pagination.size
);
return arr;
}
},
watch: {
value(value) {
if (value === this.currentValue) return;
this.currentValue = value;
this.changeCurrentValue();
},
showList(value) {
if (this.filterable && value) {
this.$nextTick(function () {
this.$refs.searchInput.focus();
});
}
},
options: {
handler() {
this.myOptions = this.makeData();
this.noChildren = this.myOptions.length;
},
deep: true
}
},
created() {
this.addOptionCache = [];
const debounce = function (fn) {
let waiting;
return function () {
if (waiting) return;
waiting = true;
const context = this;
const args = arguments;
const later = function () {
waiting = false;
fn.apply(context, args);
};
this.$nextTick(later);
};
};
this.debouncedAppend = debounce(function () {
this.myOptions = this.myOptions.concat(this.addOptionCache);
this.addOptionCache = [];
});
},
beforeDestroy() {
// 如果Select组件销毁则把这个清除。然后removeOption的逻辑就不会执行则不会触发可能的改变currentValue的逻辑
this.myOptions = [];
},
methods: {
addTitleAttr(target, text) {
if (target.clientWidth < target.scrollWidth) {
target.setAttribute('title', text);
} else {
target.setAttribute('title', '');
}
},
handleTitleAttr(e) {
this.addTitleAttr(e.target, e.target.innerText);
},
addOption(option) {
this.addOptionCache.push(option);
this.debouncedAppend();
},
removeOption(option) {
if (this.myOptions.length === 0) {
return;
}
const index = this.myOptions.indexOf(option);
if (index != -1) {
this.myOptions.splice(index, 1);
}
// 如果删除的option在选中状态则清除选中的值
if (option.selected) {
if (this.multiple) {
const i = this.currentValue.indexOf(option.value);
if (i != -1) {
this.currentValue.splice(index, 1);
}
} else {
this.currentValue = '';
}
this.changeCurrentValue();
}
},
makeData() {
const myOptions = [];
const use = [];
this.options.forEach((option) => {
const text = option[this.optionsTextName];
const value = option[this.optionsValueName];
let selected;
if (this.multiple) {
selected = this.value.indexOf(value) != -1;
} else {
selected = this.value === value;
}
selected && use.push(value);
myOptions.push(
Object.assign(
{
selected: selected || false,
isFocus: false,
currentLabel: text,
value,
text
},
option
)
);
});
// 如果value中的某个值不在options中的话则清除value中的那个值
// 只有初始化之后的才这样
if (this.searching) {
if (this.multiple) {
if (
this.value.length !== use.length
|| !this.value.every(val => use.indexOf(val) != -1)
) {
this.currentValue = use;
this.changeCurrentValue();
}
} else if (!use.some(val => val === this.value)) {
setTimeout(() => {
this.currentValue = '';
this.changeCurrentValue();
}, 0);
}
this.searching = false;
}
return myOptions;
},
selectOption(e) {
if (this.disabled || this.readonly) {
return;
}
const key = e.target.getAttribute('data-key');
const options = this.myOptions.filter(
option => option.value == key
);
if (options.length > 0) {
const option = options[0];
if (option.disabled) {
return;
}
if (this.disableLimit && !option.selected) {
return;
}
this.clickOption(option);
}
},
clickOption(option) {
if (this.disabled || this.readonly) return;
if (this.multiple) {
const index = this.currentValue.indexOf(option.value);
if (index != -1) {
this.currentValue.splice(index, 1);
} else if (
this.multipleLimit <= 0
|| this.currentValue.length < this.multipleLimit
) {
this.currentValue.push(option.value);
}
} else {
this.showList = false;
this.currentValue = option.value;
this.finished();
}
this.changeCurrentValue();
},
changeCurrentValue() {
// 如果是高性能模式则当值改变时需要变更selected状态
if (this.noChildren) {
this.myOptions.forEach((option) => {
let selected;
if (this.multiple) {
selected = this.currentValue.indexOf(option.value) != -1;
} else {
selected = this.currentValue === option.value;
}
option.selected = selected;
});
}
this.$emit('input', this.currentValue);
this.$emit('change', this.currentValue);
this.$emit('on-change', this.currentValue);
this.dispatch('FormItem', 'on-form-change', [this.currentValue]);
},
removeSelectedOption(option) {
if (this.disabled || this.readonly) return;
const index = this.currentValue.indexOf(option.value);
if (index != -1) {
this.currentValue.splice(index, 1);
}
this.finished();
this.changeCurrentValue();
},
changeSearchValue: util.debounce(function () {
this.searching = true;
this.$emit('on-search', this.searchValue);
this.pagination.current = 0;
}, 200),
blur() {
console.log('on-blur');
this.$emit('on-blur');
if (!this.disabled && !this.readonly && this.clearable) {
this.showClear = false;
}
},
focus() {
this.$emit('on-focus');
if (
!this.disabled
&& !this.readonly
&& this.clearable
&& (this.currentValue != '' || this.currentValue.length > 0)
) {
this.showClear = true;
}
},
toggle() {
this.$emit('on-click');
if (this.disabled || this.readonly) return;
this.showList = !this.showList;
this.$nextTick(function () {
const bottom = util.getViewport().height
- this.$el.getBoundingClientRect().bottom;
if (!this.listHeight) {
this.listHeight = this.$refs.list.clientHeight;
}
if (bottom - this.listHeight < 0) {
this.$refs.list.style.top = `-${this.listHeight + 10}px`;
} else {
this.$refs.list.style.top = '32px';
}
});
},
clear() {
if (this.disabled || this.readonly) return;
this.showClear = false;
if (!this.multiple) {
this.currentValue = '';
} else {
this.currentValue = [];
}
this.searchValue = '';
this.finished();
this.changeCurrentValue();
},
close() {
if (this.showList) {
this.finished();
this.dispatch('FormItem', 'on-form-blur', [this.currentValue]);
}
this.showList = false;
this.searchValue = '';
},
finished() {
this.$emit('finished', this.currentValue);
},
prev() {
if (this.pagination.current > 0) {
this.pagination.current -= 1;
this.$nextTick(function () {
this.$refs.list.scrollTop = 0;
});
}
},
next() {
if (
this.pagination.current
< Math.ceil(this.searchOptions.length / this.pagination.size)
) {
this.pagination.current += 1;
this.$nextTick(function () {
this.$refs.list.scrollTop = 0;
});
}
},
doFocus(option) {
if (this.focusOption) {
this.focusOption.isFocus = false;
}
option.isFocus = true;
this.focusOption = option;
},
keydown(e) {
if (this.disabled || this.readonly) return;
if (!this.showList) {
if (
e.keyCode == keyCode.SPACE
|| e.keyCode == keyCode.ENTER
|| e.keyCode == keyCode.MAC_ENTER
) {
e.preventDefault();
this.showList = true;
}
} else {
if (e.keyCode == keyCode.DOWN || e.keyCode == keyCode.UP) {
e.preventDefault();
this.mouseChooseOption(e.keyCode);
}
if (
e.keyCode == keyCode.ENTER
|| e.keyCode == keyCode.MAC_ENTER
|| e.keyCode == keyCode.SPACE
) {
e.preventDefault();
if (!this.focusOption) return;
if (this.focusOption.disabled) return;
if (this.disableLimit && !this.focusOption.selected) {
return;
}
this.clickOption(this.focusOption);
this.$refs.selection.focus();
}
}
},
mouseChooseOption(direction) {
let index = direction == keyCode.DOWN ? 0 : this.myOptions.length - 1;
this.myOptions.forEach((item, _index) => {
if (this.focusOption && item.value === this.focusOption.value) {
index = direction == keyCode.DOWN ? _index + 1 : _index - 1;
if (direction == keyCode.DOWN) {
if (index > this.myOptions.length - 1) {
index = 0;
}
} else if (direction == keyCode.UP) {
if (index < 0) {
index = this.myOptions.length - 1;
}
}
}
});
const focusOption = this.myOptions[index];
if (this.focusOption) {
this.focusOption.isFocus = false;
this.focusOption = null;
}
focusOption.isFocus = true;
this.focusOption = focusOption;
if (
this.focusOption.disabled
|| (this.disableLimit && !this.focusOption.selected)
) {
this.mouseChooseOption(direction);
}
}
}
};
</script>