/* * jQuery selectBox - A cosmetic, styleable replacement for SELECT elements * * Licensed under the MIT license: http://opensource.org/licenses/MIT * * v1.2.0 * * https://github.com/marcj/jquery-selectBox */ ;(function ($) { /** * SelectBox class. * * @param {HTMLElement|jQuery} select If it's a jQuery object, we use the first element. * @param {Object} options * @constructor */ var SelectBox = this.SelectBox = function (select, options) { if (select instanceof jQuery) { if (select.length > 0) { select = select[0]; } else { return; } } this.typeTimer = null; this.typeSearch = ''; this.isMac = navigator.platform.match(/mac/i); options = 'object' === typeof options ? options : {}; this.selectElement = select; // Disable for iOS devices (their native controls are more suitable for a touch device) if (!options.mobile && navigator.userAgent.match(/iPad|iPhone|Android|IEMobile|BlackBerry/i)) { return false; } // Element must be a select control if ('select' !== select.tagName.toLowerCase()) { return false; } this.init(options); } /** * @type {String} */ SelectBox.prototype.version = '1.2.0'; /** * @param {Object} options * * @returns {Boolean} */ SelectBox.prototype.init = function (options) { var select = $(this.selectElement); if (select.data('selectBox-control')) { return false; } var control = $('') , inline = select.attr('multiple') || parseInt(select.attr('size')) > 1 , settings = options || {} , tabIndex = parseInt(select.prop('tabindex')) || 0 , self = this; control .width(select.outerWidth()) .addClass(select.attr('class')) .attr('title', select.attr('title') || '') .attr('tabindex', tabIndex) .css('display', 'inline-block') .bind('focus.selectBox', function () { if (this !== document.activeElement && document.body !== document.activeElement) { $(document.activeElement).blur(); } if (control.hasClass('selectBox-active')) { return; } control.addClass('selectBox-active'); select.trigger('focus'); }) .bind('blur.selectBox', function () { if (!control.hasClass('selectBox-active')) { return; } control.removeClass('selectBox-active'); select.trigger('blur'); }); if (!$(window).data('selectBox-bindings')) { $(window) .data('selectBox-bindings', true) .bind('scroll.selectBox', this.hideMenus) .bind('resize.selectBox', this.hideMenus); } if (select.attr('disabled')) { control.addClass('selectBox-disabled'); } // Focus on control when label is clicked select.bind('click.selectBox', function (event) { control.focus(); event.preventDefault(); }); // Generate control if (inline) { // Inline controls options = this.getOptions('inline'); control .append(options) .data('selectBox-options', options).addClass('selectBox-inline selectBox-menuShowing') .bind('keydown.selectBox', function (event) { self.handleKeyDown(event); }) .bind('keypress.selectBox',function (event) { self.handleKeyPress(event); }) .bind('mousedown.selectBox',function (event) { if (1 !== event.which) { return; } if ($(event.target).is('A.selectBox-inline')) { event.preventDefault(); } if (!control.hasClass('selectBox-focus')) { control.focus(); } }) .insertAfter(select); // Auto-height based on size attribute if (!select[0].style.height) { var size = select.attr('size') ? parseInt(select.attr('size')) : 5; // Draw a dummy control off-screen, measure, and remove it var tmp = control .clone() .removeAttr('id') .css({ position: 'absolute', top: '-9999em' }) .show() .appendTo('body'); tmp.find('.selectBox-options').html('
  • \u00A0
  • '); var optionHeight = parseInt(tmp.find('.selectBox-options A:first').html(' ').outerHeight()); tmp.remove(); control.height(optionHeight * size); } this.disableSelection(control); } else { // Dropdown controls var label = $(''), arrow = $(''); // Update label label.attr('class', this.getLabelClass()).text(this.getLabelText()); options = this.getOptions('dropdown'); options.appendTo('BODY'); control .data('selectBox-options', options) .addClass('selectBox-dropdown') .append(label) .append(arrow) .bind('mousedown.selectBox', function (event) { if (1 === event.which) { if (control.hasClass('selectBox-menuShowing')) { self.hideMenus(); } else { event.stopPropagation(); // Webkit fix to prevent premature selection of options options .data('selectBox-down-at-x', event.screenX) .data('selectBox-down-at-y', event.screenY); self.showMenu(); } } }) .bind('keydown.selectBox', function (event) { self.handleKeyDown(event); }) .bind('keypress.selectBox', function (event) { self.handleKeyPress(event); }) .bind('open.selectBox',function (event, triggerData) { if (triggerData && triggerData._selectBox === true) { return; } self.showMenu(); }) .bind('close.selectBox', function (event, triggerData) { if (triggerData && triggerData._selectBox === true) { return; } self.hideMenus(); }) .insertAfter(select); // Set label width var labelWidth = control.width() - arrow.outerWidth() - parseInt(label.css('paddingLeft')) || 0 - parseInt(label.css('paddingRight')) || 0; label.width(labelWidth); this.disableSelection(control); } // Store data for later use and show the control select .addClass('selectBox') .data('selectBox-control', control) .data('selectBox-settings', settings) .hide(); }; /** * @param {String} type 'inline'|'dropdown' * @returns {jQuery} */ SelectBox.prototype.getOptions = function (type) { var options; var select = $(this.selectElement); var self = this; // Private function to handle recursion in the getOptions function. var _getOptions = function (select, options) { // Loop through the set in order of element children. select.children('OPTION, OPTGROUP').each(function () { // If the element is an option, add it to the list. if ($(this).is('OPTION')) { // Check for a value in the option found. if ($(this).length > 0) { // Create an option form the found element. self.generateOptions($(this), options); } else { // No option information found, so add an empty. options.append('
  • \u00A0
  • '); } } else { // If the element is an option group, add the group and call this function on it. var optgroup = $('
  • '); optgroup.text($(this).attr('label')); options.append(optgroup); options = _getOptions($(this), options); } }); // Return the built strin return options; }; switch (type) { case 'inline': options = $('
      '); options = _getOptions(select, options); options .find('A') .bind('mouseover.selectBox', function (event) { self.addHover($(this).parent()); }) .bind('mouseout.selectBox',function (event) { self.removeHover($(this).parent()); }) .bind('mousedown.selectBox',function (event) { if (1 !== event.which) { return } event.preventDefault(); // Prevent options from being "dragged" if (!select.selectBox('control').hasClass('selectBox-active')) { select.selectBox('control').focus(); } }) .bind('mouseup.selectBox', function (event) { if (1 !== event.which) { return; } self.hideMenus(); self.selectOption($(this).parent(), event); }); this.disableSelection(options); return options; case 'dropdown': options = $('
        '); options = _getOptions(select, options); options .data('selectBox-select', select) .css('display', 'none') .appendTo('BODY') .find('A') .bind('mousedown.selectBox', function (event) { if (event.which === 1) { event.preventDefault(); // Prevent options from being "dragged" if (event.screenX === options.data('selectBox-down-at-x') && event.screenY === options.data('selectBox-down-at-y')) { options.removeData('selectBox-down-at-x').removeData('selectBox-down-at-y'); self.hideMenus(); } } }) .bind('mouseup.selectBox', function (event) { if (1 !== event.which) { return; } if (event.screenX === options.data('selectBox-down-at-x') && event.screenY === options.data('selectBox-down-at-y')) { return; } else { options.removeData('selectBox-down-at-x').removeData('selectBox-down-at-y'); } self.selectOption($(this).parent()); self.hideMenus(); }) .bind('mouseover.selectBox', function (event) { self.addHover($(this).parent()); }) .bind('mouseout.selectBox', function (event) { self.removeHover($(this).parent()); }); // Inherit classes for dropdown menu var classes = select.attr('class') || ''; if ('' !== classes) { classes = classes.split(' '); for (var i in classes) { options.addClass(classes[i] + '-selectBox-dropdown-menu'); } } this.disableSelection(options); return options; } }; /** * Returns the current class of the selected option. * * @returns {String} */ SelectBox.prototype.getLabelClass = function () { var selected = $(this.selectElement).find('OPTION:selected'); return ('selectBox-label ' + (selected.attr('class') || '')).replace(/\s+$/, ''); }; /** * Returns the current label of the selected option. * * @returns {String} */ SelectBox.prototype.getLabelText = function () { var selected = $(this.selectElement).find('OPTION:selected'); return selected.text() || '\u00A0'; }; /** * Sets the label. * This method uses the getLabelClass() and getLabelText() methods. */ SelectBox.prototype.setLabel = function () { var select = $(this.selectElement); var control = select.data('selectBox-control'); if (!control) { return; } control .find('.selectBox-label') .attr('class', this.getLabelClass()) .text(this.getLabelText()); }; /** * Destroys the SelectBox instance and shows the origin select element. * */ SelectBox.prototype.destroy = function () { var select = $(this.selectElement); var control = select.data('selectBox-control'); if (!control) { return; } var options = control.data('selectBox-options'); options.remove(); control.remove(); select .removeClass('selectBox') .removeData('selectBox-control') .data('selectBox-control', null) .removeData('selectBox-settings') .data('selectBox-settings', null) .show(); }; /** * Refreshes the option elements. */ SelectBox.prototype.refresh = function () { var select = $(this.selectElement), control = select.data('selectBox-control'), dropdown = control.hasClass('selectBox-dropdown'), menuOpened = control.hasClass('selectBox-menuShowing'); select.selectBox('options', select.html()); // Restore opened dropdown state (original menu was trashed) if (dropdown && menuOpened) { this.showMenu(); } }; /** * Shows the dropdown menu. */ SelectBox.prototype.showMenu = function () { var self = this , select = $(this.selectElement) , control = select.data('selectBox-control') , settings = select.data('selectBox-settings') , options = control.data('selectBox-options'); if (control.hasClass('selectBox-disabled')) { return false; } this.hideMenus(); var borderBottomWidth = parseInt(control.css('borderBottomWidth')) || 0; // Menu position options .width(control.innerWidth()) .css({ top: control.offset().top + control.outerHeight() - borderBottomWidth, left: control.offset().left }); if (select.triggerHandler('beforeopen')) { return false; } var dispatchOpenEvent = function () { select.triggerHandler('open', { _selectBox: true }); }; // Show menu switch (settings.menuTransition) { case 'fade': options.fadeIn(settings.menuSpeed, dispatchOpenEvent); break; case 'slide': options.slideDown(settings.menuSpeed, dispatchOpenEvent); break; default: options.show(settings.menuSpeed, dispatchOpenEvent); break; } if (!settings.menuSpeed) { dispatchOpenEvent(); } // Center on selected option var li = options.find('.selectBox-selected:first'); this.keepOptionInView(li, true); this.addHover(li); control.addClass('selectBox-menuShowing'); $(document).bind('mousedown.selectBox', function (event) { if (1 === event.which) { if ($(event.target).parents().andSelf().hasClass('selectBox-options')) { return; } self.hideMenus(); } }); }; /** * Hides the menu of all instances. */ SelectBox.prototype.hideMenus = function () { if ($(".selectBox-dropdown-menu:visible").length === 0) { return; } $(document).unbind('mousedown.selectBox'); $(".selectBox-dropdown-menu").each(function () { var options = $(this) , select = options.data('selectBox-select') , control = select.data('selectBox-control') , settings = select.data('selectBox-settings'); if (select.triggerHandler('beforeclose')) { return false; } var dispatchCloseEvent = function () { select.triggerHandler('close', { _selectBox: true }); }; if (settings) { switch (settings.menuTransition) { case 'fade': options.fadeOut(settings.menuSpeed, dispatchCloseEvent); break; case 'slide': options.slideUp(settings.menuSpeed, dispatchCloseEvent); break; default: options.hide(settings.menuSpeed, dispatchCloseEvent); break; } if (!settings.menuSpeed) { dispatchCloseEvent(); } control.removeClass('selectBox-menuShowing'); } else { $(this).hide(); $(this).triggerHandler('close', { _selectBox: true }); $(this).removeClass('selectBox-menuShowing'); } }); }; /** * Selects an option. * * @param {HTMLElement} li * @param {DOMEvent} event * @returns {Boolean} */ SelectBox.prototype.selectOption = function (li, event) { var select = $(this.selectElement); li = $(li); var control = select.data('selectBox-control') , settings = select.data('selectBox-settings'); if (control.hasClass('selectBox-disabled')) { return false; } if (0 === li.length || li.hasClass('selectBox-disabled')) { return false; } if (select.attr('multiple')) { // If event.shiftKey is true, this will select all options between li and the last li selected if (event.shiftKey && control.data('selectBox-last-selected')) { li.toggleClass('selectBox-selected'); var affectedOptions; if (li.index() > control.data('selectBox-last-selected').index()) { affectedOptions = li .siblings() .slice(control.data('selectBox-last-selected').index(), li.index()); } else { affectedOptions = li .siblings() .slice(li.index(), control.data('selectBox-last-selected').index()); } affectedOptions = affectedOptions.not('.selectBox-optgroup, .selectBox-disabled'); if (li.hasClass('selectBox-selected')) { affectedOptions.addClass('selectBox-selected'); } else { affectedOptions.removeClass('selectBox-selected'); } } else if ((this.isMac && event.metaKey) || (!this.isMac && event.ctrlKey)) { li.toggleClass('selectBox-selected'); } else { li.siblings().removeClass('selectBox-selected'); li.addClass('selectBox-selected'); } } else { li.siblings().removeClass('selectBox-selected'); li.addClass('selectBox-selected'); } if (control.hasClass('selectBox-dropdown')) { control.find('.selectBox-label').text(li.text()); } // Update original control's value var i = 0, selection = []; if (select.attr('multiple')) { control.find('.selectBox-selected A').each(function () { selection[i++] = $(this).attr('rel'); }); } else { selection = li.find('A').attr('rel'); } // Remember most recently selected item control.data('selectBox-last-selected', li); // Change callback if (select.val() !== selection) { select.val(selection); this.setLabel(); select.trigger('change'); } return true; }; /** * Adds the hover class. * * @param {HTMLElement} li */ SelectBox.prototype.addHover = function (li) { li = $(li); var select = $(this.selectElement) , control = select.data('selectBox-control') , options = control.data('selectBox-options'); options.find('.selectBox-hover').removeClass('selectBox-hover'); li.addClass('selectBox-hover'); }; /** * Returns the original HTML select element. * * @returns {HTMLElement} */ SelectBox.prototype.getSelectElement = function () { return this.selectElement; }; /** * Remove the hover class. * * @param {HTMLElement} li */ SelectBox.prototype.removeHover = function (li) { li = $(li); var select = $(this.selectElement) , control = select.data('selectBox-control') , options = control.data('selectBox-options'); options.find('.selectBox-hover').removeClass('selectBox-hover'); }; /** * Checks if the widget is in the view. * * @param {jQuery} li * @param {Boolean} center */ SelectBox.prototype.keepOptionInView = function (li, center) { if (!li || li.length === 0) { return; } var select = $(this.selectElement) , control = select.data('selectBox-control') , options = control.data('selectBox-options') , scrollBox = control.hasClass('selectBox-dropdown') ? options : options.parent() , top = parseInt(li.offset().top -scrollBox.position().top) , bottom = parseInt(top + li.outerHeight()); if (center) { scrollBox.scrollTop(li.offset().top - scrollBox.offset().top + scrollBox.scrollTop() - (scrollBox.height() / 2)); } else { if (top < 0) { scrollBox.scrollTop(li.offset().top - scrollBox.offset().top + scrollBox.scrollTop()); } if (bottom > scrollBox.height()) { scrollBox.scrollTop((li.offset().top + li.outerHeight()) - scrollBox.offset().top + scrollBox.scrollTop() - scrollBox.height()); } } }; /** * Handles the keyDown event. * Handles open/close and arrow key functionality * * @param {DOMEvent} event */ SelectBox.prototype.handleKeyDown = function (event) { var select = $(this.selectElement) , control = select.data('selectBox-control') , options = control.data('selectBox-options') , settings = select.data('selectBox-settings') , totalOptions = 0, i = 0; if (control.hasClass('selectBox-disabled')) { return; } switch (event.keyCode) { case 8: // backspace event.preventDefault(); this.typeSearch = ''; break; case 9: // tab case 27: // esc this.hideMenus(); this.removeHover(); break; case 13: // enter if (control.hasClass('selectBox-menuShowing')) { this.selectOption(options.find('LI.selectBox-hover:first'), event); if (control.hasClass('selectBox-dropdown')) { this.hideMenus(); } } else { this.showMenu(); } break; case 38: // up case 37: // left event.preventDefault(); if (control.hasClass('selectBox-menuShowing')) { var prev = options.find('.selectBox-hover').prev('LI'); totalOptions = options.find('LI:not(.selectBox-optgroup)').length; i = 0; while (prev.length === 0 || prev.hasClass('selectBox-disabled') || prev.hasClass('selectBox-optgroup')) { prev = prev.prev('LI'); if (prev.length === 0) { if (settings.loopOptions) { prev = options.find('LI:last'); } else { prev = options.find('LI:first'); } } if (++i >= totalOptions) { break; } } this.addHover(prev); this.selectOption(prev, event); this.keepOptionInView(prev); } else { this.showMenu(); } break; case 40: // down case 39: // right event.preventDefault(); if (control.hasClass('selectBox-menuShowing')) { var next = options.find('.selectBox-hover').next('LI'); totalOptions = options.find('LI:not(.selectBox-optgroup)').length; i = 0; while (0 === next.length || next.hasClass('selectBox-disabled') || next.hasClass('selectBox-optgroup')) { next = next.next('LI'); if (next.length === 0) { if (settings.loopOptions) { next = options.find('LI:first'); } else { next = options.find('LI:last'); } } if (++i >= totalOptions) { break; } } this.addHover(next); this.selectOption(next, event); this.keepOptionInView(next); } else { this.showMenu(); } break; } }; /** * Handles the keyPress event. * Handles type-to-find functionality * * @param {DOMEvent} event */ SelectBox.prototype.handleKeyPress = function (event) { var select = $(this.selectElement) , control = select.data('selectBox-control') , options = control.data('selectBox-options'); if (control.hasClass('selectBox-disabled')) { return; } switch (event.keyCode) { case 9: // tab case 27: // esc case 13: // enter case 38: // up case 37: // left case 40: // down case 39: // right // Don't interfere with the keydown event! break; default: // Type to find if (!control.hasClass('selectBox-menuShowing')) { this.showMenu(); } event.preventDefault(); clearTimeout(this.typeTimer); this.typeSearch += String.fromCharCode(event.charCode || event.keyCode); options.find('A').each(function () { if ($(this).text().substr(0, this.typeSearch.length).toLowerCase() === this.typeSearch.toLowerCase()) { this.addHover($(this).parent()); this.selectOption($(this).parent(), event); this.keepOptionInView($(this).parent()); return false; } }); // Clear after a brief pause this.typeTimer = setTimeout(function () { this.typeSearch = ''; }, 1000); break; } }; /** * Enables the selectBox. */ SelectBox.prototype.enable = function () { var select = $(this.selectElement); select.prop('disabled', false); var control = select.data('selectBox-control'); if (!control) { return; } control.removeClass('selectBox-disabled'); }; /** * Disables the selectBox. */ SelectBox.prototype.disable = function () { var select = $(this.selectElement); select.prop('disabled', true); var control = select.data('selectBox-control'); if (!control) { return; } control.addClass('selectBox-disabled'); }; /** * Sets the current value. * * @param {String} value */ SelectBox.prototype.setValue = function (value) { var select = $(this.selectElement); select.val(value); value = select.val(); // IE9's select would be null if it was set with a non-exist options value if (null === value) { // So check it here and set it with the first option's value if possible value = select.children().first().val(); select.val(value); } var control = select.data('selectBox-control'); if (!control) { return; } var settings = select.data('selectBox-settings') , options = control.data('selectBox-options'); // Update label this.setLabel(); // Update control values options.find('.selectBox-selected').removeClass('selectBox-selected'); options.find('A').each(function () { if (typeof(value) === 'object') { for (var i = 0; i < value.length; i++) { if ($(this).attr('rel') == value[i]) { $(this).parent().addClass('selectBox-selected'); } } } else { if ($(this).attr('rel') == value) { $(this).parent().addClass('selectBox-selected'); } } }); if (settings.change) { settings.change.call(select); } }; /** * Sets the option elements. * * @param {String|Object} options */ SelectBox.prototype.setOptions = function (options) { var select = $(this.selectElement) , control = select.data('selectBox-control') , settings = select.data('selectBox-settings') , type; switch (typeof(options)) { case 'string': select.html(options); break; case 'object': select.html(''); for (var i in options) { if (options[i] === null) { continue; } if (typeof(options[i]) === 'object') { var optgroup = $(''); for (var j in options[i]) { optgroup.append(''); } select.append(optgroup); } else { var option = $(''); select.append(option); } } break; } if (!control) { return; } // Remove old options control.data('selectBox-options').remove(); // Generate new options type = control.hasClass('selectBox-dropdown') ? 'dropdown' : 'inline'; options = this.getOptions(type); control.data('selectBox-options', options); switch (type) { case 'inline': control.append(options); break; case 'dropdown': // Update label this.setLabel(); $("BODY").append(options); break; } }; /** * Disables the selection. * * @param {*} selector */ SelectBox.prototype.disableSelection = function (selector) { $(selector).css('MozUserSelect', 'none').bind('selectstart', function (event) { event.preventDefault(); }); }; /** * Generates the options. * * @param {jQuery} self * @param {jQuery} options */ SelectBox.prototype.generateOptions = function (self, options) { var li = $('
      • '), a = $(''); li.addClass(self.attr('class')); li.data(self.data()); a.attr('rel', self.val()).text(self.text()); li.append(a); if (self.attr('disabled')) { li.addClass('selectBox-disabled'); } if (self.attr('selected')) { li.addClass('selectBox-selected'); } options.append(li); }; /** * Extends the jQuery.fn object. */ $.extend($.fn, { selectBox: function (method, options) { var selectBox; switch (method) { case 'control': return $(this).data('selectBox-control'); case 'settings': if (!options) { return $(this).data('selectBox-settings'); } $(this).each(function () { $(this).data('selectBox-settings', $.extend(true, $(this).data('selectBox-settings'), options)); }); break; case 'options': // Getter if (undefined === options) { return $(this).data('selectBox-control').data('selectBox-options'); } // Setter $(this).each(function () { if (selectBox = $(this).data('selectBox')) { selectBox.setOptions(options); } }); break; case 'value': // Empty string is a valid value if (undefined === options) { return $(this).val(); } $(this).each(function () { if (selectBox = $(this).data('selectBox')) { selectBox.setValue(options); } }); break; case 'refresh': $(this).each(function () { if (selectBox = $(this).data('selectBox')) { selectBox.refresh(); } }); break; case 'enable': $(this).each(function () { if (selectBox = $(this).data('selectBox')) { selectBox.enable(this); } }); break; case 'disable': $(this).each(function () { if (selectBox = $(this).data('selectBox')) { selectBox.disable(); } }); break; case 'destroy': $(this).each(function () { if (selectBox = $(this).data('selectBox')) { selectBox.destroy(); $(this).data('selectBox', null); } }); break; case 'instance': return $(this).data('selectBox'); default: $(this).each(function (idx, select) { if (!$(select).data('selectBox')) { $(select).data('selectBox', new SelectBox(select, method)); } }); break; } return $(this); } }); })(jQuery);