Thinking in programming

Things I like to talk about programming

betterSelects – Much better HTML select-elements

with 4 comments

Introduction: betterSelects is intended to “transform” the old-fashioned HTML select-elements in much more sophisticated and flexible controls (see a demo here and screen-shots below). The plugin is released under the Apache License version 2.

For me, there are some essential premises at the moment of writing an UI-Control:

  • Must offers important advantages over already existing related controls
  • Usability
  • Flexibility to customize
  • Performance
  • And for JavaScript controls, them must degrades successfully

Background: a normal HTML select element is too much rigid regarding the customization of its look and feel, as it does not support the coolest CSS effects (aka rounded corners, transparency, shadows, columns, animations, etc.). Thus, this plug-in was created to provide such features.

Currently supported features:

  • displaying items in columns
  • transparency
  • rounded corners
  • animations
  • shadows
  • customization of the expansion button
  • degrades successfully when JavaScript is not enabled
  • and in general, a full customization through CSS

Planned features for future versions:

  • navigation by using arrows keys
  • support for select-multiple
  • auto completion (useful?)
  • among other minor improvements

The usage is very straightforward; just include the provided CSS file and the JavaScript file, then:

$('select').betterSelects({
  zIndexMax: 99, // The first betterSelect control will use this value, it will be smaller for following controls
  expandTime: 100, // The time it takes to expand the options
  marginTop: 1, // Margin between the preview and the expanded options list
  itemsPerCol: 6 // Number of items per column. Use 0 for a single column
});

See the plugin in action here: http://betterselects.appspot.com/

The source is located on Google Code: http://code.google.com/p/betterselects/

A read-only copy of the code can be obtained from http://betterselects.googlecode.com/svn/trunk/

And at last but not at least, it would be great if you wish to do any kind of collaboration with this project, suggestions, corrections, comments, are very welcome. You can ask for commit access if you wish, of course!.

Screen-shots v0.5:

This slideshow requires JavaScript.

JavaScript sourcode v0.5:

/**
 * jQuery.betterSelects plugin v0.5
 * Date: 2010.10.09
 * Web: https://rogerpadilla.wordpress.com/betterSelects
 * Copyright 2010, Roger Padilla Camacho - rogerjose81@gmail.com
 * Licensed under the Apache License version 2
 */

/**
 * This plug-in is intended to "transform" the old-fashioned HTML select-elements in much more sophisticated
 * and flexible controls.
 * 
 * Background: a normal HTML select element is too much rigid regarding the customization of its look and feel,
 * as it does not support the coolest CSS effects (aka rounded corners, transparency, shadows, columns,
 * animations, etc.). Thus, to count with such features in that control is the purpose of this plug-in.
 * 
 * Currently supported features: displaying items in columns, transparency, rounded corners, animations,
 * shadows, customization of the expansion button, and in general, a full customization through CSS.
 * The plug-in degrades successfully when JavaScript is not enabled.
 
 * Planned features for future versions: navigation by using arrows keys, support for select-multiple,
 * auto completion (useful?), among other minor improvements.
 * 
 * The usage is very straightforward; just include the provided CSS file and the JavaScript file, then:
 * <code>$('select').betterSelects();</code>
 */
(function($) {

	/**
	 * Main function; used to initialize the plugin
	 */
	$.fn.betterSelects = function() {

		if(this.length >= 0) {
			// keep a reference of the jQuery object to access it on other functions
			$.fn.betterSelects.self = this;
			// Creates a 'betterSelect' control for each 'select' element in the matched set using the given configuration
			$.fn.betterSelects.opts = $.extend({}, $.fn.betterSelects.defaults, arguments[0]);
			$.fn.betterSelects.createBetterSelects();
		}

		return this;
	};
	
	
	/**
	 * Default configuration
	 */
	$.fn.betterSelects.defaults = {
		zIndexMax: 99, // The first betterSelect control will use this value, it will be smaller for following controls
		expandTime: 100, // The time it takes to expand the options
		marginTop: 1, // Margin between the preview and the expanded options list
		itemsPerCol: 6 // Number of items per column. Use 0 for a single column
	};


	/**
	 * Creates a 'betterSelect' control for each HTML select-element in the matched set using the given configuration
	 */
	$.fn.betterSelects.createBetterSelects = function(){

		var selectsData = [];
		var bsHTML, bs, bsExpandedHeight, pos, ul, itMinHeight, ele;

		/** Loop through the matched set of HTML select-elements to build a 'betterSelect' control emulating each 'select' in the set **/
		$.fn.betterSelects.self.each(function(){
			
			// TODO remove this when support for select-multiple get added
			if(!this.multiple){

				ele = $(this);
	
				// Hide the original HTML select-element
				this.className += 'accesible-hidden-element';
	
				// the select's index being processed
				pos = selectsData.length;
	
				// Build the HTML for the 'betterSelect' control'
				bsHTML = createBetterSelectHTML(this, pos);
	
				// Inserts the 'betterSelect' control in the DOM
				bs = this.parentNode.insertBefore(bsHTML, this.nextSibling);
				bs = $(bs);
	
				bs.children('.betterSelect-preview').css({minWidth: ele.width()});
				
				// Obtains the height of the betterSelect's options on expanded state
				ul = bs.children('.betterSelect');
				bsExpandedHeight = ul.height();
				// Collapse the betterSelect by default
				ul[0].style.height = 0;
				ul[0].style.zIndex = $.fn.betterSelects.opts.zIndexMax - pos;
				itMinHeight = bs.height();
				// Store data related to the original 'select' DOM-element to reference it later
				selectsData.push({height: bsExpandedHeight, itMinHeight: itMinHeight});
				ul.addClass('betterSelect-processed');
			}
		});


		/**
		 * Obtains the data source from the given HTML select-element
		 * @param srcElem Source select-element
		 * @return Extracted select's data
		 */
		function getSelectData(srcElem){

			var bsData = [];
			var selectedIndex = 0;

			for(var i=0, l=srcElem.options.length; i<l; i++) {
				if(srcElem.options[i].selected) {
					selectedIndex = i;
				}
				bsData.push(srcElem.options[i].innerHTML);
			}

			return {options: bsData, selectedIndex: selectedIndex};
		}


		/**
		 * Build the HTML displaying the options
		 * @param src Data source
		 * @param pos The index of the data source being processed
		 * @return Options's data
		 */
		function createBetterSelectOptionsHTML(src, pos) {

			var optionsHTML = '<div class="betterSelect-nc">';
			var classes = '';

			// if the data source is a HTML select-element
			if(src.nodeType) {
				src = getSelectData(src);
			}

			for(var i=0, l=src.options.length; i<l; i++){
				classes = 'betterSelect-it betterSelect-it-i-' + i;
				// marks the selected option
				if(i == src.selectedIndex) {
					classes += ' betterSelect-it-state-selected';
				}
				// close the previous colum and open a new one
				if(i > 0 && (i % $.fn.betterSelects.opts.itemsPerCol) === 0){
					optionsHTML += '</div>\
									<div class="betterSelect-nc">';
				}
				// adds an option to the list of options
				optionsHTML +=
					'<li id="betterSelect_it:' + pos + '.' + i + '" class="' + classes + '">\
						<span class="betterSelect-it-val"></span><span class="betterSelect-it-label">' + src.options[i] + '</span>\
					</li>';
			}

			optionsHTML += '</div>';

			return {options: optionsHTML, selectedIndex: src.selectedIndex, selected: src.options[src.selectedIndex]};
		}


		/**
		 * Build the betterSelect's HTML using the given datasource
		 * @param src Datasource
		 * @param pos The index of the datasource being processed
		 * @return betterSelect's HTML
		 */
		function createBetterSelectHTML(src, pos){

			var bsData = createBetterSelectOptionsHTML(src, pos);

			bsHTML = document.createElement('div');
			bsHTML.id = 'betterSelect_box:' + pos;
			bsHTML.className = 'betterSelect-box';
			bsHTML.innerHTML = '\
				<span id="betterSelect_preview:' + pos + '" class="betterSelect-preview">' + bsData.selected + '</span>\
				<span id="betterSelect_maximize:' + pos +'" class="betterSelect-maximize"></span>\
				<ul id="betterSelect:' + pos +'" class="betterSelect">' + bsData.options + '</ul>\
			';

			return bsHTML;
		}


		/**
		 * Expands/collapses the 'betterSelect' control in the given index
		 * @param pos Index of the 'betterSelect' control
		 */
		function changeSelectMaximizeState(pos){
			var ul = $('#betterSelect\\:' + pos);
			ul.stop();
			if(ul.is('.betterSelect-state-expanded')){
				minimizeSelect(pos, ul);
			} else {
				maximizeSelect(pos, ul);
			}
		}


		/**
		 * Expands the 'betterSelect' control specified by the given index
		 * @param pos Index of the 'betterSelect' control
		 * @param ul Options of the 'betterSelect' control
		 */
		function maximizeSelect(pos, ul){
			$('#betterSelect_maximize\\:' + pos).addClass('betterSelect-maximize-state-expanded');
			var bs = $(ul[0].parentNode);
			var bsOffset = bs.offset();
			bsOffset.top += selectsData[pos].itMinHeight + $.fn.betterSelects.opts.marginTop;
			ul.css({top: bsOffset.top, left: bsOffset.left});
			ul.addClass('betterSelect-state-expanded');				
			ul.animate({height: selectsData[pos].height}, $.fn.betterSelects.opts.expandTime, function(){
				// TODO move the following line to single-selects handlers
				ul.find('.betterSelect-it-state-selected').addClass('betterSelect-it-hover');
				ul.focus();
			});
		}


		/**
		 * Collapses the 'betterSelect' control specified by the given index
		 * @param pos Index of the 'betterSelect' control
		 * @param ul Options container of the 'betterSelect' control
		 */
		function minimizeSelect(pos, ul){
			ul[0].style.height = 0;
			$('#betterSelect_maximize\\:' + pos).removeClass('betterSelect-maximize-state-expanded');
			ul.removeClass('betterSelect-state-expanded');
			ul.find('.betterSelect-it').removeClass('betterSelect-it-hover');
		}


		/**
		 * Processes a click on an item of a 'betteSelect' control
		 * @param elem The clicked item
		 */
		function changeSelectSingle(elem) {
			var ul = $(elem[0].parentNode.parentNode);
			var id = elem[0].id;
			pos = id.substr(id.indexOf(':') + 1).split('.');
			var it = $.fn.betterSelects.self[pos[0]].options[pos[1]];
			// updates the preview with the content of label of the selected item
			ul.siblings('.betterSelect-preview')[0].innerHTML = it.innerHTML;
			ul.find('.betterSelect-it').removeClass('betterSelect-it-state-selected');
			elem.addClass('betterSelect-it-state-selected');
			// updates the original HTML select-element with the current selection
			it.selected = 'selected';
		}


		/**
		 * Adds handlers for common behaviors for both select types: single and multiple
		 */
		function initCommon(selectsBoxes, items){

			// Handler to expand/collapse the 'betterSelect' controls
			selectsBoxes.click(function(evt){
				pos = this.id.substr(this.id.indexOf(':') + 1);
				changeSelectMaximizeState(pos);
			});

			items.filter(':last-child').addClass('betterSelect-it-last');
		}

		
		/**
		 * Adds handlers for single-selects' peculiar behaviors
		 */
		function initSingles(selectBoxes, items){
			
			items.click(function(evt){
				changeSelectSingle($(this));
			});
			
			items.hover(function(){
				$(this.parentNode.parentNode).find('.betterSelect-it').removeClass('betterSelect-it-hover');
				$(this).addClass('betterSelect-it-hover');
			}, function(){
				$(this.parentNode.parentNode).find('.betterSelect-it').removeClass('betterSelect-it-hover');
			});
		}
		

		// Read all the created 'betterSelect' controls which have not been yet processed
		selectsBoxes = $('.betterSelect-box:not(.betterSelect-box-processed)');
		items = selectsBoxes.find('.betterSelect-it');

		// Adds handlers for common behaviors of both select types: single and multiple
		initCommon(selectsBoxes, items);
		
		// Adds handlers for single-selects' peculiar behaviors
		initSingles(selectsBoxes, items);

		// Marks controls as processed
		selectsBoxes.addClass('betterSelect-box-processed');
	};

})(jQuery);

CSS sourcode v0.5:

.betterSelect-box {
	overflow: auto;	
	margin: 0.5em 0;
	width: auto;
}
.betterSelect-preview {
	cursor: pointer;
	float: left;
	padding: 0.1em 0.3em;
	background: none repeat scroll 0 0 #fff;
	border: 1px solid #BBB;
	margin: 0;
	border-radius: 4px;
	-moz-border-radius: 4px;
	-webkit-border-radius: 4px;
}
.betterSelect-maximize {
	float: left;
	cursor: pointer;
	height: 20px;
	width: 20px;
	background: url("../images/expand.png") no-repeat scroll 0 0 transparent;
}
.betterSelect-maximize-state-expanded {
	background-position: 0 -21px;
}
.betterSelect {
	position: absolute;
	border: 0 none;
	margin: 0;
	padding: 0;
	list-style: none none;
	overflow: auto;
	background: none repeat scroll 0 0 #FEAA25;
	color: #FFF;
	opacity: 0.95;
	filter: alpha(opacity=95);
	-ms-filter: "alpha(opacity=95)";
	border-radius: 2px 2px 6px 6px;
	-moz-border-radius: 2px 2px 6px 6px;
	-webkit-border-radius: 2px 2px 6px 6px;
}
.betterSelect-state-expanded {
	padding: 2px 0 1px 0;
	border: 1px solid #BBB;
	border-top: 1px inset;
}
.betterSelect-nc {
	float: left;
}
.betterSelect-it {
	width: auto;
	margin: 0 4px 4px 4px;
	padding: 4px 2px 6px 0;
	overflow: hidden;
	cursor: pointer;
	border-radius: 4px;
	-moz-border-radius: 4px;
	-webkit-border-radius: 4px;
}
.betterSelect-it-hover {
	background-color: #FF7F2A;
}
.betterSelect-it-last {
	margin: 0 4px;
}
.betterSelect-it-val, .betterSelect-it-label {
	height: 14px;
	display: block;
	float: left;
}
.betterSelect-it-label {
	padding: 0 0 0 5px;
}
.betterSelect-it-val {
	background: url("../images/check.png") no-repeat scroll 0 0 transparent;
	width: 14px;
}
.betterSelect-it-state-selected &gt; .betterSelect-it-val  {
	background-position: 0 -14px;
}
.accesible-hidden-element {
	position: absolute;
	left: -1999px;
}

4 Responses

Subscribe to comments with RSS.

  1. Hi roger padilla,

    I was looking around for a jquery-plugin like this, and suprisingly no one was made. I had just decided to write my own when i stumbled over your brand new plugin – great! 🙂

    There was a problem when using more than 20 or so option-elements, it could quickly be “corrected” by the betterSelect-it-last style, and i also changed a little bit in code. I will go further with this, and inform you if i come op with some useful ideas – you can see the plugin in action here and here. As you may see, i am in a sort of test and design-phase (like you) just having made some of my tools at least useable. I have a lot of very complex code, so I do not want to spend a lot of time reinventing the wheel for design, thats why it so very great that you made exact the plugin i had in mind.

    You can expect som suggestions / code for the plugin rather soon, i think – better positioning, no border below the it-box and so on..Also an idea is a limit for “top-options” with a “..more options”-link (it can easily be very heavy)

    regards,

    David K

    October 25, 2010 at 01:29

  2. Well, if you not want to coorporate I just make my own implementation to the public.

    David K

    October 29, 2010 at 15:26

  3. Hi David,

    I apologize for my delay on answering. Thanks for letting me know this library is useful for you.

    I really like your suggestions but unfortunately I’ve not had time to perform them, I will make a detailed review to these suggestions this weekend.

    PD: I released the code under the Apache Licence v2.0, so you are welcome if you want to contribute improving this library.

    roger.padilla

    October 29, 2010 at 15:40

  4. …And if your want, I can give you a commiter role in the googlecode repository, please let me know a gmail account and I will invite you.

    roger.padilla

    October 29, 2010 at 15:43


Leave a comment