David Sills's muse is fueled by Bach, Beethoven, and Brahms (and Berlioz and Boulez). He has no life, which comports well with the über-geekitude to which he aspires. He reads and writes fitfully in English, Spanish, French, German, Italian (mostly medieval), C, VB, Perl.... Oh, and Java and XSLT, lots and lots of Java and XSLT. Trained somewhere in darkest Central America, he works today at DataSource, Inc., on the product team for the CASE tool Abri. David has posted 9 posts at DZone. View Full User Profile

An HTML List Builder: A Study in Applying jQuery

12.30.2008
| 38171 views |
  • submit to reddit
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.

Legacy
Article Resources: 
Published at DZone with permission of its author, David Sills.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)