PageSourceSearch

https://lecardohotel.ru/wp-content/themes/lecardo/assets/js/jquery.selectric.js?ver=1787826433

js lecardohotel.ru collected 2026-10-08 16:48:02 UTC 36,422 bytes, 1,127 lines download raw bytes

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">&#x25be;</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.