1/** 2 * Plugin Name: Combo Select 3 * Author : Vinay@Pebbleroad 4 * Date: 23/11/2014 5 * Description:https://github.com/PebbleRoad/combo-select/blob/master/jquery.combo.select.js 6 * Converts a select box into a searchable and keyboard friendly interface. Fallbacks to native select on mobile and tablets 7 */ 8 9// Expose plugin as an AMD module if AMD loader is present: 10(function (factory) { 11 'use strict'; 12 if (typeof define === 'function' && define.amd) { 13 // AMD. Register as an anonymous module. 14 define(['jquery'], factory); 15 } else if (typeof exports === 'object' && typeof require === 'function') { 16 // Browserify 17 factory(require('jquery')); 18 } else { 19 // Browser globals 20 factory(jQuery); 21 } 22}(function ( $, undefined ) { 23 24 var pluginName = "comboSelect", 25 dataKey = 'comboselect'; 26 var defaults = { 27 comboClass : 'combo-select', 28 comboArrowClass : 'combo-arrow', 29 comboDropDownClass: 'combo-dropdown', 30 inputClass : 'combo-input text-input', 31 disabledClass : 'option-disabled', 32 hoverClass : 'option-hover', 33 selectedClass : 'option-selected', 34 markerClass : 'combo-marker', 35 themeClass : '', 36 maxHeight : 200, 37 extendStyle : true, 38 focusInput : true, 39 showLabel : false 40 }; 41 42 /** 43 * Utility functions 44 */ 45 46 var keys = { 47 ESC: 27, 48 TAB: 9, 49 RETURN: 13, 50 LEFT: 37, 51 UP: 38, 52 RIGHT: 39, 53 DOWN: 40, 54 ENTER: 13, 55 SHIFT: 16 56 }, 57 isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase())); 58 59 /** 60 * Constructor 61 * @param {[Node]} element [Select element] 62 * @param {[Object]} options [Option object] 63 */ 64 function Plugin ( element, options ) { 65 66 /* Name of the plugin */ 67 68 this._name = pluginName; 69 70 /* Reverse lookup */ 71 72 this.el = element 73 74 /* Element */ 75 76 this.$el = $(element) 77 78 /* If multiple select: stop */ 79 80 if(this.$el.prop('multiple')) return; 81 82 /* Settings */ 83 84 this.settings = $.extend( {}, defaults, options, this.$el.data() ); 85 86 /* Defaults */ 87 88 this._defaults = defaults; 89 90 /* Options */ 91 92 this.$options = this.$el.find('option, optgroup') 93 94 /* Initialize */ 95 96 this.init(); 97 98 /* Instances */ 99 100 $.fn[ pluginName ].instances.push(this); 101 102 } 103 104 $.extend(Plugin.prototype, { 105 init: function () { 106 107 /* Construct the comboselect */ 108 109 this._construct(); 110 111 112 /* Add event bindings */ 113 114 this._events(); 115 116 117 }, 118 _construct: function(){ 119 120 var self = this 121 122 /** 123 * Add negative TabIndex to `select` 124 * Preserves previous tabindex 125 */ 126 127 this.$el.data('plugin_'+ dataKey + '_tabindex', this.$el.prop('tabindex')) 128 129 /* Add a tab index for desktop browsers */ 130 131 !isMobile && this.$el.prop("tabIndex", -1) 132 133 /** 134 * Wrap the Select 135 */ 136 137 this.$container = this.$el.wrapAll('<div class="' + this.settings.comboClass + ' '+ this.settings.themeClass + '" />').parent(); 138 139 /** 140 * Check if select has a width attribute 141 */ 142 if(this.settings.extendStyle && this.$el.attr('style')){ 143 144 this.$container.attr('style', this.$el.attr("style")) 145 146 } 147 148 149 /** 150 * Append dropdown arrow 151 */ 152 153 this.$arrow = $('<div class="'+ this.settings.comboArrowClass+ '" />').appendTo(this.$container) 154 155 156 /** 157 * Append dropdown 158 */ 159 160 this.$dropdown = $('<ul class="'+this.settings.comboDropDownClass+'" />').appendTo(this.$container) 161 162 163 /** 164 * Create dropdown options 165 */ 166 167 this._build(); 168 169 /** 170 * Append Input 171 */ 172 173 this.$input = $('<input type="text"' + (isMobile? 'tabindex="-1"': '') + ' placeholder="'+ this.getPlaceholder() +'" class="'+ this.settings.inputClass + '">').appendTo(this.$container) 174 175 /** 176 * Append Label 177 */ 178 179 if(this.settings.showLabel){ 180 181 this.$container.addClass('combo-has-label'); 182 183 this.$label = $('<input />').attr({ 184 'class' : 'combo-label-input text-input', 185 tabIndex: '-1' 186 }) 187 .val(this.getPlaceholder()) 188 .appendTo(this.$container) 189 } 190
191 /* Update input text */ 192 193 this._updateInput() 194 195 }, 196 getPlaceholder: function(){ 197 198 var p = ''; 199 200 this.$options.filter(function(idx, opt){ 201 202 return opt.nodeName == 'OPTION' 203 }).each(function(idx, e){ 204 205 if(e.value == '') p = e.innerHTML 206 }); 207 208 return p 209 }, 210 _build: function(){ 211 212 var self = this; 213 214 var o = '', k = 0; 215 216 this.$options.each(function(i, e){ 217 218 if(e.nodeName.toLowerCase() == 'optgroup'){ 219 220 return o+='<li class="option-group">'+this.label+'</li>' 221 } 222 223 o+='<li class="'+(this.disabled? self.settings.disabledClass : "option-item") + ' ' +(k == self.$el.prop('selectedIndex')? self.settings.selectedClass : '')+ '" data-index="'+(k)+'" data-value="'+this.value+'">'+ (this.innerHTML) + '</li>' 224 225 k++; 226 }) 227 228 this.$dropdown.html(o) 229 230 /** 231 * Items 232 */ 233 234 this.$items = this.$dropdown.children(); 235 }, 236 237 _events: function(){ 238 239 /* Input: focus */ 240 241 this.$container.on('focus.input', 'input', $.proxy(this._focus, this)) 242 243 /** 244 * Input: mouseup 245 * For input select() event to function correctly 246 */ 247 this.$container.on('mouseup.input', 'input', function(e){ 248 e.preventDefault() 249 }) 250 251 /* Input: blur */ 252 253 this.$container.on('blur.input', 'input', $.proxy(this._blur, this)) 254 255 /* Select: change */ 256 257 this.$el.on('change.select', $.proxy(this._change, this)) 258 259 /* Select: focus */ 260 261 this.$el.on('focus.select', $.proxy(this._focus, this)) 262 263 /* Select: blur */ 264 265 this.$el.on('blur.select', $.proxy(this._blurSelect, this)) 266 267 /* Dropdown Arrow: click */ 268 269 this.$container.on('click.arrow', '.'+this.settings.comboArrowClass , $.proxy(this._toggle, this)) 270 271 /* Dropdown: close */ 272 273 this.$container.on('comboselect:close', $.proxy(this._close, this)) 274 275 /* Dropdown: open */ 276 277 this.$container.on('comboselect:open', $.proxy(this._open, this)) 278 279 /* Dropdown: update */ 280 281 this.$container.on('comboselect:update', $.proxy(this._update, this)); 282 283 284 /* HTML Click */ 285 286 $('html').off('click.comboselect').on('click.comboselect', function(){ 287 288 $.each($.fn[ pluginName ].instances, function(i, plugin){ 289 290 plugin.$container.trigger('comboselect:close') 291 292 }) 293 }); 294 295 /* Stop `event:click` bubbling */ 296 297 this.$container.on('click.comboselect', function(e){ 298 e.stopPropagation(); 299 }) 300 301 302 /* Input: keydown */ 303 304 this.$container.on('keydown', 'input', $.proxy(this._keydown, this)) 305 306 /* Input: keyup */ 307 308 this.$container.on('keyup', 'input', $.proxy(this._keyup, this)) 309 310 /* Dropdown item: click */ 311 312 this.$container.on('click.item', '.option-item', $.proxy(this._select, this)) 313 314 }, 315 316 _keydown: function(event){ 317 318 319 320 switch(event.which){ 321 322 case keys.UP: 323 this._move('up', event) 324 break; 325 326 case keys.DOWN: 327 this._move('down', event) 328 break; 329 330 case keys.TAB: 331 this._enter(event) 332 break; 333 334 case keys.RIGHT: 335 this._autofill(event); 336 break; 337 338 case keys.ENTER: 339 this._enter(event); 340 break; 341 342 default: 343 break; 344 345 346 } 347 348 }, 349 350 351 _keyup: function(event){ 352 353 switch(event.which){ 354 case keys.ESC: 355 this.$container.trigger('comboselect:close') 356 break; 357 358 case keys.ENTER: 359 case keys.UP: 360 case keys.DOWN: 361 case keys.LEFT: 362 case keys.RIGHT: 363 case keys.TAB: 364 case keys.SHIFT: 365 break; 366 367 default: 368 this._filter(event.target.value) 369 break; 370 } 371 }, 372 373 _enter: function(event){ 374 375 var item = this._getHovered() 376 377 item.length && this._select(item); 378 379 /* Check if it enter key */ 380 if(event && event.which == keys.ENTER){ 381 382 if(!item.length) { 383 384 /* Check if its illegal value */ 385 386 this._blur(); 387 388 return true; 389 } 390 391 event.preventDefault(); 392 } 393 394 395 }, 396 _move: function(dir){ 397 398 var items = this._getVisible(), 399 current = this._getHovered(), 400 index = current.prevAll('.option-item').filter(':visible').length, 401 total = items.length 402 403 404 switch(dir){ 405 case 'up': 406 index--; 407 (index < 0) && (index = (total - 1)); 408 break; 409 410 case 'down': 411 index++; 412 (index >= total) && (index = 0); 413 break; 414 } 415 416 417 items 418 .removeClass(this.settings.hoverClass) 419 .eq(index) 420 .addClass(this.settings.hoverClass) 421 422 423 if(!this.opened) this.$container.trigger('comboselect:open'); 424 425 this._fixScroll() 426 }, 427 428 _select: function(event){ 429 430 var item = event.currentTarget? $(event.currentTarget) : $(event); 431 432 if(!item.length) return; 433 434 /** 435 * 1. get Index 436 */ 437 438 var index = item.data('index'); 439 440 this._selectByIndex(index); 441 442 //this.$container.trigger('comboselect:close') 443 444 this.$input.focus(); 445 446 this.$container.trigger('comboselect:close'); 447 448 }, 449 450 _selectByIndex: function(index){ 451 452 /** 453 * Set selected index and trigger change 454 * @type {[type]} 455 */ 456 var self = this; 457 458 if(typeof index == 'undefined'){ 459 460 index = 0; 461 462 this.$el.prop('selectedIndex', index); 463 464 } 465 466 if(this.$el.prop('selectedIndex') != index){ 467 468 this.$el.prop('selectedIndex', index).trigger('change'); 469 } 470 471 }, 472 473 _autofill: function(){ 474 475 var item = this._getHovered(); 476 477 if(item.length){ 478 479 var index = item.data('index') 480 481 this._selectByIndex(index) 482 483 } 484 485 }, 486 487 488 _filter: function(search){ 489 490 var self = this, 491 items = this._getAll(); 492 needle = $.trim(search).toLowerCase(), 493 reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'), 494 pattern = '(' + search.replace(reEscape, '\\$1') + ')'; 495 496 497 /** 498 * Unwrap all markers 499 */ 500 501 $('.'+self.settings.markerClass, items).contents().unwrap(); 502 503 /* Search */ 504 505 if(needle){ 506 507 /* Hide Disabled and optgroups */ 508 509 this.$items.filter('.option-group, .option-disabled').hide(); 510 511 512 items 513 .hide() 514 .filter(function(){ 515 516 var $this = $(this), 517 text = $.trim($this.text()).toLowerCase(); 518 519 /* Found */ 520 if(text.toString().indexOf(needle) != -1){ 521 522 /** 523 * Wrap the selection 524 */ 525 526 $this 527 .html(function(index, oldhtml){ 528 return oldhtml.replace(new RegExp(pattern, 'gi'), '<span class="'+self.settings.markerClass+'">$1</span>') 529 }) 530 531 return true 532 } 533 534 }) 535 .show() 536 }else{ 537 538 539 this.$items.show(); 540 } 541 542 /* Open the comboselect */ 543 544 this.$container.trigger('comboselect:open') 545 546 547 }, 548 549 _highlight: function(){ 550 551 /* 552 1. Check if there is a selected item 553 2. Add hover class to it 554 3. If not add hover class to first item 555 */ 556 557 var visible = this._getVisible().removeClass(this.settings.hoverClass), 558 $selected = visible.filter('.'+this.settings.selectedClass) 559 560 if($selected.length){ 561 562 $selected.addClass(this.settings.hoverClass); 563 564 }else{ 565 566 visible 567 .removeClass(this.settings.hoverClass) 568 .first() 569 .addClass(this.settings.hoverClass) 570 } 571 572 }, 573 574 _updateInput: function(){ 575 576 var selected = this.$el.prop('selectedIndex') 577 578 if(this.$el.val()){ 579 580 text = this.$el.find('option').eq(selected).text() 581 582 this.$input.val(text) 583 584 }else{ 585 586 this.$input.val('') 587 588 } 589 590 this.$container.toggleClass('combo-has-value', !!this.$input.val()) 591 592 return this._getAll() 593 .removeClass(this.settings.selectedClass) 594 .filter(function(){ 595 596 return $(this).data('index') == selected 597 }) 598 .addClass(this.settings.selectedClass) 599 600 }, 601 _blurSelect: function(){ 602 603 this.$container.removeClass('combo-focus'); 604 605 }, 606 _focus: function(event){ 607 608 /* Toggle focus class */ 609 610 this.$container.toggleClass('combo-focus', !this.opened); 611 612 /* If mobile: stop */ 613 614 if(isMobile) return; 615 616 /* Open combo */ 617 618 if(!this.opened) this.$container.trigger('comboselect:open'); 619 620 /* Select the input */ 621 622 this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == 'INPUT' && event.currentTarget.select() 623 }, 624 625 _blur: function(){ 626 627 /** 628 * 1. Get hovered item 629 * 2. If not check if input value == select option 630 * 3. If none 631 */ 632 633 var val = $.trim(this.$input.val()).toLowerCase().replace(/\s\s+/gi, ' '), 634 isNumber = !isNaN(val); 635 636 637 var index = this.$options.filter(function(){ 638 return this.nodeName == 'OPTION' 639 }).filter(function(){ 640 641 if(isNumber){ 642 return parseInt($.trim(this.innerText).toLowerCase()) == val 643 } 644 645 var _val = $.trim(this.innerText).toLowerCase().replace(/\s\s+/gi, ' '); 646 647 return _val == val 648 649 }).prop('index') 650 651 652 if(!index && this.$el.prop('selectedIndex') != 'undefined'){ 653 return this._updateInput(); 654 } 655 656 657 /* Select by Index */ 658 659 this._selectByIndex(index) 660 661 }, 662 663 _change: function(){ 664 665 666 this._updateInput(); 667 668 }, 669 670 _getAll: function(){ 671 672 return this.$items.filter('.option-item') 673 674 }, 675 _getVisible: function(){ 676 677 return this.$items.filter('.option-item').filter(':visible') 678 679 }, 680 681 _getHovered: function(){ 682 683 return this._getVisible().filter('.' + this.settings.hoverClass); 684 685 }, 686 687 _open: function(){ 688 689 var self = this 690 691 this.$container.addClass('combo-open') 692 693 this.opened = true 694 695 /* Focus input field */ 696 697 this.settings.focusInput && setTimeout(function(){ !self.$input.is(':focus') && self.$input.focus(); }); 698 699 /* Highligh the items */ 700 701 this._highlight() 702 703 /* Fix scroll */ 704 705 this._fixScroll() 706 707 /* Close all others */ 708 709 710 $.each($.fn[ pluginName ].instances, function(i, plugin){ 711 712 if(plugin != self && plugin.opened) plugin.$container.trigger('comboselect:close') 713 }) 714 715 }, 716 717 _toggle: function(){ 718 719 this.opened? this._close.call(this) : this._open.call(this) 720 }, 721 722 _close: function(){ 723 724 this.$container.removeClass('combo-open combo-focus') 725 726 this.$container.trigger('comboselect:closed') 727 728 this.opened = false 729 730 /* Show all items */ 731 732 this.$items.show(); 733 734 }, 735 _fixScroll: function(){ 736 737 /** 738 * If dropdown is hidden 739 */ 740 if(this.$dropdown.is(':hidden')) return; 741 742 743 /** 744 * Else 745 */ 746 var item = this._getHovered(); 747 748 if(!item.length) return; 749 750 /** 751 * Scroll 752 */ 753 754 var offsetTop, 755 upperBound, 756 lowerBound, 757 heightDelta = item.outerHeight() 758 759 offsetTop = item[0].offsetTop; 760 761 upperBound = this.$dropdown.scrollTop(); 762 763 lowerBound = upperBound + this.settings.maxHeight - heightDelta; 764 765 if (offsetTop < upperBound) { 766 767 this.$dropdown.scrollTop(offsetTop); 768 769 } else if (offsetTop > lowerBound) { 770 771 this.$dropdown.scrollTop(offsetTop - this.settings.maxHeight + heightDelta); 772 } 773 774 }, 775 toggleDisplay: function(condition){ 776 777 this.$el.toggle(condition) 778 }, 779 /** 780 * Update API 781 */ 782 783 _update: function(){ 784 785 this.$options = this.$el.find('option, optgroup') 786 787 this.$dropdown.empty(); 788 this.$input.val(''); 789 790 this._build(); 791 }, 792 793 /** 794 * Destroy API 795 */ 796 797 dispose: function(){ 798
799 /* Remove combo arrow, input, dropdown */ 800 801 this.$arrow.remove() 802 803 this.$input.remove() 804 805 this.$dropdown.remove() 806 807 /* Remove tabindex property */ 808 this.$el 809 .removeAttr("tabindex") 810 811 /* Check if there is a tabindex set before */ 812 813 if(!!this.$el.data('plugin_'+ dataKey + '_tabindex')){ 814 this.$el.prop('tabindex', this.$el.data('plugin_'+ dataKey + '_tabindex')) 815 } 816 817 /* Unwrap */ 818 819 this.$el.unwrap() 820 821 /* Remove data */ 822 823 this.$el.removeData('plugin_'+dataKey) 824 825 /* Remove tabindex data */ 826 827 this.$el.removeData('plugin_'+dataKey + '_tabindex') 828 829 /* Remove change event on select */ 830 831 this.$el.off('change.select focus.select blur.select'); 832 833 } 834 }); 835 836 837 838 // A really lightweight plugin wrapper around the constructor, 839 // preventing against multiple instantiations 840 $.fn[ pluginName ] = function ( options, args ) { 841 842 this.each(function() { 843 844 var $e = $(this), 845 instance = $e.data('plugin_'+dataKey) 846 847 if (typeof options === 'string') { 848 849 if (instance && typeof instance[options] === 'function') { 850 instance[options](args); 851 } 852 853 }else{ 854 855 if (instance && instance.dispose) { 856 instance.dispose(); 857 } 858 859 $.data( this, "plugin_" + dataKey, new Plugin( this, options ) ); 860 861 } 862 863 }); 864 865 // chain jQuery functions 866 return this; 867 }; 868 869 $.fn[ pluginName ].instances = []; 870 871}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.