/*jshint asi:true, expr:true */
/**
* Plugin Name: Combo Select
* Author : Vinay@Pebbleroad
* Date: 23/11/2014
* Description:
* Converts a select box into a searchable and keyboard friendly interface. Fallbacks to native select on mobile and tablets
*/
// Expose plugin as an AMD module if AMD loader is present:
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object' && typeof require === 'function') {
// Browserify
factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function ( $, undefined ) {
var pluginName = "comboSelect",
dataKey = 'comboselect';
var defaults = {
comboClass : 'combo-select',
comboArrowClass : 'combo-arrow',
comboDropDownClass : 'combo-dropdown',
inputClass : 'combo-input text-input',
disabledClass : 'option-disabled',
hoverClass : 'option-hover',
selectedClass : 'option-selected',
markerClass : 'combo-marker',
themeClass : '',
maxHeight : 200,
extendStyle : true,
focusInput : true
};
/**
* Utility functions
*/
var keys = {
ESC: 27,
TAB: 9,
RETURN: 13,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
ENTER: 13,
SHIFT: 16
},
isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
/**
* Constructor
* @param {[Node]} element [Select element]
* @param {[Object]} options [Option object]
*/
function Plugin ( element, options ) {
/* Name of the plugin */
this._name = pluginName;
/* Reverse lookup */
this.el = element
/* Element */
this.$el = $(element)
/* If multiple select: stop */
if(this.$el.prop('multiple')) return;
/* Settings */
this.settings = $.extend( {}, defaults, options, this.$el.data() );
/* Defaults */
this._defaults = defaults;
/* Options */
this.$options = this.$el.find('option, optgroup')
/* Initialize */
this.init();
/* Instances */
$.fn[ pluginName ].instances.push(this);
}
$.extend(Plugin.prototype, {
init: function () {
/* Construct the comboselect */
this._construct();
/* Add event bindings */
this._events();
},
_construct: function(){
var self = this
/**
* Add negative TabIndex to `select`
* Preserves previous tabindex
*/
this.$el.data('plugin_'+ dataKey + '_tabindex', this.$el.prop('tabindex'))
/* Add a tab index for desktop browsers */
!isMobile && this.$el.prop("tabIndex", -1)
/**
* Wrap the Select
*/
this.$container = this.$el.wrapAll('<div class="' + this.settings.comboClass + ' '+ this.settings.themeClass + '" />').parent();
/**
* Check if select has a width attribute
*/
if(this.settings.extendStyle && this.$el.attr('style')){
this.$container.attr('style', this.$el.attr("style"))
}
/**
* Append dropdown arrow
*/
this.$arrow = $('<div class="'+ this.settings.comboArrowClass+ '" />').appendTo(this.$container)
/**
* Append dropdown
*/
this.$dropdown = $('<ul class="'+this.settings.comboDropDownClass+'" />').appendTo(this.$container)
/**
* Create dropdown options
*/
var o = '', k = 0, p = '';
this.selectedIndex = this.$el.prop('selectedIndex')
this.$options.each(function(i, e){
if(e.nodeName.toLowerCase() == 'optgroup'){
return o+='<li class="option-group">'+this.label+'</li>'
}
if(!e.value) p = e.innerHTML
o+='<li class="'+(this.disabled? self.settings.disabledClass : "option-item") + ' ' +(k == self.selectedIndex? self.settings.selectedClass : '')+ '" data-index="'+(k)+'" data-value="'+this.value+'">'+ (this.innerHTML) + '</li>'
k++;
})
this.$dropdown.html(o)
/**
* Items
*/
this.$items = this.$dropdown.children();
/**
* Append Input
*/
this.$input = $('<input type="text"' + (isMobile? 'tabindex="-1"': '') + ' placeholder="'+p+'" class="'+ this.settings.inputClass + '">').appendTo(this.$container)
/* Update input text */
this._updateInput()
},
_events: function(){
/* Input: focus */
this.$container.on('focus.input', 'input', $.proxy(this._focus, this))
/**
* Input: mouseup
* For input select() event to function correctly
*/
this.$container.on('mouseup.input', 'input', function(e){
e.preventDefault()
})
/* Input: blur */
this.$container.on('blur.input', 'input', $.proxy(this._blur, this))
/* Select: change */
this.$el.on('change.select', $.proxy(this._change, this))
/* Select: focus */
this.$el.on('focus.select', $.proxy(this._focus, this))
/* Select: blur */
this.$el.on('blur.select', $.proxy(this._blurSelect, this))
/* Dropdown Arrow: click */
this.$container.on('click.arrow', '.'+this.settings.comboArrowClass , $.proxy(this._toggle, this))
/* Dropdown: close */
this.$container.on('comboselect:close', $.proxy(this._close, this))
/* Dropdown: open */
this.$container.on('comboselect:open', $.proxy(this._open, this))
/* HTML Click */
$('html').off('click.comboselect').on('click.comboselect', function(){
$.each($.fn[ pluginName ].instances, function(i, plugin){
plugin.$container.trigger('comboselect:close')
})
});
/* Stop `event:click` bubbling */
this.$container.on('click.comboselect', function(e){
e.stopPropagation();
})
/* Input: keydown */
this.$container.on('keydown', 'input', $.proxy(this._keydown, this))
/* Input: keyup */
this.$container.on('keyup', 'input', $.proxy(this._keyup, this))
/* Dropdown item: click */
this.$container.on('click.item', '.option-item', $.proxy(this._select, this))
},
_keydown: function(event){
switch(event.which){
case keys.UP:
this._move('up', event)
break;
case keys.DOWN:
this._move('down', event)
break;
case keys.TAB:
this._enter(event)
break;
case keys.RIGHT:
this._autofill(event);
break;
case keys.ENTER:
this._enter(event);
break;
default:
break;
}
},
_keyup: function(event){
switch(event.which){
case keys.ESC:
this.$container.trigger('comboselect:close')
break;
case keys.ENTER:
case keys.UP:
case keys.DOWN:
case keys.LEFT:
case keys.RIGHT:
case keys.TAB:
case keys.SHIFT:
break;
default:
this._filter(event.target.value)
break;
}
},
_enter: function(event){
var item = this._getHovered()
item.length && this._select(item);
/* Check if it enter key */
if(event && event.which == keys.ENTER){
if(!item.length) {
/* Check if its illegal value */
this._blur();
return true;
}
event.preventDefault();
}
},
_move: function(dir){
var items = this._getVisible(),
current = this._getHovered(),
index = current.prevAll('.option-item').filter(':visible').length,
total = items.length
switch(dir){
case 'up':
index--;
(index < 0) && (index = (total - 1));
break;
case 'down':
index++;
(index >= total) && (index = 0);
break;
}
items
.removeClass(this.settings.hoverClass)
.eq(index)
.addClass(this.settings.hoverClass)
if(!this.opened) this.$container.trigger('comboselect:open');
this._fixScroll()
},
_select: function(event){
var item = event.currentTarget? $(event.currentTarget) : $(event);
if(!item.length) return;
/**
* 1. get Index
*/
var index = item.data('index');
this._selectByIndex(index);
this.$container.trigger('comboselect:close')
},
_selectByIndex: function(index){
/**