原文链接:点我
已经有很多成熟的智能输入框组件,如Form.js。但是现在MVVM框架,如vue、react的为了实现双向数据绑定会重绘所有的元素,这样就会难以兼容使用。所以笔者开发了Vue组件-智能输入框。
包含的功能大同小异:
- 获得焦点时显示所有备选项
- 失去焦点时隐藏备选项面板
- 输入字符后,检索可能的备选项
- 支持上下键和回车键进行选中
- 支持点击选中
- 支持多选
- 以逗号进行多选的分割
效果图:
图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 }
使用方式:
- 在页面中引入vue.js和bootstrap库
- 在页面中引入smartInput.js和smartInput.css
- 在你的页面中建立vue对象:
new Vue({el: '#root'})
- 在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个参数。
后续需要完善的功能:
- 支持自定义分割符,添加参数
delimiter: '-'
- 支持数据校验(不合法的不允许输入),添加参数
stric: true
- 完善接口文档和补充在线测试用例