--- /dev/null
+/*!
+ * ,/
+ * ,'/
+ * ,' /
+ * ,' /_____,
+ * .'____ ,'
+ * / ,'
+ * / ,'
+ * /,'
+ * /'
+ *
+ * Selectric ϟ v1.13.0 (Aug 22 2017) - http://lcdsantos.github.io/jQuery-Selectric/
+ *
+ * Copyright (c) 2017 Leonardo Santos; MIT License
+ *
+ */
+
+(function(factory) {
+ /* global define */
+ /* istanbul ignore next */
+ if ( typeof define === 'function' && define.amd ) {
+ define(['jquery'], factory);
+ } else if ( typeof module === 'object' && module.exports ) {
+ // Node/CommonJS
+ module.exports = function( root, jQuery ) {
+ if ( jQuery === undefined ) {
+ if ( typeof window !== 'undefined' ) {
+ jQuery = require('jquery');
+ } else {
+ jQuery = require('jquery')(root);
+ }
+ }
+ factory(jQuery);
+ return jQuery;
+ };
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+}(function($) {
+ 'use strict';
+
+ var $doc = $(document);
+ var $win = $(window);
+
+ var pluginName = 'selectric';
+ var classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Focus Hover Responsive Above Below Scroll Group GroupLabel';
+ var eventNamespaceSuffix = '.sl';
+
+ var chars = ['a', 'e', 'i', 'o', 'u', 'n', 'c', 'y'];
+ var diacritics = [
+ /[\xE0-\xE5]/g, // a
+ /[\xE8-\xEB]/g, // e
+ /[\xEC-\xEF]/g, // i
+ /[\xF2-\xF6]/g, // o
+ /[\xF9-\xFC]/g, // u
+ /[\xF1]/g, // n
+ /[\xE7]/g, // c
+ /[\xFD-\xFF]/g // y
+ ];
+
+ /**
+ * Create an instance of Selectric
+ *
+ * @constructor
+ * @param {Node} element - The <select> element
+ * @param {object} opts - Options
+ */
+ var Selectric = function(element, opts) {
+ var _this = this;
+
+ _this.element = element;
+ _this.$element = $(element);
+
+ _this.state = {
+ multiple : !!_this.$element.attr('multiple'),
+ enabled : false,
+ opened : false,
+ currValue : -1,
+ selectedIdx : -1,
+ highlightedIdx : -1
+ };
+
+ _this.eventTriggers = {
+ open : _this.open,
+ close : _this.close,
+ destroy : _this.destroy,
+ refresh : _this.refresh,
+ init : _this.init
+ };
+
+ _this.init(opts);
+ };
+
+ Selectric.prototype = {
+ utils: {
+ /**
+ * Detect mobile browser
+ *
+ * @return {boolean}
+ */
+ isMobile: function() {
+ return /android|ip(hone|od|ad)/i.test(navigator.userAgent);
+ },
+
+ /**
+ * Escape especial characters in string (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
+ *
+ * @param {string} str - The string to be escaped
+ * @return {string} The string with the special characters escaped
+ */
+ escapeRegExp: function(str) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+ },
+
+ /**
+ * Replace diacritics
+ *
+ * @param {string} str - The string to replace the diacritics
+ * @return {string} The string with diacritics replaced with ascii characters
+ */
+ replaceDiacritics: function(str) {
+ var k = diacritics.length;
+
+ while (k--) {
+ str = str.toLowerCase().replace(diacritics[k], chars[k]);
+ }
+
+ return str;
+ },
+
+ /**
+ * Format string
+ * https://gist.github.com/atesgoral/984375
+ *
+ * @param {string} f - String to be formated
+ * @return {string} String formated
+ */
+ format: function(f) {
+ var a = arguments; // store outer arguments
+ return ('' + f) // force format specifier to String
+ .replace( // replace tokens in format specifier
+ /\{(?:(\d+)|(\w+))\}/g, // match {token} references
+ function(
+ s, // the matched string (ignored)
+ i, // an argument index
+ p // a property name
+ ) {
+ return p && a[1] // if property name and first argument exist
+ ? a[1][p] // return property from first argument
+ : a[i]; // assume argument index and return i-th argument
+ });
+ },
+
+ /**
+ * Get the next enabled item in the options list.
+ *
+ * @param {object} selectItems - The options object.
+ * @param {number} selected - Index of the currently selected option.
+ * @return {object} The next enabled item.
+ */
+ nextEnabledItem: function(selectItems, selected) {
+ while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) {
+ // empty
+ }
+ return selected;
+ },
+
+ /**
+ * Get the previous enabled item in the options list.
+ *
+ * @param {object} selectItems - The options object.
+ * @param {number} selected - Index of the currently selected option.
+ * @return {object} The previous enabled item.
+ */
+ previousEnabledItem: function(selectItems, selected) {
+ while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) {
+ // empty
+ }
+ return selected;
+ },
+
+ /**
+ * Transform camelCase string to dash-case.
+ *
+ * @param {string} str - The camelCased string.
+ * @return {string} The string transformed to dash-case.
+ */
+ toDash: function(str) {
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
+ },
+
+ /**
+ * Calls the events registered with function name.
+ *
+ * @param {string} fn - The name of the function.
+ * @param {number} scope - Scope that should be set on the function.
+ */
+ triggerCallback: function(fn, scope) {
+ var elm = scope.element;
+ var func = scope.options['on' + fn];
+ var args = [elm].concat([].slice.call(arguments).slice(1));
+
+ if ( $.isFunction(func) ) {
+ func.apply(elm, args);
+ }
+
+ $(elm).trigger(pluginName + '-' + this.toDash(fn), args);
+ },
+
+ /**
+ * Transform array list to concatenated string and remove empty values
+ * @param {array} arr - Class list
+ * @return {string} Concatenated string
+ */
+ arrayToClassname: function(arr) {
+ var newArr = $.grep(arr, function(item) {
+ return !!item;
+ });
+
+ return $.trim(newArr.join(' '));
+ }
+ },
+
+ /** Initializes */
+ init: function(opts) {
+ var _this = this;
+
+ // Set options
+ _this.options = $.extend(true, {}, $.fn[pluginName].defaults, _this.options, opts);
+
+ _this.utils.triggerCallback('BeforeInit', _this);
+
+ // Preserve data
+ _this.destroy(true);
+
+ // Disable on mobile browsers
+ if ( _this.options.disableOnMobile && _this.utils.isMobile() ) {
+ _this.disableOnMobile = true;
+ return;
+ }
+
+ // Get classes
+ _this.classes = _this.getClassNames();
+
+ // Create elements
+ var input = $('<input/>', { 'class': _this.classes.input, 'readonly': _this.utils.isMobile() });
+ var items = $('<div/>', { 'class': _this.classes.items, 'tabindex': -1 });
+ var itemsScroll = $('<div/>', { 'class': _this.classes.scroll });
+ var wrapper = $('<div/>', { 'class': _this.classes.prefix, 'html': _this.options.arrowButtonMarkup });
+ var label = $('<span/>', { 'class': 'label' });
+ var outerWrapper = _this.$element.wrap('<div/>').parent().append(wrapper.prepend(label), items, input);
+ var hideSelectWrapper = $('<div/>', { 'class': _this.classes.hideselect });
+
+ _this.elements = {
+ input : input,
+ items : items,
+ itemsScroll : itemsScroll,
+ wrapper : wrapper,
+ label : label,
+ outerWrapper : outerWrapper
+ };
+
+ if ( _this.options.nativeOnMobile && _this.utils.isMobile() ) {
+ _this.elements.input = undefined;
+ hideSelectWrapper.addClass(_this.classes.prefix + '-is-native');
+
+ _this.$element.on('change', function() {
+ _this.refresh();
+ });
+ }
+
+ _this.$element
+ .on(_this.eventTriggers)
+ .wrap(hideSelectWrapper);
+
+ _this.originalTabindex = _this.$element.prop('tabindex');
+ _this.$element.prop('tabindex', -1);
+
+ _this.populate();
+ _this.activate();
+
+ _this.utils.triggerCallback('Init', _this);
+ },
+
+ /** Activates the plugin */
+ activate: function() {
+ var _this = this;
+ var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow);
+ var originalWidth = _this.$element.width();
+
+ hiddenChildren.removeClass(_this.classes.tempshow);
+
+ _this.utils.triggerCallback('BeforeActivate', _this);
+
+ _this.elements.outerWrapper.prop('class',
+ _this.utils.arrayToClassname([
+ _this.classes.wrapper,
+ _this.$element.prop('class').replace(/\S+/g, _this.classes.prefix + '-$&'),
+ _this.options.responsive ? _this.classes.responsive : ''
+ ])
+ );
+
+ if ( _this.options.inheritOriginalWidth && originalWidth > 0 ) {
+ _this.elements.outerWrapper.width(originalWidth);
+ }
+
+ _this.unbindEvents();
+
+ if ( !_this.$element.prop('disabled') ) {
+ _this.state.enabled = true;
+
+ // Not disabled, so... Removing disabled class
+ _this.elements.outerWrapper.removeClass(_this.classes.disabled);
+
+ // Remove styles from items box
+ // Fix incorrect height when refreshed is triggered with fewer options
+ _this.$li = _this.elements.items.removeAttr('style').find('li');
+
+ _this.bindEvents();
+ } else {
+ _this.elements.outerWrapper.addClass(_this.classes.disabled);
+
+ if ( _this.elements.input ) {
+ _this.elements.input.prop('disabled', true);
+ }
+ }
+
+ _this.utils.triggerCallback('Activate', _this);
+ },
+
+ /**
+ * Generate classNames for elements
+ *
+ * @return {object} Classes object
+ */
+ getClassNames: function() {
+ var _this = this;
+ var customClass = _this.options.customClass;
+ var classesObj = {};
+
+ $.each(classList.split(' '), function(i, currClass) {
+ var c = customClass.prefix + currClass;
+ classesObj[currClass.toLowerCase()] = customClass.camelCase ? c : _this.utils.toDash(c);
+ });
+
+ classesObj.prefix = customClass.prefix;
+
+ return classesObj;
+ },
+
+ /** Set the label text */
+ setLabel: function() {
+ var _this = this;
+ var labelBuilder = _this.options.labelBuilder;
+
+ if ( _this.state.multiple ) {
+ // Make sure currentValues is an array
+ var currentValues = $.isArray(_this.state.currValue) ? _this.state.currValue : [_this.state.currValue];
+ // I'm not happy with this, but currentValues can be an empty
+ // array and we need to fallback to the default option.
+ currentValues = currentValues.length === 0 ? [0] : currentValues;
+
+ var labelMarkup = $.map(currentValues, function(value) {
+ return $.grep(_this.lookupItems, function(item) {
+ return item.index === value;
+ })[0]; // we don't want nested arrays here
+ });
+
+ labelMarkup = $.grep(labelMarkup, function(item) {
+ // Hide default (please choose) if more then one element were selected.
+ // If no option value were given value is set to option text by default
+ if ( labelMarkup.length > 1 || labelMarkup.length === 0 ) {
+ return $.trim(item.value) !== '';
+ }
+ return item;
+ });
+
+ labelMarkup = $.map(labelMarkup, function(item) {
+ return $.isFunction(labelBuilder)
+ ? labelBuilder(item)
+ : _this.utils.format(labelBuilder, item);
+ });
+
+ // Limit the amount of selected values shown in label
+ if ( _this.options.multiple.maxLabelEntries ) {
+ if ( labelMarkup.length >= _this.options.multiple.maxLabelEntries + 1 ) {
+ labelMarkup = labelMarkup.slice(0, _this.options.multiple.maxLabelEntries);
+ labelMarkup.push(
+ $.isFunction(labelBuilder)
+ ? labelBuilder({ text: '...' })
+ : _this.utils.format(labelBuilder, { text: '...' }));
+ } else {
+ labelMarkup.slice(labelMarkup.length - 1);
+ }
+ }
+ _this.elements.label.html(labelMarkup.join(_this.options.multiple.separator));
+
+ } else {
+ var currItem = _this.lookupItems[_this.state.currValue];
+
+ _this.elements.label.html(
+ $.isFunction(labelBuilder)
+ ? labelBuilder(currItem)
+ : _this.utils.format(labelBuilder, currItem)
+ );
+ }
+ },
+
+ /** Get and save the available options */
+ populate: function() {
+ var _this = this;
+ var $options = _this.$element.children();
+ var $justOptions = _this.$element.find('option');
+ var $selected = $justOptions.filter(':selected');
+ var selectedIndex = $justOptions.index($selected);
+ var currIndex = 0;
+ var emptyValue = (_this.state.multiple ? [] : 0);
+
+ if ( $selected.length > 1 && _this.state.multiple ) {
+ selectedIndex = [];
+ $selected.each(function() {
+ selectedIndex.push($(this).index());
+ });
+ }
+
+ _this.state.currValue = (~selectedIndex ? selectedIndex : emptyValue);
+ _this.state.selectedIdx = _this.state.currValue;
+ _this.state.highlightedIdx = _this.state.currValue;
+ _this.items = [];
+ _this.lookupItems = [];
+
+ if ( $options.length ) {
+ // Build options markup
+ $options.each(function(i) {
+ var $elm = $(this);
+
+ if ( $elm.is('optgroup') ) {
+
+ var optionsGroup = {
+ element : $elm,
+ label : $elm.prop('label'),
+ groupDisabled : $elm.prop('disabled'),
+ items : []
+ };
+
+ $elm.children().each(function(i) {
+ var $elm = $(this);
+
+ optionsGroup.items[i] = _this.getItemData(currIndex, $elm, optionsGroup.groupDisabled || $elm.prop('disabled'));
+
+ _this.lookupItems[currIndex] = optionsGroup.items[i];
+
+ currIndex++;
+ });
+
+ _this.items[i] = optionsGroup;
+
+ } else {
+
+ _this.items[i] = _this.getItemData(currIndex, $elm, $elm.prop('disabled'));
+
+ _this.lookupItems[currIndex] = _this.items[i];
+
+ currIndex++;
+
+ }
+ });
+
+ _this.setLabel();
+ _this.elements.items.append( _this.elements.itemsScroll.html( _this.getItemsMarkup(_this.items) ) );
+ }
+ },
+
+ /**
+ * Generate items object data
+ * @param {integer} index - Current item index
+ * @param {node} $elm - Current element node
+ * @param {boolean} isDisabled - Current element disabled state
+ * @return {object} Item object
+ */
+ getItemData: function(index, $elm, isDisabled) {
+ var _this = this;
+
+ return {
+ index : index,
+ element : $elm,
+ value : $elm.val(),
+ className : $elm.prop('class'),
+ text : $elm.html(),
+ slug : $.trim(_this.utils.replaceDiacritics($elm.html())),
+ alt : $elm.attr('data-alt'),
+ selected : $elm.prop('selected'),
+ disabled : isDisabled
+ };
+ },
+
+ /**
+ * Generate options markup
+ *
+ * @param {object} items - Object containing all available options
+ * @return {string} HTML for the options box
+ */
+ getItemsMarkup: function(items) {
+ var _this = this;
+ var markup = '<ul>';
+
+ if ( $.isFunction(_this.options.listBuilder) && _this.options.listBuilder ) {
+ items = _this.options.listBuilder(items);
+ }
+
+ $.each(items, function(i, elm) {
+ if ( elm.label !== undefined ) {
+
+ markup += _this.utils.format('<ul class="{1}"><li class="{2}">{3}</li>',
+ _this.utils.arrayToClassname([
+ _this.classes.group,
+ elm.groupDisabled ? 'disabled' : '',
+ elm.element.prop('class')
+ ]),
+ _this.classes.grouplabel,
+ elm.element.prop('label')
+ );
+
+ $.each(elm.items, function(i, elm) {
+ markup += _this.getItemMarkup(elm.index, elm);
+ });
+
+ markup += '</ul>';
+
+ } else {
+
+ markup += _this.getItemMarkup(elm.index, elm);
+
+ }
+ });
+
+ return markup + '</ul>';
+ },
+
+ /**
+ * Generate every option markup
+ *
+ * @param {number} index - Index of current item
+ * @param {object} itemData - Current item
+ * @return {string} HTML for the option
+ */
+ getItemMarkup: function(index, itemData) {
+ var _this = this;
+ var itemBuilder = _this.options.optionsItemBuilder;
+ // limit access to item data to provide a simple interface
+ // to most relevant options.
+ var filteredItemData = {
+ value: itemData.value,
+ text : itemData.text,
+ slug : itemData.slug,
+ index: itemData.index
+ };
+
+ return _this.utils.format('<li data-index="{1}" class="{2}">{3}</li>',
+ index,
+ _this.utils.arrayToClassname([
+ itemData.className,
+ index === _this.items.length - 1 ? 'last' : '',
+ itemData.disabled ? 'disabled' : '',
+ itemData.selected ? 'selected' : ''
+ ]),
+ $.isFunction(itemBuilder)
+ ? _this.utils.format(itemBuilder(itemData, this.$element, index), itemData)
+ : _this.utils.format(itemBuilder, filteredItemData)
+ );
+ },
+
+ /** Remove events on the elements */
+ unbindEvents: function() {
+ var _this = this;
+
+ _this.elements.wrapper
+ .add(_this.$element)
+ .add(_this.elements.outerWrapper)
+ .add(_this.elements.input)
+ .off(eventNamespaceSuffix);
+ },
+
+ /** Bind events on the elements */
+ bindEvents: function() {
+ var _this = this;
+
+ _this.elements.outerWrapper.on('mouseenter' + eventNamespaceSuffix + ' mouseleave' + eventNamespaceSuffix, function(e) {
+ $(this).toggleClass(_this.classes.hover, e.type === 'mouseenter');
+
+ // Delay close effect when openOnHover is true
+ if ( _this.options.openOnHover ) {
+ clearTimeout(_this.closeTimer);
+
+ if ( e.type === 'mouseleave' ) {
+ _this.closeTimer = setTimeout($.proxy(_this.close, _this), _this.options.hoverIntentTimeout);
+ } else {
+ _this.open();
+ }
+ }
+ });
+
+ // Toggle open/close
+ _this.elements.wrapper.on('click' + eventNamespaceSuffix, function(e) {
+ _this.state.opened ? _this.close() : _this.open(e);
+ });
+
+ // Translate original element focus event to dummy input.
+ // Disabled on mobile devices because the default option list isn't
+ // shown due the fact that hidden input gets focused
+ if ( !(_this.options.nativeOnMobile && _this.utils.isMobile()) ) {
+ _this.$element.on('focus' + eventNamespaceSuffix, function() {
+ _this.elements.input.focus();
+ });
+
+ _this.elements.input
+ .prop({ tabindex: _this.originalTabindex, disabled: false })
+ .on('keydown' + eventNamespaceSuffix, $.proxy(_this.handleKeys, _this))
+ .on('focusin' + eventNamespaceSuffix, function(e) {
+ _this.elements.outerWrapper.addClass(_this.classes.focus);
+
+ // Prevent the flicker when focusing out and back again in the browser window
+ _this.elements.input.one('blur', function() {
+ _this.elements.input.blur();
+ });
+
+ if ( _this.options.openOnFocus && !_this.state.opened ) {
+ _this.open(e);
+ }
+ })
+ .on('focusout' + eventNamespaceSuffix, function() {
+ _this.elements.outerWrapper.removeClass(_this.classes.focus);
+ })
+ .on('input propertychange', function() {
+ var val = _this.elements.input.val();
+ var searchRegExp = new RegExp('^' + _this.utils.escapeRegExp(val), 'i');
+
+ // Clear search
+ clearTimeout(_this.resetStr);
+ _this.resetStr = setTimeout(function() {
+ _this.elements.input.val('');
+ }, _this.options.keySearchTimeout);
+
+ if ( val.length ) {
+ // Search in select options
+ $.each(_this.items, function(i, elm) {
+ if (elm.disabled) {
+ return;
+ }
+ if (searchRegExp.test(elm.text) || searchRegExp.test(elm.slug)) {
+ _this.highlight(i);
+ return;
+ }
+ if (!elm.alt) {
+ return;
+ }
+ var altItems = elm.alt.split('|');
+ for (var ai = 0; ai < altItems.length; ai++) {
+ if (!altItems[ai]) {
+ break;
+ }
+ if (searchRegExp.test(altItems[ai].trim())) {
+ _this.highlight(i);
+ return;
+ }
+ }
+ });
+ }
+ });
+ }
+
+ _this.$li.on({
+ // Prevent <input> blur on Chrome
+ mousedown: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ },
+ click: function() {
+ _this.select($(this).data('index'));
+
+ // Chrome doesn't close options box if select is wrapped with a label
+ // We need to 'return false' to avoid that
+ return false;
+ }
+ });
+ },
+
+ /**
+ * Behavior when keyboard keys is pressed
+ *
+ * @param {object} e - Event object
+ */
+ handleKeys: function(e) {
+ var _this = this;
+ var key = e.which;
+ var keys = _this.options.keys;
+
+ var isPrevKey = $.inArray(key, keys.previous) > -1;
+ var isNextKey = $.inArray(key, keys.next) > -1;
+ var isSelectKey = $.inArray(key, keys.select) > -1;
+ var isOpenKey = $.inArray(key, keys.open) > -1;
+ var idx = _this.state.highlightedIdx;
+ var isFirstOrLastItem = (isPrevKey && idx === 0) || (isNextKey && (idx + 1) === _this.items.length);
+ var goToItem = 0;
+
+ // Enter / Space
+ if ( key === 13 || key === 32 ) {
+ e.preventDefault();
+ }
+
+ // If it's a directional key
+ if ( isPrevKey || isNextKey ) {
+ if ( !_this.options.allowWrap && isFirstOrLastItem ) {
+ return;
+ }
+
+ if ( isPrevKey ) {
+ goToItem = _this.utils.previousEnabledItem(_this.lookupItems, idx);
+ }
+
+ if ( isNextKey ) {
+ goToItem = _this.utils.nextEnabledItem(_this.lookupItems, idx);
+ }
+
+ _this.highlight(goToItem);
+ }
+
+ // Tab / Enter / ESC
+ if ( isSelectKey && _this.state.opened ) {
+ _this.select(idx);
+
+ if ( !_this.state.multiple || !_this.options.multiple.keepMenuOpen ) {
+ _this.close();
+ }
+
+ return;
+ }
+
+ // Space / Enter / Left / Up / Right / Down
+ if ( isOpenKey && !_this.state.opened ) {
+ _this.open();
+ }
+ },
+
+ /** Update the items object */
+ refresh: function() {
+ var _this = this;
+
+ _this.populate();
+ _this.activate();
+ _this.utils.triggerCallback('Refresh', _this);
+ },
+
+ /** Set options box width/height */
+ setOptionsDimensions: function() {
+ var _this = this;
+
+ // Calculate options box height
+ // Set a temporary class on the hidden parent of the element
+ var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow);
+ var maxHeight = _this.options.maxHeight;
+ var itemsWidth = _this.elements.items.outerWidth();
+ var wrapperWidth = _this.elements.wrapper.outerWidth() - (itemsWidth - _this.elements.items.width());
+
+ // Set the dimensions, minimum is wrapper width, expand for long items if option is true
+ if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth ) {
+ _this.finalWidth = wrapperWidth;
+ } else {
+ // Make sure the scrollbar width is included
+ _this.elements.items.css('overflow', 'scroll');
+
+ // Set a really long width for _this.elements.outerWrapper
+ _this.elements.outerWrapper.width(9e4);
+ _this.finalWidth = _this.elements.items.width();
+ // Set scroll bar to auto
+ _this.elements.items.css('overflow', '');
+ _this.elements.outerWrapper.width('');
+ }
+
+ _this.elements.items.width(_this.finalWidth).height() > maxHeight && _this.elements.items.height(maxHeight);
+
+ // Remove the temporary class
+ hiddenChildren.removeClass(_this.classes.tempshow);
+ },
+
+ /** Detect if the options box is inside the window */
+ isInViewport: function() {
+ var _this = this;
+
+ if (_this.options.forceRenderAbove === true) {
+ _this.elements.outerWrapper.addClass(_this.classes.above);
+ } else if (_this.options.forceRenderBelow === true) {
+ _this.elements.outerWrapper.addClass(_this.classes.below);
+ } else {
+ var scrollTop = $win.scrollTop();
+ var winHeight = $win.height();
+ var uiPosX = _this.elements.outerWrapper.offset().top;
+ var uiHeight = _this.elements.outerWrapper.outerHeight();
+
+ var fitsDown = (uiPosX + uiHeight + _this.itemsHeight) <= (scrollTop + winHeight);
+ var fitsAbove = (uiPosX - _this.itemsHeight) > scrollTop;
+
+ // If it does not fit below, only render it
+ // above it fit's there.
+ // It's acceptable that the user needs to
+ // scroll the viewport to see the cut off UI
+ var renderAbove = !fitsDown && fitsAbove;
+ var renderBelow = !renderAbove;
+
+ _this.elements.outerWrapper.toggleClass(_this.classes.above, renderAbove);
+ _this.elements.outerWrapper.toggleClass(_this.classes.below, renderBelow);
+ }
+ },
+
+ /**
+ * Detect if currently selected option is visible and scroll the options box to show it
+ *
+ * @param {Number|Array} index - Index of the selected items
+ */
+ detectItemVisibility: function(index) {
+ var _this = this;
+ var $filteredLi = _this.$li.filter('[data-index]');
+
+ if ( _this.state.multiple ) {
+ // If index is an array, we can assume a multiple select and we
+ // want to scroll to the uppermost selected item!
+ // Math.min.apply(Math, index) returns the lowest entry in an Array.
+ index = ($.isArray(index) && index.length === 0) ? 0 : index;
+ index = $.isArray(index) ? Math.min.apply(Math, index) : index;
+ }
+
+ var liHeight = $filteredLi.eq(index).outerHeight();
+ var liTop = $filteredLi[index].offsetTop;
+ var itemsScrollTop = _this.elements.itemsScroll.scrollTop();
+ var scrollT = liTop + liHeight * 2;
+
+ _this.elements.itemsScroll.scrollTop(
+ scrollT > itemsScrollTop + _this.itemsHeight ? scrollT - _this.itemsHeight :
+ liTop - liHeight < itemsScrollTop ? liTop - liHeight :
+ itemsScrollTop
+ );
+ },
+
+ /**
+ * Open the select options box
+ *
+ * @param {Event} e - Event
+ */
+ open: function(e) {
+ var _this = this;
+
+ if ( _this.options.nativeOnMobile && _this.utils.isMobile()) {
+ return false;
+ }
+
+ _this.utils.triggerCallback('BeforeOpen', _this);
+
+ if ( e ) {
+ e.preventDefault();
+ if (_this.options.stopPropagation) {
+ e.stopPropagation();
+ }
+ }
+
+ if ( _this.state.enabled ) {
+ _this.setOptionsDimensions();
+
+ // Find any other opened instances of select and close it
+ $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close');
+
+ _this.state.opened = true;
+ _this.itemsHeight = _this.elements.items.outerHeight();
+ _this.itemsInnerHeight = _this.elements.items.height();
+
+ // Toggle options box visibility
+ _this.elements.outerWrapper.addClass(_this.classes.open);
+
+ // Give dummy input focus
+ _this.elements.input.val('');
+ if ( e && e.type !== 'focusin' ) {
+ _this.elements.input.focus();
+ }
+
+ // Delayed binds events on Document to make label clicks work
+ setTimeout(function() {
+ $doc
+ .on('click' + eventNamespaceSuffix, $.proxy(_this.close, _this))
+ .on('scroll' + eventNamespaceSuffix, $.proxy(_this.isInViewport, _this));
+ }, 1);
+
+ _this.isInViewport();
+
+ // Prevent window scroll when using mouse wheel inside items box
+ if ( _this.options.preventWindowScroll ) {
+ /* istanbul ignore next */
+ $doc.on('mousewheel' + eventNamespaceSuffix + ' DOMMouseScroll' + eventNamespaceSuffix, '.' + _this.classes.scroll, function(e) {
+ var orgEvent = e.originalEvent;
+ var scrollTop = $(this).scrollTop();
+ var deltaY = 0;
+
+ if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; }
+ if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; }
+ if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; }
+ if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; }
+
+ if ( scrollTop === (this.scrollHeight - _this.itemsInnerHeight) && deltaY < 0 || scrollTop === 0 && deltaY > 0 ) {
+ e.preventDefault();
+ }
+ });
+ }
+
+ _this.detectItemVisibility(_this.state.selectedIdx);
+
+ _this.highlight(_this.state.multiple ? -1 : _this.state.selectedIdx);
+
+ _this.utils.triggerCallback('Open', _this);
+ }
+ },
+
+ /** Close the select options box */
+ close: function() {
+ var _this = this;
+
+ _this.utils.triggerCallback('BeforeClose', _this);
+
+ // Remove custom events on document
+ $doc.off(eventNamespaceSuffix);
+
+ // Remove visible class to hide options box
+ _this.elements.outerWrapper.removeClass(_this.classes.open);
+
+ _this.state.opened = false;
+
+ _this.utils.triggerCallback('Close', _this);
+ },
+
+ /** Select current option and change the label */
+ change: function() {
+ var _this = this;
+
+ _this.utils.triggerCallback('BeforeChange', _this);
+
+ if ( _this.state.multiple ) {
+ // Reset old selected
+ $.each(_this.lookupItems, function(idx) {
+ _this.lookupItems[idx].selected = false;
+ _this.$element.find('option').prop('selected', false);
+ });
+
+ // Set new selected
+ $.each(_this.state.selectedIdx, function(idx, value) {
+ _this.lookupItems[value].selected = true;
+ _this.$element.find('option').eq(value).prop('selected', true);
+ });
+
+ _this.state.currValue = _this.state.selectedIdx;
+
+ _this.setLabel();
+
+ _this.utils.triggerCallback('Change', _this);
+ } else if ( _this.state.currValue !== _this.state.selectedIdx ) {
+ // Apply changed value to original select
+ _this.$element
+ .prop('selectedIndex', _this.state.currValue = _this.state.selectedIdx)
+ .data('value', _this.lookupItems[_this.state.selectedIdx].text);
+
+ // Change label text
+ _this.setLabel();
+
+ _this.utils.triggerCallback('Change', _this);
+ }
+ },
+
+ /**
+ * Highlight option
+ * @param {number} index - Index of the options that will be highlighted
+ */
+ highlight: function(index) {
+ var _this = this;
+ var $filteredLi = _this.$li.filter('[data-index]').removeClass('highlighted');
+
+ _this.utils.triggerCallback('BeforeHighlight', _this);
+
+ // Parameter index is required and should not be a disabled item
+ if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) {
+ return;
+ }
+
+ $filteredLi
+ .eq(_this.state.highlightedIdx = index)
+ .addClass('highlighted');
+
+ _this.detectItemVisibility(index);
+
+ _this.utils.triggerCallback('Highlight', _this);
+ },
+
+ /**
+ * Select option
+ *
+ * @param {number} index - Index of the option that will be selected
+ */
+ select: function(index) {
+ var _this = this;
+ var $filteredLi = _this.$li.filter('[data-index]');
+
+ _this.utils.triggerCallback('BeforeSelect', _this, index);
+
+ // Parameter index is required and should not be a disabled item
+ if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) {
+ return;
+ }
+
+ if ( _this.state.multiple ) {
+ // Make sure selectedIdx is an array
+ _this.state.selectedIdx = $.isArray(_this.state.selectedIdx) ? _this.state.selectedIdx : [_this.state.selectedIdx];
+
+ var hasSelectedIndex = $.inArray(index, _this.state.selectedIdx);
+ if ( hasSelectedIndex !== -1 ) {
+ _this.state.selectedIdx.splice(hasSelectedIndex, 1);
+ } else {
+ _this.state.selectedIdx.push(index);
+ }
+
+ $filteredLi
+ .removeClass('selected')
+ .filter(function(index) {
+ return $.inArray(index, _this.state.selectedIdx) !== -1;
+ })
+ .addClass('selected');
+ } else {
+ $filteredLi
+ .removeClass('selected')
+ .eq(_this.state.selectedIdx = index)
+ .addClass('selected');
+ }
+
+ if ( !_this.state.multiple || !_this.options.multiple.keepMenuOpen ) {
+ _this.close();
+ }
+
+ _this.change();
+
+ _this.utils.triggerCallback('Select', _this, index);
+ },
+
+ /**
+ * Unbind and remove
+ *
+ * @param {boolean} preserveData - Check if the data on the element should be removed too
+ */
+ destroy: function(preserveData) {
+ var _this = this;
+
+ if ( _this.state && _this.state.enabled ) {
+ _this.elements.items.add(_this.elements.wrapper).add(_this.elements.input).remove();
+
+ if ( !preserveData ) {
+ _this.$element.removeData(pluginName).removeData('value');
+ }
+
+ _this.$element.prop('tabindex', _this.originalTabindex).off(eventNamespaceSuffix).off(_this.eventTriggers).unwrap().unwrap();
+
+ _this.state.enabled = false;
+ }
+ }
+ };
+
+ // A really lightweight plugin wrapper around the constructor,
+ // preventing against multiple instantiations
+ $.fn[pluginName] = function(args) {
+ return this.each(function() {
+ var data = $.data(this, pluginName);
+
+ if ( data && !data.disableOnMobile ) {
+ (typeof args === 'string' && data[args]) ? data[args]() : data.init(args);
+ } else {
+ $.data(this, pluginName, new Selectric(this, args));
+ }
+ });
+ };
+
+ /**
+ * Default plugin options
+ *
+ * @type {object}
+ */
+ $.fn[pluginName].defaults = {
+ onChange : function(elm) { $(elm).change(); },
+ maxHeight : 300,
+ keySearchTimeout : 500,
+ arrowButtonMarkup : '<b class="button">▾</b>',
+ disableOnMobile : false,
+ nativeOnMobile : true,
+ openOnFocus : true,
+ openOnHover : false,
+ hoverIntentTimeout : 500,
+ expandToItemText : false,
+ responsive : false,
+ preventWindowScroll : true,
+ inheritOriginalWidth : false,
+ allowWrap : true,
+ forceRenderAbove : false,
+ forceRenderBelow : false,
+ stopPropagation : true,
+ optionsItemBuilder : '{text}', // function(itemData, element, index)
+ labelBuilder : '{text}', // function(currItem)
+ listBuilder : false, // function(items)
+ keys : {
+ previous : [37, 38], // Left / Up
+ next : [39, 40], // Right / Down
+ select : [9, 13, 27], // Tab / Enter / Escape
+ open : [13, 32, 37, 38, 39, 40], // Enter / Space / Left / Up / Right / Down
+ close : [9, 27] // Tab / Escape
+ },
+ customClass : {
+ prefix: pluginName,
+ camelCase: false
+ },
+ multiple : {
+ separator: ', ',
+ keepMenuOpen: true,
+ maxLabelEntries: false
+ }
+ };
+}));