123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- <template>
- <div
- class="el-select"
- v-clickoutside="handleClose"
- :class="{ 'is-multiple': multiple, 'is-small': size === 'small' }">
- <div class="el-select__tags" v-if="multiple" @click.stop="toggleMenu" ref="tags" :style="{ 'max-width': inputWidth - 32 + 'px' }">
- <transition-group @after-leave="resetInputHeight">
- <el-tag
- v-for="item in selected"
- :key="item"
- closable
- :hit="item.hitState"
- type="primary"
- @close="deleteTag($event, item)"
- close-transition>{{ item.currentLabel }}</el-tag>
- </transition-group>
- <input
- type="text"
- class="el-select__input"
- @focus="visible = true"
- @keyup="managePlaceholder"
- @keydown="resetInputState"
- @keydown.down.prevent="navigateOptions('next')"
- @keydown.up.prevent="navigateOptions('prev')"
- @keydown.enter.prevent="selectOption"
- @keydown.esc.prevent="visible = false"
- @keydown.delete="deletePrevTag"
- v-model="query"
- :debounce="remote ? 300 : 0"
- v-if="filterable"
- :style="{ width: inputLength + 'px', 'max-width': inputWidth - 42 + 'px' }"
- ref="input">
- </div>
- <el-input
- ref="reference"
- v-model="selectedLabel"
- type="text"
- :placeholder="currentPlaceholder"
- :name="name"
- :disabled="disabled"
- :readonly="!filterable || multiple"
- @click.native="toggleMenu"
- @keyup.native="debouncedOnInputChange"
- @keydown.native.down.prevent="navigateOptions('next')"
- @keydown.native.up.prevent="navigateOptions('prev')"
- @keydown.native.enter.prevent="selectOption"
- @keydown.native.esc.prevent="visible = false"
- @keydown.native.tab="visible = false"
- @mouseenter.native="inputHovering = true"
- @mouseleave.native="inputHovering = false"
- :icon="iconClass">
- </el-input>
- <transition name="md-fade-bottom" @after-leave="doDestroy">
- <el-select-menu
- ref="popper"
- v-show="visible && emptyText !== false">
- <ul class="el-select-dropdown__list" v-show="options.length > 0 && filteredOptionsCount > 0 && !loading">
- <slot></slot>
- </ul>
- <p class="el-select-dropdown__empty" v-if="emptyText">{{ emptyText }}</p>
- </el-select-menu>
- </transition>
- </div>
- </template>
- <script type="text/babel">
- import Emitter from 'element-ui/src/mixins/emitter';
- import Locale from 'element-ui/src/mixins/locale';
- import ElInput from 'element-ui/packages/input';
- import ElSelectMenu from './select-dropdown.vue';
- import ElTag from 'element-ui/packages/tag';
- import debounce from 'throttle-debounce/debounce';
- import Clickoutside from 'element-ui/src/utils/clickoutside';
- import { addClass, removeClass, hasClass } from 'wind-dom/src/class';
- import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
- import { t } from 'element-ui/src/locale';
- export default {
- mixins: [Emitter, Locale],
- name: 'ElSelect',
- componentName: 'select',
- computed: {
- iconClass() {
- return this.showCloseIcon ? 'circle-close' : (this.remote && this.filterable ? '' : 'caret-top');
- },
- debounce() {
- return this.remote ? 300 : 0;
- },
- showCloseIcon() {
- let criteria = this.clearable && this.inputHovering && !this.multiple && this.options.indexOf(this.selected) > -1;
- if (!this.$el) return false;
- let icon = this.$el.querySelector('.el-input__icon');
- if (icon) {
- if (criteria) {
- icon.addEventListener('click', this.deleteSelected);
- addClass(icon, 'is-show-close');
- } else {
- icon.removeEventListener('click', this.deleteSelected);
- removeClass(icon, 'is-show-close');
- }
- }
- return criteria;
- },
- emptyText() {
- if (this.loading) {
- return this.t('el.select.loading');
- } else {
- if (this.voidRemoteQuery) {
- this.voidRemoteQuery = false;
- return false;
- }
- if (this.filterable && this.filteredOptionsCount === 0) {
- return this.t('el.select.noMatch');
- }
- if (this.options.length === 0) {
- return this.t('el.select.noData');
- }
- }
- return null;
- }
- },
- components: {
- ElInput,
- ElSelectMenu,
- ElTag
- },
- directives: { Clickoutside },
- props: {
- name: String,
- value: {},
- size: String,
- disabled: Boolean,
- clearable: Boolean,
- filterable: Boolean,
- loading: Boolean,
- remote: Boolean,
- remoteMethod: Function,
- filterMethod: Function,
- multiple: Boolean,
- placeholder: {
- type: String,
- default() {
- return t('el.select.placeholder');
- }
- }
- },
- data() {
- return {
- options: [],
- selected: {},
- isSelect: true,
- inputLength: 20,
- inputWidth: 0,
- valueChangeBySelected: false,
- cachedPlaceHolder: '',
- optionsCount: 0,
- filteredOptionsCount: 0,
- dropdownUl: null,
- visible: false,
- selectedLabel: '',
- selectInit: false,
- hoverIndex: -1,
- query: '',
- voidRemoteQuery: false,
- bottomOverflowBeforeHidden: 0,
- optionsAllDisabled: false,
- inputHovering: false,
- currentPlaceholder: ''
- };
- },
- watch: {
- placeholder(val) {
- this.currentPlaceholder = val;
- },
- value(val) {
- if (this.valueChangeBySelected) {
- this.valueChangeBySelected = false;
- return;
- }
- this.$nextTick(() => {
- if (this.multiple && Array.isArray(val)) {
- this.$nextTick(() => {
- this.resetInputHeight();
- });
- this.selectedInit = true;
- this.selected = [];
- this.currentPlaceholder = this.cachedPlaceHolder;
- val.forEach(item => {
- let option = this.options.filter(option => option.value === item)[0];
- if (option) {
- this.$emit('addOptionToValue', option);
- }
- });
- }
- if (!this.multiple) {
- let option = this.options.filter(option => option.value === val)[0];
- if (option) {
- this.$emit('addOptionToValue', option);
- } else {
- this.selected = {};
- this.selectedLabel = '';
- }
- }
- this.resetHoverIndex();
- });
- },
- selected(val, oldVal) {
- if (this.multiple) {
- if (this.selected.length > 0) {
- this.currentPlaceholder = '';
- } else {
- this.currentPlaceholder = this.cachedPlaceHolder;
- }
- this.$nextTick(() => {
- this.resetInputHeight();
- });
- if (this.selectedInit) {
- this.selectedInit = false;
- return;
- }
- this.valueChangeBySelected = true;
- const result = val.map(item => item.value);
- this.$emit('input', result);
- this.$emit('change', result);
- this.dispatch('form-item', 'el.form.change', val);
- if (this.filterable) {
- this.query = '';
- this.hoverIndex = -1;
- this.$refs.input.focus();
- this.inputLength = 20;
- }
- } else {
- if (this.selectedInit) {
- this.selectedInit = false;
- return;
- }
- if (val.value === oldVal.value) return;
- this.$emit('input', val.value);
- this.$emit('change', val.value);
- }
- },
- query(val) {
- this.$nextTick(() => {
- this.broadcast('select-dropdown', 'updatePopper');
- });
- if (this.multiple && this.filterable) {
- this.resetInputHeight();
- }
- if (this.remote && typeof this.remoteMethod === 'function') {
- this.hoverIndex = -1;
- this.remoteMethod(val);
- this.voidRemoteQuery = val === '';
- this.broadcast('option', 'resetIndex');
- } else if (typeof this.filterMethod === 'function') {
- this.filterMethod(val);
- } else {
- this.filteredOptionsCount = this.optionsCount;
- this.broadcast('option', 'queryChange', val);
- }
- },
- visible(val) {
- if (!val) {
- this.$refs.reference.$el.querySelector('input').blur();
- if (this.$el.querySelector('.el-input__icon')) {
- removeClass(this.$el.querySelector('.el-input__icon'), 'is-reverse');
- }
- this.broadcast('select-dropdown', 'destroyPopper');
- if (this.$refs.input) {
- this.$refs.input.blur();
- }
- this.resetHoverIndex();
- if (!this.multiple) {
- if (this.dropdownUl && this.selected.$el) {
- this.bottomOverflowBeforeHidden = this.selected.$el.getBoundingClientRect().bottom - this.$refs.popper.$el.getBoundingClientRect().bottom;
- }
- if (this.selected && this.selected.value) {
- this.selectedLabel = this.selected.currentLabel;
- }
- }
- } else {
- let icon = this.$el.querySelector('.el-input__icon');
- if (icon && !hasClass(icon, 'el-icon-circle-close')) {
- addClass(this.$el.querySelector('.el-input__icon'), 'is-reverse');
- }
- this.broadcast('select-dropdown', 'updatePopper');
- if (this.filterable) {
- this.query = this.selectedLabel;
- if (this.multiple) {
- this.$refs.input.focus();
- } else {
- this.broadcast('input', 'inputSelect');
- }
- }
- if (!this.dropdownUl) {
- let dropdownChildNodes = this.$refs.popper.$el.childNodes;
- this.dropdownUl = [].filter.call(dropdownChildNodes, item => item.tagName === 'UL')[0];
- }
- if (!this.multiple && this.dropdownUl) {
- if (this.bottomOverflowBeforeHidden > 0) {
- this.dropdownUl.scrollTop += this.bottomOverflowBeforeHidden;
- }
- }
- }
- },
- options(val) {
- this.optionsAllDisabled = val.length === val.filter(item => item.disabled === true).length;
- }
- },
- methods: {
- doDestroy() {
- 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()) {
- this.selected.pop();
- }
- },
- addOptionToValue(option, init) {
- if (this.multiple) {
- if (this.selected.indexOf(option) === -1 && (this.remote ? this.value.indexOf(option.value) === -1 : true)) {
- this.selectedInit = !!init;
- this.selected.push(option);
- this.resetHoverIndex();
- }
- } else {
- this.selectedInit = !!init;
- this.selected = option;
- this.selectedLabel = option.currentLabel;
- this.hoverIndex = option.index;
- }
- },
- 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;
- },
- resetInputHeight() {
- this.$nextTick(() => {
- let inputChildNodes = this.$refs.reference.$el.childNodes;
- let input = [].filter.call(inputChildNodes, item => item.tagName === 'INPUT')[0];
- input.style.height = Math.max(this.$refs.tags.clientHeight + 6, this.size === 'small' ? 28 : 36) + 'px';
- this.broadcast('select-dropdown', '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) {
- if (!this.multiple) {
- this.selected = option;
- this.selectedLabel = option.currentLabel;
- this.visible = false;
- } else {
- let optionIndex = -1;
- this.selected.forEach((item, index) => {
- if (item === option || item.currentLabel === option.currentLabel) {
- optionIndex = index;
- }
- });
- if (optionIndex > -1) {
- this.selected.splice(optionIndex, 1);
- } else {
- this.selected.push(option);
- }
- }
- },
- toggleMenu() {
- if (this.filterable && this.query === '' && this.visible) {
- return;
- }
- if (!this.disabled) {
- this.visible = !this.visible;
- }
- },
- navigateOptions(direction) {
- if (!this.visible) {
- this.visible = true;
- return;
- }
- if (!this.optionsAllDisabled) {
- if (direction === 'next') {
- this.hoverIndex++;
- if (this.hoverIndex === this.options.length) {
- this.hoverIndex = 0;
- }
- this.resetScrollTop();
- if (this.options[this.hoverIndex].disabled === true ||
- this.options[this.hoverIndex].groupDisabled === true ||
- !this.options[this.hoverIndex].visible) {
- this.navigateOptions('next');
- }
- }
- if (direction === 'prev') {
- this.hoverIndex--;
- if (this.hoverIndex < 0) {
- this.hoverIndex = this.options.length - 1;
- }
- this.resetScrollTop();
- if (this.options[this.hoverIndex].disabled === true ||
- this.options[this.hoverIndex].groupDisabled === true ||
- !this.options[this.hoverIndex].visible) {
- this.navigateOptions('prev');
- }
- }
- }
- },
- resetScrollTop() {
- let bottomOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().bottom - this.$refs.popper.$el.getBoundingClientRect().bottom;
- let topOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().top - this.$refs.popper.$el.getBoundingClientRect().top;
- if (bottomOverflowDistance > 0) {
- this.dropdownUl.scrollTop += bottomOverflowDistance;
- }
- if (topOverflowDistance < 0) {
- this.dropdownUl.scrollTop += topOverflowDistance;
- }
- },
- selectOption() {
- if (this.options[this.hoverIndex]) {
- this.handleOptionSelect(this.options[this.hoverIndex]);
- }
- },
- deleteSelected(event) {
- event.stopPropagation();
- this.selected = {};
- this.selectedLabel = '';
- this.$emit('input', '');
- this.$emit('change', '');
- this.visible = false;
- },
- deleteTag(event, tag) {
- let index = this.selected.indexOf(tag);
- if (index > -1) {
- this.selected.splice(index, 1);
- }
- event.stopPropagation();
- },
- onInputChange() {
- if (this.filterable && this.selectedLabel !== this.value) {
- this.query = this.selectedLabel;
- }
- },
- onOptionDestroy(option) {
- this.optionsCount--;
- this.filteredOptionsCount--;
- let index = this.options.indexOf(option);
- if (index > -1) {
- this.options.splice(index, 1);
- }
- this.broadcast('option', 'resetIndex');
- },
- resetInputWidth() {
- this.inputWidth = this.$refs.reference.$el.getBoundingClientRect().width;
- }
- },
- created() {
- this.cachedPlaceHolder = this.currentPlaceholder = this.placeholder;
- if (this.multiple) {
- this.selectedInit = true;
- this.selected = [];
- }
- if (this.remote) {
- this.voidRemoteQuery = true;
- }
- this.debouncedOnInputChange = debounce(this.debounce, () => {
- this.onInputChange();
- });
- this.$on('addOptionToValue', this.addOptionToValue);
- this.$on('handleOptionClick', this.handleOptionSelect);
- this.$on('onOptionDestroy', this.onOptionDestroy);
- },
- mounted() {
- addResizeListener(this.$el, this.resetInputWidth);
- if (this.remote && this.multiple && Array.isArray(this.value)) {
- this.selected = this.options.reduce((prev, curr) => {
- return this.value.indexOf(curr.value) > -1 ? prev.concat(curr) : prev;
- }, []);
- this.$nextTick(() => {
- this.resetInputHeight();
- });
- }
- this.$nextTick(() => {
- if (this.$refs.reference.$el) {
- this.inputWidth = this.$refs.reference.$el.getBoundingClientRect().width;
- }
- });
- },
- destroyed() {
- if (this.resetInputWidth) removeResizeListener(this.$el, this.resetInputWidth);
- }
- };
- </script>
|