/*!
* ,/
* ,'/
* ,' /
* ,' /_____,
* .'____ ,'
* / ,'
* / ,'
* /,'
* /'
*
* Selectric ? v1.9.6 (Mar 24 2016) - http://lcdsantos.github.io/jQuery-Selectric/
*
* Copyright (c) 2016 Leonardo Santos; MIT License
*
*/
(function(factory) {
/* 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 pluginName = 'selectric',
classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Hover Responsive Above Scroll Group GroupLabel',
bindSufix = '.sl',
defaults = {
onChange: function(elm) { $(elm).change(); },
maxHeight: 300,
keySearchTimeout: 500,
arrowButtonMarkup: '▾',
disableOnMobile: true,
openOnHover: false,
hoverIntentTimeout: 500,
expandToItemText: false,
responsive: false,
preventWindowScroll: true,
inheritOriginalWidth: false,
allowWrap: true,
customClass: {
prefix: pluginName,
camelCase: false
},
optionsItemBuilder: '{text}', // function(itemData, element, index)
labelBuilder: '{text}' // function(currItem)
},
hooks = {
add: function(callbackName, hookName, fn) {
if ( !this[callbackName] )
this[callbackName] = {};
this[callbackName][hookName] = fn;
},
remove: function(callbackName, hookName) {
delete this[callbackName][hookName];
}
},
_utils = {
// Replace diacritics
replaceDiacritics: function(s) {
// /[\340-\346]/g, // a
// /[\350-\353]/g, // e
// /[\354-\357]/g, // i
// /[\362-\370]/g, // o
// /[\371-\374]/g, // u
// /[\361]/g, // n
// /[\347]/g, // c
// /[\377]/g // y
var d = '40-46 50-53 54-57 62-70 71-74 61 47 77'.replace(/\d+/g, '\\3$&').split(' '),
k = d.length;
while (k--)
s = s.toLowerCase().replace(RegExp('[' + d[k] + ']', 'g'), 'aeiouncy'.charAt(k));
return s;
},
// https://gist.github.com/atesgoral/984375
format: function(f) {var a=arguments;return(""+f).replace(/{(\d+|(\w+))}/g,function(s,i,p) {return p&&a[1]?a[1][p]:a[i]})},
nextEnabledItem: function(selectItems, selected) {
while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) {}
return selected;
},
previousEnabledItem: function(selectItems, selected) {
while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) {}
return selected;
},
toDash: function(str) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
},
triggerCallback: function(fn, scope) {
var elm = scope.element,
func = scope.options['on' + fn];
if ( $.isFunction(func) )
func.call(elm, elm, scope);
if ( hooks[fn] ) {
$.each(hooks[fn], function() {
this.call(elm, elm, scope);
});
}
$(elm).trigger(pluginName + '-' + _utils.toDash(fn), scope);
}
},
$doc = $(document),
$win = $(window),
Selectric = function(element, opts) {
var _this = this,
$original = $(element),
$input, $items, $itemsScroll, $wrapper, $label, $outerWrapper, $li,
isOpen = false,
isEnabled = false,
selected,
currValue,
itemsHeight,
itemsInnerHeight,
finalWidth,
optionsLength,
eventTriggers,
isMobile = /android|ip(hone|od|ad)/i.test(navigator.userAgent),
tabindex = $original.prop('tabindex'),
labelBuilder;
function _init(opts) {
_this.options = $.extend(true, {}, defaults, _this.options, opts);
_this.classes = {};
_this.element = element;
_utils.triggerCallback('BeforeInit', _this);
// Disable on mobile browsers
if ( _this.options.disableOnMobile && isMobile ) {
_this.disableOnMobile = true;
return;
}
// Preserve data
_destroy(true);
// Generate classNames for elements
var customClass = _this.options.customClass,
postfixes = classList.split(' '),
originalWidth = $original.width();
$.each(postfixes, function(i, currClass) {
var c = customClass.prefix + currClass;
_this.classes[currClass.toLowerCase()] = customClass.camelCase ? c : _utils.toDash(c);
});
$input = $('', { 'class': _this.classes.input, 'readonly': isMobile });
$items = $('
', { 'class': _this.classes.items, 'tabindex': -1 });
$itemsScroll = $('', { 'class': _this.classes.scroll });
$wrapper = $('', { 'class': customClass.prefix, 'html': _this.options.arrowButtonMarkup });
$label = $('');
$outerWrapper = $original.wrap('').parent().append($wrapper.prepend($label), $items, $input);
eventTriggers = {
open : _open,
close : _close,
destroy : _destroy,
refresh : _refresh,
init : _init
};
$original.on(eventTriggers).wrap('
');
$.extend(_this, eventTriggers);
labelBuilder = _this.options.labelBuilder;
if ( _this.options.inheritOriginalWidth && originalWidth > 0 )
$outerWrapper.width(originalWidth);
_populate();
}
// Generate options markup and event binds
function _populate() {
_this.items = [];
var $options = $original.children(),
_$li = '
',
$justOptions = $original.find('option'),
selectedIndex = $justOptions.index($justOptions.filter(':selected')),
currIndex = 0;
currValue = (selected = ~selectedIndex ? selectedIndex : 0);
if ( optionsLength = $options.length ) {
// Build options markup
$options.each(function() {
var $elm = $(this);
if ( $elm.is('optgroup') ) {
var groupDisabled = $elm.prop('disabled'),
$children = $elm.children();
_$li += _utils.format('- {3}
',
$.trim([_this.classes.group, groupDisabled ? 'disabled' : '', $elm.prop('class')].join(' ')),
_this.classes.grouplabel,
$elm.prop('label')
);
if ( groupDisabled ) {
$children.prop('disabled', true);
}
$children.each(buildOption);
_$li += '
';
} else {
buildOption.call($elm);
}
function buildOption() {
var $elm = $(this),
optionText = $elm.html(),
selectDisabled = $elm.prop('disabled'),
itemBuilder = _this.options.optionsItemBuilder;
_this.items[currIndex] = {
element : $elm,
value : $elm.val(),
text : optionText,
slug : _utils.replaceDiacritics(optionText),
disabled : selectDisabled
};
_$li += _utils.format('- {3}
',
currIndex,
$.trim([currIndex == currValue ? 'selected' : '', currIndex == optionsLength - 1 ? 'last' : '', selectDisabled ? 'disabled' : ''].join(' ')),
$.isFunction(itemBuilder) ? itemBuilder(_this.items[currIndex], $elm, currIndex) : _utils.format(itemBuilder, _this.items[currIndex])
);
currIndex++;
}
});
$items.append( $itemsScroll.html(_$li + '
') );
$label.html(
$.isFunction(labelBuilder) ? labelBuilder(_this.items[currValue]) : _utils.format(labelBuilder, _this.items[currValue])
)
}
$wrapper.add($original).add($outerWrapper).add($input).off(bindSufix);
$outerWrapper.prop('class', [
_this.classes.wrapper,
$original.prop('class').replace(/\S+/g, _this.options.customClass.prefix + '-$&'),
_this.options.responsive ? _this.classes.responsive : ''
].join(' '));
if ( !$original.prop('disabled') ) {
isEnabled = true;
// Not disabled, so... Removing disabled class and bind hover
$outerWrapper.removeClass(_this.classes.disabled).on('mouseenter' + bindSufix + ' mouseleave' + bindSufix, function(e) {
$(this).toggleClass(_this.classes.hover);
// Delay close effect when openOnHover is true
if ( _this.options.openOnHover ) {
clearTimeout(_this.closeTimer);
e.type == 'mouseleave' ? _this.closeTimer = setTimeout(_close, _this.options.hoverIntentTimeout) : _open();
}
});
// Toggle open/close
$wrapper.on('click' + bindSufix, function(e) {
isOpen ? _close() : _open(e);
});
$input
.prop({
tabindex: tabindex,
disabled: false
})
.on('keypress' + bindSufix, _handleSystemKeys)
.on('keydown' + bindSufix, function(e) {
_handleSystemKeys(e);
// Clear search
clearTimeout(_this.resetStr);
_this.resetStr = setTimeout(function() {
$input.val('');
}, _this.options.keySearchTimeout);
var key = e.keyCode || e.which;
// If it's a directional key
// 37 => Left
// 38 => Up
// 39 => Right
// 40 => Down
if ( key > 36 && key < 41 ) {
if ( !_this.options.allowWrap ) {
if ( (key < 39 && selected == 0) || (key > 38 && (selected + 1) == _this.items.length) ) {
return;
}
}
_select(_utils[(key < 39 ? 'previous' : 'next') + 'EnabledItem'](_this.items, selected));
}
})
.on('focusin' + bindSufix, function(e) {
isOpen || _open(e);
})
.on('oninput' in $input[0] ? 'input' : 'keyup', function() {
if ( $input.val().length ) {
// Search in select options
$.each(_this.items, function(i, elm) {
if ( RegExp('^' + $input.val(), 'i').test(elm.slug) && !elm.disabled ) {
_select(i);
return false;
}
});
}
});
$original.prop('tabindex', false);
// Remove styles from items box
// Fix incorrect height when refreshed is triggered with fewer options
$li = $('li', $items.removeAttr('style')).on({
// Prevent
blur on Chrome
mousedown: function(e) {
e.preventDefault();
e.stopPropagation();
},
click: function() {
// The second parameter is to close the box after click
_select($(this).data('index'), true);
// Chrome doesn't close options box if select is wrapped with a label
// We need to 'return false' to avoid that
return false;
}
}).filter('[data-index]');
} else {
$outerWrapper.addClass(_this.classes.disabled);
$input.prop('disabled', true);
}
_utils.triggerCallback('Init', _this);
}
function _refresh() {
_utils.triggerCallback('Refresh', _this);
_populate();
}
// Behavior when system keys is pressed
function _handleSystemKeys(e) {
var key = e.keyCode || e.which;
if ( key == 13 ) {
e.preventDefault();
}
// Tab / Enter / ESC
if ( /^(9|13|27)$/.test(key) ) {
e.stopPropagation();
_select(selected, true);
}
}
// Set options box width/height
function _calculateOptionsDimensions() {
// Calculate options box height
// Set a temporary class on the hidden parent of the element
var hiddenChildren = $items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow),
maxHeight = _this.options.maxHeight,
itemsWidth = $items.outerWidth(),
wrapperWidth = $wrapper.outerWidth() - (itemsWidth - $items.width());
// Set the dimensions, minimum is wrapper width, expand for long items if option is true
if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth )
finalWidth = wrapperWidth;
else {
// Make sure the scrollbar width is included
$items.css('overflow', 'scroll');
// Set a really long width for $outerWrapper
$outerWrapper.width(9e4);
finalWidth = $items.width();
// Set scroll bar to auto
$items.css('overflow', '');
$outerWrapper.width('');
}
$items.width(finalWidth).height() > maxHeight && $items.height(maxHeight);
// Remove the temporary class
hiddenChildren.removeClass(_this.classes.tempshow);
}
// Open the select options box
function _open(e) {
_utils.triggerCallback('BeforeOpen', _this);
if ( e ) {
e.preventDefault();
e.stopPropagation();
}
if ( isEnabled ) {
_calculateOptionsDimensions();
// Find any other opened instances of select and close it
$('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close');
isOpen = true;
itemsHeight = $items.outerHeight();
itemsInnerHeight = $items.height();
// Toggle options box visibility
$outerWrapper.addClass(_this.classes.open);
// Give dummy input focus
$input.val('');
e && e.type !== 'focusin' && $input.focus();
$doc.on('click' + bindSufix, _close).on('scroll' + bindSufix, _isInViewport);
_isInViewport();
// Prevent window scroll when using mouse wheel inside items box
if ( _this.options.preventWindowScroll ) {
$doc.on('mousewheel' + bindSufix + ' DOMMouseScroll' + bindSufix, '.' + _this.classes.scroll, function(e) {
var orgEvent = e.originalEvent,
scrollTop = $(this).scrollTop(),
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 - itemsInnerHeight) && deltaY < 0 || scrollTop == 0 && deltaY > 0 ) {
e.preventDefault();
}
});
}
_detectItemVisibility(selected);
_utils.triggerCallback('Open', _this);
}
}
// Detect is the options box is inside the window
function _isInViewport() {
var scrollTop = $win.scrollTop();
var winHeight = $win.height();
var uiPosX = $outerWrapper.offset().top;
var uiHeight = $outerWrapper.outerHeight();
var fitsDown = (uiPosX + uiHeight + itemsHeight) <= (scrollTop + winHeight);
var fitsAbove = (uiPosX - 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;
$outerWrapper.toggleClass(_this.classes.above, renderAbove);
}
// Close the select options box
function _close() {
_utils.triggerCallback('BeforeClose', _this);
if ( currValue != selected ) {
_utils.triggerCallback('BeforeChange', _this);
var text = _this.items[selected].text;
// Apply changed value to original select
$original
.prop('selectedIndex', currValue = selected)
.data('value', text);
// Change label text
$label.html(
$.isFunction(labelBuilder) ? labelBuilder(_this.items[selected]) : _utils.format(labelBuilder, _this.items[selected])
)
_utils.triggerCallback('Change', _this);
}
// Remove custom events on document
$doc.off(bindSufix);
// Remove visible class to hide options box
$outerWrapper.removeClass(_this.classes.open);
isOpen = false;
_utils.triggerCallback('Close', _this);
}
// Select option
function _select(index, close) {
// Parameter index is required
if ( index == undefined ) {
return;
}
// If element is disabled, can't select it
if ( !_this.items[index].disabled ) {
// If 'close' is false (default), the options box won't close after
// each selected item, this is necessary for keyboard navigation
$li
.removeClass('selected')
.eq(selected = index)
.addClass('selected');
_detectItemVisibility(index);
close && _close();
}
}
// Detect if currently selected option is visible and scroll the options box to show it
function _detectItemVisibility(index) {
var liHeight = $li.eq(index).outerHeight(),
liTop = $li[index].offsetTop,
itemsScrollTop = $itemsScroll.scrollTop(),
scrollT = liTop + liHeight * 2;
$itemsScroll.scrollTop(
scrollT > itemsScrollTop + itemsHeight ? scrollT - itemsHeight :
liTop - liHeight < itemsScrollTop ? liTop - liHeight :
itemsScrollTop
);
}
// Unbind and remove
function _destroy(preserveData) {
if ( isEnabled ) {
$items.add($wrapper).add($input).remove();
!preserveData && $original.removeData(pluginName).removeData('value');
$original.prop('tabindex', tabindex).off(bindSufix).off(eventTriggers).unwrap().unwrap();
isEnabled = false;
}
}
_init(opts);
};
// 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 )
(''+args === args && data[args]) ? data[args]() : data.init(args);
else
$.data(this, pluginName, new Selectric(this, args));
});
};
$.fn[pluginName].hooks = hooks;
}));