Multi-column lists with jQuery, an alternative method

So I needed a method to take a long, nested list and turning it into a compact, multiple acolumn list, in order to display it as sort of a site map for the home page for a site I'm working on.

Being a huge fan of jQuery, it was naturally my go-to library of choice.

Scanning the plugins site, I found a possible solution from a feller called Ingo Schommer called columnizeList.

Score, right? Well... not exactly, at least for my case.

Ingo used some of the methodoligies outlined in this article on multi-column lists on A List Apart. One of the caveats of his methodology is that each list item has to be the same height. This works Ok for a lot of use cases, but since I'm using a Drupal menu as the source for the list, it could contain arbitrary text I don't control.

So, I started from scratch. Instead of relying on consistent line heights, and applying different margin settings to list elements, I instead decided to decompose the large source list into several smaller lists (one for each column) and then use a css float parameter to make them all appear side-by-side.

Here's a sample list for a demonstration, cribbed from Ingo's example:

  1. harold (3550)
  2. horatio (1320)
  3. hitler (1120)
  4. henry (784)
  5. hector (358)
  6. haploid (315)
  7. hopping (50)
  8. herbert mulroney (44)
  9. hopscotching (29)
  10. hominibus (19)
  11. honkey (19)
  12. hermoine (18)
  13. hieronymus (13)
  14. halliburton (12)
  15. hummer (10)
  16. harlod (10)
  17. heironymious (9)
  18. hemorrhoids (7)
  19. hammersack (6)

(apparently a list of the most common fillers for the middle initial in Jesus H. Christ)

Anyway, here's what my script does to the above list:

  1. harold (3550)
  2. horatio (1320)
  3. hitler (1120)
  4. henry (784)
  5. hector (358)
  6. haploid (315)
  7. hopping (50)
  8. herbert mulroney (44)
  9. hopscotching (29)
  10. hominibus (19)
  11. honkey (19)
  12. hermoine (18)
  13. hieronymus (13)
  14. halliburton (12)
  15. hummer (10)
  16. harlod (10)
  17. heironymious (9)
  18. hemorrhoids (7)
  19. hammersack (6)

And here's the code:


/*
Copyright (c) 2007 Christian yates
christianyates.com
chris [at] christianyates [dot] com
Licensed under the MIT License: 
http://www.opensource.org/licenses/mit-license.php
 
Inspired by work of Ingo Schommer
http://chillu.com/2007/9/30/jquery-columnizelist-plugin
*/
(function($){
  $.fn.columnizeList = function(settings){
    settings = $.extend({
      cols: 3,
      constrainWidth: 0
    }, settings);
    // var type=this.getNodeType();
    var container = this;
    if (container.length == 0) { return; }
    var prevColNum = 10000; // Start high to avoid appending to the wrong column
    var size = $('li',this).size();
    var percol = Math.ceil(size/settings.cols);
    var tag = container[0].tagName.toLowerCase();
    var classN = container[0].className;
    var colwidth = Math.floor($(container).width()/settings.cols);
    var maxheight = 0;
    // Prevent stomping on existing ids with pseudo-random string
    var rand = Math.floor(Math.random().toPrecision(6)*10e6);
    $('<ul id="container'+rand+'" class="'+classN+'"></ul>').css({width:$(container).width()+'px'}).insertBefore(container);
    $('li',this).each(function(i) {
      var currentColNum = Math.floor(i/percol);
      if(prevColNum != currentColNum) {
        if ($("#col" + rand + "-" + prevColNum).height() > maxheight) { maxheight = $("#col" + rand + "-" + prevColNum).height(); }
        $("#container"+rand).append('<li class="list-column-processed"><'+tag+' id="col'+rand+'-'+currentColNum+'"></'+tag+'></li>');
      }
      $(this).attr("value",i+1).appendTo("#col"+rand+'-'+currentColNum);
      prevColNum = currentColNum;
    });
    $("li.list-column-processed").css({
      'float':'left',
      'list-style':'none',
      'margin':0,
      'padding':0
    });
    if (settings.constrainWidth) {
      $(".list-column-processed").css({'width':colwidth + "px"});
    };
    $("#container"+rand).after('<div style="clear: both;"></div>');
    $("#container"+rand+" "+tag).height(maxheight);
    // Add CSS to columns
    this.remove();        
    return this;
  };
})(jQuery);

Download

There are only two parameters - cols, the number of columns to break the list into, and constrainWidth a boolean (defaulting to false) to specify whether you want all columns to be the same width.

I've tested with IE 6&7, FF3, Safari3 and Opera 9.something (for the three Opera users on the planet). The code could use a bit of refactoring perhaps for the purpose of beautification.

Update: I've added this to the jQuery Plugin site.

Comments

Me no understand.


Blogs are suppose to be fun to read not hurt my brain. Ouch!


What, you don't find this fun? Oh, how I pity you, Mr. Webb.


This is very nice :)
but if i would like to do this, i would make it manually instead of
relying on JS code to do me the work. for small things as this manual coding
is preferred I believe.


Nice work! This helped me out a lot.

Two things that can be improved in my opinion.

  1. Why do you put a .css({width:$(container).width()+'px'}) on the first <ul>? This will cause problems with flexible layouts when someone resizes the browser window.
    Why use it anyway, as <ul> is a block-level element which automatically scales to the size of its parent. If you really want to have a fixed width on this <ul> you can put it in your stylesheet, now there is no way to overwrite it.
  2. Unfortunately it does not work with ordered lists

My bad. It DOES work with ordered lists. You're the man :)


@Sjoerd - You're correct. The css addition is probably superfluous, and left over from an earlier iteration I can no longer remember. Taking it out doesn't change the output. Thanks for the feedback!


chris's picture

The numbers on the above ordered list don't show on this installation of IE7. Ideas?


Ah. I hate Internet Explorer.

If you search for 'ie7 list float markers' you'll find that IE just does silly stuff when floats are involved with list elements.

I've put a hack in place here that adds a margin-left of 30px to each list element, which prevents IE from hiding them. I still need to update the jQuery plugins site.


chris's picture

Thanks Chris. I hate IE too. Works beautifully now, well done and thanks again.


Hello Chris,

I really like this script. I am trying to figure out how to get rid of left margin for the first column. I tried to control it with css by setting left-margin: 0px for the ul and li but it didn't work.

Thanks in advance.


Great job on the plugin. Only problem I found is that whenever the random id for the ul that is generated is a decimal, my whole list disappears (test by refreshing the page with the columns repeatedly).

To fix it I changed
var rand = Math.random().toPrecision(6)*10e6;
to
var rand = Math.floor(Math.random().toPrecision(6)*10e6);

It seems to work now.


Hi Chris, great script. Works beautifully. However, I'm working on an application that will allow a user to select the number of columns they want their list to be. Some may want to default to 1 column. If I set the script to create a single column, I lose the list altogether. Any ideas?

Thanks!


Thanks Chris.


Hi had the same behavior and applied the same fix in my code. No need to share it as it has already posted by Parag here :-) This fix works fine for me. Christian, you should patch your 'official' code to avoid new users to encounter the same bug again.


Thanks for the nudge Chris. I've updated the code here and on the jQuery plugins page.


chris's picture

Pretty nice stuff Chris.

Would be better, if you can implement list sorting option also, like alpha order in the list etc in the plugin option.

Sometime columns creates alignment issues in FF 3.5.7 on the other hand it works pretty cool in IE 6.0

Good work!

-Mohammed Arif


Thanks Mohammed. If you could point to an example where the alignment is incorrect in FF, I'll take a look.

As for the sorting -- I'm not sure if that's something I'd include in this plugin, simply because there are probably better list-sorting plugins out there already that take into account the multiple ways you can sort a list (alpha, numeric, currency, time, etc.). You could always sort the list, then columnize it.


chris's picture

On pages that didn't need the columnizeList function I was getting an error relating to container[0].tagName.toLowerCase();. I suspect it's because the 'container' array was not populated with any items, so I added if (container.length == 0) { return } above the container[0].tagName.toLowerCase(); line and that fixed the problem. Perhaps not an elegant solution but it seems to have fixed the errors. (I'm using jQuery 1.4.2)


Andy,

Thanks for the note. I've not tested against jQuery 1.4.x yet, but will do so soon.


chris's picture

This is great but would be even better if it took a "gutter" setting, which would be the space between each column. My problem is, I'm using it with an unordered list. I thought I would be able to set padding or margins. But any padding or margin I add seems to mess up the width calculations, and pushes the second column below the first.


@gkasp

When we use jQuery's .css() method, this is essentially like adding styles inline to the element. In jquery.columnizelists.js, we're doing the following:

$("li.list-column-processed").css({
'float':'left',
'list-style':'none',
'margin':0,
'padding':0
});

Which jQuery injects into the selected objects as:

style="float: left; list-style-type: none; list-style-position: initial; list-style-image: initial; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "

So following the rules of CSS precedence, the CSS that we're applying with jQuery takes precedence over those rules applied in an external stylesheet or in a block.

In order to get around this, you can use the !important parameter in the property you with to override.

Also, rather than overriding the individual list elements (the lowest <li>), I've refactored the plugin so that we're applying a class to the column (.list-column-processed) that you can add styles to as in this example. This gives you the gutter effect.

Note that this refactoring is in the latest version of the code, which you can download or check out on the Google code page. The same technique works for the older version of the script, though you'll have to set up the selector differently, like #container > li or similar.

@Andy Ford -

I re-read your post - I assume you're populating a list from the server side, but always using a static $().columnizeList(); in your template, which would indeed generate an error regardless of the browser or jQuery version, because you're trying to call a DOM element that doesn't exist. Your solution is about as simple as it can get, so I've included it in the latest release.


chris's picture

Why do you set the value here:

$(this).attr("value",i+1).appendTo("#col"+rand+'-'+currentColNum);

We actually use this value in an onclick event, so this was messing up our values. I can fix it, just wondering why you did this.


@gkasp - That's the method we're using to properly set the numbering of the <li> elements when this script is used on <ol>'s (ordered lists).

W3C seems to be waffling on whether the value and start attributes are deprecated or not. They deprecated both in the HTML4 spec ... only to bring them back in HTML5. However, this is the only way (that doesn't involve a bunch of css hackery) to make the columnized list appear continuously numbered (note - the start attribute on the <ol> element works too.

If we omit these attributes, the columnized list would appear as multiple separate lists, each starting from 1. So instead of
1,2,3 || 4,5,6 || 7,8,9
you'd get
1,2,3 || 1,2,3 || 1,2,3

You should note that both specs state that the value should only contain an integer representing the number of the current list item. If you're stashing data there, it would be better to use jQuery's Data method. Also note that value != $(element).val() in this case.


chris's picture

Chris, thanks for the great suggestion, I've switched to using jQuery's data().

Meanwhile, just FYI this doesn't work well with just one item in the list. I know, why would you even use it in that case ... but I was trying to be consistent in treating dynamic lists that can be from 1 to 1000 items (we switch to a scroll box when too many for columns). If you have just one, the height doesn't get set. This might not even be visibly obvious depending on how the element is used, but since we were using it to mask another object, that object behind was always shown!

Anyway, thanks so much for contributing this excellent plugin. And for replying to messages.


First let me say that I've used this plugin a few times before and I've been very happy with the code it produces. It's always done the job well and without a lot of fuss.

I was pretty shocked to see that it has big issues when being applied to multiple lists on the same document, though. It takes the <li>s from all selected <ul>s and merges them into one big list.

I've never written a jQuery plugin before, so maybe I'm just missing something, but I went ahead and put in an each() loop so it will properly treat each matched <ul> as a separate list and do its thing for each one.

I've put the very slightly modified code on pastebin since I didn't see any mention of this being on svn or git anywhere.

Anyway, thanks again for the great work. It has certainly saved me some headaches.


Yikes, I just noticed I left an alert in there. Here's the fixed code: http://pastebin.com/AWTDqRSt.


@ JR, yes, this is an excellent point. If you use a selector that matches multiple containers, I bet it makes a mess. I'll review your patch. BTW - it's in SVN here.


chris's picture

hey nice work on the script. works in chrome but the html5 multi column script works in chrome my problem is ie. I have yet to find a script for multi column that works in ie. heres a link to the page that doesn't work. it seems to work on your demo site in ie so can you tell me what my site has thats messing it up? thanks a million! http://futuregamespc.com/demo/few-column-list.html (the page that your script doesn't work on in ie.


I'm seeing another error on that page, probably before the multi-column script loads - browserdetect.js causes an error. It doesn't look related to my script as far as I can tell.


chris's picture

oh wow i'm stupid should have looked at the console before... On the other hand though I realized IE8's console has really improved a ton! Haha thanks for the help its working great now. I love your script its the only multi column thing I have found on the internet that works in internet explorer! :D


Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <img> <b> <blockquote> <s>
  • Lines and paragraphs break automatically.
  • Easily link to terms in various wikis. For help, see <a href="/interwiki/1">interwiki</a>.
  • Images can be added to this post.

More information about formatting options