Platinum Partner
java,javascript,xhtml,jquery

An HTML List Builder: A Study in Applying jQuery

Recently, a client requested a particular implementation for a control in a web application. What they wanted looked like a standard Windows widget, called by Microsoft a list builder. You've undoubtedly seen a list builder: two lists next to one another, the left one initially filled with options, the right one initially empty, with buttons to move selected items from one list to the other. I recognized the idiom and got to work, thinking I'd find a solution without too much difficulty. Of course, I wasn't about to reinvent the wheel, so the first thing I did was to check through toolkits I was already using.
 
Nowadays I simply don't write JavaScript without jQuery, so I checked out the jQuery UI project, which I knew had some building blocks for making UI widgets. No such luck. There were bits I could have used if I hadn't been dealing with a client who was only interested in solutions that were useful to those needing assistive technologies. No mouse-driven drag-and-drop solutions, then. Besides, such solutions would have violated the principle of least surprise: that's not how Windows list builders work.
 
In fact, as I looked around, I was bemused to discover that no matter how widely I spread my seach net, I couldn't seem to catch a really good overall solution for this problem. There were a few good, incomplete suggestions, without a doubt, but it was increasingly clear I would have to roll my own.
 
Requirements
 
Naturally, the first thing to put together about any solution is a set of requirements. The obvious ones were these:
  1. The solution had to contain two lists
  2. Items had to be able to be moved using buttons from each list to the other
  3. The user should be able to use the solution with the keyboard only
  4. The solution had to be useful within an HTML form

Requirement 4 meant that the solution had to return a state representation that could be interpreted on a server.
After some consideration, I added the following:

  1. The selects should have the same height and width regardless of their current contents
  2. The buttons should be vertically centered on the selects and horizontally centered on each other
(These were included to suggest visually that all of the pieces belong to a single widget.)
  1. There should be buttons to move all of the contents of each list to the other
  2. There should be buttons to move any combination of items within each list up or down (in other words, to reorder the lists)
  3.  

The Elements
 
The first step was obviously to put the main elements into place, two selects (obvious choices for the lists) and buttons (including a submit button for the form the list builder would belong to):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>List Builder</title>
<link rel="stylesheet" type="text/css" href="css/list-builder.css"/>
<script type="text/javascript" src="js/jquery-1.2.6.min.js"></script>
<script type="text/javascript" src="js/list-builder.js"></script>
</head>
<body>
<form method="post" action="list-builder.html">
<fieldset>
<div>
<div id="leftButtons">
<button type="button" id="leftUpButton" class="button">Up</button>
<button type="button" id="leftDownButton" class="button">Down</button>
</div>
<div id="leftList">
<select id="leftSelect" size="10" multiple="multiple">
<option value="first">First</option>
<option value="second">Second</option>
<option value="third">Third</option>
<option value="fourth">Fourth</option>
<option value="fifth">Fifth</option>
<option value="sixth">Sixth</option>
<option value="seventh">Seventh</option>
<option value="eighth">Eighth</option>
<option value="ninth">Ninth</option>
<option value="tenth">Tenth</option>
<option value="eleventh">Eleventh</option>
<option value="twelfth">Twelfth</option>
<option value="thirteenth">Thirteenth</option>
<option value="fourteenth">Fourteenth</option>
<option value="fifteenth">Fifteenth</option>
</select>
</div>
<div id="middleButtons">
<div id="addRemoveButtons">
<button type="button" id="addButton" class="button">Add</button>
<button type="button" id="removeButton" class="button">Remove</button>
</div>
<div id="addAllRemoveAllButtons">
<button type="button" id="addAllButton" class="button">Add All</button>
<button type="button" id="removeAllButton" class="button">Remove All</button>
</div>
</div>
<div id="rightList">
<select id="rightSelect" size="10" multiple="multiple">
</select>
</div>
<div id="rightButtons">
<button type="button" id="rightUpButton" class="button">Up</button>
<button type="button" id="rightDownButton" class="button">Down</button>
</div>
</div>
<div style="clear: both; padding-top: 15px;">
<input type="submit" value="Submit"/>
</div>
</fieldset>
</form>
</body>
</html>
At this point, I had the basic parts, but it didn't look like much:
[img_assist|nid=6915|title=List Builder HTML only|desc=|link=none|align=center|width=375|height=490]
The next step was to try to style these parts in a minimalist way:
@charset "utf-8";

fieldset {
border: 0;
margin: 0;
padding: 0;
}

#leftButtons, #leftList, #rightButtons, #middleButtons, #rightList {
float: left;
}

/* spacing between the add-remove group and the addAll-removeAll group */
#addAllRemoveAllButtons {
margin-top: 15px;
}

.button {
display: block;
margin-bottom: 3px;
}
This looked a little better:
[img_assist|nid=6916|title=List Builder HTML and CSS|desc=|link=none|align=center|width=330|height=220]
 
 Far from perfect, but at least requirement 1 had been met. Still, there was the question of vertical centering (not to mention the actual functionality, of course!). It occurred to me that while I could probably figure out a CSS solution that would accomplish some or even most of what I had in mind, the browser has already done most of the computation I needed: all I needed to do was to build on what it had already done.
 
This, of course, jQuery makes remarkably easy. The jQuery JavaScript library is increasingly widely accepted, in part precisely because it makes working with HTML DOM elements such a tractable problem. In particular, its formulation of the document-ready function, a function that is run after the DOM has been initialized by the browser (and so the brower's computation of initial layout has been completed) and before images have been loaded and the view has been rendered. This enables modifications to be performed that do not cause the flickering and visible redrawing that are among the hallmarks of standard onload functions.

Sizes
Since I had provided most of the elements within the HTML with id attributes, sizing the lists was pretty easily accomplished with jQuery, which has excellent support for CSS selectors:
  var leftSelect = $('#leftSelect');
var rightSelect = $('#rightSelect');
var selectWidth = leftSelect.width();
var selectHeight = leftSelect.height();
leftSelect.width(selectWidth).height(selectHeight);
rightSelect.width(selectWidth).height(selectHeight);
A quick explanation for those unfamiliar with jQuery: the selector "$('#leftSelect')" in jQuery returns the element with the id attribute "leftSelect", namely the left select (you can see the imagination I bring to my ids), wrapped in a jQuery object that acts pretty much like an array. Of course, since I have used an id attribute, this had better be a one-element array!, so I could have retrieved the actual select using "$('#leftSelect')[0]". However, it was more useful to have the wrapper, since it has a number methods that simplify and regularize access to DOM properties. These can be seen in the remainder of the code and are largely self-explanatory, with the proviso that methods that take no parameters generally return the value of the property and those that take parameters generally reset the value of the property.
 
The div element that holds the right select must also be wide enough to push the up and down buttons on its right to the right location:
  $('#rightList').width(selectWidth);
Requirement 5 is now met:
[img_assist|nid=6917|title=List Builder selects sized|desc=|link=none|align=center|width=400|height=210]
Centering
The browser already knows the sizes of everything and needs only to be asked to reveal its results. However, we first need to be able to interpret the string values of style settings as a number:
  var numericValue = function(value)
{
var digits = value.match(/[0-9]+/);
if (digits && digits.length > 0)
{
return Number(digits[0]);
}
return Number(0);
};
We also need the height of a div representing the row:
  var divHeight = $('#leftList').height();
Once we have these prerequisites, vertical centering of the up and down button div is straightforward:
  var leftButtons = $('#leftButtons');
var leftButtonsHeight = leftButtons.height() + numericValue(leftButtons.css('margin-top')) +
numericValue(leftButtons.css('margin-bottom')) + numericValue(leftButtons.css('padding-top')) +
numericValue(leftButtons.css('padding-bottom'));
var leftButtonsMargin = ((divHeight - leftButtonsHeight) / 2) + 'px';
leftButtons.css('margin-top', leftButtonsMargin);
Horizontal centering is a bit more complicated, but not terribly so. It requires knowing the expected height of the div:
  var leftUpButton = $('#leftUpButton');
var leftDownButton = $('#leftDownButton');
var divWidth = leftButtons.width();
var leftUpMargin = ((divWidth - leftUpButton.width()) / 2) + 'px';
leftUpButton.css({ 'margin-left': leftUpMargin, 'margin-right': leftUpMargin });
var leftDownMargin = ((divWidth - leftDownButton.width()) / 2) + 'px';
leftDownButton.css({ 'margin-left': leftDownMargin, 'margin-right': leftDownMargin });
The same applies, of course, to the right up and down buttons.
The principle is the same with the middle buttons; there are simply more of them:
// center the middle buttons vertically
var middleButtons = $('#middleButtons');
var middleButtonsHeight = middleButtons.height() + numericValue(middleButtons.css('margin-top')) +
numericValue(middleButtons.css('margin-bottom')) + numericValue(middleButtons.css('padding-top')) +
numericValue(middleButtons.css('padding-bottom'));
var middleButtonsMargin = ((divHeight - middleButtonsHeight) / 2) + 'px';
middleButtons.css('margin-top', middleButtonsMargin);
// center the middle buttons horizontally
var addButton = $('#addButton');
var removeButton = $('#removeButton');
var addAllButton = $('#addAllButton');
var removeAllButton = $('#removeAllButton');
divWidth = middleButtons.width();
var addMargin = ((divWidth - addButton.width()) / 2) + 'px';
addButton.css({ 'margin-left': addMargin, 'margin-right': addMargin });
var removeMargin = ((divWidth - removeButton.width()) / 2) + 'px';
removeButton.css({ 'margin-left': removeMargin, 'margin-right': removeMargin } );
var addAllMargin = ((divWidth - addAllButton.width()) / 2) + 'px';
addAllButton.css({ 'margin-left': addAllMargin, 'margin-right': addAllMargin });
var removeAllMargin = ((divWidth - removeAllButton.width()) / 2) + 'px';
removeAllButton.css({ 'margin-left': removeAllMargin, 'margin-right': removeAllMargin });
Now the buttons and selects appear correctly:
[img_assist|nid=6918|title=List Builder final appearance|desc=|link=none|align=center|width=435|height=205]
and requirement 6 is met.

Returning State
 
Since selects only return the values of selected options, a complete representation of the state of the list builder is impossible to guarantee without some JavaScript intervention. A hidden input in the HTML:
  <input type="hidden" name="listBuilderState" id="listBuilderState"/>
and a method that can be called within event handlers:
  var updateListBuilderState = function() {
var state = '<?xml version="1.0" encoding="UTF-8"?>\n';
state += '<state>\n';
state += ' <select instance="left">\n';
var selected = $('#leftSelect option');
for (var i = 0; i < selected.length; i++)
{
var option = selected[i];
state += ' <value';
if (option.selected)
{
state += ' selected="true"';
}
state += '>';
state += option.value;
state += '</value>\n';
}
state += ' </select>\n';
state += ' <select instance="right">\n';
selected = $('#rightSelect option');
for (var i = 0; i < selected.length; i++)
{
var option = selected[i];
state += ' <value';
if (option.selected)
{
state += ' selected="true"';
}
state += '>';
state += option.value;
state += '</value>\n';
}
state += ' </select>\n';
state += '</state>\n';
$('#listBuilderState').val(state);
}
allows the list builder to return an XML document as part of the form submission. This makes POST the preferred method for submitting forms using the list builder, since GET URLs may be limited in length.
 
This method will be called during our document-ready function in order to initialize the value of the form hidden input, and updated each time the left or right selects are used, either by mouse or keyboard:
  updateListBuilderState();
var updateState = function(event)
{
updateListBuilderState();
};
leftSelect.click(updateState).keyup(updateState);
rightSelect.click(updateState).keyup(updateState);
This satisfies requirement 4.
 
Add-Remove Functionality
 
Thanks to jQuery's advanced selectors, the functionality to transfer list entries from one select to the other is uncomplicated:
  handler = function(event)
{
$('#leftSelect option:selected').appendTo('#rightSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
addButton.click(handler).keyup(handler);
handler = function(event)
{
$('#rightSelect option:selected').appendTo('#leftSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
removeButton.click(handler).keyup(handler);
handler = function(event)
{
$('#leftSelect option').appendTo('#rightSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
addAllButton.click(handler).keyup(handler);
handler = function(event)
{
$('#rightSelect option').appendTo('#leftSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
removeAllButton.click(handler).keyup(handler);
This meets requirements 2 and 7.

Movin' on Up
 
The algorithm for moving an entry up within a select is not complicated; there are two relevant methods in the HTML DOM Select object: remove and add. The way they work is that the remove method takes an index number (zero-based) and deletes the corresponding HTML DOM Option object from the select and renumbers the index numbers for the remaining Option objects. The add method takes the Option object to be added and an Option object already in the list before which the supplied option is to be inserted. If the second option is null, the first option is appended to the end of the list (that is to say, before the first empty entry).
 
For a single entry, when you remove an entry from the list, it is certain that either that option is first (at index 0) or it has a neighbor with an index lower by 1. The logic is simple: put a guardian to ensure that if the index of the option to be removed is 0, do nothing; if it is greater than 0, find this neighbor, and use it as the second parameter in the add method.
 
Again jQuery comes to the rescue, simplifying the process. First, we find the selected options to move (we'll return in a few paragraphs to exactly how to do this):
      var selected = ...;
If there are any, we check the first one for index 0; if the first has the index 0, we simply exit:
      if (selected.length > 0)
{
if (selected[0].index == 0)
{
return;
}
// main logic here
}
The comment above shows where the main logic goes. This loops through the selected options, calling a function that gets the index of the option and gets the Select object containing the whole list (again, we'll return in a few paragraphs as to how this is done), finds the previous option in the list, and calls the add and remove methods as described above:
        selected.each(function(index) {
var i = this.index;
var list = ...;
var previous = list.options[i - 1];
list.remove(i);
list.add(this, previous);
});
So far, so good, but how to attach this to a button? jQuery provides easy methods for binding functions as event handlers; the functions should take an event as a parameter, which won't be needed in this particular algorithm.
 
However, we don't want to cut and paste the logic twice, once for the left and once for the right button. Instead, we want to parameterize the function so as to be able to tell it which select we want at runtime. What to do?
 
The answer depends upon the nature of JavaScript, which treats functions differently from many familiar languages by not treating them differently from other objects. In other words, functions are simply objects you can access with a particular syntax. In fact, JavaScript functions are actually closures. A closure is a function that comes with an environment of "magically" assigned external values upon which it can operate. In the case of a JavaScript function, those magic external values consist of any variable that was in scope at the moment the function was created.
 
So we can use a function that returns the handler function we want. The logic is identical for both lists save for the id of the list; we will pass the id of the list into the outer function as a parameter:
  var moveUpHandler = function(selectId)
{
return function(event) {
var selected = $('#' + selectId + ' option:selected');
// logic as before
Now we can see how the list of selected options was retrieved: even though the moveUpHandler function is long finished and out of scope by the time the actual handler (the function it returns) fires, the value of the parameter selectId, since it was in scope when the handler was created, is still available for use in the jQuery selector.
 
Something similar is done inside the function applied to each of the selected options, since selectId is still in scope even there:
          var list = $('#' + selectId)[0];
Now the entire function returning the event handler can be seen together:
  var moveUpHandler = function(selectId)
{
return function(event) {
var selected = $('#' + selectId + ' option:selected');
if (selected.length > 0)
{
if (selected[0].index == 0)
{
return;
}
selected.each(function(idx) {
var i = this.index;
var list = $('#' + selectId)[0];
var previous = list.options[i - 1];
list.remove(i);
list.add(this, previous);
});
}
updateListBuilderState();
this.blur();
};
}
Note that at the end of the logic for moving values, we update the hidden input with the current state of the list builder and blur (that is, unfocus) the button. This last is a convenience, but the code reminds us of the many meanings of this in JavaScript: in the function applied to each of the selected option, this is the option; in the handler function, this is the button pressed.
 
Now assigning the handler to events fired by the left button is simple:
  var handler = moveUpHandler('leftSelect');
leftUpButton.click(handler).keyup(handler);
Assigning the handler to the right button is similar.

Moving Down
 
More complicated is the functionality to move list entries down. In particular, moving multiple entries down is complex. What, in fact, does it mean?
 
The question becomes interesting when you consider moving blocks of entries. For example, consider a list of five entries (we'll call them 1, 2, 3, 4, and 5). Suppose you want to move all the entries but the last down by one. There are at least two configurations with which you might expect to wind up: 5, 1, 2 3, 4; and 1, 2, 3, 5, 4. The first configuration takes the block of entries, maintains its integrity, and allows the surrounding entries to flow around it. The second actually reverses the problem: it considers the problem as taking the fifth entry (the only one not originally selected to be moved) and moving that entry up.
 
As posed so far, both definitions of what constitutes moving multiple entries are reasonable. The problems with the second definition are clarified, however, when the selections do not constitute a block.
 
Suppose you select 1, 2, and 4 to move down. According to the second definition, the result of the movement is 1, 3, 2, 5, 4. The first selection hasn't moved at all! If you try moving the block up again using the logic above, nothing happens, since the first element of the block has the index 0. This seems counter-intuitive. Using the first definition results in the configuration 3, 1, 2, 5, 4, which both seems more intuitive as a result and is easily reversed by moving the block up according to the logic we have already seen. Reversibility is an intuitively useful trait, so we will use the first definition here.
 
To implement it, we will use the same methodology described above to create a closure. First, we must ensure there is something to be moved:
      var selected = $('#' + selectId + ' option:selected');
if (selected.length > 0)
{
// logic here
}
Then we must determine whether or not the last selected option is also last in the original list. If so, we will do nothing:
        var options = $('#' + selectId + ' option');
if (selected[selected.length - 1].index == options.length - 1)
{
return;
}
Then we must determine what option each selection to be moved will eventually appear before (remember, this is how the add function works). We'll simplify the problem to consider moving only one selection at a time, and call the option before which this selection is to be moved x. Our problem, as so often in high-school algebra, is to solve for x.
 
To do this, we need a helper function that will allow us to distinguish between options selected to be moved and those not selected. This is not complicated:
  var contains = function(list, value)
{
if (! value)
{
return false;
}
for (var i = 0; i < list.length; i++)
{
if (list[i] == value)
{
return true;
}
}
return false;
}
With this in place, we can write the actual logic. We need to hold on to two variables. The first is a candidate option x (remember, this is a candidate option for the second parameter to the add method). To initialize it, we simply count forward two options from the index of the selection to be moved, skipping the first.
 
The second is the list of options having a higher index than the selection to be moved. We loop through this list one option at a time: if the option encountered is a member of the options selected to be moved, skip the candidate x one further forward (this maintains the relationships between the selected options) and go on to consider the next option in the list. If we wind up past the end of the list, stop; x is null and we will append to the end of the list. Otherwise, stop when the index of the candidate x is greater than the index of the current option in the list.
        var befores = new Array();
for (var i = 0; i < selected.length; i++)
{
var currentIndex = selected[i].index;
var beforeIndex = currentIndex + 2;
var before = options[beforeIndex];
for (var j = 1; before; j++)
{
if (contains(selected, options[currentIndex + j]))
{
beforeIndex++;
before = options[beforeIndex];
}
else if (before.index > options[currentIndex + j].index)
{
break;
}
}
befores.push(before ? before : null);
}
Then, of course, the problem is a simple one:
        selected.each(function(index) {
list = $('#' + selectId)[0];
list.remove(this.index);
list.add(this, befores[index]);
});
Finally, we can see the whole thing together:
  var moveDownHandler = function(selectId)
{
return function(event) {
var selected = $('#' + selectId + ' option:selected');
if (selected.length > 0)
{
var options = $('#' + selectId + ' option');
if (selected[selected.length - 1].index == options.length - 1)
{
return;
}
var befores = new Array();
for (var i = 0; i < selected.length; i++)
{
var currentIndex = selected[i].index;
var beforeIndex = currentIndex + 2;
var before = options[beforeIndex];
for (var j = 1; before; j++)
{
if (contains(selected, options[currentIndex + j]))
{
beforeIndex++;
before = options[beforeIndex];
}
else if (before.index > options[currentIndex + j].index)
{
break;
}
}
befores.push(before);
}
selected.each(function(index) {
list = $('#' + selectId)[0];
list.remove(this.index);
list.add(this, befores[index] ? befores[index] : null);
});
}
updateListBuilderState();
this.blur();
};
};
Again, we update the hidden input with the current state of the list builder and blur the button after moving. Assigning the handler to events fired by the left button is simple:
  handler = moveDownHandler('leftSelect');
leftDownButton.click(handler).keyup(handler);
Again, assigning the handler to the right button is similar.
This meets requirements 8.

Accessibility
 
One more detail of implementation is needed to complete requirement 3. We have used a number of onkeyup handlers, which are useful where accessibility via keyboard is an issue. However, moving from one control to the other on an HTML page uses the tab key (or shift-tab, for reversing the motion) and we don't want the use of these keys to fire the onkeyup handler when all we are trying to do is navigate from one control to another!
 
The solution is a simple function that wraps another function, checking the event key code to ensure that it isn't either the tab key (ASCII character 9) or the shift key (16). If the key code passes the test, the inner function will be called:
  var doNotFireOnTab = function(handler) {
return function(event)
{
var keyCode = event.keyCode;
// ignore tab and shift-tab
if (keyCode == 9 || keyCode == 16)
{
return;
}
handler(event);
}
}
Now, we can slightly change the assignment given above:
  handler = moveDownHandler('leftSelect');
leftDownButton.click(handler).keyup(doNotFireOnTab(handler));
Similar changes will be applied to all the onkeyup handler assignments.
 
Conclusion
 
This was a not inconsiderable set of requirements. The simplicity of this solution owes almost entirely to jQuery. Without the underlying library, the code for the solution would be far more complex. Thanks, jQuery!
 
Download
 
I have set up the solution in a WAR file for anyone thinking of giving it a try. Your local Tomcat should easily deploy it; similar solutions in other languages should be relatively straightforward. The solution alternates between an XHTML form containing the list builder (NB. this may confuse older versions of IE) and a page showing the XML sent back to the server with a link returning to the form page.
Have fun!

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}