Vue组件 - 智能联想输入框

原文链接:点我

已经有很多成熟的智能输入框组件,如Form.js。但是现在MVVM框架,如vue、react的为了实现双向数据绑定会重绘所有的元素,这样就会难以兼容使用。所以笔者开发了Vue组件-智能输入框。

包含的功能大同小异:

  1. 获得焦点时显示所有备选项
  2. 失去焦点时隐藏备选项面板
  3. 输入字符后,检索可能的备选项
  4. 支持上下键和回车键进行选中
  5. 支持点击选中
  6. 支持多选
  7. 以逗号进行多选的分割

效果图:

图1:

图片描述

图2:

图片描述

智能输入框组件封装

将Vue组件封装到js文件中,smartInput.js:

  1 // 智能输入框Vue组件
  2 Vue.component('smart-input', {
  3     template: `<div class="friendSearchContainer">
  4         <input v-model="input" class="form-control smartInput"
  5             placeholder="输入文本自动检索,上下键选取,回车选中,可点选"
  6             data-toggle="tooltip" @click="init" @keydown="search" @blur="blur" />
  7         <ul v-show="searching" class="friendSearchList">
  8             <p v-if="!filtered.length">空数据</p>
  9             <li v-else v-for="(item, index) in filtered" @click.stop="clickOne">{{ item }}</li>
 10         </ul>
 11         <div v-show="searching" class="friendSearchModal" @click="searching=false"></div>
 12     </div>`,
 13     // 接收list/multiple/value参数
 14     props: ['props'],
 15     data() {
 16         return {
 17             searching: false,
 18             timer: null,
 19             filtered: {},
 20             input: '',
 21             focusIndex: 0,
 22             invalidData: ''
 23         };
 24     },
 25     computed: {
 26         listLength() {
 27             return this.filtered.length;
 28         },
 29         key() {
 30             return /(?:.*,)*(.*)$/.exec(this.input)[1];
 31         }
 32     },
 33     mounted() {
 34         // 支持初始化参数值
 35         this.input = this.props.value || '';
 36     },
 37     methods: {
 38         // 调整联想搜索面板的大小和位置
 39         init(e) {
 40             this.searching = true;
 41             this.filtered = this.props.list;
 42         },
 43         // 失去焦点时关闭面板,主要是按下tab键切换时的作用,随之带来的是所有相关的事件都要清除该定时器
 44         blur() {
 45             this.timer = setTimeout(() => {
 46                 this.searching = false;
 47             }, 200);
 48         },
 49         // 在上下键索引后调整视口
 50         scrollViewport() {
 51             let ul = $(this.$el).find('ul');
 52             ul.find('li.hover').removeClass('hover');
 53             ul.find('li').eq(this.focusIndex).addClass('hover');
 54             $('.friendSearchList').scrollTop(this.focusIndex * 26 - 26);
 55         },
 56         // 联想搜索的主体功能函数,这里使用keydown是为了保证持续性的上下键能够保证执行
 57         search(e) {
 58             let preSearching = this.searching;
 59             // 非搜索状态进行点击,则呼出面板
 60             if (!this.searching) {
 61                 this.searching = true;
 62             }
 63             e = e || window.event;
 64             // 通过上下键和回车选择
 65             if (e.keyCode === 38) {
 66                 this.focusIndex = (this.focusIndex - 1 + this.listLength) % this.listLength;
 67                 this.scrollViewport();
 68             } else if (e.keyCode === 40) {
 69                 this.focusIndex = (this.focusIndex + 1 + this.listLength) % this.listLength;
 70                 this.scrollViewport();
 71             } else if (e.keyCode === 13) {
 72                 if (preSearching && this.focusIndex < this.listLength) {
 73                     this.selectOne();
 74                 }
 75             } else {
 76                 // 延时搜索,降低卡顿
 77                 clearTimeout(this.timer);
 78                 this.timer = setTimeout(() => {
 79                     // 进行可选项过滤
 80                     this.filtered = this.props.list.filter(item => {
 81                         return item.toLowerCase().includes(this.key.toLowerCase());
 82                     });
 83                     this.focusIndex = 0;
 84                 }, 800);
 85             }
 86         },
 87         clickOne(e) {
 88             let target = $((e || event).target);
 89             clearTimeout(this.timer);
 90             e = e || window.event;
 91             let value = target.text();
 92             this.focusIndex = target.index();
 93             if (this.props.multiple) {
 94                 let arr = this.input.split(',');
 95                 let has = target.hasClass('active');
 96                 if (has) {
 97                     target.removeClass('active');
 98                     let index = arr.indexOf(value);
 99                     arr.splice(index, 1);
100                     this.input = arr.join(',');
101                 } else {
102                     target.addClass('active');
103                     arr.splice(arr.length - 1, 1, value);
104                     this.input = arr.join(',') + ',';
105                 }
106             } else {
107                 target.addClass('active').siblings('li').removeClass('active');
108                 this.input = value;
109                 this.searching = false;
110             }
111         },
112         // 选择一个参数
113         selectOne(e) {
114             clearTimeout(this.timer);
115             let target = $(this.$el).find('ul li').eq(this.focusIndex);
116             let value = target.text();
117             if (this.props.multiple) {
118                 let arr = this.input.split(',');
119                 let has = target.hasClass('active');
120                 if (has) {
121                     target.removeClass('active');
122                     let index = arr.indexOf(value);
123                     arr.splice(index, 1);
124                     this.input = arr.join(',');
125                 } else {
126                     target.addClass('active');
127                     arr.splice(arr.length - 1, 1, value);
128                     this.input = arr.join(',') + ',';
129                 }
130             } else {
131                 target.addClass('active').siblings('li').removeClass('active');
132                 this.input = value;
133                 this.searching = false;
134             }
135         }
136     },
137     watch: {
138         input(val) {
139             let inputArr = val.split(',');
140             if (this.props.multiple) {
141                 inputArr.pop();
142                 let invalidData = [];
143                 inputArr.forEach(item => {
144                     if (!this.props.list.includes(item)) {
145                         invalidData.push(item);
146                     }
147                 });
148                 let $input = $('input', $(this.$el));
149                 if (invalidData.length) {
150                     $input.attr('title', invalidData.join(',') + '数据不合法');
151                     $input.tooltip();
152                 } else {
153                     $input.tooltip('hide');
154                 }
155             }
156             // 触发标签内声明的sync函数,用于传递数据给父组件
157             this.$emit('sync', this.input);
158         }
159     }
160 });

 

将样式表封装为smartInput.css:

 1 // smartInput输入框需要的样式表
 2 .friendSearchContainer {
 3     position: relative;
 4 }
 5 .friendSearchList {
 6     width: 100%;
 7     padding: 6px 12px;
 8     overflow-y: scroll;
 9     max-height: 300px;
10     background: #fff;
11     z-index: 10;
12     box-shadow: 0 10px 10px rgba(0, 0, 0, .2);
13     border: 1px solid #ccc;
14     position: absolute;
15 }
16 .friendSearchList li {
17     padding: 3px 12px;
18 }
19 .friendSearchList li:hover {
20     background-color: #36bc7f;
21     color: #fff;
22 }
23 .friendSearchList li.active {
24     background: #337ab7;
25     color: #fff;
26 }
27 .friendSearchList li.hover {
28     background-color: #36bc7f;
29     color: #fff;
30 }
31 .friendSearchList li.active:hover {
32     background-color: #36bc7f;
33 }
34 .friendSearchModal {
35     position: fixed;
36     top: 0;
37     left: 0;
38     height: 100%;
39     width: 100%;
40     z-index: 1;
41 }

使用方式:

  1. 在页面中引入vue.js和bootstrap库
  2. 在页面中引入smartInput.js和smartInput.css
  3. 在你的页面中建立vue对象:new Vue({el: '#root'})
  4. 在root根组件里直接添加<smart-input>标签即可

实例:

在html页面里新建DOM,直接包含<smart-input></smart-input>标签即可:

 1 <div role="tabpanel" class="tab-pane active" id="flowDispatch">
 2      <div class="row">
 3           <div class="col-md-6">
 4                <div class="form-group">
 5                     <label for="service" class="col-sm-2 control-label">业务:</label>
 6                     <div class="col-sm-10">
 7                           <smart-input id="service" placeholder="Email" @sync="syncService" :props="serviceList"></smart-input>
 8                     </div>
 9                </div>
10            </div>
11            <div class="col-md-6">
12                 <div class="form-group">
13                      <label for="service" class="col-sm-2 control-label">地区:</label>
14                      <div class="col-sm-10">
15                           <smart-input id="service" placeholder="Email" @sync="syncArea" :props="areaList"></smart-input>
16                      </div>
17                  </div>
18            </div>
19       </div>
20 </div>

在index.js里初始化Vue对象:

 1 $(function () {
 2     let flowDispatch = new Vue({
 3         el: '#flowDispatch',
 4         data: {
 5             serviceList: {
 6                 list: ['apk','pcs','opencdn','kafka','cdn','ssl'],
 7                 // 支持参数多选
 8                 multiple: true
 9             },
10             service: '',
11             areaList: {
12                 list: ['河北','河南','山东','天津','重庆','全国'],
13                 // 支持初始值设定
14                 value: '我是初始值'
15             },
16             area: ''
17         },
18         mounted() {
19             this.init();
20         },
21         methods: {
22             // 初始化页面参数
23             init() {
24                 // 接口获取数据列表,而不是硬写死
25                 // this.getArea();
26             },
27             // 获取地区列表
28             getArea() {
29                 OSS.apiAjaxAccess({
30                     url: '?r=gslb/api/area',
31                     statusCodeCheck: true,
32                     success: data => {
33                         this.areaList.list = data.data;
34                     }
35                 });
36             },
37             // 跟智能输入框同步选中的业务
38             syncService(data) {
39                 this.service = data;
40             },
41             syncArea(data) {
42                 this.area = data;
43             },
44         }
45     });
46 });

接口文档

我们只需要在初始化的vue对象里设置好相关的属性即可生效:

1 serviceList: {
2     list: ['apk','pcs','opencdn','kafka','cdn','ssl'],
3     multiple: true,
4     value: '我是初始值'
5 },

暂时只支持这3个参数。

后续需要完善的功能:

  1. 支持自定义分割符,添加参数delimiter: '-'
  2. 支持数据校验(不合法的不允许输入),添加参数stric: true
  3. 完善接口文档和补充在线测试用例
posted @ 2021-09-09 13:08  王小道  阅读(1948)  评论(0编辑  收藏  举报