view-design / ViewUI

A high quality UI Toolkit built on Vue.js 2.0
https://www.iviewui.com/
Other
2.65k stars 797 forks source link

[Feature Request]建议重写Select控件,以解决Select控件所有Bugs #801

Open lijing666 opened 3 years ago

lijing666 commented 3 years ago

What problem does this feature solve?

解决Select控件遗留的无数个bug,当前版本的Select控件由于内部实现使用了$slots.default等非响应式的方式,导致动态添加Option或者slots变化时,Select控件渲染问题非常多,提议从根本上解决以上问题,使用响应式属性数据控制Select的Option的渲染

What does the proposed API look like?

重写Select控件,使用方式不兼容,数据由options属性传递进去:

解决方案

直接上代码,这段是对viewUI的源码select.vue的重写版本,去掉了之前使用$slots.default的所有逻辑,Select控件接收options属性,渲染使用filteredOptions计算属性渲染Option,简单明了:

<template>
    <div
        :class="classes"
        v-click-outside:[capture]="onClickOutside"
        v-click-outside:[capture].mousedown="onClickOutside"
        v-click-outside:[capture].touchstart="onClickOutside"
    >
        <div
            ref="reference"

            :class="selectionCls"
            :tabindex="selectTabindex"

            @blur="toggleHeaderFocus"
            @focus="toggleHeaderFocus"

            @click="toggleMenu"
            @keydown.esc="handleKeydown"
            @keydown.enter="handleKeydown"
            @keydown.up.prevent="handleKeydown"
            @keydown.down.prevent="handleKeydown"
            @keydown.tab="handleKeydown"
            @keydown.delete="handleKeydown"

            @mouseenter="hasMouseHoverHead = true"
            @mouseleave="hasMouseHoverHead = false"

        >
            <slot name="input">
                <input type="hidden" :name="name" :value="publicValue">
                <select-head
                    :filterable="filterable"
                    :multiple="multiple"
                    :values="values"
                    :clearable="canBeCleared"
                    :prefix="prefix"
                    :disabled="itemDisabled"
                    :remote="remote"
                    :input-element-id="elementId"
                    :initial-label="initialLabel"
                    :placeholder="placeholder"
                    :query-prop="query"
                    :max-tag-count="maxTagCount"
                    :max-tag-placeholder="maxTagPlaceholder"
                    :allow-create="allowCreate"
                    :show-create-item="showCreateItem"

                    @on-query-change="onQueryChange"
                    @on-input-focus="isFocused = true"
                    @on-input-blur="isFocused = false"
                    @on-clear="clearSingleSelect"
                    @on-enter="handleCreateItem"
                >
                    <slot name="prefix" slot="prefix"></slot>
                </select-head>
            </slot>
        </div>
        <transition name="transition-drop">
            <Drop
                :class="dropdownCls"
                v-show="dropVisible"
                :placement="placement"
                ref="dropdown"
                :data-transfer="transfer"
                :transfer="transfer"
                v-transfer-dom
            >
                <ul v-show="showNotFoundLabel && !allowCreate" :class="[prefixCls + '-not-found']"><li>{{ localeNotFoundText }}</li></ul>
                <ul :class="prefixCls + '-dropdown-list'">
                    <li :class="prefixCls + '-item'" v-if="showCreateItem" @click="handleCreateItem">
                        {{ query }}
                        <Icon type="md-return-left" :class="prefixCls + '-item-enter'" />
                    </li>
                    <!-- <functional-options
                        v-if="(!remote) || (remote && !loading)"
                        :options="selectOptions"
                        :slot-update-hook="updateSlotOptions"
                        :slot-options="slotOptions"
                    ></functional-options> -->
                    <template v-if="(!remote) || (remote && !loading)">
                        <template v-for="optItem in filteredOptions">
                            <slot name="option" :option="optItem">
                                <Option
                                    :selected="optItem.selected"
                                    :tag="optItem.tag"
                                    :disabled="optItem.disabled"
                                    :value="optItem.value"
                                    :label="optItem.label">
                                </Option>
                            </slot>
                        </template>
                    </template>
                </ul>
                <ul v-show="loading" :class="[prefixCls + '-loading']">{{ localeLoadingText }}</ul>
            </Drop>
        </transition>
    </div>
</template>
<script>
    import Drop from './dropdown.vue';
    import Icon from '../icon';
    import {directive as clickOutside} from '../../directives/v-click-outside-x';
    import TransferDom from '../../directives/transfer-dom';
    import { oneOf, findComponentsDownward } from '../../utils/assist';
    import Emitter from '../../mixins/emitter';
    import mixinsForm from '../../mixins/form';
    import Locale from '../../mixins/locale';
    import SelectHead from './select-head.vue';
    //import FunctionalOptions from './functional-options.vue';
    import Option from './option';

    const prefixCls = 'ivu-select';
    //const optionRegexp = /^i-option$|^Option$/i;
    //const optionGroupRegexp = /option-?group/i;

    const findChild = (instance, checkFn) => {
        let match = checkFn(instance);
        if (match) return instance;
        for (let i = 0, l = instance.$children.length; i < l; i++){
            const child = instance.$children[i];
            match = findChild(child, checkFn);
            if (match) return match;
        }
    };

    /*const findOptionsInVNode = (node) => {
        const opts = node.componentOptions;
        if (opts && opts.tag.match(optionRegexp)) return [node];
        if (!node.children && (!opts || !opts.children)) return [];
        const children = [...(node.children || []), ...(opts && opts.children || [])];
        const options = children.reduce(
            (arr, el) => [...arr, ...findOptionsInVNode(el)], []
        ).filter(Boolean);
        return options.length > 0 ? options : [];
    };*/

    /*const extractOptions = (options) => options.reduce((options, slotEntry) => {
        return options.concat(findOptionsInVNode(slotEntry));
    }, []);*/

    /*const applyProp = (node, propName, value) => {
        return {
            ...node,
            componentOptions: {
                ...node.componentOptions,
                propsData: {
                    ...node.componentOptions.propsData,
                    [propName]: value,
                }
            }
        };
    };*/

    /*const getNestedProperty = (obj, path) => {
        const keys = path.split('.');
        return keys.reduce((o, key) => o && o[key] || null, obj);
    };*/

    /*const getOptionLabel = option => {
        if (option.componentOptions.propsData.label) return option.componentOptions.propsData.label;
        const textContent = (option.componentOptions.children || []).reduce((str, child) => str + (child.text || ''), '');
        const innerHTML = getNestedProperty(option, 'data.domProps.innerHTML');
        return textContent || (typeof innerHTML === 'string' ? innerHTML : '');
    };*/

    const checkValuesNotEqual = (value,publicValue,values) => {
        const strValue = JSON.stringify(value);
        const strPublic = JSON.stringify(publicValue);
        const strValues = JSON.stringify(values.map( item => {
            return item.value;
        }));
        return strValue !== strPublic || strValue !== strValues || strValues !== strPublic;
    };

    const ANIMATION_TIMEOUT = 300;

    export default {
        name: 'iSelect',
        mixins: [ Emitter, Locale, mixinsForm ],
        components: { /*FunctionalOptions,*/ Drop, SelectHead, Icon, Option },
        directives: { clickOutside, TransferDom },
        props: {
            value: {
                type: [String, Number, Array],
                default: ''
            },
            // 使用时,也得设置 value 才行
            label: {
                type: [String, Number, Array],
                default: ''
            },
            multiple: {
                type: Boolean,
                default: false
            },
            disabled: {
                type: Boolean,
                default: false
            },
            clearable: {
                type: Boolean,
                default: false
            },
            placeholder: {
                type: String
            },
            filterable: {
                type: Boolean,
                default: false
            },
            filterMethod: {
                type: Function
            },
            remoteMethod: {
                type: Function
            },
            loading: {
                type: Boolean,
                default: false
            },
            loadingText: {
                type: String
            },
            size: {
                validator (value) {
                    return oneOf(value, ['small', 'large', 'default']);
                },
                default () {
                    return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
                }
            },
            labelInValue: {
                type: Boolean,
                default: false
            },
            notFoundText: {
                type: String
            },
            placement: {
                validator (value) {
                    return oneOf(value, ['top', 'bottom', 'top-start', 'bottom-start', 'top-end', 'bottom-end']);
                },
                default: 'bottom-start'
            },
            transfer: {
                type: Boolean,
                default () {
                    return !this.$IVIEW || this.$IVIEW.transfer === '' ? false : this.$IVIEW.transfer;
                }
            },
            // Use for AutoComplete
            autoComplete: {
                type: Boolean,
                default: false
            },
            name: {
                type: String
            },
            elementId: {
                type: String
            },
            transferClassName: {
                type: String
            },
            // 3.4.0
            prefix: {
                type: String
            },
            // 3.4.0
            maxTagCount: {
                type: Number
            },
            // 3.4.0
            maxTagPlaceholder: {
                type: Function
            },
            // 4.0.0
            allowCreate: {
                type: Boolean,
                default: false
            },
            // 4.0.0
            capture: {
                type: Boolean,
                default () {
                    return !this.$IVIEW ? true : this.$IVIEW.capture;
                }
            },
            // 重大变化:设置Select框的数据集合,之前通过vnode去找Select下的Option在远程模式问题很多,控件经常出现数据变化界面渲染不成功,点击一下界面又显示正确了;
            // 因为vnode本身并不是响应式的,所以这里修改为依赖响应式数据来进行Option的渲染
            options:{
                type:Array,
                required:true,
                default(){
                    return [];
                }
            },
            // 自定义本地查询时使用的数据匹配
            customLabel: {
                type: Function,
                default () {
                    return '';
                }
            }
        },
        mounted(){
            this.$on('on-select-selected', this.onOptionClick);

            // set the initial values if there are any
            if (!this.remote && this.options.length > 0){
                this.values = this.getInitialValue().map(value => {
                    if (typeof value !== 'number' && !value) return null;
                    return this.getOptionData(value);
                }).filter(Boolean);
            }

            this.checkUpdateStatus();
        },
        data () {

            return {
                prefixCls: prefixCls,
                values: [],
                dropDownWidth: 0,
                visible: false,
                focusIndex: -1,
                isFocused: false,
                query: '',
                initialLabel: this.label,
                hasMouseHoverHead: false,
                //slotOptions: this.$slots.default,
                caretPosition: -1,
                lastRemoteQuery: '',
                unchangedQuery: true,
                hasExpectedValue: false,
                preventRemoteCall: false,
                filterQueryChange: false,  // #4273
            };
        },
        computed: {
            classes () {
                return [
                    `${prefixCls}`,
                    {
                        [`${prefixCls}-visible`]: this.visible,
                        [`${prefixCls}-disabled`]: this.itemDisabled,
                        [`${prefixCls}-multiple`]: this.multiple,
                        [`${prefixCls}-single`]: !this.multiple,
                        [`${prefixCls}-show-clear`]: this.showCloseIcon,
                        [`${prefixCls}-${this.size}`]: !!this.size
                    }
                ];
            },
            dropdownCls () {
                return {
                    [prefixCls + '-dropdown-transfer']: this.transfer,
                    [prefixCls + '-multiple']: this.multiple && this.transfer,
                    ['ivu-auto-complete']: this.autoComplete,
                    [this.transferClassName]: this.transferClassName
                };
            },
            selectionCls () {
                return {
                    [`${prefixCls}-selection`]: !this.autoComplete,
                    [`${prefixCls}-selection-focused`]: this.isFocused
                };
            },
            localeNotFoundText () {
                if (typeof this.notFoundText === 'undefined') {
                    return this.t('i.select.noMatch');
                } else {
                    return this.notFoundText;
                }
            },
            localeLoadingText () {
                if (typeof this.loadingText === 'undefined') {
                    return this.t('i.select.loading');
                } else {
                    return this.loadingText;
                }
            },
            showCreateItem () {
                let state = false;
                if (this.allowCreate && this.query !== '') {
                    state = true;
                    const $options = findComponentsDownward(this, 'iOption');
                    if ($options && $options.length) {
                        if ($options.find(item => item.showLabel === this.query)) state = false;
                    }
                }
                return  state;
            },
            transitionName () {
                return this.placement === 'bottom' ? 'slide-up' : 'slide-down';
            },
            dropVisible () {
                let status = true;
                const noOptions = !this.filteredOptions || this.filteredOptions.length === 0;
                if (!this.loading && this.remote && this.query === '' && noOptions) status = false;

                if (this.autoComplete && noOptions) status = false;

                return this.visible && status;
            },
            showNotFoundLabel () {
                const {loading, remote} = this;
                let options=this.filteredOptions;
                return options && options.length === 0 && (!remote || (remote && !loading));
            },
            publicValue(){
                if (this.labelInValue){
                    return this.multiple ? this.values : this.values[0];
                } else {
                    return this.multiple ? this.values.map(option => option.value) : (this.values[0] || {}).value;
                }
            },
            canBeCleared(){
                const uiStateMatch = this.hasMouseHoverHead || this.active;
                const qualifiesForClear = !this.multiple && !this.itemDisabled && this.clearable;
                return uiStateMatch && qualifiesForClear && this.reset; // we return a function
            },
            /*selectOptions() {
                const selectOptions = [];
                const slotOptions = (this.slotOptions || []);
                let optionCounter = -1;
                const currentIndex = this.focusIndex;
                const selectedValues = this.values.filter(Boolean).map(({value}) => value);
                if (this.autoComplete) {
                    const copyChildren = (node, fn) => {
                        return {
                            ...node,
                            children: (node.children || []).map(fn).map(child => copyChildren(child, fn))
                        };
                    };
                    const autoCompleteOptions = extractOptions(slotOptions);
                    const selectedSlotOption = autoCompleteOptions[currentIndex];

                    return slotOptions.map(node => {
                        if (node === selectedSlotOption || getNestedProperty(node, 'componentOptions.propsData.value') === this.value) return applyProp(node, 'isFocused', true);
                        return copyChildren(node, (child) => {
                            if (child !== selectedSlotOption) return child;
                            return applyProp(child, 'isFocused', true);
                        });
                    });
                }
                for (let option of slotOptions) {

                    const cOptions = option.componentOptions;
                    if (!cOptions) continue;
                    if (cOptions.tag.match(optionGroupRegexp)){
                        let children = cOptions.children;

                        // remove filtered children
                        if (this.filterable){
                            children = children.filter(
                                ({componentOptions}) => this.validateOption(componentOptions)
                            );
                        }

                        // fix #4371
                        children = children.map(opt => {
                            optionCounter = optionCounter + 1;
                            return this.processOption(opt, selectedValues, optionCounter === currentIndex);
                        });

                        // keep the group if it still has children  // fix #4371
                        if (children.length > 0) selectOptions.push({...option,componentOptions:{...cOptions,children:children}});
                    } else {
                        // ignore option if not passing filter
                        if (this.filterQueryChange) {
                            const optionPassesFilter = this.filterable ? this.validateOption(cOptions) : option;
                            if (!optionPassesFilter) continue;
                        }

                        optionCounter = optionCounter + 1;
                        selectOptions.push(this.processOption(option, selectedValues, optionCounter === currentIndex));
                    }
                }

                return selectOptions;
            },
            flatOptions(){
                return extractOptions(this.selectOptions);
            },*/
            selectTabindex(){
                return this.itemDisabled || this.filterable ? -1 : 0;
            },
            remote(){
                return typeof this.remoteMethod === 'function';
            },
            //本地数据过滤需要根据query过滤掉不符合条件的option,远程数据由用户自行重设options
            filteredOptions(){
                if(!this.options||this.options.length===0){
                    return [];
                }
                let options=this.options.map(optItem=>{
                    return {
                        value:optItem.value,
                        label:optItem.label,
                        disabled:optItem.disabled||false,
                        selected:optItem.selected||false,
                        isFocused:false,
                        tag:optItem.tag
                    };
                });
                if(this.filterable && !this.remote){
                    if(!this.query){
                        return options;
                    }
                    let filteredOptions=options.filter(
                        optItem=>{
                            let value=optItem.value;
                            let label=optItem.label;
                            let textContent=this.customLabel(optItem);
                            const stringValues = JSON.stringify([value, label, textContent]);
                            const query = this.query.toLowerCase().trim();
                            return stringValues.toLowerCase().includes(query);
                        }
                    );
                    return filteredOptions;
                }else{
                    return options;
                }
            }
        },
        methods: {
            setQuery(query){ // PUBLIC API
                if (query) {
                    this.onQueryChange(query);
                    return;
                }
                if (query === null) {
                    this.onQueryChange('');
                    this.values = [];
                    // #5620,修复清空搜索关键词后,重新搜索相同的关键词没有触发远程搜索
                    this.lastRemoteQuery = '';
                }
            },
            clearSingleSelect(){ // PUBLIC API
                this.$emit('on-clear');
                this.hideMenu();
                if (this.clearable) this.reset();
            },
            getOptionData(value){
                const option = this.options.find(optItem => optItem.value===value);
                if (!option) return null;
                const label = option.label;
                return {
                    value: value,
                    label: label,
                };
            },
            getInitialValue(){
                const {multiple, remote, value} = this;
                let initialValue = Array.isArray(value) ? value : [value];
                if (!multiple && (typeof initialValue[0] === 'undefined' || (String(initialValue[0]).trim() === '' && !Number.isFinite(initialValue[0])))) initialValue = [];
                if (remote && !multiple && value) {
                    const data = this.getOptionData(value);
                    this.query = data ? data.label : String(value);
                }
                return initialValue.filter((item) => {
                    return Boolean(item) || item === 0;
                });
            },
            /*processOption(option, values, isFocused){
                if (!option.componentOptions) return option;
                const optionValue = option.componentOptions.propsData.value;
                const disabled = option.componentOptions.propsData.disabled;
                const isSelected = values.includes(optionValue);

                const propsData = {
                    ...option.componentOptions.propsData,
                    selected: isSelected,
                    isFocused: isFocused,
                    disabled: typeof disabled === 'undefined' ? false : disabled !== false,
                };

                return {
                    ...option,
                    componentOptions: {
                        ...option.componentOptions,
                        propsData: propsData
                    }
                };
            },*/

            /*validateOption({children, elm, propsData}){
                const value = propsData.value;
                const label = propsData.label || '';
                const textContent = (elm && elm.textContent) || (children || []).reduce((str, node) => {
                    const nodeText = node.elm ? node.elm.textContent : node.text;
                    return `${str} ${nodeText}`;
                }, '') || '';
                const stringValues = JSON.stringify([value, label, textContent]);
                const query = this.query.toLowerCase().trim();
                return stringValues.toLowerCase().includes(query);
            },*/

            toggleMenu (e, force) {
                if (this.itemDisabled) {
                    return false;
                }

                this.visible = typeof force !== 'undefined' ? force : !this.visible;
                if (this.visible){
                    this.dropDownWidth = this.$el.getBoundingClientRect().width;
                    this.broadcast('Drop', 'on-update-popper');
                }
            },
            hideMenu () {
                this.toggleMenu(null, false);
                setTimeout(() => this.unchangedQuery = true, ANIMATION_TIMEOUT);
            },
            onClickOutside(event){
                if (this.visible) {
                    if (event.type === 'mousedown') {
                        event.preventDefault();
                        return;
                    }

                    if (this.transfer) {
                        const {$el} = this.$refs.dropdown;
                        if ($el === event.target || $el.contains(event.target)) {
                            return;
                        }
                    }

                    if (this.filterable) {
                        const input = this.$el.querySelector('input[type="text"]');
                        this.caretPosition = input.selectionStart;
                        this.$nextTick(() => {
                            const caretPosition = this.caretPosition === -1 ? input.value.length : this.caretPosition;
                            input.setSelectionRange(caretPosition, caretPosition);
                        });
                    }

                    if (!this.autoComplete) event.stopPropagation();
                    event.preventDefault();
                    this.hideMenu();
                    this.isFocused = true;
                    this.$emit('on-clickoutside', event);
                } else {
                    this.caretPosition = -1;
                    this.isFocused = false;
                }
            },
            reset(){
                this.query = '';
                this.focusIndex = -1;
                this.unchangedQuery = true;
                this.values = [];
                this.filterQueryChange = false;
            },
            handleKeydown (e) {
                const key = e.key || e.code;
                if (key === 'Backspace'){
                    return; // so we don't call preventDefault
                }

                if (this.visible) {
                    e.preventDefault();
                    if (key === 'Tab'){
                        e.stopPropagation();
                    }

                    // Esc slide-up
                    if (key === 'Escape') {
                        e.stopPropagation();
                        this.hideMenu();
                    }
                    // next
                    if (key === 'ArrowUp') {
                        this.navigateOptions(-1);
                    }
                    // prev
                    if (key === 'ArrowDown') {
                        this.navigateOptions(1);
                    }
                    // enter
                    if (key === 'Enter') {
                        if (this.focusIndex === -1) return this.hideMenu();
                        const optItem = this.filteredOptions[this.focusIndex];

                        // fix a script error when searching
                        if (optItem) {
                            const option = this.getOptionData(optItem.value);
                            this.onOptionClick(option);
                        } else {
                            this.hideMenu();
                        }
                    }
                } else {
                    const keysThatCanOpenSelect = ['ArrowUp', 'ArrowDown'];
                    if (keysThatCanOpenSelect.includes(e.key)) this.toggleMenu(null, true);
                }

            },
            navigateOptions(direction){
                const optionsLength = this.filteredOptions.length - 1;

                let index = this.focusIndex + direction;
                if (index < 0) index = optionsLength;
                if (index > optionsLength) index = 0;

                // find nearest option in case of disabled options in between
                if (direction > 0){
                    let nearestActiveOption = -1;
                    for (let i = 0; i < this.filteredOptions.length; i++){
                        const optionIsActive = !this.filteredOptions[i].disabled;
                        if (optionIsActive) nearestActiveOption = i;
                        if (nearestActiveOption >= index) break;
                    }
                    index = nearestActiveOption;
                } else {
                    let nearestActiveOption = this.filteredOptions.length;
                    for (let i = optionsLength; i >= 0; i--){
                        const optionIsActive = !this.filteredOptions[i].disabled;
                        if (optionIsActive) nearestActiveOption = i;
                        if (nearestActiveOption <= index) break;
                    }
                    index = nearestActiveOption;
                }

                this.focusIndex = index;
            },
            onOptionClick(option) {
                if (this.multiple){

                    // keep the query for remote select
                    if (this.remote) this.lastRemoteQuery = this.lastRemoteQuery || this.query;
                    else this.lastRemoteQuery = '';

                    const valueIsSelected = this.values.find(({value}) => value === option.value);
                    if (valueIsSelected){
                        this.values = this.values.filter(({value}) => value !== option.value);
                    } else {
                        this.values = this.values.concat(option);
                    }

                    this.isFocused = true; // so we put back focus after clicking with mouse on option elements
                } else {
                    this.query = String(option.label).trim();
                    this.values = [option];
                    this.lastRemoteQuery = '';
                    this.hideMenu();
                }

                this.focusIndex = this.filteredOptions.findIndex((optItem) => {
                    return optItem.value===option.value;
                });

                if (this.filterable){
                    const inputField = this.$el.querySelector('input[type="text"]');
                    if (!this.autoComplete) this.$nextTick(() => inputField.focus());
                }
                this.broadcast('Drop', 'on-update-popper');
                setTimeout(() => {
                    this.filterQueryChange = false;
                }, ANIMATION_TIMEOUT);
            },
            onQueryChange(query) {
                if (query.length > 0 && query !== this.query) {
                  // in 'AutoComplete', when set an initial value asynchronously,
                  // the 'dropdown list' should be stay hidden.
                  // [issue #5150]
                    if (this.autoComplete) {
                        let isInputFocused =
                            document.hasFocus &&
                            document.hasFocus() &&
                            document.activeElement === this.$el.querySelector('input');
                        this.visible = isInputFocused;
                    } else {
                        this.visible = true;
                    }
                }

                this.query = query;
                this.unchangedQuery = this.visible;
                this.filterQueryChange = true;
            },
            toggleHeaderFocus({type}){
                if (this.itemDisabled) {
                    return;
                }
                this.isFocused = type === 'focus';
            },
            /*updateSlotOptions(){
                this.slotOptions = this.$slots.default;
            },*/
            checkUpdateStatus() {
                if (this.getInitialValue().length > 0 && this.options.length === 0) {
                    this.hasExpectedValue = true;
                }
            },
            // 4.0.0 create new item
            handleCreateItem () {
                if (this.allowCreate && this.query !== '' && this.showCreateItem) {
                    const query = this.query;
                    this.$emit('on-create', query);
                    this.query = '';

                    const option = {
                        value: query,
                        label: query,
                        tag: undefined
                    };
                    if (this.multiple) {
                        this.onOptionClick(option);
                    } else {
                        // 单选时如果不在 nextTick 里执行,无法赋值
                        this.$nextTick(() => this.onOptionClick(option));
                    }
                }
            }
        },
        watch: {
            value(value){
                const {getInitialValue, getOptionData, publicValue, values} = this;

                this.checkUpdateStatus();

                if (value === '') this.values = [];
                else if (checkValuesNotEqual(value,publicValue,values)) {
                    this.$nextTick(() => this.values = getInitialValue().map(getOptionData).filter(Boolean));
                    if (!this.multiple) this.dispatch('FormItem', 'on-form-change', this.publicValue);
                }
            },
            values(now, before){
                const newValue = JSON.stringify(now);
                const oldValue = JSON.stringify(before);
                // v-model is always just the value, event with labelInValue === true
                const vModelValue = (this.publicValue && this.labelInValue) ?
                    (this.multiple ? this.publicValue.map(({value}) => value) : this.publicValue.value) :
                    this.publicValue;
                const shouldEmitInput = newValue !== oldValue && vModelValue !== this.value;
                if (shouldEmitInput) {
                    this.$emit('input', vModelValue); // to update v-model
                    this.$emit('on-change', this.publicValue);
                    this.dispatch('FormItem', 'on-form-change', this.publicValue);
                }
            },
            query (query) {
                this.$emit('on-query-change', query);
                const {remoteMethod, lastRemoteQuery} = this;
                const hasValidQuery = query !== '' && (query !== lastRemoteQuery || !lastRemoteQuery);
                const shouldCallRemoteMethod = remoteMethod && hasValidQuery && !this.preventRemoteCall;
                this.preventRemoteCall = false; // remove the flag

                if (shouldCallRemoteMethod){
                    this.focusIndex = -1;
                    //const promise = this.remoteMethod(query);
                    this.remoteMethod(query);
                    this.initialLabel = '';
                    /*if (promise && promise.then){
                        promise.then(options => {
                            if (options) this.options = options;
                        });
                    }*/
                }
                if (query !== '' && this.remote) this.lastRemoteQuery = query;
            },
            /*loading(state){
                if (state === false){
                    this.updateSlotOptions();
                }
            },*/
            isFocused(focused){
                const el = this.filterable ? this.$el.querySelector('input[type="text"]') : this.$el;
                el[this.isFocused ? 'focus' : 'blur']();

                // restore query value in filterable single selects
                const [selectedOption] = this.values;
                if (selectedOption && this.filterable && !this.multiple && !focused){
                    const selectedLabel = String(selectedOption.label || selectedOption.value).trim();
                    if (selectedLabel && this.query !== selectedLabel) {
                        this.preventRemoteCall = true;
                        this.query = selectedLabel;
                    }
                }
            },
            focusIndex(index){
                if (index < 0 || this.autoComplete) return;
                // update scroll
                const optionValue = this.filteredOptions[index].value;
                const optionInstance = findChild(this, ({$options}) => {
                    return $options.componentName === 'select-item' && $options.propsData.value === optionValue;
                });

                let bottomOverflowDistance = optionInstance.$el.getBoundingClientRect().bottom - this.$refs.dropdown.$el.getBoundingClientRect().bottom;
                let topOverflowDistance = optionInstance.$el.getBoundingClientRect().top - this.$refs.dropdown.$el.getBoundingClientRect().top;
                if (bottomOverflowDistance > 0) {
                    this.$refs.dropdown.$el.scrollTop += bottomOverflowDistance;
                }
                if (topOverflowDistance < 0) {
                    this.$refs.dropdown.$el.scrollTop += topOverflowDistance;
                }
            },
            dropVisible(open){
                this.broadcast('Drop', open ? 'on-update-popper' : 'on-destroy-popper');
            },
            options(){
                if (this.hasExpectedValue && this.options.length > 0){
                    if (this.values.length === 0) {
                        this.values = this.getInitialValue();
                    }
                    this.values = this.values.map(this.getOptionData).filter(Boolean);
                    this.hasExpectedValue = false;
                }

                if (this.options && this.options.length === 0){
                    this.query = '';
                }

                 // 当 dropdown 一开始在控件下部显示,而滚动页面后变成在上部显示,如果选项列表的长度由内部动态变更了(搜索情况)
                 // dropdown 的位置不会重新计算,需要重新计算
                this.broadcast('Drop', 'on-update-popper');
            },
            visible(state){
                this.$emit('on-open-change', state);
            },
            /*slotOptions(options, old){
                // #4626,当 Options 的 label 更新时,v-model 的值未更新
                // remote 下,调用 getInitialValue 有 bug
                if (!this.remote) {
                    const values = this.getInitialValue();
                    if (this.flatOptions && this.flatOptions.length && values.length && !this.multiple) {
                        this.values = values.map(this.getOptionData).filter(Boolean);
                    }
                }

                // 当 dropdown 在控件上部显示时,如果选项列表的长度由外部动态变更了,
                // dropdown 的位置会有点问题,需要重新计算
                if (options && old && options.length !== old.length) {
                    this.broadcast('Drop', 'on-update-popper');
                }
            },*/
        }
    };
</script>
lijing666 commented 3 years ago

@icarusion 大佬你好,当前版本的Select控件,由于代码实现的缺陷,使用的$slots等,导致很多bug,我这边提议改掉之前的实现方式,采用直接传递options数据到Select控件,无论是本地还是远程数据,options数据就是传递给Select的选项模型数据,Select内部不需要根据$slots去做一堆的计算,代码我已经贴出来了,我就基于源码稍微改了一下基本就可以跑起来了,你看看是不是考虑优化一下呢,谢谢

qifengzhang007 commented 3 years ago

Sselect Option bug非常多,最近被这个组件坑惨了,最近的几条issue都是反映这个组件的bug 作者也不赶紧 解决一下吗??!!! @icarusion

lijing666 commented 3 years ago

@icarusion 我公司这边是买了iview pro专业版的用户了,一方面觉得iview产品发展性还不错,然后觉得值得购买就买了,但是现在发现挺多组件深入使用问题也挺多,真希望能好好发展下去吧,修复问题的速度也能提升一下下

springwarms commented 3 years ago

VX:iso_9001 方便加下我VX 有事情请教您 

lijing666 commented 3 years ago

VX:iso_9001 方便加下我VX 有事情请教您

@springwarms 有什么问题可以在这里讨论哈,vx不方便