Recently I was asked to produce a web page with the following layout for some of its list data

HorizontallyWrappingLists2

Floating all items, left

My first thought was “a single list ordered list, with all the list items floated left”. That worked well but didn’t really read all that well with each “row” running left to right rather than up and down, which is how people prefer to read lists.  The other problem I had was when one of the items wrapped onto multiple lines. The extra height of wrapped items meant items on the next “row” would get stuck trying to float past it. See the “Koodoo” in the following list of Animals

FloatedLeft-Lists - 2

The issue was easy to get around by clearing on every 5th item i.e. The start of each “row”. The floating left technique works OK if the list has no particular order such as a list of images but for the data I was going to have to display left to right layout just wasn’t going to cut it. The trouble is that browsers just don’t do vertical wrapping of lists. It was at this point I was directed towards  Paul Novitski’s A List Apart article, Multi-Column Lists.

EDIT: Check out the use of inline-block, rather than floats, described at sitepoint,  for this kind of layout.

A List Apart’s approach

I am not going to repeat the entire article here it is well worth reading yourself. The author takes a very thorough approach showing 6 different methods with the 6th one being his and my preferred approach. Once you have read it come back and we can discuss where I went from there because that was certainly not the end of the journey for me.

Phew! You know that you are going to be in for a bumpy ride when the author describes what you are trying to achieve as a “minor holy grail of XHTML and CSS”. What I had to do over and above Paul’s solution was deal with dynamic data and mark-up generated on the server. The first thing I am going to show is the ASP.NET code and then finally the Javascript work I had to do.

Generating the mark-up

The only thing that was fixed was the number of columns, 4. The number of items varied from 1 to 100. The alistapart article has a fixed set of items and includes column classes in the static mark-up, mine are added in the ItemDataBound event of a repeater.

   1: int wrapAfterCount = Math.Max(textArray.Length / NumberOfColumns, 10);
   2:  
   3: private void Repeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
   4: {
   5:     if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
   6:     {
   7:         Repeater repeater = sender as Repeater;
   8:         
   9:         TextContent text = e.Item.Controls[0] as TextContent;
  10:         int wrapAfterCount = Convert.ToInt32(ViewState[String.Concat(repeater.ID, "_wrapAfterCount")]);
  11:         
  12:         double colPos = (double)(e.Item.ItemIndex + 1) / wrapAfterCount;
  13:         double col = Math.Min(Math.Ceiling(colPos), NumberOfColumns);
  14:         
  15:         string columnClass = string.Format("col{0}", col);
  16:         
  17:         text.AddClassName(columnClass);
  18:  
  19:         string key = String.Format("{0}_firstItemFor_{1}_Output", repeater.ID, columnClass);
  20:         bool firstItemForColumnOutput = this.ViewState[key] != null ? true : false;
  21:  
  22:         // Mark the FIRST item of each column with the 'topItem' class (other than the first column). This class
  23:         // is used in the client side script to offset column positions aliging the "tops" of them
  24:         if (col > 1.0 && firstItemForColumnOutput == false) 
  25:         {
  26:             text.AddClassName("topItem");
  27:             this.ViewState.Add(key, "true");
  28:         }
  29:         
  30:         text.Text += Convert.ToString(e.Item.DataItem).Trim();
  31:     }
  32: }

wrapAfterCount (Line 1) was set when the repeater was created and had its DataBind called. The reason for the Math.Min is that if the wraps list after less than 10 they look unnaturally short. It kind of depends how much room you have to play with in your layout but for me having 10 items wrapping to 4 columns after two items, looked wrong. The rest of the code runs in the ItemDataBound event

Setting the offset in Javascript

Having basically ported the static mark-up of the alistapart article to ASP.NET I found that the reality of real data meant it was a lot harder. Originally the offset for columns 2, 3 and 4 was NumberOfItemsInColumn x LineHeightOfSingleItem. This of course falls down when items start to wrap and you can’t know the height of a single item until it has been loaded in the browser . Therefore I decided the offsetting calculation was going to have to be done in Javascript. Now that may well upset a few but it is not difficult to ensure this layout degrades well when Javascript is not present.

function alignHorizontallyWrappedList(list, colCount) {
 
    var accumulatedHeight = 0;
    var accumulatedHeights = new Array();
 
    // Obtain the height if each column. This height will be used to offset the next column so that the tops of columns > 1 are 
    // all aligned
 
    for (var col = 1; col <= colCount; col++) {
        accumulatedHeight = 0;
        jQuery('.col' + col, list).each(function(index) {
            accumulatedHeight += jQuery(this).outerHeight(true);
        })
        accumulatedHeights[col - 1] = accumulatedHeight;
    }
 
    // The margin tops are set once all columns are measured rather than as we go because the margining effects the height.
    for (var heightIndex = 0; heightIndex <= accumulatedHeights.length - 1; heightIndex++) {
        jQuery('.col' + (heightIndex + 2) + '.topItem', list).css('marginTop', '-' + accumulatedHeights[heightIndex] + 'px')
    }
 
    jQuery(list).addClass('wrapped');
}

 

You may well be asking “Was it really worth it”, well that is not for me to decide but it was a fun and interesting challenge and that is why we are developers right? Many thanks to Paul Novitski as without his original article I would have given up much earlier in the search for this “holy grail”