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 :)

Christian's picture

@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!

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

Christian's picture

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.

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.

Christian's picture

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

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

Christian's picture

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.

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)

Christian's picture

Andy,

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

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.

Christian's picture

@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 <style/> 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 $(<selector>).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.

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.

Christian's picture

@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, 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.

Christian's picture

@ 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.

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.

Christian's picture

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.

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

congrats for your plugin, love it!
i'm working with an horizontal scroll, so i would like to list 3 items by column, without a fixed number of them. In other words, I would like to specify rows number, not column ones. I tried to do so but was imposible to me. is your code able to do that?
thanks in advance

I'm working on a Drupal-based site with the Nice Menus module. I have a taxonomy generated submenu of our various departments. There are lots and lots of them. Your plug-in sounds like what I've been hunting for. Unfortunately, I'm a javascript/jquery newbie. Can you point me in the right direction in terms of basics for applying your plug-in? I have jquery.min.js linked up from the JQuery Update module. Is that enough? And where I'm really stuck is - what do I need to change in your plug-in code or my html ids and classes to target the right menu list? Thanks!

Figured it out by viewing source on this page. Easier than I thought. Now to figure out how to make it play nice with Nice Menus... Thanks for the great code!

I recently had to format data dynamically into newspaper like columns using jQuery. The problem with most scripts I encountered was that the load times varied from 30 seconds to 2 minutes due to the amount of data. Thankfully I came a crossed your plugin, which brought load times down to an average of 6-10 seconds.

Christian's picture

Steven,

I'm glad this script works well for you, and I'm happy to hear that it delivered good performance also. To tell you the truth, I hadn't done any optimization for performance.

-c

This works perfectly and is easy to implement. However, I haven't been able to get it to work inside a dropdown menu. Is there any way to create a multi-column list inside a menu with this? Thanks!!

I'm trying to get this script to work with Textpattern. Do I need to install jquery first?

Christian's picture

@James - yes, you need to load jQuery prior to this script.

这是一个很好的插件.谢谢.

不过我还是有一些疑问.插件中的列数是固定的.如何让它不固定呢?让它随浏览器宽度而定.比如http://www.pagesthink.com/

希望你能帮上我.

I am a Chinese. English is not good. Sorry

This is a good plug-ins. Thank you.

But I still have some doubts. Plugins in the number of columns is fixed. How it is not fixed it? Let it be with the browser width. For example http://www.pagesthink.com/

I hope you can help on me.

Wonderful! I don't have to reinvent the wheel! Thanks mate!