1/*! 2 * ,/ 3 * ,'/ 4 * ,' / 5 * ,' /_____, 6 * .'____ ,' 7 * / ,' 8 * / ,' 9 * /,' 10 * /' 11 * 12 * Selectric Ï v1.13.0 (Aug 22 2017) - http://lcdsantos.github.io/jQuery-Selectric/ 13 * 14 * Copyright (c) 2017 Leonardo Santos; MIT License 15 * 16 */ 17 18(function(factory) { 19 /* global define */ 20 /* istanbul ignore next */ 21 if ( typeof define === 'function' && define.amd ) { 22 define(['jquery'], factory); 23 } else if ( typeof module === 'object' && module.exports ) { 24 // Node/CommonJS 25 module.exports = function( root, jQuery ) { 26 if ( jQuery === undefined ) { 27 if ( typeof window !== 'undefined' ) { 28 jQuery = require('jquery'); 29 } else { 30 jQuery = require('jquery')(root); 31 } 32 } 33 factory(jQuery); 34 return jQuery; 35 }; 36 } else { 37 // Browser globals 38 factory(jQuery); 39 } 40}(function($) { 41 'use strict'; 42 43 var $doc = $(document); 44 var $win = $(window); 45 46 var pluginName = 'selectric'; 47 var classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Focus Hover Responsive Above Below Scroll Group GroupLabel'; 48 var eventNamespaceSuffix = '.sl'; 49 50 var chars = ['a', 'e', 'i', 'o', 'u', 'n', 'c', 'y']; 51 var diacritics = [ 52 /[\xE0-\xE5]/g, // a 53 /[\xE8-\xEB]/g, // e 54 /[\xEC-\xEF]/g, // i 55 /[\xF2-\xF6]/g, // o 56 /[\xF9-\xFC]/g, // u 57 /[\xF1]/g, // n 58 /[\xE7]/g, // c 59 /[\xFD-\xFF]/g // y 60 ]; 61 62 /** 63 * Create an instance of Selectric 64 * 65 * @constructor 66 * @param {Node} element - The <select> element 67 * @param {object} opts - Options 68 */ 69 var Selectric = function(element, opts) { 70 var _this = this; 71 72 _this.element = element; 73 _this.$element = $(element); 74 75 _this.state = { 76 multiple : !!_this.$element.attr('multiple'), 77 enabled : false, 78 opened : false, 79 currValue : -1, 80 selectedIdx : -1, 81 highlightedIdx : -1 82 }; 83 84 _this.eventTriggers = { 85 open : _this.open, 86 close : _this.close, 87 destroy : _this.destroy, 88 refresh : _this.refresh, 89 init : _this.init 90 }; 91 92 _this.init(opts); 93 }; 94 95 Selectric.prototype = { 96 utils: { 97 /** 98 * Detect mobile browser 99 * 100 * @return {boolean} 101 */ 102 isMobile: function() { 103 return /android|ip(hone|od|ad)/i.test(navigator.userAgent); 104 }, 105 106 /** 107 * Escape especial characters in string (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) 108 * 109 * @param {string} str - The string to be escaped 110 * @return {string} The string with the special characters escaped 111 */ 112 escapeRegExp: function(str) { 113 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 114 }, 115 116 /** 117 * Replace diacritics 118 * 119 * @param {string} str - The string to replace the diacritics 120 * @return {string} The string with diacritics replaced with ascii characters 121 */ 122 replaceDiacritics: function(str) { 123 var k = diacritics.length; 124 125 while (k--) { 126 str = str.toLowerCase().replace(diacritics[k], chars[k]); 127 } 128 129 return str; 130 }, 131 132 /** 133 * Format string 134 * https://gist.github.com/atesgoral/984375 135 * 136 * @param {string} f - String to be formated 137 * @return {string} String formated 138 */ 139 format: function(f) { 140 var a = arguments; // store outer arguments 141 return ('' + f) // force format specifier to String 142 .replace( // replace tokens in format specifier 143 /\{(?:(\d+)|(\w+))\}/g, // match {token} references 144 function( 145 s, // the matched string (ignored) 146 i, // an argument index 147 p // a property name 148 ) { 149 return p && a[1] // if property name and first argument exist 150 ? a[1][p] // return property from first argument 151 : a[i]; // assume argument index and return i-th argument 152 }); 153 }, 154 155 /** 156 * Get the next enabled item in the options list. 157 * 158 * @param {object} selectItems - The options object. 159 * @param {number} selected - Index of the currently selected option. 160 * @return {object} The next enabled item. 161 */ 162 nextEnabledItem: function(selectItems, selected) { 163 while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) { 164 // empty 165 } 166 return selected; 167 }, 168 169 /** 170 * Get the previous enabled item in the options list. 171 * 172 * @param {object} selectItems - The options object. 173 * @param {number} selected - Index of the currently selected option. 174 * @return {object} The previous enabled item. 175 */ 176 previousEnabledItem: function(selectItems, selected) { 177 while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) { 178 // empty 179 } 180 return selected; 181 }, 182 183 /** 184 * Transform camelCase string to dash-case. 185 * 186 * @param {string} str - The camelCased string. 187 * @return {string} The string transformed to dash-case. 188 */ 189 toDash: function(str) { 190 return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); 191 }, 192 193 /** 194 * Calls the events registered with function name. 195 * 196 * @param {string}
196 fn - The name of the function. 197 * @param {number} scope - Scope that should be set on the function. 198 */ 199 triggerCallback: function(fn, scope) { 200 var elm = scope.element; 201 var func = scope.options['on' + fn]; 202 var args = [elm].concat([].slice.call(arguments).slice(1)); 203 204 if ( $.isFunction(func) ) { 205 func.apply(elm, args); 206 } 207 208 $(elm).trigger(pluginName + '-' + this.toDash(fn), args); 209 }, 210 211 /** 212 * Transform array list to concatenated string and remove empty values 213 * @param {array} arr - Class list 214 * @return {string} Concatenated string 215 */ 216 arrayToClassname: function(arr) { 217 var newArr = $.grep(arr, function(item) { 218 return !!item; 219 }); 220 221 return $.trim(newArr.join(' ')); 222 } 223 }, 224 225 /** Initializes */ 226 init: function(opts) { 227 var _this = this; 228 229 // Set options 230 _this.options = $.extend(true, {}, $.fn[pluginName].defaults, _this.options, opts); 231 232 _this.utils.triggerCallback('BeforeInit', _this); 233 234 // Preserve data 235 _this.destroy(true); 236 237 // Disable on mobile browsers 238 if ( _this.options.disableOnMobile && _this.utils.isMobile() ) { 239 _this.disableOnMobile = true; 240 return; 241 } 242 243 // Get classes 244 _this.classes = _this.getClassNames(); 245 246 // Create elements 247 var input = $('<input/>', { 'class': _this.classes.input, 'readonly': _this.utils.isMobile() }); 248 var items = $('<div/>', { 'class': _this.classes.items, 'tabindex': -1 }); 249 var itemsScroll = $('<div/>', { 'class': _this.classes.scroll }); 250 var wrapper = $('<div/>', { 'class': _this.classes.prefix, 'html': _this.options.arrowButtonMarkup }); 251 var label = $('<span/>', { 'class': 'label' }); 252 var outerWrapper = _this.$element.wrap('<div/>').parent().append(wrapper.prepend(label), items, input); 253 var hideSelectWrapper = $('<div/>', { 'class': _this.classes.hideselect }); 254 255 _this.elements = { 256 input : input, 257 items : items, 258 itemsScroll : itemsScroll, 259 wrapper : wrapper, 260 label : label, 261 outerWrapper : outerWrapper 262 }; 263 264 if ( _this.options.nativeOnMobile && _this.utils.isMobile() ) { 265 _this.elements.input = undefined; 266 hideSelectWrapper.addClass(_this.classes.prefix + '-is-native'); 267 268 _this.$element.on('change', function() { 269 _this.refresh(); 270 }); 271 } 272 273 _this.$element 274 .on(_this.eventTriggers) 275 .wrap(hideSelectWrapper); 276 277 _this.originalTabindex = _this.$element.prop('tabindex'); 278 _this.$element.prop('tabindex', -1); 279 280 _this.populate(); 281 _this.activate(); 282 283 _this.utils.triggerCallback('Init', _this); 284 }, 285 286 /** Activates the plugin */ 287 activate: function() { 288 var _this = this; 289 var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow); 290 var originalWidth = _this.$element.width(); 291 292 hiddenChildren.removeClass(_this.classes.tempshow); 293 294 _this.utils.triggerCallback('BeforeActivate', _this); 295 296 _this.elements.outerWrapper.prop('class', 297 _this.utils.arrayToClassname([ 298 _this.classes.wrapper, 299 _this.$element.prop('class').replace(/\S+/g, _this.classes.prefix + '-$&'), 300 _this.options.responsive ? _this.classes.responsive : '' 301 ]) 302 ); 303 304 if ( _this.options.inheritOriginalWidth && originalWidth > 0 ) { 305 _this.elements.outerWrapper.width(originalWidth); 306 } 307 308 _this.unbindEvents(); 309 310 if ( !_this.$element.prop('disabled') ) { 311 _this.state.enabled = true; 312 313 // Not disabled, so... Removing disabled class 314 _this.elements.outerWrapper.removeClass(_this.classes.disabled); 315 316 // Remove styles from items box 317 // Fix incorrect height when refreshed is triggered with fewer options 318 _this.$li = _this.elements.items.removeAttr('style').find('li'); 319 320 _this.bindEvents(); 321 } else { 322 _this.elements.outerWrapper.addClass(_this.classes.disabled); 323 324 if ( _this.elements.input ) { 325 _this.elements.input.prop('disabled', true); 326 } 327 } 328 329 _this.utils.triggerCallback('Activate', _this); 330 }, 331 332 /** 333 * Generate classNames for elements 334 * 335 * @return {object} Classes object 336 */ 337 getClassNames: function() { 338 var _this = this; 339 var customClass = _this.options.customClass; 340 var classesObj = {}; 341 342 $.each(classList.split(' '), function(i, currClass) { 343 var c = customClass.prefix + currClass; 344 classesObj[currClass.toLowerCase()] = customClass.camelCase ? c : _this.utils.toDash(c); 345 }); 346 347 classesObj.prefix = customClass.prefix; 348 349 return classesObj; 350 }, 351 352 /** Set the label text */ 353 setLabel: function() { 354 var _this = this; 355 var labelBuilder = _this.options.labelBuilder; 356 357 if ( _this.state.multiple ) { 358 // Make sure currentValues is an array 359 var currentValues = $.isArray(_this.state.currValue) ? _this.state.currValue : [_this.state.currValue]; 360 // I'm not happy with this, but currentValues can be an empty 361 // array and we need to fallback to the default option. 362 currentValues = currentValues.length === 0 ? [0] : currentValues; 363 364 var labelMarkup = $.map(currentValues, function(value) { 365 return $.grep(_this.lookupItems, function(item) { 366 return item.index === value; 367 })[0]; // we don't want nested arrays here 368 }); 369 370 labelMarkup = $.grep(labelMarkup, function(item) { 371 // Hide default (please choose) if more then one element were selected. 372 // If no option value were given value is set to option text by default 373 if ( labelMarkup.length > 1 || labelMarkup.length === 0 ) { 374 return $.trim(item.value) !== ''; 375 } 376 return item; 377 }); 378 379 labelMarkup = $.map(labelMarkup, function(item) { 380 return $.isFunction(labelBuilder) 381 ? labelBuilder(item) 382 : _this.utils.format(labelBuilder, item); 383 }); 384 385 // Limit the amount of selected values shown in label 386 if ( _this.options.multiple.maxLabelEntries ) { 387 if ( labelMarkup.length >= _this.options.multiple.maxLabelEntries + 1 ) { 388 labelMarkup = labelMarkup.slice(0, _this.options.multiple.maxLabelEntries); 389 labelMarkup.push( 390 $.isFunction(labelBuilder) 391 ? labelBuilder({ text: '...' }) 392 : _this.utils.format(labelBuilder, { text: '...' })); 393 } else { 394 labelMarkup.slice(labelMarkup.length - 1); 395 } 396 } 397 _this.elements.label.html(labelMarkup.join(_this.options.multiple.separator)); 398 399 } else { 400 var currItem = _this.lookupItems[_this.state.currValue]; 401 402 _this.elements.label.html( 403 $.isFunction(labelBuilder) 404 ? labelBuilder(currItem) 405 : _this.utils.format(labelBuilder, currItem) 406 ); 407 } 408 }, 409 410 /** Get and save the available options */ 411 populate: function() { 412 var _this = this; 413 var $options = _this.$element.children(); 414 var $justOptions = _this.$element.find('option'); 415 var $selected = $justOptions.filter(':selected'); 416 var selectedIndex = $justOptions.index($selected); 417 var currIndex = 0; 418 var emptyValue = (_this.state.multiple ? [] : 0); 419 420 if ( $selected.length > 1 && _this.state.multiple ) { 421 selectedIndex = []; 422 $selected.each(function() { 423 selectedIndex.push($(this).index()); 424 }); 425 } 426 427 _this.state.currValue = (~selectedIndex ? selectedIndex : emptyValue); 428 _this.state.selectedIdx = _this.state.currValue; 429 _this.state.highlightedIdx = _this.state.currValue; 430 _this.items = []; 431 _this.lookupItems = []; 432 433 if ( $options.length ) { 434 // Build options markup 435 $options.each(function(i) { 436 var $elm = $(this); 437 438 if ( $elm.is('optgroup') ) { 439 440 var optionsGroup = { 441 element : $elm, 442 label : $elm.prop('label'), 443 groupDisabled : $elm.prop('disabled'), 444 items : [] 445 }; 446 447 $elm.children().each(function(i) { 448 var $elm = $(this); 449 450 optionsGroup.items[i] = _this.getItemData(currIndex, $elm, optionsGroup.groupDisabled || $elm.prop('disabled')); 451 452 _this.lookupItems[currIndex] = optionsGroup.items[i]; 453 454 currIndex++; 455 }); 456 457 _this.items[i] = optionsGroup; 458 459 } else { 460 461 _this.items[i] = _this.getItemData(currIndex, $elm, $elm.prop('disabled')); 462 463 _this.lookupItems[currIndex] = _this.items[i]; 464 465 currIndex++; 466 467 } 468 }); 469 470 _this.setLabel(); 471 _this.elements.items.append( _this.elements.itemsScroll.html( _this.getItemsMarkup(_this.items) ) ); 472 } 473 }, 474 475 /** 476 * Generate items object data 477 * @param {integer} index - Current item index 478 * @param {node} $elm - Current element node 479 * @param {boolean} isDisabled - Current element disabled state 480 * @return {object} Item object 481 */ 482 getItemData: function(index, $elm, isDisabled) { 483 var _this = this; 484 485 return { 486 index : index, 487 element : $elm, 488 value : $elm.val(), 489 className : $elm.prop('class'), 490 text : $elm.html(), 491 slug : $.trim(_this.utils.replaceDiacritics($elm.html())), 492 alt : $elm.attr('data-alt'), 493 selected : $elm.prop('selected'), 494 disabled : isDisabled 495 }; 496 }, 497 498 /** 499 * Generate options markup 500 * 501 * @param {object} items - Object containing all available options 502 * @return {string} HTML for the options box 503 */ 504 getItemsMarkup: function(items) { 505 var _this = this; 506 var markup = '<ul>'; 507 508 if ( $.isFunction(_this.options.listBuilder) && _this.options.listBuilder ) { 509 items = _this.options.listBuilder(items); 510 } 511 512 $.each(items, function(i, elm) { 513 if ( elm.label !== undefined ) { 514 515 markup += _this.utils.format('<ul class="{1}"><li class="{2}">{3}</li>', 516 _this.utils.arrayToClassname([ 517 _this.classes.group, 518 elm.groupDisabled ? 'disabled' : '', 519 elm.element.prop('class') 520 ]), 521 _this.classes.grouplabel, 522 elm.element.prop('label') 523 ); 524 525 $.each(elm.items, function(i, elm) { 526 markup += _this.getItemMarkup(elm.index, elm); 527 }); 528 529 markup += '</ul>'; 530 531 } else { 532 533 markup += _this.getItemMarkup(elm.index, elm); 534 535 } 536 }); 537 538 return markup + '</ul>'; 539 }, 540 541 /** 542 * Generate every option markup 543 * 544 * @param {number} index - Index of current item 545 * @param {object} itemData - Current item 546 * @return {string} HTML for the option 547 */ 548 getItemMarkup: function(index, itemData) { 549 var _this = this; 550 var itemBuilder = _this.options.optionsItemBuilder; 551 // limit access to item data to provide a simple interface 552 // to most relevant options. 553 var filteredItemData = { 554 value: itemData.value, 555 text : itemData.text, 556 slug : itemData.slug, 557 index: itemData.index 558 }; 559 560 return _this.utils.format('<li data-index="{1}" class="{2}">{3}</li>', 561 index, 562 _this.utils.arrayToClassname([ 563 itemData.className, 564 index === _this.items.length - 1 ? 'last' : '', 565 itemData.disabled ? 'disabled' : '', 566 itemData.selected ? 'selected' : '' 567 ]), 568 $.isFunction(itemBuilder) 569 ? _this.utils.format(itemBuilder(itemData, this.$element, index), itemData) 570 : _this.utils.format(itemBuilder, filteredItemData) 571 ); 572 }, 573 574 /** Remove events on the elements */ 575 unbindEvents: function() { 576 var _this = this; 577 578 _this.elements.wrapper 579 .add(_this.$element) 580 .add(_this.elements.outerWrapper) 581 .add(_this.elements.input) 582 .off(eventNamespaceSuffix); 583 }, 584 585 /** Bind events on the elements */ 586 bindEvents: function() { 587 var _this = this; 588 589 _this.elements.outerWrapper.on('mouseenter' + eventNamespaceSuffix + ' mouseleave' + eventNamespaceSuffix, function(e) { 590 $(this).toggleClass(_this.classes.hover, e.type === 'mouseenter'); 591 592 // Delay close effect when openOnHover is true 593 if ( _this.options.openOnHover ) { 594 clearTimeout(_this.closeTimer); 595 596 if ( e.type === 'mouseleave' ) { 597 _this.closeTimer = setTimeout($.proxy(_this.close, _this), _this.options.hoverIntentTimeout); 598 } else { 599 _this.open(); 600 } 601 } 602 }); 603 604 // Toggle open/close
605 _this.elements.wrapper.on('click' + eventNamespaceSuffix, function(e) { 606 _this.state.opened ? _this.close() : _this.open(e); 607 }); 608 609 // Translate original element focus event to dummy input. 610 // Disabled on mobile devices because the default option list isn't 611 // shown due the fact that hidden input gets focused 612 if ( !(_this.options.nativeOnMobile && _this.utils.isMobile()) ) { 613 _this.$element.on('focus' + eventNamespaceSuffix, function() { 614 _this.elements.input.focus(); 615 }); 616 617 _this.elements.input 618 .prop({ tabindex: _this.originalTabindex, disabled: false }) 619 .on('keydown' + eventNamespaceSuffix, $.proxy(_this.handleKeys, _this)) 620 .on('focusin' + eventNamespaceSuffix, function(e) { 621 _this.elements.outerWrapper.addClass(_this.classes.focus); 622 623 // Prevent the flicker when focusing out and back again in the browser window 624 _this.elements.input.one('blur', function() { 625 _this.elements.input.blur(); 626 }); 627 628 if ( _this.options.openOnFocus && !_this.state.opened ) { 629 _this.open(e); 630 } 631 }) 632 .on('focusout' + eventNamespaceSuffix, function() { 633 _this.elements.outerWrapper.removeClass(_this.classes.focus); 634 }) 635 .on('input propertychange', function() { 636 var val = _this.elements.input.val(); 637 var searchRegExp = new RegExp('^' + _this.utils.escapeRegExp(val), 'i'); 638 639 // Clear search 640 clearTimeout(_this.resetStr); 641 _this.resetStr = setTimeout(function() { 642 _this.elements.input.val(''); 643 }, _this.options.keySearchTimeout); 644 645 if ( val.length ) { 646 // Search in select options 647 $.each(_this.items, function(i, elm) { 648 if (elm.disabled) { 649 return; 650 } 651 if (searchRegExp.test(elm.text) || searchRegExp.test(elm.slug)) { 652 _this.highlight(i); 653 return; 654 } 655 if (!elm.alt) { 656 return; 657 } 658 var altItems = elm.alt.split('|'); 659 for (var ai = 0; ai < altItems.length; ai++) { 660 if (!altItems[ai]) { 661 break; 662 } 663 if (searchRegExp.test(altItems[ai].trim())) { 664 _this.highlight(i); 665 return; 666 } 667 } 668 }); 669 } 670 }); 671 } 672 673 _this.$li.on({ 674 // Prevent <input> blur on Chrome 675 mousedown: function(e) { 676 e.preventDefault(); 677 e.stopPropagation(); 678 }, 679 click: function() { 680 _this.select($(this).data('index')); 681 682 // Chrome doesn't close options box if select is wrapped with a label 683 // We need to 'return false' to avoid that 684 return false; 685 } 686 }); 687 }, 688 689 /** 690 * Behavior when keyboard keys is pressed 691 * 692 * @param {object} e - Event object 693 */ 694 handleKeys: function(e) { 695 var _this = this; 696 var key = e.which; 697 var keys = _this.options.keys; 698 699 var isPrevKey = $.inArray(key, keys.previous) > -1; 700 var isNextKey = $.inArray(key, keys.next) > -1; 701 var isSelectKey = $.inArray(key, keys.select) > -1; 702 var isOpenKey = $.inArray(key, keys.open) > -1; 703 var idx = _this.state.highlightedIdx; 704 var isFirstOrLastItem = (isPrevKey && idx === 0) || (isNextKey && (idx + 1) === _this.items.length); 705 var goToItem = 0; 706 707 // Enter / Space 708 if ( key === 13 || key === 32 ) { 709 e.preventDefault(); 710 } 711 712 // If it's a directional key 713 if ( isPrevKey || isNextKey ) { 714 if ( !_this.options.allowWrap && isFirstOrLastItem ) { 715 return; 716 } 717 718 if ( isPrevKey ) { 719 goToItem = _this.utils.previousEnabledItem(_this.lookupItems, idx); 720 } 721 722 if ( isNextKey ) { 723 goToItem = _this.utils.nextEnabledItem(_this.lookupItems, idx); 724 } 725 726 _this.highlight(goToItem); 727 } 728 729 // Tab / Enter / ESC 730 if ( isSelectKey && _this.state.opened ) { 731 _this.select(idx); 732 733 if ( !_this.state.multiple || !_this.options.multiple.keepMenuOpen ) { 734 _this.close(); 735 } 736 737 return; 738 } 739 740 // Space / Enter / Left / Up / Right / Down 741 if ( isOpenKey && !_this.state.opened ) { 742 _this.open(); 743 } 744 745 if (!isOpenKey && !isSelectKey && !isPrevKey && !isNextKey) { 746 var charToFind = String.fromCharCode(key) 747 $.each(_this.items, function(i, elm) { 748 if (elm.disabled) { 749 return; 750 } 751 if (elm.element[0].label.startsWith(charToFind)) { 752 _this.highlight(i) 753 return false;
754 } 755 }); 756 } 757 }, 758 759 /** Update the items object */ 760 refresh: function() { 761 var _this = this; 762 763 _this.populate(); 764 _this.activate(); 765 _this.utils.triggerCallback('Refresh', _this); 766 }, 767 768 /** Set options box width/height */ 769 setOptionsDimensions: function() { 770 var _this = this; 771 772 // Calculate options box height 773 // Set a temporary class on the hidden parent of the element 774 var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow); 775 var maxHeight = _this.options.maxHeight; 776 var itemsWidth = _this.elements.items.outerWidth(); 777 var wrapperWidth = _this.elements.wrapper.outerWidth() - (itemsWidth - _this.elements.items.width()); 778 779 // Set the dimensions, minimum is wrapper width, expand for long items if option is true 780 if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth ) { 781 _this.finalWidth = wrapperWidth; 782 } else { 783 // Make sure the scrollbar width is included 784 _this.elements.items.css('overflow', 'scroll'); 785 786 // Set a really long width for _this.elements.outerWrapper 787 _this.elements.outerWrapper.width(9e4); 788 _this.finalWidth = _this.elements.items.width(); 789 // Set scroll bar to auto 790 _this.elements.items.css('overflow', ''); 791 _this.elements.outerWrapper.width(''); 792 } 793 794 _this.elements.items.width(_this.finalWidth).height() > maxHeight && _this.elements.items.height(maxHeight); 795 796 // Remove the temporary class 797 hiddenChildren.removeClass(_this.classes.tempshow); 798 }, 799 800 /** Detect if the options box is inside the window */ 801 isInViewport: function() { 802 var _this = this; 803 804 if (_this.options.forceRenderAbove === true) { 805 _this.elements.outerWrapper.addClass(_this.classes.above); 806 } else if (_this.options.forceRenderBelow === true) { 807 _this.elements.outerWrapper.addClass(_this.classes.below); 808 } else { 809 var scrollTop = $win.scrollTop(); 810 var winHeight = $win.height(); 811 var uiPosX = _this.elements.outerWrapper.offset().top; 812 var uiHeight = _this.elements.outerWrapper.outerHeight(); 813 814 var fitsDown = (uiPosX + uiHeight + _this.itemsHeight) <= (scrollTop + winHeight); 815 var fitsAbove = (uiPosX - _this.itemsHeight) > scrollTop; 816 817 // If it does not fit below, only render it 818 // above it fit's there. 819 // It's acceptable that the user needs to 820 // scroll the viewport to see the cut off UI 821 var renderAbove = !fitsDown && fitsAbove; 822 var renderBelow = !renderAbove; 823 824 _this.elements.outerWrapper.toggleClass(_this.classes.above, renderAbove); 825 _this.elements.outerWrapper.toggleClass(_this.classes.below, renderBelow); 826 } 827 }, 828 829 /** 830 * Detect if currently selected option is visible and scroll the options box to show it 831 * 832 * @param {Number|Array} index - Index of the selected items 833 */ 834 detectItemVisibility: function(index) { 835 var _this = this; 836 var $filteredLi = _this.$li.filter('[data-index]'); 837 838 if ( _this.state.multiple ) { 839 // If index is an array, we can assume a multiple select and we 840 // want to scroll to the uppermost selected item! 841 // Math.min.apply(Math, index) returns the lowest entry in an Array. 842 index = ($.isArray(index) && index.length === 0) ? 0 : index; 843 index = $.isArray(index) ? Math.min.apply(Math, index) : index; 844 } 845 846 var liHeight = $filteredLi.eq(index).outerHeight(); 847 var liTop = $filteredLi[index].offsetTop; 848 var itemsScrollTop = _this.elements.itemsScroll.scrollTop(); 849 var scrollT = liTop + liHeight * 2; 850 851 _this.elements.itemsScroll.scrollTop( 852 scrollT > itemsScrollTop + _this.itemsHeight ? scrollT - _this.itemsHeight : 853 liTop - liHeight < itemsScrollTop ? liTop - liHeight : 854 itemsScrollTop 855 ); 856 }, 857 858 /** 859 * Open the select options box 860 * 861 * @param {Event} e - Event 862 */ 863 open: function(e) { 864 var _this = this; 865 866 if ( _this.options.nativeOnMobile && _this.utils.isMobile()) { 867 return false;
868 } 869 870 _this.utils.triggerCallback('BeforeOpen', _this); 871 872 if ( e ) { 873 e.preventDefault(); 874 if (_this.options.stopPropagation) { 875 e.stopPropagation(); 876 } 877 } 878 879 if ( _this.state.enabled ) { 880 _this.setOptionsDimensions(); 881 882 // Find any other opened instances of select and close it 883 $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close'); 884 885 _this.state.opened = true; 886 _this.itemsHeight = _this.elements.items.outerHeight(); 887 _this.itemsInnerHeight = _this.elements.items.height(); 888 889 // Toggle options box visibility 890 _this.elements.outerWrapper.addClass(_this.classes.open); 891 892 // Give dummy input focus 893 _this.elements.input.val(''); 894 if ( e && e.type !== 'focusin' ) { 895 _this.elements.input.focus(); 896 } 897 898 // Delayed binds events on Document to make label clicks work 899 setTimeout(function() { 900 $doc 901 .on('click' + eventNamespaceSuffix, $.proxy(_this.close, _this)) 902 .on('scroll' + eventNamespaceSuffix, $.proxy(_this.isInViewport, _this)); 903 }, 1); 904 905 _this.isInViewport(); 906 907 // Prevent window scroll when using mouse wheel inside items box 908 if ( _this.options.preventWindowScroll ) { 909 /* istanbul ignore next */ 910 $doc.on('mousewheel' + eventNamespaceSuffix + ' DOMMouseScroll' + eventNamespaceSuffix, '.' + _this.classes.scroll, function(e) { 911 var orgEvent = e.originalEvent; 912 var scrollTop = $(this).scrollTop(); 913 var deltaY = 0; 914 915 if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } 916 if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } 917 if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } 918 if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; } 919 920 if ( scrollTop === (this.scrollHeight - _this.itemsInnerHeight) && deltaY < 0 || scrollTop === 0 && deltaY > 0 ) { 921 e.preventDefault(); 922 } 923 }); 924 } 925 926 _this.detectItemVisibility(_this.state.selectedIdx); 927 928 _this.highlight(_this.state.multiple ? -1 : _this.state.selectedIdx); 929 930 _this.utils.triggerCallback('Open', _this); 931 } 932 }, 933 934 /** Close the select options box */ 935 close: function() { 936 var _this = this; 937 938 _this.utils.triggerCallback('BeforeClose', _this); 939 940 // Remove custom events on document 941 $doc.off(eventNamespaceSuffix); 942 943 // Remove visible class to hide options box 944 _this.elements.outerWrapper.removeClass(_this.classes.open); 945 946 _this.state.opened = false; 947 948 _this.utils.triggerCallback('Close', _this); 949 }, 950 951 /** Select current option and change the label */ 952 change: function() { 953 var _this = this; 954 955 _this.utils.triggerCallback('BeforeChange', _this); 956 957 if ( _this.state.multiple ) { 958 // Reset old selected 959 $.each(_this.lookupItems, function(idx) { 960 _this.lookupItems[idx].selected = false; 961 _this.$element.find('option').prop('selected', false); 962 }); 963 964 // Set new selected 965 $.each(_this.state.selectedIdx, function(idx, value) { 966 _this.lookupItems[value].selected = true; 967 _this.$element.find('option').eq(value).prop('selected', true); 968 }); 969 970 _this.state.currValue = _this.state.selectedIdx; 971 972 _this.setLabel(); 973 974 _this.utils.triggerCallback('Change', _this); 975 } else if ( _this.state.currValue !== _this.state.selectedIdx ) { 976 // Apply changed value to original select 977 _this.$element 978 .prop('selectedIndex', _this.state.currValue = _this.state.selectedIdx) 979 .data('value', _this.lookupItems[_this.state.selectedIdx].text); 980 981 // Change label text 982 _this.setLabel(); 983 984 _this.utils.triggerCallback('Change', _this); 985 } 986 }, 987 988 /** 989 * Highlight option 990 * @param {number} index - Index of the options that will be highlighted 991 */ 992 highlight: function(index) { 993 var _this = this; 994 var $filteredLi = _this.$li.filter('[data-index]').removeClass('highlighted'); 995 996 _this.utils.triggerCallback('BeforeHighlight', _this); 997 998 // Parameter index is required and should not be a disabled item 999 if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) { 1000 return; 1001 } 1002 1003 $filteredLi 1004 .eq(_this.state.highlightedIdx = index) 1005 .addClass('highlighted'); 1006 1007 _this.detectItemVisibility(index); 1008 1009 _this.utils.triggerCallback('Highlight', _this); 1010 }, 1011 1012 /** 1013 * Select option 1014 * 1015 * @param {number} index - Index of the option that will be selected 1016 */ 1017 select: function(index) { 1018 var _this = this; 1019 var $filteredLi = _this.$li.filter('[data-index]'); 1020 1021 _this.utils.triggerCallback('BeforeSelect', _this, index); 1022 1023 // Parameter index is required and should not be a disabled item 1024 if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) { 1025 return; 1026 } 1027 1028 if ( _this.state.multiple ) { 1029 // Make sure selectedIdx is an array 1030 _this.state.selectedIdx = $.isArray(_this.state.selectedIdx) ? _this.state.selectedIdx : [_this.state.selectedIdx]; 1031 1032 var hasSelectedIndex = $.inArray(index, _this.state.selectedIdx); 1033 if ( hasSelectedIndex !== -1 ) { 1034 _this.state.selectedIdx.splice(hasSelectedIndex, 1); 1035 } else { 1036 _this.state.selectedIdx.push(index); 1037 } 1038 1039 $filteredLi 1040 .removeClass('selected') 1041 .filter(function(index) { 1042 return $.inArray(index, _this.state.selectedIdx) !== -1; 1043 }) 1044 .addClass('selected'); 1045 } else { 1046 $filteredLi 1047 .removeClass('selected') 1048 .eq(_this.state.selectedIdx = index) 1049 .addClass('selected'); 1050 } 1051 1052 if ( !_this.state.multiple || !_this.options.multiple.keepMenuOpen ) { 1053 _this.close(); 1054 } 1055 1056 _this.change(); 1057 1058 _this.utils.triggerCallback('Select', _this, index); 1059 }, 1060 1061 /** 1062 * Unbind and remove 1063 * 1064 * @param {boolean} preserveData - Check if the data on the element should be removed too 1065 */ 1066 destroy: function(preserveData) { 1067 var _this = this; 1068 1069 if ( _this.state && _this.state.enabled ) { 1070 _this.elements.items.add(_this.elements.wrapper).add(_this.elements.input).remove(); 1071 1072 if ( !preserveData ) { 1073 _this.$element.removeData(pluginName).removeData('value'); 1074 } 1075 1076 _this.$element.prop('tabindex', _this.originalTabindex).off(eventNamespaceSuffix).off(_this.eventTriggers).unwrap().unwrap(); 1077 1078 _this.state.enabled = false;
1079 } 1080 } 1081 }; 1082 1083 // A really lightweight plugin wrapper around the constructor, 1084 // preventing against multiple instantiations 1085 $.fn[pluginName] = function(args) { 1086 return this.each(function() { 1087 var data = $.data(this, pluginName); 1088 1089 if ( data && !data.disableOnMobile ) { 1090 (typeof args === 'string' && data[args]) ? data[args]() : data.init(args); 1091 } else { 1092 $.data(this, pluginName, new Selectric(this, args)); 1093 } 1094 }); 1095 }; 1096 1097 /** 1098 * Default plugin options 1099 * 1100 * @type {object} 1101 */ 1102 $.fn[pluginName].defaults = { 1103 onChange : function(elm) { $(elm).change(); }, 1104 maxHeight : 300, 1105 keySearchTimeout : 500, 1106 arrowButtonMarkup : '<b class="button">▾</b>', 1107 disableOnMobile : false, 1108 nativeOnMobile : true, 1109 openOnFocus : true, 1110 openOnHover : false, 1111 hoverIntentTimeout : 500, 1112 expandToItemText : false, 1113 responsive : false, 1114 preventWindowScroll : true, 1115 inheritOriginalWidth : false, 1116 allowWrap : true, 1117 forceRenderAbove : false, 1118 forceRenderBelow : false, 1119 stopPropagation : true, 1120 optionsItemBuilder : '{text}', // function(itemData, element, index) 1121 labelBuilder : '{text}', // function(currItem) 1122 listBuilder : false, // function(items) 1123 keys : { 1124 previous : [37, 38], // Left / Up 1125 next : [39, 40], // Right / Down 1126 select : [9, 13, 27], // Tab / Enter / Escape 1127 open : [13, 32, 37, 38, 39, 40], // Enter / Space / Left / Up / Right / Down 1128 close : [9, 27] // Tab / Escape 1129 }, 1130 customClass : { 1131 prefix: pluginName, 1132 camelCase: false 1133 }, 1134 multiple : { 1135 separator: ', ', 1136 keepMenuOpen: true, 1137 maxLabelEntries: false 1138 } 1139 }; 1140}));
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.