UNPKG

jquery-selectme

Version:

jQuery-plugin to replace large select elements with a neat and searchable solution.

526 lines (473 loc) 16 kB
/** * jQuery-plugin to replace large <select> elements with a neat and searchable solution. * * @Version: 0.2.0 * @Author: Benjamin Schueller <benjamin.schueller@gmail.com> * @License: MIT * * Usage: * $('select').selectMe(); * * $('select').selectMe({ * cssFile: '../lib/jquery.selectMe.css', * width: '100%', * columnCount: 3, * search: true, * locale: 'en', * localeResource: { * 'en': { * none: 'None selected', * from: 'from', * search: 'Search', * selectAll: 'Select all', * unselectAll: 'Unselect all', * showSelected: 'Show selected', * } * }, * // Callbacks * onLoad: function( element ) {}, * onDropdownOpen: function( element ) {}, * onDropdownClose: function( element ) {}, * onSearch: function( element ) {}, * onOptionValueChanged: function( element ) {}, * }); * * $.fn.selectMe.getSummaryText = function(selectedOptionsArray, allOptionsCount, messageSource) { * return 'your individual placeholder text'; // something like '1 from 10' or 'SelectedTextOne, SelectedTextTwo, ...' * } **/ (function ( $ ) { /* * Plugin definition. */ $.fn.selectMe = function( options ) { // Extend our default options with those provided. // Note that the first argument to extend is an empty // object – this is to keep from overriding our "defaults" object. var opts = $.extend({}, $.fn.selectMe.defaults, options ); // Add our css file to head if ($('head > link[href="' + opts.cssFile + '"]').length == 0) { $('head').append('<link rel="stylesheet" type="text/css" href="' + opts.cssFile + '" /> '); } // Our plugin implementation code goes here. return this.each(function() { var selectMe = new SelectMe(this, opts); }); }; /* * Plugin defaults – added as a property on our plugin function. */ $.fn.selectMe.defaults = { cssFile: '../lib/jquery.selectMe.css', width: '100%', columnCount: 1, search: true, locale: 'en', localeResource: { 'en': { none: 'None selected', from: 'from', search: 'Search', selectAll: 'Select all', unselectAll: 'Unselect all', showSelected: 'Show selected', }, 'de': { none: 'Keine ausgewählt', from: 'von', search: 'Suche', selectAll: 'Alle auswählen', unselectAll: 'Alle abwählen', showSelected: 'Nur ausgewählte', } }, // Callbacks onLoad: function element ( element ) {}, onDropdownOpen: function( element ) {}, onDropdownClose: function( element ) {}, onSearch: function( element ) {}, onOptionValueChanged: function( element ) {}, } /* * This function is to create the text in the placeholder. * It may be replaced with an own implementation. */ $.fn.selectMe.getSummaryText = function(selectedOptionsArray, allOptionsCount, messageSource, isMultiple) { if (isMultiple) { var selectedOptionsCount = selectedOptionsArray.length; if (selectedOptionsCount > 0) { return selectedOptionsCount + ' ' + messageSource.from + ' ' + allOptionsCount; } return messageSource.none; } else { return selectedOptionsArray[0]; } } /* * Constructor of a select element which should be replaced */ function SelectMe(element, options) { if (element.nodeName == 'SELECT') { this.$select = $(element).hide(); this.$wrapper = null; this.$placeholder = null; this.$dropdown = null; this.$search = null; this.$options = null; this.$selectableElements = $([]); this.options = options; this._init(); this.options.onLoad( this.$wrapper ); } } /* * Basic initialize function. * It calls the sub initialize functions and do some basics. */ SelectMe.prototype._init = function() { var self = this; self._initWrapper(); $(document).on('click.ac-hideoptions', function( event ) { if( !$(event.target).closest(self.$wrapper).length ) { self.$wrapper.find('> .selectme-dropdown:visible').each(function(){ self.closeDropdown(); }); } }); } /* * Create wrapper for our new select box */ SelectMe.prototype._initWrapper = function() { var self = this; self.$wrapper = $('<div class="selectme-wrapper"></div>').insertAfter(self.$select); self.$wrapper.outerWidth(self.options.width); self.$wrapper.keydown(function(e) { var keycode = e.keyCode; if (keycode == 27 || keycode == 9) { // ESC || TAB self.closeDropdown(); self.$placeholder.focus(); } }) self._initPlaceholder(); self._initDropdown(); } /* * Render placeholder */ SelectMe.prototype._initPlaceholder = function() { var self = this; self.$placeholder = $('<button type = "button"></button>').appendTo(self.$wrapper); self._updatePlaceholderText(); self.$placeholder.click(function() { if (self.$dropdown.is(':visible')) { self.closeDropdown(); } else { self.openDropdown(); } }); } /* * Render dropdown element */ SelectMe.prototype._initDropdown = function() { var self = this; self.$dropdown = $('<div class="selectme-dropdown"></div>').appendTo(self.$wrapper); self._initSearch(); self._initShortcuts(); self._initOptions(); self._initOptionSelection(); } /* * Render shortcuts */ SelectMe.prototype._initShortcuts = function() { var self = this; if (self.isMultiple()) { var $shortcuts = $('<div class="selectme-shortcuts"></div>').appendTo(self.$dropdown); self._registerKeydownForFocusSearchField($shortcuts); var $selectall = $('<a href="#">' + self.getMessageSource().selectAll + '</a>').appendTo($shortcuts); $selectall.click(function(e) { e.preventDefault(); self.$options.find('li:not(.optgroup):visible').each(function(){ $(this).find('label > input').prop('checked', true).trigger("change"); }); }); var $unselectall = $('<a href="#">' + self.getMessageSource().unselectAll + '</a>').appendTo($shortcuts); $unselectall.click(function(e) { e.preventDefault(); self.$options.find('li:not(.optgroup):visible').each(function(){ $(this).find('label > input').prop('checked', false).trigger("change"); }); }); var $showSelected = $('<a href="#">' + self.getMessageSource().showSelected + '</a>').appendTo($shortcuts); $showSelected.click(function(e) { e.preventDefault(); self.$options.find('li:not(.optgroup)').each(function(){ var $option = $(this).find('input'); if ($option.is(':checked')) { $(this).show(); var $optGroup = $(this).closest('.optgroup'); if ($optGroup) { $optGroup.show(); } } else { $(this).hide(); var $optGroup = $(this).closest('.optgroup'); if ($optGroup && $optGroup.find('li:visible').length == 0) { $optGroup.hide(); } } }); }); } } /* * Render search field */ SelectMe.prototype._initSearch = function() { var self = this; if (self.options.search) { var $searchWrapper = $('<div class="selectme-search"></div>').appendTo(self.$dropdown); self.$search = $('<input type="text" value="" placeholder="' + self.getMessageSource().search + '" />').appendTo($searchWrapper); self.$search.focus(function() { if (!self.$search.is(':visible')) { self.openDropdown(); } }); self.$search.on('keyup', function(){ self.options.onSearch( this ); // search non optgroup li's self.$options.find('li:not(.optgroup)').each(function(){ var optText = $(this).text(); // show option if string exists if( optText.toLowerCase().indexOf( self.$search.val().toLowerCase() ) > -1 ) { $(this).show(); var $optGroup = $(this).closest('.optgroup'); if ($optGroup) { $optGroup.show(); } } // don't hide selected items else { $(this).hide(); var $optGroup = $(this).closest('.optgroup'); if ($optGroup && $optGroup.find('li:visible').length == 0) { $optGroup.hide(); } } }); }); } } /* * Render options in our select box */ SelectMe.prototype._initOptions = function() { var self = this; self.$options = $('<ul></ul>').appendTo(self.$dropdown); self.$options.css({ 'column-count' : self.options.columnCount, 'column-gap' : 0, '-webkit-column-count': self.options.columnCount, '-webkit-column-gap' : 0, '-moz-column-count' : self.options.columnCount, '-moz-column-gap' : 0 }); var options = self._resolveOptions(self.$select); var fieldName = self.randomString(); self._insertOptions(self.$options, options, fieldName); self._registerKeydownForFocusSearchField(self.$options); } /* * Make options selectable via arrows up and down */ SelectMe.prototype._initOptionSelection = function() { var self = this; self.$wrapper.keydown(function(e) { var $selected = $(this).find('.selectme-marked'); if (e.keyCode == 38) { // up self.$options.find('li').removeClass('selectme-marked') if ($selected.length == 0) { $selected = self.$selectableElements.filter( ':visible' ).last(); } else if ($selected.is(self.$selectableElements.filter( ':visible' ).first())) { $selected = self.$selectableElements.filter( ':visible' ).last(); } else { var index = self.$selectableElements.filter(':visible').index($selected); $selected = self.$selectableElements.filter(':visible').eq(index - 1); } $selected.addClass('selectme-marked'); $selected.find('label').focus(); e.preventDefault(); } else if (e.keyCode == 40) { // down self.$options.find('li').removeClass('selectme-marked'); if ($selected.length == 0) { $selected = self.$selectableElements.filter( ':visible' ).first(); } else if ($selected.is(self.$selectableElements.filter( ':visible' ).last())) { $selected = self.$selectableElements.filter( ':visible' ).first(); } else { var index = self.$selectableElements.filter(':visible').index($selected); $selected = self.$selectableElements.filter(':visible').eq(index + 1); } $selected.addClass('selectme-marked'); $selected.find('label').focus(); e.preventDefault(); } else if (e.keyCode == 13) { // enter if (self.$dropdown.is(':visible')) { self.closeDropdown(); self.$placeholder.focus(); e.preventDefault(); } } }); } /* * Resolve options from this element */ SelectMe.prototype._resolveOptions = function($element) { var self = this; var options = []; $element.children().each(function(){ if( this.nodeName == 'OPTION' ) { options.push({ name : $(this).text(), value : $(this).val(), checked : $(this).prop( 'selected' ) }); } if( this.nodeName == 'OPTGROUP' ) { options.push({ name : $(this).prop('label'), options : self._resolveOptions($(this)) }); } else { // bad option return true; } }); return options; } SelectMe.prototype._insertOptions = function( $element, options, fieldName ) { var self = this; for( var key in options ) { // Prevent prototype methods injected into options from being iterated over. if( !options.hasOwnProperty( key ) ) { continue; } var thisOption = options[ key ]; var $container = $('<li></li>'); if( thisOption.hasOwnProperty('value') ) { // option $container.text( thisOption.name ); var $thisCheckbox = (self.isMultiple()) ? $('<input type="checkbox" value="" title="" />') : $('<input type="radio" name="' + fieldName + '" value="" title="" />'); $thisCheckbox .val( thisOption.value ) .prop( 'title', thisOption.name ) .prop( 'checked', thisOption.checked ) .change(function() { self.$select.find('option[value="' + $(this).val() + '"]').prop('selected', $(this).prop('checked')); self._updatePlaceholderText(); self.options.onOptionValueChanged(this); }); $container.prepend( $thisCheckbox ); var $label = $('<label></label>'); $label.prop('title', thisOption.name); $label.click(function() { self.$dropdown.find('li').removeClass('selectme-marked'); $(this).parent().addClass('selectme-marked'); }); $container.wrapInner( $label ); $element.append( $container ); self.$selectableElements = self.$selectableElements.add($container); } else { // optgroup var $label = $('<label>' + thisOption.name + '</label>'); $label.prop('title', thisOption.name); $container.addClass('optgroup'); $container.append( $label ); var $innerOptions = $('<ul></ul>').appendTo($container); $element.append( $container ); self._insertOptions($innerOptions, thisOption.options, fieldName); } } } /* * Keydown event for jumping to search field if a valid key is pressed */ SelectMe.prototype._registerKeydownForFocusSearchField = function(element) { var self = this; if (self.options.search) { element.keydown(function(e) { var keycode = e.keyCode; var valid = (keycode == 8) || // backspace (keycode > 47 && keycode < 58) || // number keys (keycode > 64 && keycode < 91) || // letter keys (keycode > 95 && keycode < 112) || // numpad keys (keycode > 185 && keycode < 193) || // ;=,-./` (in order) (keycode > 218 && keycode < 223); // [\]' (in order) if (valid) { self.$dropdown.find('> ul > li').removeClass('selectme-marked'); self.$search.val(''); self.$search.focus(); } }); } } /* * Update text in the placeholder */ SelectMe.prototype._updatePlaceholderText = function() { var self = this; // get selected options var selOpts = []; self.$select.find('option:selected').each(function(){ selOpts.push( $(this).text() ); }); // update placeholder text var allCount = self.$select.find('option').length; var placeholderText = $.fn.selectMe.getSummaryText(selOpts, allCount, self.getMessageSource(), self.isMultiple()); self.$placeholder.text(placeholderText); } /* * Open our dropdown element */ SelectMe.prototype.openDropdown = function() { var self = this; self.options.onDropdownOpen( self.$wrapper ); self.$dropdown.show(); if (self.options.search) { self.$search.focus(); } } /* * Close our dropdown element */ SelectMe.prototype.closeDropdown = function() { var self = this; self.options.onDropdownClose( self.$wrapper ); self.$dropdown.hide(); } /* * Return message source for the configured language */ SelectMe.prototype.getMessageSource = function() { var self = this; var locale = self.options.locale; var messageSource = self.options.localeResource[locale]; return messageSource; } /* * Check if our select box has multiple selection active */ SelectMe.prototype.isMultiple = function() { return this.$select.prop('multiple'); } /* * Generate a random string with five digits */ SelectMe.prototype.randomString = function() { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for( var i=0; i < 5; i++ ) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } }( jQuery ));