A good 8 years ago or so, a co-worker of mine and I worked out how to use javascript with select objects to create a “to and from control” like you’d see in Outlook for selecting to, cc and bcc from your address book. Essentially we setup 2 selects with arrows in between. We populated to the two and then used javascript to move the options back and forth between the two. It’s a fairly straight forward process and the javascript is pretty simple once you strip it down. Since then I’ve always wanted to figure out how to do a to and from control using optgroups but have never taken the time to do so. Well, a recent project at work finally gave me the excuse to figure it out.

The requirements for this control were pretty straight forward. First, it must function in IE6, IE7 and Firefox. It should work by moving an option from one select to the other, while keeping the option within its defined optgroup. For instance, if Kansas is in the optgroup Midwest, when Kansas is moved to the other select, it needs to go in the Midwest optgroup. If the Midwest optgroup doesn’t exist then the optgroup needs to be created first. I also toyed with the idea of removing the optgroups once they were empty. I got it working but there are some odd idiosyncrasies to the way the hasChildNodes function works so I’m most likely not going to include it in my project. The final, and perhaps most important requirement, is that the tool needs to keep track of what items are added and removed from the right-hand side select. So, how do we do this?

To start, we pass in the ids of the three controls. The to select, the from select and the hidden that will be storing our values. Assuming they load appropriately, we should be good to go.

var DELETE_OPTGROUPS = false;

function optgroupMove(toID, fromID, addID)
{
  var to = document.getElementById(toID);
  var from = document.getElementById(fromID);
  var deletedOptions = new Array();
  var hidden = document.getElementById(addID);
  var index=0;

Next, we cycle over the from select. Any option that is currently selected will be moved to the “to” select. To do this, we retrieve the selected option, get its optgroup, create a new option, add it to the select and then assign the text and value to it. It may seem odd to do the text/value assign last but it was the only way to get the appendChild function to work in IE.

for(x=0; x<from.options.length; x++)
{
  if(from.options[x].selected == true)
  {
    /* Create the option, add to select and then add text and value.
     * We do this because IE6 and IE7 lose the text and value when
     * added to the select after the text and values have been set.
     */
    var option = from.options[x];
    var optgroup = option.parentNode;
    var optgroupMove = document.getElementById(toID + optgroup.label);
    var newOption = new Option();
    to.appendChild(newOption);
    newOption.text = option.text;
    newOption.value = option.value;

So far, we’ve just stuck the option in the select. It’s not associated with an optgroup. But, the optgroup we want may not exist. So, we check to see if it does and if not, we create one.

    // Check to see if the optgroup exists. If it doesn't create it
    if (!optgroupMove)
    {
      var optgroupNew = document.createElement('OPTGROUP');
      optgroupNew.id = toID + optgroup.label;
      optgroupNew.label = optgroup.label;
      to.appendChild(optgroupNew);
      optgroupMove = document.getElementById(toID+optgroup.label);
    }

Now that we’re sure that we have an optgroup we can append our option to it. We also then add the old option to the deletedOptions array, as well as the provided hidden variable, if it exists.

    // Add our wayward option to the optgroup
    optgroupMove.appendChild(newOption);
    deletedOptions[index++] = option;
    if(hidden)
    {
      hidden.value = hidden.value + option.value + ',';
    }
  }
}

The final piece of the puzzle is to remove the option from the originating select. To do this we simply loop over the deleted options array, removing each option from its corresponding optgroup. This block also has the code to remove the optgroup if it’s empty. This is a bit finicky in how it works though. I’ll explain more below…

for (y = 0; y < deletedOptions.length; y++)
{
  var optgroup = deletedOptions[y].parentNode;
  optgroup.removeChild(deletedOptions[y]);
  /* The following removes the optgroup from the select if it is
   * empty. I made this optional because there is odd behavior
   * in the return from hasChildNodes if there is any whitespace at
   * all within the optgroup as it was orignally defined.  White space
   * constitutes a child node.
   */
  if(DELETE_OPTGROUPS && !optgroup.hasChildNodes())
  {
    var pNode = optgroup.parentNode;
    pNode.removeChild(optgroup);
  }
}

 

The problem with deleting the otpgroups (from what I’ve gathered so far) is that the hasChildNodes function will return true if there is any whitespace within the optgroup. In other words, when you define your optgroup, it has to be on one continuous line otherwise the whitespace is reported as a childnode and hasChildNodes returns true. This isn’t an issue if you populate your select through javascript but if you do it by hand or dynamically through php, jsp, etc. you’ll most likely put the whitespace in. If you don’t, then the delete will function appropriately.

Finally, the last piece is to deselect the selects so the have no options selected.

  // Unselect all options in both "to" and "from" select
  to.options.selectedIndex = -1;
  from.options.selectedIndex = -1;
}

And that’s it. View/test the code. Enjoy!