Rearranging HTML list items in a Drupal menu using jQuery

At some point, you might find the need to rearrange or change the order of HTML unordered list items (<ul> <li>). There are a couple potential reasons: you might want to sort the list alphabetically, or, as in the case of a Drupal site we recently launched, you might need to turn an unordered list menu into columns.

Custom horizontal jQuery accordion menu

The products menu at ClarkDietrich.com is based on a custom-built horizontal accordion UI:

ClarkDietrich.com products menu

 

The menu for Interior Framing is set up like this:

Drupal menu structure

 

By default, Drupal outputs menus as unordered lists. If you want to put a list into columns, the best way to do it is to float the list items via CSS. Be sure to specify <li> width; it determines column width and, therefore, the number of columns within a fixed width container:

#block-panels_mini-product_slider li {
  float: left;
  width: 325px;
  list-style: none inside none;
  margin: 0 15px 0 0;
  padding: 0;
  position: relative;
}

The problem is that, because we're floating each <li> left, the order would be left to right, then top to bottom:

Item 1    Item 2
Item 3    Item 4
Item 4    Item 5
Item 5    Item 8

But the client wanted them to flow down the first column in order, then down the second column:

Item 1    Item 5
Item 2    Item 6
Item 3    Item 7
Item 4    Item 8

As is often the case, a simple CSS solution was suddenly untenable.

One solution would be to have my client change the order of the items in the menu editor. Drupal has a pretty nice drag-and-drop menu editor, so it wouldn't be too hard – in theory. But if he then decided to add one additional product in the middle, he would have to rearrange them all again. It would quickly become a nightmare. Also, we need the menus to be in a non-floated sequential order elsewhere in the site (for a product sitemap or a non-CSS version of the same menu).

Another solution would have been to write a Drupal module that overrides the menu display functions. But time was of the essence, and research into this quickly led me to believe it would be more complicated than I wanted to undertake at the time – delaying launch and adding to the scope. And, as with the previous method, going down this road would mean that the menu items would be in the wrong order if they weren't floated.

Enter jQuery.

Solution: jQuery function to turn list items into sorted columns

I knew it would be pretty simple to write a JavaScript or jQuery function to sort the list items. I've commented the code below to explain what's going on.

function columnize_list (curr_list) {

    // curr_list is the <ul>

    // Create new array to hold sorted list items.
    var sorted_list_items = new Array();

    // Determine the column length (2 columns).
    var curr_list_length = curr_list.children().size();
    var tab = parseInt((curr_list_length + 1) / 2);

    // First, check to see if this list has already been columnized.
    if (!curr_list.hasClass('columnized')) {

        // I won't get into explaning the algorithm.
        // Normally, creating algorithms is one of my strongest points, but for some reason,
        // this simple math had me spinning my wheels for quite a while. :)
        i = 0;
        curr_list.children().each(function () {
            if (i < tab) {
                new_index = i * 2
            } else {
                new_index = ((i - tab) * 2) + 1
            }
            sorted_list_items[new_index] = $(this);
            i++;
        });

        // Remove all items from the original <ul> sent to the function.
        curr_list.children().remove();
        // Add the newly sorted list items back into the original <ul>.
        // Note: this brings each <li> item's children along with it.
        for (i = 0; i < curr_list_length; i++) {
            curr_list.append(sorted_list_items[i]);
        }

        // Lastly, tag this <ul> with a class so we know not to attempt to columnize
        // it again.
        curr_list.addClass('columnized');

        // Not sure why, but 'display: inline' was being added to all the <a> tags.
        // Removing it.
        curr_list.find('a').attr('style', '');

    }

} // function columnize_list

One thing I wanted to avoid was doing a bunch of heavy JavaScript lifting on each page load. There are 300 products in this menu, and I don't like delays; even a 1/10 of a second delay adds up. So I decided to call the columnize function only when you select a product category tab. Running the function on 10 items, rather than 300, is acceptable.

When the user presses a product category tab (or clicks on one of the list items to dig deeper into a category), we call the columnize_list function as follows:

var my_ul = $(this).parent().find('div > ul.menu');
columnize_list(my_ul);

Because we're calling the function each time we select a new category or sub-category, we need to tag the <ul> as "columnized" within the function to prevent sorting it twice.

Summary

At some point, I'd like to write a couple modules to do this type of work within PHP, rather than in JavaScript and jQuery. But I'm quite happy with the results. If you have a question, comment, or a more elegant solution, I'd love to hear it.