带本地搜索功能的选择插件,效果图:
在使用selectfield的过程中,数据过大时,数据加载缓慢,没有模糊查询用户体验也不好,
在selectfield的基础上上稍作修改而成,使用方式同selectfield,代码:
1 Ext.define('ux.field.Select', { 2 extend: 'Ext.field.Text', 3 xtype: 'uxSelectfield', 4 alternateClassName: 'ux.form.Select', 5 requires: [ 6 'Ext.Panel', 7 'Ext.picker.Picker', 8 'Ext.data.Store', 9 'Ext.data.StoreManager', 10 'Ext.dataview.List' 11 ], 12 13 /** 14 * @event change 15 * Fires when an option selection has changed 16 * @param {Ext.field.Select} this 17 * @param {Mixed} newValue The new value 18 * @param {Mixed} oldValue The old value 19 */ 20 21 /** 22 * @event focus 23 * Fires when this field receives input focus. This happens both when you tap on the field and when you focus on the field by using 24 * 'next' or 'tab' on a keyboard. 25 * 26 * Please note that this event is not very reliable on Android. For example, if your Select field is second in your form panel, 27 * you cannot use the Next button to get to this select field. This functionality works as expected on iOS. 28 * @param {Ext.field.Select} this This field 29 * @param {Ext.event.Event} e 30 */ 31 32 config: { 33 /** 34 * @cfg 35 * @inheritdoc 36 */ 37 ui: 'select', 38 39 /** 40 * @cfg {Boolean} useClearIcon 41 * @hide 42 */ 43 44 /** 45 * @cfg {String/Number} valueField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this 46 * Select control. 47 * @accessor 48 */ 49 valueField: 'value', 50 51 /** 52 * @cfg {String/Number} displayField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this 53 * Select control. This resolved value is the visibly rendered value of the available selection options. 54 * @accessor 55 */ 56 displayField: 'text', 57 58 /** 59 * @cfg {Ext.data.Store/Object/String} store The store to provide selection options data. 60 * Either a Store instance, configuration object or store ID. 61 * @accessor 62 */ 63 store: null, 64 65 /** 66 * @cfg {Array} options An array of select options. 67 * 68 * [ 69 * {text: 'First Option', value: 'first'}, 70 * {text: 'Second Option', value: 'second'}, 71 * {text: 'Third Option', value: 'third'} 72 * ] 73 * 74 * __Note:__ Option object member names should correspond with defined {@link #valueField valueField} and {@link #displayField displayField} values. 75 * This config will be ignored if a {@link #store store} instance is provided. 76 * @accessor 77 */ 78 options: null, 79 80 /** 81 * @cfg {String} hiddenName Specify a `hiddenName` if you're using the {@link Ext.form.Panel#standardSubmit standardSubmit} option. 82 * This name will be used to post the underlying value of the select to the server. 83 * @accessor 84 */ 85 hiddenName: null, 86 87 /** 88 * @cfg {Object} component 89 * @accessor 90 * @hide 91 */ 92 component: { 93 useMask: true 94 }, 95 96 /** 97 * @cfg {Boolean} clearIcon 98 * @hide 99 * @accessor 100 */ 101 clearIcon: false, 102 103 /** 104 * 请勿改动此配置 105 */ 106 usePicker: false, 107 108 /** 109 * @cfg {Boolean} autoSelect 110 * `true` to auto select the first value in the {@link #store} or {@link #options} when they are changed. Only happens when 111 * the {@link #value} is set to `null`. 112 */ 113 autoSelect: true, 114 115 /** 116 * @cfg {Object} defaultPhonePickerConfig 117 * The default configuration for the picker component when you are on a phone. 118 */ 119 defaultPhonePickerConfig: null, 120 121 /** 122 * @cfg {Object} defaultTabletPickerConfig 123 * The default configuration for the picker component when you are on a tablet. 124 */ 125 defaultTabletPickerConfig: null, 126 127 /** 128 * @cfg 129 * @inheritdoc 130 */ 131 name: 'picker', 132 133 /** 134 * @cfg {String} pickerSlotAlign 135 * The alignment of text in the picker created by this Select 136 * @private 137 */ 138 pickerSlotAlign: 'center' 139 }, 140 141 platformConfig: [ 142 { 143 theme: ['Windows'], 144 pickerSlotAlign: 'left' 145 }, 146 { 147 theme: ['Tizen'], 148 usePicker: false 149 } 150 ], 151 152 // @private 153 initialize: function () { 154 var me = this, 155 component = me.getComponent(); 156 157 me.callParent(); 158 159 component.on({ 160 scope: me, 161 masktap: 'onMaskTap' 162 }); 163 164 component.doMaskTap = Ext.emptyFn; 165 166 if (Ext.browser.is.AndroidStock2) { 167 component.input.dom.disabled = true; 168 } 169 170 if (Ext.theme.is.Blackberry) { 171 this.label.on({ 172 scope: me, 173 tap: "onFocus" 174 }); 175 } 176 }, 177 178 getElementConfig: function () { 179 if (Ext.theme.is.Blackberry) { 180 var prefix = Ext.baseCSSPrefix; 181 182 return { 183 reference: 'element', 184 className: 'x-container', 185 children: [ 186 { 187 reference: 'innerElement', 188 cls: prefix + 'component-outer', 189 children: [ 190 { 191 reference: 'label', 192 cls: prefix + 'form-label', 193 children: [{ 194 reference: 'labelspan', 195 tag: 'span' 196 }] 197 } 198 ] 199 } 200 ] 201 }; 202 } else { 203 return this.callParent(arguments); 204 } 205 }, 206 207 /** 208 * @private 209 */ 210 updateDefaultPhonePickerConfig: function (newConfig) { 211 var picker = this.picker; 212 if (picker) { 213 picker.setConfig(newConfig); 214 } 215 }, 216 217 /** 218 * @private 219 */ 220 updateDefaultTabletPickerConfig: function (newConfig) { 221 var listPanel = this.listPanel; 222 if (listPanel) { 223 listPanel.setConfig(newConfig); 224 } 225 }, 226 227 /** 228 * @private 229 * Checks if the value is `auto`. If it is, it only uses the picker if the current device type 230 * is a phone. 231 */ 232 applyUsePicker: function (usePicker) { 233 if (usePicker == "auto") { 234 usePicker = (Ext.os.deviceType == 'Phone'); 235 } 236 237 return Boolean(usePicker); 238 }, 239 240 syncEmptyCls: Ext.emptyFn, 241 242 /** 243 * @private 244 */ 245 applyValue: function (value) { 246 var record = value, 247 index, store; 248 249 //we call this so that the options configruation gets intiailized, so that a store exists, and we can 250 //find the correct value 251 this.getOptions(); 252 253 store = this.getStore(); 254 255 if ((value != undefined && !value.isModel) && store) { 256 index = store.find(this.getValueField(), value, null, null, null, true); 257 258 if (index == -1) { 259 index = store.find(this.getDisplayField(), value, null, null, null, true); 260 } 261 262 record = store.getAt(index); 263 } 264 265 return record; 266 }, 267 268 updateValue: function (newValue, oldValue) { 269 this.record = newValue; 270 this.callParent([(newValue && newValue.isModel) ? newValue.get(this.getDisplayField()) : '']); 271 }, 272 273 getValue: function () { 274 var record = this.record; 275 return (record && record.isModel) ? record.get(this.getValueField()) : null; 276 }, 277 278 /** 279 * Returns the current selected {@link Ext.data.Model record} instance selected in this field. 280 * @return {Ext.data.Model} the record. 281 */ 282 getRecord: function () { 283 return this.record; 284 }, 285 286 // @private 287 getPhonePicker: function () { 288 var config = this.getDefaultPhonePickerConfig(); 289 290 if (!this.picker) { 291 this.picker = Ext.create('Ext.picker.Picker', Ext.apply({ 292 slots: [ 293 { 294 align: this.getPickerSlotAlign(), 295 name: this.getName(), 296 valueField: this.getValueField(), 297 displayField: this.getDisplayField(), 298 value: this.getValue(), 299 store: this.getStore() 300 } 301 ], 302 listeners: { 303 change: this.onPickerChange, 304 scope: this 305 } 306 }, config)); 307 } 308 309 return this.picker; 310 }, 311 312 // @private 313 getTabletPicker: function () { 314 var config = this.getDefaultTabletPickerConfig(); 315 316 if (!this.listPanel) { 317 this.listPanel = Ext.create('Ext.Panel', Ext.apply({ 318 left: 0, 319 top: 0, 320 modal: true, 321 cls: Ext.baseCSSPrefix + 'select-overlay', 322 layout: 'fit', 323 hideOnMaskTap: true, 324 width: Ext.os.is.Phone ? '14em' : '18em', 325 height: (Ext.os.is.BlackBerry && Ext.os.version.getMajor() === 10) ? '12em' : (Ext.os.is.Phone ? '12.5em' : '22em'), 326 items: [{ 327 xtype: 'toolbar', 328 docked: 'top', 329 items: [ 330 //新增的搜索栏,用于支持模糊查询 331 { 332 xtype: 'searchfield', 333 placeHolder: '请输入关键词', 334 width:'100%', 335 clearIcon:false, 336 listeners: { 337 keyup: 'onSearch', 338 scope: this 339 } 340 } 341 ] 342 }, { 343 xtype: 'list', 344 store: this.getStore(), 345 itemTpl: '<span class="x-list-label">{' + this.getDisplayField() + ':htmlEncode}</span>', 346 listeners: { 347 select: this.onListSelect, 348 itemtap: this.onListTap, 349 scope: this 350 } 351 }] 352 }, config)); 353 } 354 355 return this.listPanel; 356 }, 357 //进行模糊查询 358 onSearchKeyUp: function (value) { 359 //得到数据仓库和搜索关键词 360 var store = this.getStore(); 361 362 //如果是新的关键词,则清除过滤 363 store.clearFilter(!!value); 364 //检查值是否存在 365 if (value) { 366 //the user could have entered spaces, so we must split them so we can loop through them all 367 var key = this.getDisplayField(), 368 searches = value.split(','), 369 regexps = [], 370 //获取现实值的name 371 i, regex; 372 373 //loop them all 374 for (i = 0; i < searches.length; i++) { 375 //if it is nothing, continue 376 if (!searches[i]) continue; 377 378 regex = searches[i].trim(); 379 regex = regex.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 380 381 //if found, create a new regular expression which is case insenstive 382 regexps.push(new RegExp(regex.trim(), 'i')); 383 } 384 385 //now filter the store by passing a method 386 //the passed method will be called for each record in the store 387 store.filter(function (record) { 388 var matched = []; 389 390 //loop through each of the regular expressions 391 for (i = 0; i < regexps.length; i++) { 392 var search = regexps[i], 393 didMatch = search.test(record.get(key)); 394 395 //if it matched the first or last name, push it into the matches array 396 matched.push(didMatch); 397 } 398 399 return (regexps.length && matched.indexOf(true) !== -1); 400 }); 401 } 402 }, 403 //进行模糊查询 404 onSearch: function (field) { 405 this.onSearchKeyUp(field.getValue()); 406 }, 407 // @private 408 onMaskTap: function () { 409 this.onFocus(); 410 411 return false; 412 }, 413 414 /** 415 * Shows the picker for the select field, whether that is a {@link Ext.picker.Picker} or a simple 416 * {@link Ext.List list}. 417 */ 418 showPicker: function () { 419 var me = this, 420 store = me.getStore(), 421 value = me.getValue(); 422 423 //check if the store is empty, if it is, return 424 if (!store || store.getCount() === 0) { 425 return; 426 } 427 if (me.getReadOnly()) { 428 return; 429 } 430 me.isFocused = true; 431 432 if (me.getUsePicker()) { 433 var picker = me.getPhonePicker(), 434 name = me.getName(), 435 pickerValue = {}; 436 437 pickerValue[name] = value; 438 picker.setValue(pickerValue); 439 440 if (!picker.getParent()) { 441 Ext.Viewport.add(picker); 442 } 443 444 picker.show(); 445 } else { 446 //先过滤一下避免加载过慢 447 var record = this.getRecord(), 448 text='请搜索'; 449 if (record) { 450 text = record.get(this.getDisplayField()); 451 } 452 this.onSearchKeyUp(text); 453 454 var listPanel = me.getTabletPicker(), 455 list = listPanel.down('list'), 456 index, record; 457 458 if (!listPanel.getParent()) { 459 Ext.Viewport.add(listPanel); 460 } 461 //为搜索栏赋值 462 listPanel.down('searchfield').setValue(text); 463 listPanel.showBy(me.getComponent(), null); 464 if (value || me.getAutoSelect()) { 465 store = list.getStore(); 466 index = store.find(me.getValueField(), value, null, null, null, true); 467 record = store.getAt(index); 468 469 if (record) { 470 list.select(record, null, true); 471 } 472 } 473 } 474 }, 475 476 // @private 477 onListSelect: function (item, record) { 478 var me = this; 479 if (record) { 480 me.setValue(record); 481 } 482 }, 483 484 onListTap: function () { 485 this.listPanel.hide({ 486 type: 'fade', 487 out: true, 488 scope: this 489 }); 490 }, 491 492 // @private 493 onPickerChange: function (picker, value) { 494 var me = this, 495 newValue = value[me.getName()], 496 store = me.getStore(), 497 index = store.find(me.getValueField(), newValue, null, null, null, true), 498 record = store.getAt(index); 499 500 me.setValue(record); 501 }, 502 503 onChange: function (component, newValue, oldValue) { 504 var me = this, 505 store = me.getStore(), 506 index = (store) ? store.find(me.getDisplayField(), oldValue, null, null, null, true) : -1, 507 valueField = me.getValueField(), 508 record = (store) ? store.getAt(index) : null; 509 510 oldValue = (record) ? record.get(valueField) : null; 511 512 me.fireEvent('change', me, me.getValue(), oldValue); 513 }, 514 515 /** 516 * Updates the underlying `<options>` list with new values. 517 * 518 * @param {Array} newOptions An array of options configurations to insert or append. 519 * 520 * selectBox.setOptions([ 521 * {text: 'First Option', value: 'first'}, 522 * {text: 'Second Option', value: 'second'}, 523 * {text: 'Third Option', value: 'third'} 524 * ]).setValue('third'); 525 * 526 * __Note:__ option object member names should correspond with defined {@link #valueField valueField} and 527 * {@link #displayField displayField} values. 528 * 529 * @return {Ext.field.Select} this 530 */ 531 updateOptions: function (newOptions) { 532 var store = this.getStore(); 533 534 if (!store) { 535 this.setStore(true); 536 store = this._store; 537 } 538 539 if (!newOptions) { 540 store.clearData(); 541 } 542 else { 543 store.setData(newOptions); 544 this.onStoreDataChanged(store); 545 } 546 return this; 547 }, 548 549 applyStore: function (store) { 550 if (store === true) { 551 store = Ext.create('Ext.data.Store', { 552 fields: [this.getValueField(), this.getDisplayField()], 553 autoDestroy: true 554 }); 555 } 556 557 if (store) { 558 store = Ext.data.StoreManager.lookup(store); 559 560 store.on({ 561 scope: this, 562 addrecords: 'onStoreDataChanged', 563 removerecords: 'onStoreDataChanged', 564 updaterecord: 'onStoreDataChanged', 565 refresh: 'onStoreDataChanged' 566 }); 567 } 568 569 return store; 570 }, 571 572 updateStore: function (newStore) { 573 if (newStore) { 574 this.onStoreDataChanged(newStore); 575 } 576 577 if (this.getUsePicker() && this.picker) { 578 this.picker.down('pickerslot').setStore(newStore); 579 } else if (this.listPanel) { 580 this.listPanel.down('dataview').setStore(newStore); 581 } 582 }, 583 584 /** 585 * Called when the internal {@link #store}'s data has changed. 586 */ 587 onStoreDataChanged: function (store) { 588 var initialConfig = this.getInitialConfig(), 589 value = this.getValue(); 590 591 if (value || value == 0) { 592 this.updateValue(this.applyValue(value)); 593 } 594 595 if (this.getValue() === null) { 596 if (initialConfig.hasOwnProperty('value')) { 597 this.setValue(initialConfig.value); 598 } 599 600 if (this.getValue() === null && this.getAutoSelect()) { 601 if (store.getCount() > 0) { 602 this.setValue(store.getAt(0)); 603 } 604 } 605 } 606 }, 607 608 /** 609 * @private 610 */ 611 doSetDisabled: function (disabled) { 612 var component = this.getComponent(); 613 if (component) { 614 component.setDisabled(disabled); 615 } 616 Ext.Component.prototype.doSetDisabled.apply(this, arguments); 617 }, 618 619 /** 620 * @private 621 */ 622 setDisabled: function () { 623 Ext.Component.prototype.setDisabled.apply(this, arguments); 624 }, 625 626 // @private 627 updateLabelWidth: function () { 628 if (Ext.theme.is.Blackberry) { 629 return; 630 } else { 631 this.callParent(arguments); 632 } 633 }, 634 635 // @private 636 updateLabelAlign: function () { 637 if (Ext.theme.is.Blackberry) { 638 return; 639 } else { 640 this.callParent(arguments); 641 } 642 }, 643 644 /** 645 * Resets the Select field to the value of the first record in the store. 646 * @return {Ext.field.Select} this 647 * @chainable 648 */ 649 reset: function () { 650 var me = this, 651 record; 652 653 if (me.getAutoSelect()) { 654 var store = me.getStore(); 655 656 record = (me.originalValue) ? me.originalValue : store.getAt(0); 657 } else { 658 var usePicker = me.getUsePicker(), 659 picker = usePicker ? me.picker : me.listPanel; 660 661 if (picker) { 662 picker = picker.child(usePicker ? 'pickerslot' : 'dataview'); 663 664 picker.deselectAll(); 665 } 666 667 record = null; 668 } 669 670 me.setValue(record); 671 672 return me; 673 }, 674 675 onFocus: function (e) { 676 if (this.getDisabled()) { 677 return false; 678 } 679 var component = this.getComponent(); 680 this.fireEvent('focus', this, e); 681 682 if (Ext.os.is.Android4) { 683 component.input.dom.focus(); 684 } 685 component.input.dom.blur(); 686 687 this.isFocused = true; 688 689 this.showPicker(); 690 }, 691 692 destroy: function () { 693 this.callParent(arguments); 694 var store = this.getStore(); 695 696 if (store && store.getAutoDestroy()) { 697 Ext.destroy(store); 698 } 699 700 Ext.destroy(this.listPanel, this.picker); 701 } 702 });