/*! * ,/ * ,'/ * ,' / * ,' /_____, * .'____ ,' * / ,' * / ,' * /,' * /' * * 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 = $('', { 'class': _this.classes.input, 'readonly': _this.utils.isMobile() }); var items = $('
', { 'class': _this.classes.items, 'tabindex': -1 }); var itemsScroll = $('
', { 'class': _this.classes.scroll }); var wrapper = $('
', { 'class': _this.classes.prefix, 'html': _this.options.arrowButtonMarkup }); var label = $('', { 'class': 'label' }); var outerWrapper = _this.$element.wrap('
').parent().append(wrapper.prepend(label), items, input); var hideSelectWrapper = $('
', { '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 = '
    '; 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('
    • {3}
    • ', _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 += '
    '; } else { markup += _this.getItemMarkup(elm.index, elm); } }); return markup + '
'; }, /** * 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('
  • {3}
  • ', 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 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 : '', 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 } }; }));