Vue 折腾记 - (9) 写一个挺靠谱的typeahead组件

前言

之前那个typeahead写的太早,不满足当前的业务需求 而且有些瑕疵,还有也不方便传入数据和响应数据.. 于是就推倒了重来,写了个V2的版本

看图,多了一些细节的考虑;精简了实现的逻辑代码


效果图

实现的功能

  1. 鼠标点击下拉框之外的区域关闭下拉框
  2. 支持键盘上下键选择,支持鼠标选择
  3. 支持列表过滤搜索
  4. 支持外部传入列表JSON格式的映射
  5. 支持placeholder的传入
  6. 选中对象的响应(.sync vue2.3的组件通讯的语法糖)
  7. 箭头icon的映射,感觉作用不大,移除了

你能学到什么

见仁见智哈


用法

<select-search 
style="max-width:195px" 
placeholder="请选择广告主" 
:asyncData.sync="adHostData" 
:mapData="adHostDataList" 
:mapDataFormat="{label:'userName',value:'userId'}">
</select-search>
  • asyncData:响应的数据,也就是选中的..回来是一个对象
  • mapData : 搜索的列表数据,肯定是外部传入了...
  • mapDataFormat : 列表值映射

代码

  • selectSearch.vue
  <template>
  <div class="select-search" v-if="typeaheadData" ref="selectSearch" @click.native="showHideMenu($event)">
    <div class="select-header">
      <input type="text" autocomplete="off" readonly :placeholder="placeholder" :value="placeholderValue" @keydown.down.prevent="selectChildWidthArrowDown" @keydown.up.prevent="selectChildWidthArrowUp" @keydown.enter="selectChildWidthEnter">
      <i class="fzicon " :class="isExpand?'fz-ad-jiantou1':'fz-ad-jiantou'"></i>
    </div>
    <div class="select-body" v-if="isExpand && typeaheadData">
      <input type="text" placeholder="关键字" v-model="searchVal" autocomplete="off" @keydown.esc="resetDefaultStatus" @keydown.down.prevent="selectChildWidthArrowDown" @keydown.up.prevent="selectChildWidthArrowUp" @keydown.enter="selectChildWidthEnter">
      <transition name="el-fade-in-linear" mode="out-in">
        <div class="typeahead-filter">
          <transition-group tag="ul" name="el-fade-in-linear" v-show="typeaheadData.length>0">
            <li v-for="(item,index) in typeaheadData" :key="index" :class="item.active ? 'active':''" @mouseenter="setActiveClass(index)" @mouseleave="setActiveClass(index)" @click="selectChild(index)">
              <a href="javascript:;">
                {{item[mapDataFormat.label]}}
              </a>
            </li>
          </transition-group>
          <p class="noFound" v-show="typeaheadData && typeaheadData.length === 0">未能查询到,请重新输入!</p>
        </div>
      </transition>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'selectSearch',
    data: function () {
      return {
        placeholderValue: '',// 给看到选择内容的
        isExpand: false,
        searchVal: '', // 搜索关键字
        resultVal: '', // 保存搜索到的值
        searchList: [], //保存过滤的结果集
        currentIndex: -1, // 当前默认选中的index,
      }
    },
    computed: {
      mapFormatData () { // 外部有传入格式的时候映射mapData
        return this.mapData.map(item => {
          item[this.mapDataFormat.value] = item[this.mapDataFormat.value];
          return item;
        });
      },
      typeaheadData () {
        let temp = [];
        if (this.searchVal && this.searchVal === '') {
          return this.mapFormatData;
        } else {
          this.currentIndex = -1;  // 重置特殊情况下的索引
          this.mapFormatData.map(item => {
            if (item[this.mapDataFormat.label].indexOf(this.searchVal.toLowerCase().trim()) !== -1) {
              temp.push(item)
            }
            return item;
          })
          return temp;
        }
      }
    },
    props: {
      placeholder: {
        type: String,
        default: '--请选择--'
      },
      emptyText: {
        type: String,
        default: '暂无数据'
      },
      mapData: { // 外部传入的列表数据
        type: Array,
        default: function () {
          return []
        }
      },
      mapDataFormat: { // 映射传入数据的格式
        type: Object,
        default: function () {
          return {
            label: 'text',
            value: 'value',
            extraText: 'extraText'
          }
        }
      },
      asyncData: { // 实时响应的值
        type: [Object, String],
        default: function () {
          return {}
        }
      }
    },
    methods: {
      showHideMenu (e) { // 点击其他区域关闭下拉列表
        if (e) {
          if (this.$refs.selectSearch && this.$refs.selectSearch.contains(e.target)) {
            this.isExpand = true;
          } else {
            this.isExpand = false;
          }
        }

      },
      resetDefaultStatus () { // 清除所有选中状态
        this.searchVal = '';
        this.currentIndex = -1;
        this.typeaheadData.map(item => {
          this.$delete(item, 'active');
        })
      },
      setActiveClass (index) { // 设置样式活动类
        this.typeaheadData.map((item, innerIndex) => {
          if (index === innerIndex) {
            this.$set(item, 'active', true);
            this.currentIndex = index;  // 这句话是用来修正index,就是键盘上下键的索引,不然会跳位
          } else {
            this.$set(item, 'active', false)
          }
        })
      },
      selectChildWidthArrowDown () {
        // 判断index选中子项
        if (this.currentIndex < this.typeaheadData.length) {
          this.currentIndex++;
          this.typeaheadData.map((item, index) => {
            this.currentIndex === index ? this.$set(item, 'active', true) : this.$set(item, 'active', false);
          })
        }
      },
      selectChildWidthArrowUp () {
        // 判断index选中子项
        if (this.currentIndex > 0) {
          this.currentIndex--;
          this.typeaheadData.map((item, index) => {
            this.currentIndex === index ? this.$set(item, 'active', true) : this.$set(item, 'active', false);
          })
        }
      },
      selectChildWidthEnter () {
        // 若是结果集只有一个,则默认选中
        if (this.typeaheadData.length === 1) {
          this.$emit('update:asyncData', this.typeaheadData[0]); // emit响应的值
          this.placeholderValue = this.typeaheadData[0][this.mapDataFormat.label];

        } else {
          // 若是搜索的内容完全匹配到项内的内容,则默认选中
          this.typeaheadData.map(item => {
            if (this.searchVal === item[this.mapDataFormat.label] || item.active === true) {
              this.$emit('update:asyncData', item); // emit响应的值
              this.placeholderValue = item[this.mapDataFormat.label];

            }
          })
        }
        this.isExpand = false;
      },
      selectChild (index) {
        // 鼠标点击选择子项
        this.typeaheadData.map((item, innerIndex) => {
          if (index === innerIndex || item.active) {
            this.placeholderValue = item[this.mapDataFormat.label];
            this.$emit('update:asyncData', item); // emit响应的值
          }
        });
        this.isExpand = false;
      },
    },
    mounted () {
      window.addEventListener('click', this.showHideMenu);
    },
    beforeDestroy () {
      window.removeEventListener('click', this.showHideMenu);
    },
    watch: {
      'isExpand' (newValue) {
        if (newValue === false) {
          this.resetDefaultStatus();
        }
      }
    }
  }
</script>

<style scoped lang="scss">
  .el-fade-in-linear-enter-active,
  .el-fade-in-linear-leave-active,
  .fade-in-linear-enter-active,
  .fade-in-linear-leave-active {
    transition: opacity .2s linear;
  }

  .el-fade-in-enter,
  .el-fade-in-leave-active,
  .el-fade-in-linear-enter,
  .el-fade-in-linear-leave,
  .el-fade-in-linear-leave-active,
  .fade-in-linear-enter,
  .fade-in-linear-leave,
  .fade-in-linear-leave-active {
    opacity: 0;
  }

  .noFound {
    text-align: center;
  }

  .select-search {
    position: relative;
    z-index: 1000;
    a {
      color: #333;
      text-decoration: none;
      padding: 5px;
    }
    ul {
      list-style: none;
      padding: 6px 0;
      margin: 0;
      max-height: 200px;
      overflow-x: hidden;
      overflow-y: auto;
      li {
        display: block;
        width: 100%;
        padding: 5px;
        font-size: 14px;
        padding: 8px 10px;
        position: relative;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        color: #48576a;
        height: 36px;
        line-height: 1.5;
        box-sizing: border-box;
        cursor: pointer;
        &.active {
          background-color: #20a0ff;
          a {
            color: #fff;
          }
        }
      }
    }
    .select-header {
      position: relative;
      border-radius: 4px;
      border: 1px solid #bfcbd9;
      outline: 0;
      padding: 0 8px;
      >input {
        border: none;
        -webkit-appearance: none;
        -moz-appearance: none;
        appearance: none;
        width: 100%;
        outline: 0;
        box-sizing: border-box;
        color: #1f2d3d;
        font-size: inherit;
        height: 36px;
        line-height: 1;
      }
      >i {
        transition: all .3s linear;
        display: inline-block;
        position: absolute;
        right: 3%;
        top: 50%;
        transform: translateY(-50%);
      }
    }
    .select-body {
      position: absolute;
      border-radius: 2px;
      background-color: #fff;
      box-sizing: border-box;
      margin: 5px 0;
      padding: 8px;
      width: 100%;
      box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
      >input {
        -webkit-appearance: none;
        -moz-appearance: none;
        appearance: none;
        background-color: #fff;
        background-image: none;
        border-radius: 4px;
        border: 1px solid #bfcbd9;
        box-sizing: border-box;
        color: #1f2d3d;
        font-size: inherit;
        height: 36px;
        line-height: 1;
        outline: 0;
        padding: 3px 10px;
        transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
        width: 100%;
        display: inline-block;
        &:focus {
          outline: 0;
          border-color: #20a0ff;
        }
      }
    }
  }
</style>

总结

对一些小伙伴若是有些许帮助,就是这文章最大的价值;

有更好的实现方式可以往下面留言,谢谢

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏阮一峰的网络日志

JavaScript与有限状态机

有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。 ? 简单说,它有三个特征:   * 状态总数(stat...

3707
来自专栏为数不多的Android技巧

mac下Android studio快捷键配置

前两天重装了mac系统;之前的配置都丢了,因此纪录一下配置的过程以防惨案再次发生~

1653
来自专栏漫漫全栈路

ASP.NET MVC学习笔记06编辑方法和编辑视图

上一篇中,说到了MVC生成的Index方法,和Details方法,现在来说一下自动生成的方法和视图,应该怎么的来进行编辑。 优化日期显示 在这之前,先对前面的...

3965
来自专栏CaiRui

Python Flask-web表单

Flask-WTF扩展可以把处理web表单的过程变成一种愉悦的体验。 ? 一、跨站请求伪造保护 默认情况下,Flask-WTF能够保护所有表单免受跨站请求伪造的...

3729
来自专栏逸鹏说道

Python3 与 C# 并发编程之~ 线程篇2

其实以前的 Linux中是没有线程这个概念的, Windows程序员经常使用线程,这一看~方便啊,然后可能是当时程序员偷懒了,就把进程模块改了改(这就是为什么之...

1954
来自专栏程序员的碎碎念

PHPExcel 表格导入数据代码解析(二)

前面大概讲了phpexcel用表格导入数据到数据库中的教程,今天会详细剖析函数代码,话不多说,先上代码: public function upload...

3526
来自专栏前端知识分享

深入理解Vue的生命周期

  谈到Vue的生命周期,相信许多人并不陌生。但大部分人和我一样,只是听过而已,具体用在哪,怎么用,却不知道。我在学习vue一个多礼拜后,感觉现在还停留在初级阶...

1323
来自专栏程序员的知识天地

新鲜出炉的8月前端面试题

题目的答案提供了一个思考的方向,答案不一定正确全面,有错误的地方欢迎大家请在评论中指出,共同进步。

1032
来自专栏技术博文

jQuery插件 -- Form表单插件jquery.form.js

jQuery Form插件是一个优秀的Ajax表单插件,可以非常容易地、无侵入地升级HTML表单以支持Ajax。jQuery Form有两个核心方法 -- aj...

7485
来自专栏大学生计算机视觉学习DeepLearning

VS下如何建立一个新的MFC程序 网络编程 课设 基于C++ MFC 连接数据库 小应用 小项目浅析展示

3803

扫码关注云+社区

领取腾讯云代金券