mirror of
https://github.com/WeBankFinTech/fes.js.git
synced 2025-04-30 14:32:46 +08:00
620 lines
21 KiB
Vue
620 lines
21 KiB
Vue
<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>
|