PageSourceSearch

https://www.firecomms.com/assets/jquery.selectric/jquery.selectric.js

js firecomms.com collected 2026-10-04 02:47:28 UTC 37,274 bytes, 1,110 lines download raw bytes

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

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.