Vue自定义下拉框组件(customDropDown)

原创  郑建华   2021-07-05   78人阅读  0 条评论

    在两年前曾经写过一篇文章《文本域 自定义下拉框 支持模糊检索 关键字高亮 上下选择》,这是在做一个老项目的时候为了实现自定义下拉框编写的一个简易组件。而今天使用更新的技术vue开发的这个新项目,同样也遇到了这种需求。然而今时不同往日,使用vue编写组件比起原生js+jquery不是在一个级别的。实现效果如下图所示。

image.png

CustomDropDown

自定义下拉框组件,由于ElementUi的DropDown组件的自定义功能不足,所以编写这个组件,既支持下拉框的基本功能,又额外增加一些扩展功能。    

功能说明

基础功能:

1、输入框类型支持文本域和el-input

2、输入框聚焦弹出下拉框,并根据所在位置自动定位

3、点击空白处隐藏下拉框

4、支持placeholder

5、支持下拉框的宽高设置

6、支持直接传入下拉值域数据

扩展功能:

1、支持输入框右下角插槽和下拉框列表右侧插槽(常用于图标显示)

2、支持远程接口获取下拉值域数据,需要传入远程加载数据方法

3、支持在远程接口模式下开启滚动加载数据(可设置每次滚动条数),当鼠标滚动到底部时自动加载数据

4、支持传入输入框的refName(用于输入框的自定义操作)

5、输入框输入内容时下拉框数据进行模糊匹配,并且高亮显示匹配内容

6、支持上、下方向键选择下拉框内容,回车键确认选择下拉框内容,赋值到输入框中

使用案例

<CustomDropDown
   v-show="mode === 'edit' && row.projectName"
   :ref="`checkMethod${row.number}`"
   v-model="row.checkMethod"
   :ref-name="`checkMethod${row.number}`"
   type="textarea"
   placeholder="请填写存在问题"
   :drop-down-width="'250px'"
   :drop-down-height="'300px'"
   :remote="true"
   :scroll-page-load="true"
   :remote-load-fun="queryProblemsFun"
   :content-array="problems"
 >
   <template slot="input-icon">
     <i class="el-icon-search" />
   </template>
   <template slot="item-icon">
     <i class="el-icon-remove" style="font-size: 18px;margin-left: 5px;color:black !important;" @click.stop />
   </template>
 </CustomDropDown>
</template>

data() {
   return {
 problems: [
           {
             key: '1',
             value: '根据央视解说透露,直升机护旗梯队悬挂党旗飞越天安门广场。'
           },
           {
             key: '2',
             value: '中国人民解放军32183部队公众号“中国兵器试验”发布消息'
           },
           {
             key: '3',
             value: '根据解放军的惯例,能在大型庆祝活动中公开亮相,意味着这款装备已经正式服役'
           },
           {
             key: '4',
             value: '资料显示,已经服役多年的解放军直-8直升机,系我国上世纪90年代设计定型的13吨级多用途运输直升机,以法国进口“超黄蜂”直升机为设计蓝本。'
           },
           {
             key: '5',
             value: '最新亮相的直-8L就是直-8家族中的最新改进型,其最大变化是整体变宽了不少'
           }
     ]
}
},
methods: {
   // 远程查询数据方法
   queryProblemsFun(query, curPage, size, callback) {
     if (callback) {
       callback(this.problems)
     }
   }
}

参数说明

v-model:对应输入框绑定值
ref-name:输入框的ref的绑定名称 默认为32位随机字符串
type:输入框的类型 支持textarea、input 默认为textarea  textarea为原生textarea,input为el-input
placeholder:输入框的提示文字
drop-down-width:下拉框的宽度 默认250px
drop-down-height:下拉框的高度 默认300px
content-array:下拉框的列表数据 类型为数组 其中元素格式为{key:'',value:''}
remote: 是否开启远程加载数据接口 默认false
remote-load-fun:远程加载数据方法 支持四个参数query(当前查询字段), curPage(当前页数), size(每页条数), callback(加载完成后的回调处理)
scroll-page-load:是否开启滚动加载数据 默认false
scroll-size:滚动加载每页条数 默认为10
noHideClass:在这个class下的,点击就不进行隐藏

组件代码

<template>
  <div class="customDropDownClass" @click.stop>
    <textarea v-if="type === 'textarea'" :ref="refName" v-model="customValue" :class="refName" :placeholder="placeholder" @input="inputChange(customValue)" @focus="inputFocus(customValue)" />
    <el-input v-if="type === 'input'" :ref="refName" v-model="customValue" :class="refName" :placeholder="placeholder" @input="inputChange(customValue)" @focus="inputFocus(customValue)" />
    <div class="input-icon">
      <slot name="input-icon" />
    </div>
    <div v-show="showDropDown" :class="'dropDown-'+refName+'-scroll'" class="drop-down" :style="{'width':dropDownWidth,'height':dropDownHeight,'bottom':dropDownBottom,'top':dropDownTop}" @scroll="dropDownScrollFun">
      <div v-for="(item,index) in showContentArray" :key="index" class="drop-down-item" @click="itemClick(item)">
        <div class="contentDiv" :class="{'activeContent':index === selectIndex}">
          <div class="content" :class="['ellipsis-' + ellipsisNumber]" v-html="item.label" />
          <div class="item-icon">
            <slot name="item-icon" :selectRow="{$index:index,data:item}" />
          </div>
        </div>
        <el-divider class="dividerClass" />
      </div>
    </div>
  </div>
</template>

<script>
import $ from 'jquery'

export default {
  name: 'CustomDropDown',
  props: {
    // 输入框的值
    value: {
      type: String,
      default: ''
    },
    // 输入框类型  textarea、input
    type: {
      type: String,
      default: 'textarea'
    },
    // 参数配置
    options: {
      type: Object,
      default() {
        return {
          'key': 'key', // 下拉框key对应的field
          'value': 'value' // 下拉value对应的field
        }
      }
    },
    // 超出省略行数 0为不省略
    ellipsisNumber: {
      type: Number,
      default: 0
    },
    // 内容数组 item为{key:'',value:''}
    contentArray: {
      type: Array,
      default() {
        return []
      }
    },
    // 是否开启远程加载数据
    remote: {
      type: Boolean,
      default: false
    },
    // 是否开启滚动分页加载数据
    scrollPageLoad: {
      type: Boolean,
      default: false
    },
    // 滚动分页加载每页条数
    scrollSize: {
      type: Number,
      default: 10
    },
    // 远程加载数据方法
    remoteLoadFun: {
      type: Function,
      default() {
        return (query, curPage, size, callback) => {}
      }
    },
    // 映射项ref名称
    refName: {
      type: String,
      default() {
        return this.$string.UUIDRandomString(32)
      }
    },
    // 下拉列表高度
    dropDownHeight: {
      type: String,
      default() {
        return '300px'
      }
    },
    // 下拉列表宽度
    dropDownWidth: {
      type: String,
      default() {
        return '250px'
      }
    },
    // 输入提示
    placeholder: {
      type: String,
      default: '请选择'
    },
    // 在这个class名下就不隐藏
    noHideClass: {
      type: String,
      default: 'noHideClass'
    }
  },
  data() {
    return {
      showContentArray: [], // 显示的内容列表
      remoteContentArray: [], // 获取的远程数据数组
      customValue: '', // 自定义输入框的值
      curPage: 1, // 当前页数
      selectIndex: -1, // 选中下标
      dropDownBottom: 0,
      dropDownTop: 0,
      scrollBottomLoadDistance: 3, // 滚动到距离底部x距离时进行加载数据
      moreLoading: false, // 加载更多loading
      showDropDown: false // 下拉框显示标记
    }
  },
  watch: {
    // 监听value值变化
    value(val) {
      // 更新输入框的值
      this.customValue = val
    },
    // 监听下拉框的显示标记
    showDropDown(value) {
      // 当前为隐藏时 跳过
      if (!value) {
        return
      }
      this.$nextTick(() => {
        const brotherArray = this.$parent.$children
        if (brotherArray && brotherArray.length) {
          // 循环隐藏非当前项的下拉框
          brotherArray.forEach(item => {
            if (item.refName !== this.refName) {
              item.showDropDown = false
            }
          })
        }
      })
    }
  },
  mounted() {
    this.$nextTick(() => {
      // 给document添加点击事件 点击空白处 关闭下拉框
      document.addEventListener('click', this.hideDropDown)
      // 获取输入框对象
      // eslint-disable-next-line no-undef
      const inputObj = $('.customDropDownClass').find('.' + this.refName)
      if (inputObj && inputObj.length) {
        const that = this
        // 方向键监听
        inputObj[0].addEventListener('keydown', (event) => {
          // 上
          if (event.keyCode === 38) {
            // 当前选中第一个元素时,重置下标为最后一个元素  不是第一个元素时,下标-1 (等于-1时,如果按上键 则跳转到最后一个元素)
            that.selectIndex = that.selectIndex === 0 ? that.showContentArray.length - 1 : that.selectIndex === -1 ? that.showContentArray.length - 1 : that.selectIndex - 1
            that.$nextTick(() => {
              // 向上滚动
              that.scrollUpFun()
            })
          // 下
          } else if (event.keyCode === 40) {
            // 当前选中最后一个元素时,重置下标为0  不是最后一个元素时,下标+1
            that.selectIndex = that.selectIndex === that.showContentArray.length - 1 ? 0 : that.selectIndex + 1
            that.$nextTick(() => {
              // 向下滚动
              that.scrollDownFun()
            })
            // 回车时 且下拉框显示 且下拉框选中值不为 -1
          } else if (event.keyCode === 13 && that.showDropDown && that.selectIndex !== -1) {
            // 赋值
            that.customValue = that.showContentArray[that.selectIndex].value
            // 更新外部值
            that.$emit('input', that.customValue)
            // 隐藏下拉框
            that.showDropDown = false
          }
        })
      }
    })

    // 文本域自动调整高度
    $.fn.autoTextarea = function(options) {
      const defaults = {
        maxHeight: null,
        // 文本框是否自动撑高,默认:null,不自动撑高;如果自动撑高必须输入数值,该值作为文本框自动撑高的最大高度
        minHeight: $(this).height() // 默认最小高度,也就是文本框最初的高度,当内容高度小于这个高度的时候,文本以这个高度显示
      }

      const opts = $.extend({}, defaults, options)

      return $(this).each(function() {
        let height
        const style = this.style
        this.style.height = opts.minHeight + 'px'
        if (this.scrollHeight > opts.minHeight) {
          if (opts.maxHeight && this.scrollHeight > opts.maxHeight) {
            height = opts.maxHeight
            style.overflowY = 'scroll'
          } else {
            height = this.scrollHeight
            style.overflowY = 'hidden'
          }
          style.height = height + 'px'
        }

        $(this).bind('paste cut keydown keyup focus blur ready',
          function() {
            let height
            const style = this.style
            this.style.height = opts.minHeight + 'px'
            if (this.scrollHeight > opts.minHeight) {
              if (opts.maxHeight && this.scrollHeight > opts.maxHeight) {
                height = opts.maxHeight
                style.overflowY = 'scroll'
              } else {
                height = this.scrollHeight
                style.overflowY = 'hidden'
              }
              style.height = height + 'px'
            }
          })
      })
    }
  },
  destroyed() {
    // 给document移除点击事件
    document.removeEventListener('click', this.hideDropDown)
  },
  methods: {
    // 向上滚动方法
    scrollUpFun() {
      // 获取下拉框对象
      // eslint-disable-next-line no-undef
      const scroll = $('.customDropDownClass').find('.dropDown-' + this.refName + '-scroll')
      // 获取到下拉框中选中的内容
      const activeContent = scroll.find('.activeContent')
      // 下拉框dom对象
      const scrollDom = scroll && scroll.length ? scroll[0] : null
      // 获取到dom节点
      const activeDom = activeContent && activeContent.length ? activeContent[0] : null
      // 下标等于最后一个元素时
      if (this.selectIndex === this.showContentArray.length - 1) {
        // 滚动到底部  底部的距离顶部值 =  用整个滚动条的高度 - 可视区域的高度
        scrollDom.scrollTop = scrollDom ? scrollDom.scrollHeight - scrollDom.offsetHeight : 0
        return
      }
      // 选中元素的距离顶部高度
      const activeOffSetTop = activeDom ? activeDom.offsetTop : 0
      // 当前滚动条的 距离顶部高度
      const scrollScrollTop = scrollDom ? scrollDom.scrollTop : 0
      // 如果选中元素的距离顶部高度 小于 滚动条距离顶部的高度 说明 当前项未显示完全
      if (activeOffSetTop < scrollScrollTop) {
        // 向上滚动 差值
        scrollDom.scrollTop = scrollScrollTop - (scrollScrollTop - activeOffSetTop)
      }
    },
    // 向下滚动方法
    scrollDownFun() {
      // 获取下拉框对象
      // eslint-disable-next-line no-undef
      const scroll = $('.customDropDownClass').find('.dropDown-' + this.refName + '-scroll')
      // 获取到下拉框中选中的内容
      const activeContent = scroll.find('.activeContent')
      // 下拉框dom对象
      const scrollDom = scroll && scroll.length ? scroll[0] : null
      // 获取到dom节点
      const activeDom = activeContent && activeContent.length ? activeContent[0] : null
      // 下标为0时
      if (this.selectIndex === 0) {
        // 滚动到顶部
        scrollDom.scrollTop = 0
        return
      }
      // 可视区域底部距离顶部高度 = 滚动条距离顶部的高度 + 可视区域高度
      const visibleBottomHeight = scrollDom ? scrollDom.scrollTop + scrollDom.clientHeight : 0
      // 当前项底部距离顶部高度 = 绝对高度 + 当前项实际高度
      const curItemVisibleBottomHeight = activeDom ? activeDom.offsetTop + activeDom.offsetHeight : 0
      // 当前项距离顶部高度 小于 可视区域底部距离顶部高度时  说明 当前项未完全显示
      if (curItemVisibleBottomHeight > visibleBottomHeight) {
        // 向下滚动 差值 再多偏移2px 显示更多内容
        scrollDom.scrollTop = scrollDom.scrollTop + (curItemVisibleBottomHeight - visibleBottomHeight) + 2
      }
    },
    // 下拉框滚动条监听
    dropDownScrollFun(val) {
      // 获取下拉框对象
      const scroll = document.querySelector('.dropDown-' + this.refName + '-scroll')
      /**
       * 处理div内部滚动条滚动到底部后导致 再次滚动时触发外部的滚动条
       * 处理方法为:
       * 当前内部滚动条滚动到距离底部小于等于2px时,将其滚动条位置 重置为距离底部2px
       * scrollTop: 可视区域顶部与滚动条顶部的距离
       * scrollHeight: 整个滚动条的高度
       * clientHeight: 可视区域的高度
        */
      // 当前滚动条距离底部还剩2px时
      if (scroll.scrollHeight - (scroll.scrollTop + scroll.clientHeight) <= 2) {
        // 定位到距离底部2px的位置
        scroll.scrollTop = (scroll.scrollHeight - scroll.clientHeight - 2)
      }
      // 当前滚动条距离底部还剩 指定高度时
      if (scroll.scrollHeight - (scroll.scrollTop + scroll.clientHeight) <= this.scrollBottomLoadDistance) {
        // 触发加载数据方法
        this.scrollPageLoadFun()
      }
    },
    // 滚动分页加载数据方法
    scrollPageLoadFun() {
      // 开启远程加载 且 开启滚动分页加载
      if (this.remote && this.scrollPageLoad) {
        // 开启加载更多loading
        this.moreLoading = true
        // 页码自增
        this.curPage++
        // 调用远程加载方法 传入分页参数
        this.remoteLoadFun(this.customValue, this.curPage, this.scrollSize, (dataList) => {
          if (dataList && dataList.length) {
            // 远程数据追加
            this.remoteContentArray = this.remoteContentArray.concat(dataList)
            // 调用过滤数据
            this.filterShowArray(this.customValue)
          }
          this.moreLoading = false
        })
      }
    },
    // 输入框聚焦
    inputFocus(val) {
      // 开启远程加载数据
      if (this.remote) {
        // 远程加载数据成功后的回调
        const callBack = (dataList) => {
          // 远程数据重新赋值
          this.remoteContentArray = dataList
          // 调用过滤数据
          this.filterShowArray(val)
        }
        // 开启滚动分页加载
        if (this.scrollPageLoad) {
          // 调用远程加载方法 传入分页参数
          this.remoteLoadFun(val, this.curPage, this.scrollSize, callBack)
        } else {
          // 调用远程加载方法  无需分页 查询全部数据
          this.remoteLoadFun(val, 0, 0, callBack)
        }
      } else {
        // 过滤数据 显示下拉框
        this.filterShowArray(this.customValue)
      }
      // 获取输入框对象
      // eslint-disable-next-line no-undef
      const inputObj = $('.customDropDownClass').find('.' + this.refName)
      if (inputObj && inputObj.length) {
        // 当前输入框底部距离页面顶部的高度
        const clientRectBottom = inputObj.parent()[0].getBoundingClientRect().bottom
        // 当前输入框的高度
        const inputHeight = inputObj.parent()[0].clientHeight
        // 当前下拉框本身的高度
        const dropDownHeightVal = Number(this.dropDownHeight.replace('px', ''))
        // 当前可视区域的高度
        const pageHeight = document.body.clientHeight
        // 下拉框显示部分超出了可视区域
        if ((clientRectBottom + dropDownHeightVal) > pageHeight) {
          // 向上偏移 下拉框本身高度 额外加2
          this.dropDownBottom = (inputHeight + 2) + 'px'
          this.dropDownTop = 'auto'
        } else {
          // 向下偏移 下拉框本身高度 额外加2
          this.dropDownBottom = 'auto'
          this.dropDownTop = (inputHeight + 2) + 'px'
        }
      }
    },
    // 隐藏下拉框方法
    hideDropDown() {
      // 如果点击的不是这个class或者这个class的子节点则隐藏
      if (!($(window.event.target).closest('.' + this.noHideClass).length)) {
        this.showDropDown = false
      }
    },
    // 输入框值变化
    inputChange(val) {
      // 开启远程加载数据
      if (this.remote) {
        // 远程加载数据成功后的回调
        const callBack = (dataList) => {
          // 远程数据重新赋值
          this.remoteContentArray = dataList
          // 调用过滤数据
          this.filterShowArray(val)
        }
        // 开启滚动分页加载
        if (this.scrollPageLoad) {
          // 每次发生变化 重置页数为1
          this.curPage = 1
          // 调用远程加载方法 传入分页参数
          this.remoteLoadFun(val, this.curPage, this.scrollSize, callBack)
        } else {
          // 调用远程加载方法  无需分页 查询全部数据
          this.remoteLoadFun(val, 0, 0, callBack)
        }
      } else {
        // 过滤数据 显示下拉框
        this.filterShowArray(this.customValue)
        // 过滤数据
        this.filterShowArray(val)
      }
      // 更新外部值
      this.$emit('input', val)
    },
    // 在外部删除时同步数据,判断是否开启了远程加载
    initDataList(data) {
      if (this.remote) {
        this.remoteContentArray = data
        this.filterShowArray()
      }
    },
    // 过滤显示数据
    filterShowArray(filterValue) {
      // 获取下拉数据 如果是开启远程加载 则获取远程数据 否则获取传入数据
      const dropDownArray = (this.remote ? this.remoteContentArray : this.contentArray) || []
      // 过滤数据  如果传入值则置空数组 未传入值取全部数据
      let filterArray = filterValue ? [] : dropDownArray
      // 完全匹配下标
      let allMatchingIndex = -1
      // 如果过滤值不为空
      if (filterValue) {
        // 根据过滤值  从下拉数据中 进行模糊匹配  获取过滤后的数据
        filterArray = dropDownArray.filter(item => {
          return item && item[this.options.value] && item[this.options.value].indexOf(filterValue) !== -1
        })
        // 高亮显示值处理
        filterArray.forEach((item, index) => {
          // 获取内容值
          const value = item[this.options.value] || ''
          // 设置显示值增加加粗效果
          item.label = value.replace(filterValue, '<b>' + filterValue + '</b>')
          // 如果内容值与过滤值完全一致 则设置对应下标 否则取-1
          allMatchingIndex = value === filterValue ? index : -1
        })
      } else {
        // 处理显示值
        filterArray.forEach(item => {
          item.label = item[this.options.value]
        })
      }
      // 设置到显示值数组中
      this.showContentArray = JSON.parse(JSON.stringify(filterArray))
      // 下拉框显示隐藏处理 如果没有匹配数据 则不显示
      this.showDropDown = this.showContentArray.length > 0
      // 设置选中下标
      this.selectIndex = allMatchingIndex
    },
    // 选项点击
    itemClick(item) {
      // 赋值
      this.customValue = item[this.options.value]
      // 更新外部值
      this.$emit('input', this.customValue)
      // 隐藏下拉框
      this.showDropDown = false
      this.$nextTick(() => {
        if (this.type === 'textarea') {
          // eslint-disable-next-line no-undef
          $('.customDropDownClass textarea').autoTextarea({
            minHeight: 28,
            maxHeight: 220// 文本框是否自动撑高,默认:null,不自动撑高;如果自动撑高必须输入数值,该值作为文本框自动撑高的最大高度
          })
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.customDropDownClass{
  position: relative;
  .input-icon{
    cursor: pointer;
    position: absolute;
    bottom: 0;
    right:0;
  }
  .drop-down{
    &::-webkit-scrollbar {
      width:4px
    }
    &::-webkit-scrollbar-thumb {
      background:transparent;
      border-radius:4px
    }
    &:hover::-webkit-scrollbar-thumb {
      background:hsla(0,0%,53%,.4)
    }
    &:hover::-webkit-scrollbar-track {
      background:hsla(0,0%,53%,.1)
    }
    width: 250px;
    height: 300px;
    overflow-y: scroll;
    background-color: #FFF;
    position: absolute;
    border: 2px solid #E0E4ED;
    box-shadow: 2px 2px 8px #d8d5d5;
    border-radius: 5px;
    left: 0;
    z-index: 999999;
  }
  .drop-down-item{
    .contentDiv{
      display: flex;
      align-items:center;
    }
    .contentDiv:hover{
      background-color: #dde0e2;
      .item-icon{
        display: block;
      }
    }
    .item-icon{
      display: none;
      position: absolute;
      right: 0;
      transition: 0.3s;
    }
    .activeContent{
      color: #1890ff;
    }
    .content{
      text-align: left;
      padding-left:5px;
      margin-bottom:5px;
      cursor: pointer;
    }
    /deep/.dividerClass{
      margin:2px 0 !important;
      background-color: #e2e2e2 !important;
    }
  }
}
</style>

本文地址:https://www.zjh336.cn/?id=2045
版权声明:本文为原创文章,版权归 郑建华 所有,欢迎分享本文,转载请保留出处!

发表评论


表情

还没有留言,还不快点抢沙发?