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. 完善接口文檔和補充在線測試用例


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM