模糊搜索框 el-select的优化版

演示

左侧是el-select
右侧是kl-select
在这里插入图片描述

优化问题

  1. 在数据量较大,频繁搜索可能会出现搜索内容与显示内容不匹配
  2. 当鼠标移动到下拉框框,使用键盘上下键切换,不能正常翻页

功能介绍

  • 模糊搜索
  • 上下键 翻页
  • 根据渲染的options的长度,添加不同时长的节流
  • ~~自定义滚动条 ~~

kl-bar 自定义滚动条链接-开发中

使用

<template>
  <div>
    <h2>
      {{ currentVal }}
    </h2>
    <div class="demo">
      <el-select v-model="currentVal" filterable placeholder="请选择">
        <el-option
          v-for="item in options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        >
        </el-option>
      </el-select>

      <kl-select bar v-model="currentVal" :options="options" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentVal: '',
      options: []
    };
  },
  created() {
    // 创建假数据
    setTimeout(() => {
      for (let i = 0; i < 20; i++) {
        this.options.push({
          lable: `小明${i}`,
          value: i
        });
      }
    }, 500);
  }
};
</script>

<style lang="scss" scoped>
.demo {
  display: flex;
}
</style>

实现

<template>
    <div class="kl-select">
        <div class="kl-select-input-cover">
            <input
                ref="inputRef"
                class="kl-select-input"
                type="text"
                :placeholder="placeholder"
                v-model="inputVal"
                @focus="inputFocus"
                @blur="inputBlur"
            />
            <svg
                t="1638588522484"
                :class="['icon', isFocus ? 'top' : '']"
                viewBox="0 0 1024 1024"
                version="1.1"
                xmlns="http://www.w3.org/2000/svg"
                p-id="3335"
                width="12"
                height="12"
            >
                <path
                    d="M533.931 739.588c-12.692 12.692-33.187 12.774-45.778 0.185L74.383 326c-12.592-12.591-12.509-33.087 0.183-45.779s33.188-12.774 45.779-0.184L534.117 693.81c12.588 12.59 12.507 33.086-0.186 45.778z m413.92-458.325c12.693 12.692 12.775 33.188 0.185 45.778L534.264 740.812c-12.59 12.591-33.086 12.508-45.779-0.184-12.692-12.692-12.773-33.188-0.184-45.779l413.771-413.77c12.592-12.59 33.088-12.508 45.78 0.184z"
                    p-id="3336"
                    fill="#666666"
                ></path>
            </svg>
        </div>

        <div class="kl-ul-cover" v-show="isFocus">
            <i class="kl-sj"> </i>
            <div class="kl-ul-maxheight">
                <!-- 滚动条 -->
                <div
                    class="kl-bar"
                    v-show="isBarShow && bar"
                    @mouseup="mouseupBar"
                    @mousedown="mousedownBar($event)"
                    ref="barCoverRef"
                >
                    <div
                        class="kl-scroll-bar-cover"
                        @mouseup.stop="mouseupBarScroll"
                        @mousedown.stop="mousedownBarScroll($event)"
                        @mouseleave="mouseleaveBar"
                        ref="barRef"
                    >
                        <div class="kl-scroll-bar" :style="{ height: barHeight + 'px' }"></div>
                    </div>
                </div>

                <!-- options选项 -->
                <div :class="['kl-scroll-cover', bar ? 'bar' : '']" ref="scrollCoverRef">
                    <ul ref="ulRef">
                        <li
                            v-for="(option, index) in filtersOptions"
                            :key="index"
                            @mousedown="handlelabel(option)"
                            :class="[
                                inputVal === option.label ? 'active' : '',
                                currentIndex === index ? 'active1' : '',
                            ]"
                            :style="{ height: liOption.height + 'px' }"
                        >
                            {{ option.label }}
                        </li>
                        <li class="no-data" v-if="filtersOptions.length === 0">暂无匹配数据</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        placeholder: {
            type: String,
            default: '请选择',
        },
        value: {
            // 父组件默认传递过来的value
            type: String,
        },
        options: {
            // 传入的配置项
            type: Array,
            default: [],
            required: true,
        },
        liOption: {
            // 显示的li每项的配置信息,传递参数一旦有修改,一定要传递两
            type: Object,

            default: {
                number: 7, // 最多显示几条
                height: 34, // 每项高度
            },
        },
        bar: {
            // true使用自定义的滚动条 false使用默认的滚动条
            type: Boolean,
            default: false,
        },
    },
    data() {
        return {
            coverHeight: 0,
            disy: 0,
            disx: 0,
            barCoverTop: 0,
            isBarMouseDown: false,
            isFocus: false,
            currentIndex: 0,
            inputVal: '',
            inputVal_: '',
            filtersOptions: this.options,
            barHeight: 0,
            scrollRotate: 1,
            inputBlurTimer: null,
            isBarShow: true,
            value_: '',
            maxIndex: 0,
        }
    },
    mounted() {
        let that = this
        // 开始监听键盘的输入
        document.querySelector('body').addEventListener('keydown', function (e) {
            switch (Number(e.keyCode)) {
                case 13: // 回车选中当前值
                    that.isFocus = false
                    let option = that.filtersOptions[that.currentIndex]
                    that.inputVal = option.label
                    that.inputVal_ = that.inputVal
                    that.$emit('input', option.value)
                    that.currentIndex = 0
                    that.$refs.inputRef.blur()

                    break
                case 38: // 上键
                    that.scrollBarEvent('pre')

                    break
                case 40: // 下键
                    that.scrollBarEvent('next')
                    break
            }
        })
    },
    beforeDestroy() {
        clearTimeout(this.inputBlurTimer)
        this.inputBlurTimer = null
    },
    watch: {
        value(val) {
            let option = this.options.find((item) => {
                return item.value === val
            })

            if (option) {
                this.inputVal = option.label
                this.value_ = option.label
            }
        },
        options: {
            handler() {
                if (this.options.length === 0) {
                    return
                }

                let option = this.options.find((item) => {
                    return item.value === this.value
                })

                if (option) {
                    this.inputVal = option.label
                    this.value_ = option.label
                }
            },
        },
        inputVal: {
            handler(val) {
                if (!this.isFocus) {
                    return
                }

                if (!val) this.filtersOptions = this.options

                this.currentIndex = 0

                // 具体的筛选展示
                this.filterOptions(val)
            },
            deep: true,
            immediate: true,
        },
    },
    methods: {
        barMoveEvent(el) {
            let pageY = el.pageY
            // 获取鼠标的距离顶部的距离
            let mouseTop = pageY - this.barCoverTop
            // 获取当前滚动条距离相对定位目标顶部的距离
            let barTop = this.$refs.barRef.style.top
                ? this.$refs.barRef.style.top
                : this.$refs.barCoverRef.getBoundingClientRect().top -
                  this.$refs.barRef.getBoundingClientRect().top
            if (typeof barTop === 'string') {
                barTop = barTop.replace('px', '') - 0
            }

            this.$refs.barRef.style.top = mouseTop - this.disy + 'px'
            let top = mouseTop - this.disy
            if (top > this.coverHeight - this.barHeight) {
                this.$refs.barRef.style.top = this.coverHeight - this.barHeight + 'px'
            }

            if (top < 0) {
                this.$refs.barRef.style.top = 0 + 'px'
            }

            // 开启左侧同步
            this.$refs.scrollCoverRef.scrollTop = Math.floor(
                top * (this.coverHeight / this.barHeight),
            )
        },

        // 滚动条变化同步左侧
        syncHeight() {},
        // 内测bar down
        mousedownBarScroll(el) {
            this.isBarMouseDown = true
            // 获取相对定位元素距离页面顶部的布局
            this.barCoverTop = this.$refs.barCoverRef.getBoundingClientRect().top
            // 获取鼠标相对el的位置
            this.disy = el.pageY - el.target.getBoundingClientRect().top

            // console.log(this.disx, this.disy)
            // 开启移动监听
            document.addEventListener('mousemove', this.barMoveEvent)

            // 移除
            document.addEventListener('mouseup', () => {
                document.removeEventListener('mousemove', this.barMoveEvent)
            })
        },

        // 内测bar up
        mouseupBarScroll() {
            this.isBarMouseDown = false
            this.$refs.inputRef.focus()

            // 移除
            document.removeEventListener('mousemove', this.barMoveEvent)
        },
        // 外侧bar down
        mousedownBar(el) {
            this.isBarMouseDown = true
            // 查看当前的点击目标
            let pageY = el.pageY
            let barTop = this.$refs.barRef.getBoundingClientRect().top
            let styleTop = this.$refs.barRef.style.top.replace('px', '') - 0

            if (pageY > barTop) {
                // 向下
                if (styleTop + 2 * this.barHeight > this.coverHeight) {
                    this.$refs.barRef.style.top = this.coverHeight - this.barHeight + 'px'
                    // 同步
                    this.asycHeight()
                    return
                }
                this.$refs.barRef.style.top = styleTop + this.barHeight + 'px'
                // 同步
                this.asycHeight()
            }

            if (pageY < barTop) {
                // 向上
                if (styleTop - this.barHeight < 0) {
                    this.$refs.barRef.style.top = 0 + 'px'
                    // 同步
                    this.asycHeight()
                    return
                }
                this.$refs.barRef.style.top = styleTop - this.barHeight + 'px'
                // 同步
                this.asycHeight()
            }
        },
        asycHeight() {
            let top = this.$refs.barRef.style.top.replace('px', '') - 0
            this.$refs.scrollCoverRef.scrollTop = Math.floor(
                top * (this.coverHeight / this.barHeight),
            )
        },
        // 外侧bar up
        mouseupBar() {
            this.isBarMouseDown = false
            this.$refs.inputRef.focus()
        },

        // 鼠标离开内侧滚动条
        mouseleaveBar() {
            this.isBarMouseDown = false
            this.$refs.inputRef.focus()

            // // 清除内测监听的鼠标移动事件
            // this.$refs.barRef.removeEventListener('mousemove', this.barMoveEvent)
        },

        // 按上下键切换选项,相关
        scrollBarEvent(direction) {
            if (direction === 'next') {
                if (this.currentIndex === this.filtersOptions.length - 1) {
                    this.$refs.scrollCoverRef.scrollTop = 0
                    return (this.currentIndex = 0)
                }
                this.currentIndex += 1

                if (this.currentIndex > this.liOption.number - 1) {
                    this.$refs.scrollCoverRef.scrollTop =
                        (this.currentIndex - (this.liOption.number - 1)) * this.liOption.height
                }

                return
            }
            // pre
            if (this.currentIndex === 0) {
                this.$refs.scrollCoverRef.scrollTop =
                    (this.filtersOptions.length - 1 - (this.liOption.number - 1)) *
                    this.liOption.height
                return (this.currentIndex = this.filtersOptions.length - 1)
            }
            if (this.currentIndex > 0) {
                this.currentIndex += -1
                // 需要再确定精度 --- 这儿还缺少对 0~6
                this.$refs.scrollCoverRef.scrollTop =
                    (this.currentIndex - (this.liOption.number - 1)) * this.liOption.height
            }
        },

        // 模糊收缩列表的展示
        filterOptions(val) {
            // 只展示匹配项
            this.filtersOptions = this.options.filter((option) => {
                if (option.label.includes(val)) {
                    return option
                }
            })

            this.inputFocus()
        },
        // ul在上下键时的滚动事件
        scrollEvent(e) {
            let scrollTop = e.target.scrollTop
            let top = Math.floor(scrollTop / this.scrollRotate) + 'px'
            this.$refs.barRef.style.top = top
        },

        // focus事件
        inputFocus() {
            let that = this
            this.filtersOptions = this.options.filter((item) => {
                if (item.label.includes(this.inputVal)) {
                    return item
                }
            })
            this.isFocus = true
            this.$forceUpdate()
            // 监听ul的滚动
            this.$nextTick(() => {
                this.$refs.scrollCoverRef.style.maxHeight =
                    this.liOption.number * this.liOption.height + 'px'
                const style = window.getComputedStyle(this.$refs.scrollCoverRef)
                const style1 = window.getComputedStyle(this.$refs.ulRef)
                // 可见区高度
                let coverHeight = style.height.replace('px', '') - 0
                this.coverHeight = coverHeight
                // 可滚动区的总高度
                let scrollHeight = style1.height.replace('px', '') - 0
                if (scrollHeight <= coverHeight) {
                    this.isBarShow = false
                    return
                }

                this.isBarShow = true

                // 滚动条高度
                this.barHeight = Math.floor((coverHeight * coverHeight) / scrollHeight)

                this.scrollRotate = scrollHeight / coverHeight
            })
            this.$refs.scrollCoverRef.addEventListener('scroll', this.scrollEvent)
            this.$refs.barRef.addEventListener('mousedown', function () {
                clearTimeout(that.inputBlurTimer)
                that.inputBlurTimer = null
            })
        },

        // 失去焦点
        inputBlur() {
            if (this.isBarMouseDown) {
                return
            }
            this.isFocus = false
            if (this.inputVal_ !== this.inputVal) {
                this.inputVal = this.inputVal_ ? this.inputVal_ : this.value_
            }
        },

        // 将value值返回
        handlelabel(option) {
            this.isFocus = false
            this.inputVal = option.label
            this.inputVal_ = option.label
            this.$emit('input', option.value)
        },
    },
}
</script>

<style lang="scss" scoped>
$primary: #409eff;
$primary_: #f5f7fa;
.kl-select {
    width: 220px;
    .kl-select-input-cover {
        position: relative;
        svg {
            position: absolute;
            top: 50%;
            right: 10px;
            transform: translate(0, -50%);
            transition: all 0.5s;
        }
    }
    .kl-select-input {
        width: 220px;
        padding-right: 30px;
        padding-left: 10px;
        height: 40px;
        border: 1px solid #ccc;
        border-radius: 5px;
        color: #666;
        &:focus {
            border-color: $primary;
        }
    }

    .kl-ul-cover {
        margin-top: 13px;
        position: relative;
        .kl-sj {
            position: absolute;
            top: -5px;
            left: 30px;
            display: block;
            width: 7px;
            height: 7px;
            border: 1px solid #ddd;
            border-left: none;
            border-top: none;
            background-color: #fff;
            transform: rotate(-135deg);
            z-index: 999;
        }
    }

    .kl-ul-maxheight {
        padding: 5px 0;
        box-shadow: 0 0 5px #cdcdcd;
        position: relative;

        .kl-scroll-cover {
            border-radius: 4px;
            overflow-y: auto;
        }
    }

    ul {
        cursor: pointer;
        background-color: #fff;

        li {
            vertical-align: baseline;
            padding-left: 10px;
            display: flex;
            align-items: center;
            color: #666;
            font-size: 14px;
            &:hover {
                background-color: $primary_;
            }
        }
        .no-data {
            color: #aaa;
            padding: 5px 0;
            justify-content: center;
        }
    }
}

::-webkit-input-placeholder {
    /* Chrome/Opera/Safari */
    color: #ccc;
}
::-moz-placeholder {
    /* Firefox 19+ */
    color: #ccc;
}
:-ms-input-placeholder {
    /* IE 10+ */
    color: #ccc;
}
:-moz-placeholder {
    /* Firefox 18- */
    color: #ccc;
}

.active {
    color: $primary !important;
    font-weight: 600 !important;
}
.active1 {
    background-color: $primary_ !important;
}
.top {
    transform: translate(0, -50%) rotate(-180deg) !important;
}
.kl-bar {
    height: 238px;
    position: absolute;
    top: 5px;
    right: 0px;
    width: 12px;
    .kl-scroll-bar-cover {
        padding-right: 2px;
        position: absolute;
        top: 0;
        right: 0;
        display: flex;
        justify-content: flex-end;
        width: 12px;
        .kl-scroll-bar {
            width: 6px;
            border-radius: 3px;
            background-color: #dfdfdf;
            z-index: 9999;
        }
    }
}

.bar {
    scrollbar-width: none;
    &::-webkit-scrollbar {
        display: none;
    }
}
</style>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值