原文鏈接:點我
已經有很多成熟的智能輸入框組件,如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
- 完善接口文檔和補充在線測試用例