vendor: 36,422 bytes, lines 1-1127
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} 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 746 /** Update the items object */ 747 refresh: function() { 748 var _this = this; 749 750 _this.populate(); 751 _this.activate(); 752 _this.utils.triggerCallback('Refresh', _this); 753 }, 754 755 /** Set options box width/height */ 756 setOptionsDimensions: function() { 757 var _this = this; 758 759 // Calculate options box height 760 // Set a temporary class on the hidden parent of the element 761 var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow); 762 var maxHeight = _this.options.maxHeight; 763 var itemsWidth = _this.elements.items.outerWidth(); 764 var wrapperWidth = _this.elements.wrapper.outerWidth() - (itemsWidth - _this.elements.items.width()); 765 766 // Set the dimensions, minimum is wrapper width, expand for long items if option is true 767 if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth ) { 768 _this.finalWidth = wrapperWidth; 769 } else { 770 // Make sure the scrollbar width is included 771 _this.elements.items.css('overflow', 'scroll'); 772 773 // Set a really long width for _this.elements.outerWrapper 774 _this.elements.outerWrapper.width(9e4); 775 _this.finalWidth = _this.elements.items.width(); 776 // Set scroll bar to auto 777 _this.elements.items.css('overflow', ''); 778 _this.elements.outerWrapper.width(''); 779 } 780 781 _this.elements.items.width(_this.finalWidth).height() > maxHeight && _this.elements.items.height(maxHeight); 782 783 // Remove the temporary class 784 hiddenChildren.removeClass(_this.classes.tempshow); 785 }, 786 787 /** Detect if the options box is inside the window */ 788 isInViewport: function() { 789 var _this = this; 790 791 if (_this.options.forceRenderAbove === true) { 792 _this.elements.outerWrapper.addClass(_this.classes.above); 793 } else if (_this.options.forceRenderBelow === true) { 794 _this.elements.outerWrapper.addClass(_this.classes.below); 795 } else { 796 var scrollTop = $win.scrollTop(); 797 var winHeight = $win.height(); 798 var uiPosX = _this.elements.outerWrapper.offset().top; 799 var uiHeight = _this.elements.outerWrapper.outerHeight(); 800 801 var fitsDown = (uiPosX + uiHeight + _this.itemsHeight) <= (scrollTop + winHeight); 802 var fitsAbove = (uiPosX - _this.itemsHeight) > scrollTop; 803 804 // If it does not fit below, only render it 805 // above it fit's there. 806 // It's acceptable that the user needs to 807 // scroll the viewport to see the cut off UI 808 var renderAbove = !fitsDown && fitsAbove; 809 var renderBelow = !renderAbove; 810 811 _this.elements.outerWrapper.toggleClass(_this.classes.above, renderAbove); 812 _this.elements.outerWrapper.toggleClass(_this.classes.below, renderBelow); 813 } 814 }, 815 816 /** 817 * Detect if currently selected option is visible and scroll the options box to show it 818 * 819 * @param {Number|Array} index - Index of the selected items 820 */ 821 detectItemVisibility: function(index) { 822 var _this = this; 823 var $filteredLi = _this.$li.filter('[data-index]'); 824 825 if ( _this.state.multiple ) { 826 // If index is an array, we can assume a multiple select and we 827 // want to scroll to the uppermost selected item! 828 // Math.min.apply(Math, index) returns the lowest entry in an Array. 829 index = ($.isArray(index) && index.length === 0) ? 0 : index; 830 index = $.isArray(index) ? Math.min.apply(Math, index) : index; 831 } 832 833 var liHeight = $filteredLi.eq(index).outerHeight(); 834 var liTop = $filteredLi[index].offsetTop; 835 var itemsScrollTop = _this.elements.itemsScroll.scrollTop(); 836 var scrollT = liTop + liHeight * 2; 837 838 _this.elements.itemsScroll.scrollTop( 839 scrollT > itemsScrollTop + _this.itemsHeight ? scrollT - _this.itemsHeight : 840 liTop - liHeight < itemsScrollTop ? liTop - liHeight : 841 itemsScrollTop 842 ); 843 }, 844 845 /** 846 * Open the select options box 847 * 848 * @param {Event} e - Event 849 */ 850 open: function(e) { 851 var _this = this; 852 853 if ( _this.options.nativeOnMobile && _this.utils.isMobile()) { 854 return false; 855 } 856 857 _this.utils.triggerCallback('BeforeOpen', _this); 858 859 if ( e ) { 860 e.preventDefault(); 861 if (_this.options.stopPropagation) { 862 e.stopPropagation(); 863 } 864 } 865 866 if ( _this.state.enabled ) { 867 _this.setOptionsDimensions(); 868 869 // Find any other opened instances of select and close it 870 $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close'); 871 872 _this.state.opened = true; 873 _this.itemsHeight = _this.elements.items.outerHeight(); 874 _this.itemsInnerHeight = _this.elements.items.height(); 875 876 // Toggle options box visibility 877 _this.elements.outerWrapper.addClass(_this.classes.open); 878 879 // Give dummy input focus 880 _this.elements.input.val(''); 881 if ( e && e.type !== 'focusin' ) { 882 _this.elements.input.focus(); 883 } 884 885 // Delayed binds events on Document to make label clicks work 886 setTimeout(function() { 887 $doc 888 .on('click' + eventNamespaceSuffix, $.proxy(_this.close, _this)) 889 .on('scroll' + eventNamespaceSuffix, $.proxy(_this.isInViewport, _this)); 890 }, 1); 891 892 _this.isInViewport(); 893 894 // Prevent window scroll when using mouse wheel inside items box 895 if ( _this.options.preventWindowScroll ) { 896 /* istanbul ignore next */ 897 $doc.on('mousewheel' + eventNamespaceSuffix + ' DOMMouseScroll' + eventNamespaceSuffix, '.' + _this.classes.scroll, function(e) { 898 var orgEvent = e.originalEvent; 899 var scrollTop = $(this).scrollTop(); 900 var deltaY = 0; 901 902 if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } 903 if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } 904 if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } 905 if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; } 906 907 if ( scrollTop === (this.scrollHeight - _this.itemsInnerHeight) && deltaY < 0 || scrollTop === 0 && deltaY > 0 ) { 908 e.preventDefault(); 909 } 910 }); 911 } 912 913 _this.detectItemVisibility(_this.state.selectedIdx); 914 915 _this.highlight(_this.state.multiple ? -1 : _this.state.selectedIdx); 916 917 _this.utils.triggerCallback('Open', _this); 918 } 919 }, 920 921 /** Close the select options box */ 922 close: function() { 923 var _this = this; 924 925 _this.utils.triggerCallback('BeforeClose', _this); 926 927 // Remove custom events on document 928 $doc.off(eventNamespaceSuffix); 929 930 // Remove visible class to hide options box 931 _this.elements.outerWrapper.removeClass(_this.classes.open); 932 933 _this.state.opened = false; 934 935 _this.utils.triggerCallback('Close', _this); 936 }, 937 938 /** Select current option and change the label */ 939 change: function() { 940 var _this = this; 941 942 _this.utils.triggerCallback('BeforeChange', _this); 943 944 if ( _this.state.multiple ) { 945 // Reset old selected 946 $.each(_this.lookupItems, function(idx) { 947 _this.lookupItems[idx].selected = false; 948 _this.$element.find('option').prop('selected', false); 949 }); 950 951 // Set new selected 952 $.each(_this.state.selectedIdx, function(idx, value) { 953 _this.lookupItems[value].selected = true; 954 _this.$element.find('option').eq(value).prop('selected', true); 955 }); 956 957 _this.state.currValue = _this.state.selectedIdx; 958 959 _this.setLabel(); 960 961 _this.utils.triggerCallback('Change', _this); 962 } else if ( _this.state.currValue !== _this.state.selectedIdx ) { 963 // Apply changed value to original select 964 _this.$element 965 .prop('selectedIndex', _this.state.currValue = _this.state.selectedIdx) 966 .data('value', _this.lookupItems[_this.state.selectedIdx].text); 967 968 // Change label text 969 _this.setLabel(); 970 971 _this.utils.triggerCallback('Change', _this); 972 } 973 }, 974 975 /** 976 * Highlight option 977 * @param {number} index - Index of the options that will be highlighted 978 */ 979 highlight: function(index) { 980 var _this = this; 981 var $filteredLi = _this.$li.filter('[data-index]').removeClass('highlighted'); 982 983 _this.utils.triggerCallback('BeforeHighlight', _this); 984 985 // Parameter index is required and should not be a disabled item 986 if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) { 987 return; 988 } 989 990 $filteredLi 991 .eq(_this.state.highlightedIdx = index) 992 .addClass('highlighted'); 993 994 _this.detectItemVisibility(index); 995 996 _this.utils.triggerCallback('Highlight', _this); 997 }, 998 999 /** 1000 * Select option 1001 * 1002 * @param {number} index - Index of the option that will be selected 1003 */ 1004 select: function(index) { 1005 var _this = this; 1006 var $filteredLi = _this.$li.filter('[data-index]'); 1007 1008 _this.utils.triggerCallback('BeforeSelect', _this, index); 1009 1010 // Parameter index is required and should not be a disabled item 1011 if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) { 1012 return; 1013 } 1014 1015 if ( _this.state.multiple ) { 1016 // Make sure selectedIdx is an array 1017 _this.state.selectedIdx = $.isArray(_this.state.selectedIdx) ? _this.state.selectedIdx : [_this.state.selectedIdx]; 1018 1019 var hasSelectedIndex = $.inArray(index, _this.state.selectedIdx); 1020 if ( hasSelectedIndex !== -1 ) { 1021 _this.state.selectedIdx.splice(hasSelectedIndex, 1); 1022 } else { 1023 _this.state.selectedIdx.push(index); 1024 } 1025 1026 $filteredLi 1027 .removeClass('selected') 1028 .filter(function(index) { 1029 return $.inArray(index, _this.state.selectedIdx) !== -1; 1030 }) 1031 .addClass('selected'); 1032 } else { 1033 $filteredLi 1034 .removeClass('selected') 1035 .eq(_this.state.selectedIdx = index) 1036 .addClass('selected'); 1037 } 1038 1039 if ( !_this.state.multiple || !_this.options.multiple.keepMenuOpen ) { 1040 _this.close(); 1041 } 1042 1043 _this.change(); 1044 1045 _this.utils.triggerCallback('Select', _this, index); 1046 }, 1047 1048 /** 1049 * Unbind and remove 1050 * 1051 * @param {boolean} preserveData - Check if the data on the element should be removed too 1052 */ 1053 destroy: function(preserveData) { 1054 var _this = this; 1055 1056 if ( _this.state && _this.state.enabled ) { 1057 _this.elements.items.add(_this.elements.wrapper).add(_this.elements.input).remove(); 1058 1059 if ( !preserveData ) { 1060 _this.$element.removeData(pluginName).removeData('value'); 1061 } 1062 1063 _this.$element.prop('tabindex', _this.originalTabindex).off(eventNamespaceSuffix).off(_this.eventTriggers).unwrap().unwrap(); 1064 1065 _this.state.enabled = false; 1066 } 1067 } 1068 }; 1069 1070 // A really lightweight plugin wrapper around the constructor, 1071 // preventing against multiple instantiations 1072 $.fn[pluginName] = function(args) { 1073 return this.each(function() { 1074 var data = $.data(this, pluginName); 1075 1076 if ( data && !data.disableOnMobile ) { 1077 (typeof args === 'string' && data[args]) ? data[args]() : data.init(args); 1078 } else { 1079 $.data(this, pluginName, new Selectric(this, args)); 1080 } 1081 }); 1082 }; 1083 1084 /** 1085 * Default plugin options 1086 * 1087 * @type {object} 1088 */ 1089 $.fn[pluginName].defaults = { 1090 onChange : function(elm) { $(elm).change(); }, 1091 maxHeight : 300, 1092 keySearchTimeout : 500, 1093 arrowButtonMarkup : '<b class="button">▾</b>', 1094 disableOnMobile : false, 1095 nativeOnMobile : true, 1096 openOnFocus : true, 1097 openOnHover : false, 1098 hoverIntentTimeout : 500, 1099 expandToItemText : false, 1100 responsive : false, 1101 preventWindowScroll : true, 1102 inheritOriginalWidth : false, 1103 allowWrap : true, 1104 forceRenderAbove : false, 1105 forceRenderBelow : false, 1106 stopPropagation : true, 1107 optionsItemBuilder : '{text}', // function(itemData, element, index) 1108 labelBuilder : '{text}', // function(currItem) 1109 listBuilder : false, // function(items) 1110 keys : { 1111 previous : [37, 38], // Left / Up 1112 next : [39, 40], // Right / Down 1113 select : [9, 13, 27], // Tab / Enter / Escape 1114 open : [13, 32, 37, 38, 39, 40], // Enter / Space / Left / Up / Right / Down 1115 close : [9, 27] // Tab / Escape 1116 }, 1117 customClass : { 1118 prefix: pluginName, 1119 camelCase: false 1120 }, 1121 multiple : { 1122 separator: ', ', 1123 keepMenuOpen: true, 1124 maxLabelEntries: false 1125 } 1126 }; 1127}));
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.