Selected JavaScript Fun and Goodness

Here's a sample of JS code for allowing users to choose multiple items from a list of available options. There are many examples of code like this, however this example has the enhancement of keeping the lists well organised. To do this, we need to find the parent OPTGROUP that contains a selected OPTION, and we also need to insert a new OPTION in a specific OPTGROUP of a SELECT list.
compton, 23 November 07
Updated 16 January 11
There are several occasions where users need an ability to make a list by taking items from a 'master' list of available items. This type of mechanism is a very usable way to allow users to choose featured articles for display next to another article.

The JS code below extends the usual functionality so that the two lists are kept maintained in group and alphabetic order, enhancing usability when there are lots of items in either list (or both). There are five functions needed for full functionality, all of which are shown here in their entirety. There's also a quick and dirty test function provided out of interest.

To use the code, your page needs a HTML form containing two multiple select lists, and two buttons (plus of course the normal submit button and any other fields you may wish). One select list is for the user's currently chosen items, while the other is populated with the unselected items.

The first button is for adding items from the available items list to the current items list, and the other is for the reverse procedure (both do this by calling moveOptions() passing in the ids of the source and target select lists).

The items in both lists should be grouped within OPTGROUP elements. The JS code here will place a moved item within a matching OPTGROUP in the target list, creating the OPTGROUP if it does not already exist in the target list. Items within an OPTGROUP are maintained in alphabetic order (and so should be initially sorted thus when the select lists are populated).

A couple of variables are set to indicate the browser being used:
var NS4 = (navigator.appName == 'Netscape' && parseInt(navigator.appVersion) < 5); var IE6 = (navigator.appName == 'Microsoft Internet Explorer' && parseInt(navigator.appVersion) <= 5); var WinSafari = (navigator.userAgent.indexOf('Safari') >= 0) && (navigator.userAgent.indexOf('Windows') >= 0)
Next up is the moveOptions() function. It simply iterates through the options in the sourceSelect list, building two arrays which hold the names and values of all the selected options. It also stores the selected indexes in a third array called wewilldie, for reasons which should become clear later.

Once it has these arrays, it can run through adding all selected items to the targetSelect list. Note that it runs through in reverse order, which you'll see is necessary when we look at the addOption() function. Once the options have been recreated in the targetSelect list, we run through again, this time in forwards order, removing the options from the source select.
function moveOptions(sourceSelect, targetSelect) { var selLength = sourceSelect.length; var selectedText = new Array(); var wewilldie = new Array(); var selectedValues = new Array(); var selectedCount = 0; var index; for(index=selLength-1; index>=0; index--) { if(sourceSelect.options[index].selected && sourceSelect.options[index].text != '') { selectedText[selectedCount] = sourceSelect.options[index].text; selectedValues[selectedCount] = sourceSelect.options[index].value; wewilldie[selectedCount] = index; selectedCount++; } } for(index=selectedCount-1; index>=0; index--) addOption(sourceSelect, targetSelect, selectedText[index], selectedValues[index]);   for(index=0; index<selectedCount; index++) deleteOption(sourceSelect, wewilldie[index]);   if(NS4) history.go(0); }
The addOption() function needs to add the new option within an <optgroup> element in targetSelect having the same name as in sourceSelect. It calls the getGroupLabel() function, defined next, to get the name of the containing <optgroup> (ie its label property). Once it has this name, it then attempts to find an <optgroup> with the same label in the target select, creating one if none is found. We're then ready to add the option. To make sure the new option is added in correct alphabetic order, we run through all the children of the target <optgroup> while the new option's text property precedes the existing option's text:
function addOption(sourceSelect, targetSelect, optionText, optionValue) { var newOpt = new Option(optionText, optionValue);   // Get optgroup label of the option being moved parentGroup = getGroupLabel(sourceSelect, optionText);   // Find matching optgroup in target select var newParentGroup = false; var optGrps = targetSelect.getElementsByTagName('optgroup') for (var index = 0; index &lt; optGrps.length; index++) { if (optGrps[index].label == parentGroup) { newParentGroup = optGrps[index]; break; } } // if not found, create if (!newParentGroup) { blankOpt = new Option('', ''); blankOpt.style.visibility = 'hidden'; blankOpt.disabled = true; newParentGroup = document.createElement('optgroup'); newParentGroup.style.color = '#000099'; newParentGroup.label = parentGroup; targetSelect.appendChild(newParentGroup); targetSelect.appendChild(blankOpt); }   // Add to target optgroup in alphabetic order cursorOpt = newParentGroup.firstChild; if (cursorOpt) do { if (cursorOpt.text &gt; optionText) break; } while (cursorOpt = cursorOpt.nextSibling); if (cursorOpt) newParentGroup.insertBefore(newOpt, cursorOpt); else newParentGroup.appendChild(newOpt);   // Fix for MSIE 6 if (IE6) { cursorOpt = newParentGroup.firstChild; while (cursorOpt) { if (cursorOpt.value == optionValue) cursorOpt.text = optionText; cursorOpt = cursorOpt.nextSibling; } } }
The getGroupLabel() function returns the name of the <optgroup> that contains the <option> with the label specified by the searchText parameter. We do this by looping through all <optgroup> elements of the select box, and iterating through all the options of each until we find an option with the text property we're looking for:
function getGroupLabel(selectBox, searchText) { // Find OptGroup of item being moved var parentGroup = false; var optGrps = selectBox.getElementsByTagName('optgroup') for (var index = 0; index &lt; optGrps.length; index++) { for (var subIndex = 0; subIndex &lt; optGrps[index].childNodes.length; subIndex++) if (optGrps[index].childNodes[subIndex].text == searchText) { parentGroup = optGrps[index].label; break; } } return parentGroup; }
deleteOption() just removes the <option> with the specified index. Safari in Windows won't let you remove an option just by setting it to null, so an alternative method is used for that browser:
function deleteOption(selectBox, theIndex) { var selLength = selectBox.length; if (selLength &gt; 0) { if (WinSafari) { selectBox.options[theIndex].parentNode.removeChild(selectBox.options[theIndex]); } else { selectBox.options[theIndex] = null; } } }
We've now seen the four functions required for the manipulation of the two multiple select lists. In this case, as in most, one list holds the options that we want to store as the user's choices, the other holds the list of options that they have not selected. Once the user has chosen the options they desire, they need to submit the form containing the select boxes, so that we can save or otherwise process their particular choices. We need to make sure all the chosen options are selected before the form is submitted, for which we need a fifth function, selectAllOptions(). This is the usual function for selecting all options in a multiple select list, and it's typically fired by calling it via the form's onSubmit attribute. You don't need to worry about the state of the unselected list of course, as its contents are of no interest to us.
function selectAllOptions(selectId) { var selObj = document.getElementById(selectId); var len = selObj.options.length; for (var index=0; index&lt;len; index++) selObj.options[index].selected = true; }
The following function can be attached to a button and simply iterates through the options in the sourceSelect list displaying the label and value of each. It can be useful for testing. Note that it uses confirm() for displaying the option's properties rather than alert(), allowing us to choose whether to show the next option's properties or to break out of the loop:
function test(sourceSelect, targetSelect) { var len = sourceSelect.options.length; for (var index=0; index&lt;len; index++) if (!confirm(sourceSelect.options[index].text+'--'+sourceSelect.options[index].value)) break; }

Select All

Below is a function which will select all boxes in a specified table.
&lt;script language=&quot;javascript&quot;&gt; function setallboxes() { var state = document.getElementById('setall').checked; var elements = document.getElementsByTagName('input'); for (var offset = 0; offset &lt; elements.length; offset++) { if (elements[offset].type == 'checkbox' &amp;&amp; elements[offset].id.substr(0,6) == 'clear_') { elements[offset].checked = state; } } } &lt;/script&gt;