1/*jshint asi:true, expr:true */ 2/** 3 * Plugin Name: Combo Select 4 * Author : Vinay@Pebbleroad 5 * Date: 23/11/2014 6 * Description: 7 * Converts a select box into a searchable and keyboard friendly interface. Fallbacks to native select on mobile and tablets 8 */ 9 10// Expose plugin as an AMD module if AMD loader is present: 11(function (factory) { 12 'use strict'; 13 if (typeof define === 'function' && define.amd) { 14 // AMD. Register as an anonymous module. 15 define(['jquery'], factory); 16 } else if (typeof exports === 'object' && typeof require === 'function') { 17 // Browserify 18 factory(require('jquery')); 19 } else { 20 // Browser globals 21 factory(jQuery); 22 } 23}(function ( $, undefined ) { 24 25 var pluginName = "comboSelect", 26 dataKey = 'comboselect'; 27 var defaults = { 28 comboClass : 'combo-select', 29 comboArrowClass : 'combo-arrow', 30 comboDropDownClass : 'combo-dropdown', 31 inputClass : 'combo-input text-input', 32 disabledClass : 'option-disabled', 33 hoverClass : 'option-hover', 34 selectedClass : 'option-selected', 35 markerClass : 'combo-marker', 36 themeClass : '', 37 maxHeight : 200, 38 extendStyle : true, 39 focusInput : true 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 var o = '', k = 0, p = ''; 168 169 this.selectedIndex = this.$el.prop('selectedIndex') 170 171 this.$options.each(function(i, e){ 172 173 if(e.nodeName.toLowerCase() == 'optgroup'){ 174 175 return o+='<li class="option-group">'+this.label+'</li>' 176 } 177 178 if(!e.value) p = e.innerHTML 179 180 o+='<li class="'+(this.disabled? self.settings.disabledClass : "option-item") + ' ' +(k == self.selectedIndex? self.settings.selectedClass : '')+ '" data-index="'+(k)+'" data-value="'+this.value+'">'+ (this.innerHTML) + '</li>' 181 182 k++; 183 }) 184 185 this.$dropdown.html(o) 186 187 /** 188 * Items 189 */ 190 191 this.$items = this.$dropdown.children();
192 193 194 /** 195 * Append Input 196 */ 197 198 this.$input = $('<input type="text"' + (isMobile? 'tabindex="-1"': '') + ' placeholder="'+p+'" class="'+ this.settings.inputClass + '">').appendTo(this.$container) 199 200 /* Update input text */ 201 202 this._updateInput() 203 204 }, 205 206 _events: function(){ 207 208 /* Input: focus */ 209 210 this.$container.on('focus.input', 'input', $.proxy(this._focus, this)) 211 212 /** 213 * Input: mouseup 214 * For input select() event to function correctly 215 */ 216 this.$container.on('mouseup.input', 'input', function(e){ 217 e.preventDefault() 218 }) 219 220 /* Input: blur */ 221 222 this.$container.on('blur.input', 'input', $.proxy(this._blur, this)) 223 224 /* Select: change */ 225 226 this.$el.on('change.select', $.proxy(this._change, this)) 227 228 /* Select: focus */ 229 230 this.$el.on('focus.select', $.proxy(this._focus, this)) 231 232 /* Select: blur */ 233 234 this.$el.on('blur.select', $.proxy(this._blurSelect, this)) 235 236 /* Dropdown Arrow: click */ 237 238 this.$container.on('click.arrow', '.'+this.settings.comboArrowClass , $.proxy(this._toggle, this)) 239 240 /* Dropdown: close */ 241 242 this.$container.on('comboselect:close', $.proxy(this._close, this)) 243 244 /* Dropdown: open */ 245 246 this.$container.on('comboselect:open', $.proxy(this._open, this)) 247 248 249 /* HTML Click */ 250 251 $('html').off('click.comboselect').on('click.comboselect', function(){ 252 253 $.each($.fn[ pluginName ].instances, function(i, plugin){ 254 255 plugin.$container.trigger('comboselect:close') 256 257 }) 258 }); 259 260 /* Stop `event:click` bubbling */ 261 262 this.$container.on('click.comboselect', function(e){ 263 e.stopPropagation(); 264 }) 265 266 267 /* Input: keydown */ 268 269 this.$container.on('keydown', 'input', $.proxy(this._keydown, this)) 270 271 /* Input: keyup */ 272 273 this.$container.on('keyup', 'input', $.proxy(this._keyup, this)) 274 275 /* Dropdown item: click */ 276 277 this.$container.on('click.item', '.option-item', $.proxy(this._select, this)) 278 279 }, 280 281 _keydown: function(event){ 282 283 284 285 switch(event.which){ 286 287 case keys.UP: 288 this._move('up', event) 289 break; 290 291 case keys.DOWN: 292 this._move('down', event) 293 break; 294 295 case keys.TAB: 296 this._enter(event) 297 break; 298 299 case keys.RIGHT: 300 this._autofill(event); 301 break; 302 303 case keys.ENTER: 304 this._enter(event); 305 break; 306 307 default: 308 break; 309 310 311 } 312 313 }, 314 315 316 _keyup: function(event){ 317 318 switch(event.which){ 319 case keys.ESC: 320 this.$container.trigger('comboselect:close') 321 break; 322 323 case keys.ENTER: 324 case keys.UP: 325 case keys.DOWN: 326 case keys.LEFT: 327 case keys.RIGHT: 328 case keys.TAB: 329 case keys.SHIFT: 330 break; 331 332 default: 333 this._filter(event.target.value) 334 break; 335 } 336 }, 337 338 _enter: function(event){ 339 340 var item = this._getHovered() 341 342 item.length && this._select(item); 343 344 /* Check if it enter key */ 345 if(event && event.which == keys.ENTER){ 346 347 if(!item.length) { 348 349 /* Check if its illegal value */ 350 351 this._blur(); 352 353 return true; 354 } 355 356 event.preventDefault(); 357 } 358 359 360 }, 361 _move: function(dir){ 362 363 var items = this._getVisible(), 364 current = this._getHovered(), 365 index = current.prevAll('.option-item').filter(':visible').length, 366 total = items.length 367 368 369 switch(dir){ 370 case 'up': 371 index--; 372 (index < 0) && (index = (total - 1)); 373 break; 374 375 case 'down': 376 index++; 377 (index >= total) && (index = 0); 378 break; 379 } 380 381 382 items 383 .removeClass(this.settings.hoverClass) 384 .eq(index) 385 .addClass(this.settings.hoverClass) 386 387 388 if(!this.opened) this.$container.trigger('comboselect:open'); 389 390 this._fixScroll() 391 }, 392 393 _select: function(event){ 394 395 var item = event.currentTarget? $(event.currentTarget) : $(event); 396 397 if(!item.length) return; 398 399 /** 400 * 1. get Index 401 */ 402 403 var index = item.data('index'); 404 405 this._selectByIndex(index); 406 407 this.$container.trigger('comboselect:close') 408 409 }, 410 411 _selectByIndex: function(index){ 412 413 /** 414 * Set selected index and trigger change 415 * @type {[type]} 416 */ 417 if(typeof index == 'undefined'){ 418 419 index = 0 420 421 } 422 423 if(this.$el.prop('selectedIndex') != index){ 424 425 this.$el.prop('selectedIndex', index).trigger('change'); 426 } 427 428 }, 429 430 _autofill: function(){ 431 432 var item = this._getHovered(); 433 434 if(item.length){ 435 436 var index = item.data('index') 437 438 this._selectByIndex(index) 439 440 } 441 442 }, 443 444 445 _filter: function(search){ 446 447 var self = this, 448 items = this._getAll(); 449 needle = $.trim(search).toLowerCase(), 450 reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'), 451 pattern = '(' + search.replace(reEscape, '\\$1') + ')'; 452 453 454 /** 455 * Unwrap all markers 456 */ 457 458 $('.'+self.settings.markerClass, items).contents().unwrap(); 459 460 /* Search */ 461 462 if(needle){ 463 464 /* Hide Disabled and optgroups */ 465 466 this.$items.filter('.option-group, .option-disabled').hide(); 467 468 469 items 470 .hide() 471 .filter(function(){ 472 473 var $this = $(this), 474 text = $.trim($this.text()).toLowerCase(); 475 476 /* Found */ 477 if(text.toString().indexOf(needle) != -1){ 478 479 /** 480 * Wrap the selection 481 */ 482 483 $this 484 .html(function(index, oldhtml){ 485 return oldhtml.replace(new RegExp(pattern, 'gi'), '<span class="'+self.settings.markerClass+'">$1</span>') 486 }) 487 488 return true 489 } 490 491 }) 492 .show() 493 }else{ 494 495 496 this.$items.show(); 497 } 498 499 /* Open the comboselect */ 500 501 this.$container.trigger('comboselect:open') 502 503 504 }, 505 506 _highlight: function(){ 507 508 /* 509 1. Check if there is a selected item 510 2. Add hover class to it 511 3. If not add hover class to first item 512 */ 513 514 var visible = this._getVisible().removeClass(this.settings.hoverClass), 515 $selected = visible.filter('.'+this.settings.selectedClass) 516 517 if($selected.length){ 518 519 $selected.addClass(this.settings.hoverClass); 520 521 }else{ 522 523 visible 524 .removeClass(this.settings.hoverClass) 525 .first() 526 .addClass(this.settings.hoverClass) 527 } 528 529 }, 530 531 _updateInput: function(){ 532 533 var selected = this.$el.prop('selectedIndex') 534 535 if(this.$el.val()){ 536 537 text = this.$el.find('option').eq(selected).text() 538 539 this.$input.val(text) 540 541 }else{ 542 543 this.$input.val('') 544 545 } 546 547 return this._getAll() 548 .removeClass(this.settings.selectedClass) 549 .filter(function(){ 550 551 return $(this).data('index') == selected 552 }) 553 .addClass(this.settings.selectedClass) 554 555 }, 556 _blurSelect: function(){ 557 558 this.$container.removeClass('combo-focus'); 559 560 }, 561 _focus: function(event){ 562 563 /* Toggle focus class */ 564 565 this.$container.toggleClass('combo-focus', !this.opened); 566 567 /* If mobile: stop */ 568 569 if(isMobile) return; 570 571 /* Open combo */ 572 573 if(!this.opened) this.$container.trigger('comboselect:open'); 574 575 /* Select the input */ 576 577 this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == 'INPUT' && event.currentTarget.select() 578 }, 579 580 _blur: function(){ 581 582 /** 583 * 1. Get hovered item 584 * 2. If not check if input value == select option 585 * 3. If none 586 */ 587 588 var val = $.trim(this.$input.val().toLowerCase()), 589 isNumber = !isNaN(val); 590 591 var index = this.$options.filter(function(){ 592 593 if(isNumber){
594 return parseInt($.trim(this.innerHTML).toLowerCase()) == val 595 } 596 597 return $.trim(this.innerHTML).toLowerCase() == val 598 599 }).prop('index') 600 601 /* Select by Index */ 602 603 this._selectByIndex(index) 604 605 }, 606 607 _change: function(){ 608 609 610 this._updateInput(); 611 612 }, 613 614 _getAll: function(){ 615 616 return this.$items.filter('.option-item') 617 618 }, 619 _getVisible: function(){ 620 621 return this.$items.filter('.option-item').filter(':visible') 622 623 }, 624 625 _getHovered: function(){ 626 627 return this._getVisible().filter('.' + this.settings.hoverClass); 628 629 }, 630 631 _open: function(){ 632 633 var self = this 634 635 this.$container.addClass('combo-open') 636 637 this.opened = true 638 639 /* Focus input field */ 640 641 this.settings.focusInput && setTimeout(function(){ !self.$input.is(':focus') && self.$input.focus(); }); 642 643 /* Highligh the items */ 644 645 this._highlight() 646 647 /* Fix scroll */ 648 649 this._fixScroll() 650 651 /* Close all others */ 652 653 654 $.each($.fn[ pluginName ].instances, function(i, plugin){ 655 656 if(plugin != self && plugin.opened) plugin.$container.trigger('comboselect:close') 657 }) 658 659 }, 660 661 _toggle: function(){ 662 663 this.opened? this._close.call(this) : this._open.call(this) 664 }, 665 666 _close: function(){ 667 668 this.$container.removeClass('combo-open combo-focus') 669 670 this.$container.trigger('comboselect:closed') 671 672 this.opened = false 673 674 /* Show all items */ 675 676 this.$items.show(); 677 678 }, 679 _fixScroll: function(){ 680 681 /** 682 * If dropdown is hidden 683 */ 684 if(this.$dropdown.is(':hidden')) return; 685 686 687 /** 688 * Else 689 */ 690 var item = this._getHovered(); 691 692 if(!item.length) return; 693 694 /** 695 * Scroll 696 */ 697 698 var offsetTop, 699 upperBound, 700 lowerBound, 701 heightDelta = item.outerHeight() 702 703 offsetTop = item[0].offsetTop; 704 705 upperBound = this.$dropdown.scrollTop(); 706 707 lowerBound = upperBound + this.settings.maxHeight - heightDelta; 708 709 if (offsetTop < upperBound) { 710 711 this.$dropdown.scrollTop(offsetTop); 712 713 } else if (offsetTop > lowerBound) { 714 715 this.$dropdown.scrollTop(offsetTop - this.settings.maxHeight + heightDelta); 716 } 717 718 }, 719 /** 720 * Destroy API 721 */ 722 723 dispose: function(){ 724
725 /* Remove combo arrow, input, dropdown */ 726 727 this.$arrow.remove() 728 729 this.$input.remove() 730 731 this.$dropdown.remove() 732 733 /* Remove tabindex property */ 734 this.$el 735 .removeAttr("tabindex") 736 737 /* Check if there is a tabindex set before */ 738 739 if(!!this.$el.data('plugin_'+ dataKey + '_tabindex')){ 740 this.$el.prop('tabindex', this.$el.data('plugin_'+ dataKey + '_tabindex')) 741 } 742 743 /* Unwrap */ 744 745 this.$el.unwrap() 746 747 /* Remove data */ 748 749 this.$el.removeData('plugin_'+dataKey) 750 751 /* Remove tabindex data */ 752 753 this.$el.removeData('plugin_'+dataKey + '_tabindex') 754 755 /* Remove change event on select */ 756 757 this.$el.off('change.select focus.select blur.select'); 758 759 } 760 }); 761 762 763 764 // A really lightweight plugin wrapper around the constructor, 765 // preventing against multiple instantiations 766 $.fn[ pluginName ] = function ( options, args ) { 767 768 this.each(function() { 769 770 var $e = $(this), 771 instance = $e.data('plugin_'+dataKey) 772 773 if (typeof options === 'string') { 774 775 if (instance && typeof instance[options] === 'function') { 776 instance[options](args); 777 } 778 779 }else{ 780 781 if (instance && instance.dispose) { 782 instance.dispose(); 783 } 784 785 $.data( this, "plugin_" + dataKey, new Plugin( this, options ) ); 786 787 } 788 789 }); 790 791 // chain jQuery functions 792 return this; 793 }; 794 795 $.fn[ pluginName ].instances = []; 796 797}));
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.