Log
Splitting Lists into Two Columns with Javascript
Inspired by A List Apart’s article Bulleted Lists: Multi-Layered Fudge, I decided that a better way to make a two column list is with Javascript. That way, the markup doesn’t get polluted with extra non-semantic lists. All that is needed with my approach, which could be easily adapted, is a class on each list to be converted to two columns.
The javascript:
function twoCols(src, type)
{
var origList = src;
var leftList = document.createElement(type);
var rightList = document.createElement(type);
var container = document.createElement('div');
var items = origList.getElementsByTagName('LI');
var itemsLength = items.length/2;
for (i = 0; i < itemsLength; i++)
{
leftList.appendChild(items[0]);
}
itemsLength = items.length;
for (i = 0; i < itemsLength; i++)
{
rightList.appendChild(items[0]);
}
container.appendChild(leftList);
container.appendChild(rightList);
leftList.setAttribute('class', 'left');
rightList.setAttribute('class', 'right');
container.setAttribute('class','twocol');
if (document.all)
{
leftList.setAttribute('className', 'left');
rightList.setAttribute('className', 'right');
container.setAttribute('className','twocol');
}
if (type == 'ol')
{
rightList.setAttribute('start', leftList.getElementsByTagName('LI').length + 1 );
}
origList.parentNode.replaceChild(container, origList);
}
function allTwoCols (whichclass, type)
{
var uls = document.getElementsByTagName(type);
for (var i=0; i< uls.length; i++)
{
if (uls[i].getAttribute('class') == whichclass ||
uls[i].getAttribute('className') == whichclass)
{
twoCols(uls[i], type.toLowerCase());
}
}
}
The css:
div.twocol ul, div.twocol ol {
float: right;
width: 40%;
margin: 0;
padding: 0;
list-style-position: inside;
}
div.twocol ul {
list-style-type: square;
}
div.twocol .left {
float: left;
position: relative;
}
div.twocol {
margin: 0;
padding: 0;
}
To convert your lists:
allTwoCols('two', 'UL');
allTwoCols('two', 'OL');
The css was explained well in the ALA article, so let’s take a look at the javascript. The function twoCols does the real real work in converting the list to two columns. It takes two arguments: src and type. The first, src, is a reference to the list you want to convert. The second, type, accepts two possible values: “UL” and “OL”. We start out by using the createElement method to create new elements: two new lists and a div to hold them. We then populate a new array, items, with the existing list items: var items = origList.getElementsByTagName('LI');.
The next bit of code breaks the list items in half and appends them to the two new lists:
var itemsLength = items.length/2;
for (i = 0; i < itemsLength; i++)
{
leftList.appendChild(items[0]);
}
itemsLength = items.length;
for (i = 0; i < itemsLength; i++)
{
rightList.appendChild(items[0]);
}
Here’s the tricky part: we need to look at the number of items in the items array before we add them to the new lists. That’s because the appendChild method removes the item from the array. For the same reason, we use the first item in the items array (items[0]). Doing this also maintains the original order of the list items.
Initially, we set the variable itemsLength to be half the number of items because we only want the first half. We also assume that if there’s an odd number of list items, the left hand column will have the extra one. After we build the left list, we need to reset the value of itemsLength because we want to get all of the remaining list items.
Next, attach the new lists to the div:
container.appendChild(leftList);
container.appendChild(rightList);
Next, add class names to the new items. Internet Explorer requires that you use the className attribute to apply a class to an element, so we filter for IE with document.all.
leftList.setAttribute('class', 'left');
rightList.setAttribute('class', 'right');
container.setAttribute('class','twocol');
if (document.all)
{
leftList.setAttribute('className', 'left');
rightList.setAttribute('className', 'right');
container.setAttribute('className','twocol');
}
Next, if the list is an ordered list, we need to start the right hand list with a logical number, so we add the start attribute with a value that is calculated by grabbing the number of items in the left hand list and adding one.
if (type == 'ol')
{
rightList.setAttribute('start', leftList.getElementsByTagName('LI').length + 1 );
}
Then we replace the original list with the new div that we created:
origList.parentNode.replaceChild(container, origList);
The hard part’s done. The allTwoCols function loops through our document and converts all the lists that match our class and our type, either “UL” or “OL”.
The first line grabs all the lists matching the type and stuffs them into an array called “uls”. Then it loops through and compares each list’s class to the class we specified, again using an or operator to check for IE. If the class matches, we call the twoCols function.
function allTwoCols (whichclass, type)
{
var uls = document.getElementsByTagName(type);
for (var i=0; i< uls.length; i++)
{
if (uls[i].getAttribute('class') == whichclass ||
uls[i].getAttribute('className') == whichclass)
{
twoCols(uls[i], type.toLowerCase());
}
}
}
Works like a charm in Safari, Firefox, and IE. It probably works in other too, I just haven’t had a chance to test it.
02/17/05 04:51PM Design Geekiness
Comments
I hoped it would be useful. I would think that it could be modified to skip child LIs whose parent is not the element specified.
04/08/05 3:27PM
Hi!
Great script! It solved my longstanding problem of getting a list in two-columns. It is really neat - if the javascript is not supported or not loaded, the old style list is displayed.
Good work!
And this box is cool too - it understands html on the fly. :-)
07/14/06 1:30PM
Great script, I’m trying to get it to work with my Wordpress navigation sidebar. Pretty hard so far, will have to kill the nested listingm but that I can do.
Harder is that the current script doesn’t handle extra elements (i.e. <a href..) within the list items (the items just dissapear). Any suggestions on fixing that?
Sorry, total JS noob here…
02/04/07 9:27AM
@Ruben, what browser are you using? I’ve tested this script pretty extensively and I’ve seen no problems like you describe. Do you have a sample page where you’re trying it out?
02/07/07 8:41PM
What a great script Chris. I was just about to write a PHP and CSS write around when I thought I’d see if someone had solved it with Javascript. Thanks…
03/28/07 3:15PM
Awesome script! I modified it for three columns:
<html>
<head>
<script type=”text/javascript”>
function threeCols(src, type)
{
var origList = src;
var leftList = document.createElement(type);
var centerList = document.createElement(type);
var rightList = document.createElement(type);
var container = document.createElement(’div’);
var items = origList.getElementsByTagName(’LI’);
var itemsLength = items.length/3;
for (i = 0; i < itemsLength; i++)
{
leftList.appendChild(items[0]);
}
itemsLength = items.length/2;
for (i = 0; i < itemsLength; i++)
{
centerList.appendChild(items[0]);
}
itemsLength = items.length;
for (i = 0; i < itemsLength; i++)
{
rightList.appendChild(items[0]);
}
container.appendChild(leftList);
container.appendChild(centerList);
container.appendChild(rightList);
leftList.setAttribute(’class’, ‘left’);
centerList.setAttribute(’class’, ‘center’);
rightList.setAttribute(’class’, ‘right’);
container.setAttribute(’class’,’threecol’);
if (document.all)
{
leftList.setAttribute(’className’, ‘left’);
centerList.setAttribute(’className’, ‘center’);
rightList.setAttribute(’className’, ‘right’);
container.setAttribute(’className’,’threecol’);
}
if (type == ‘ol’)
{
centerList.setAttribute(’start’, leftlist.getElementsByTagName(’LI’).length + 1 );
rightList.setAttribute(’start’, centerList.getElementsByTagName(’LI’).length + 1 );
}
origList.parentNode.replaceChild(container, origList);
}
function allThreeCols (whichclass, type)
{
var uls = document.getElementsByTagName(type);
for (var i=0; i< uls.length; i++)
{
if (uls[i].getAttribute(’class’) == whichclass ||
uls[i].getAttribute(’className’) == whichclass)
{
threeCols(uls[i], type.toLowerCase());
}
}
}
</script>
<style type=”text/css”>
div.threecol ul, div.threecol ol {
float: left;
width: 25%;
margin: 0;
padding: 0;
list-style-position: inside;
}
div.threecol ul {
list-style-type: square;
}
div.threecol .left {
float: left;
position: relative;
}
div.threecol {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<ul ID=”example” class=”three”>
<li>a</li>
<li>b</li>
<li>c</li>
<li>d</li>
<li>e</li>
<li>f</li>
<li>g</li>
<li>h</li>
<li>i</li>
<li>j</li>
<li>k</li>
</ul>
<script type=”text/javascript”>
allThreeCols(’three’, ‘UL’);
allThreeCols(’three’, ‘OL’);
</script>
</body>
</html>
08/09/07 2:21AM
Thank you so much for very needed script. It works wonders!
Going the next step with your script, I’d like to add a box or background color behind the list.
So, I’ve tried two things:
1. Include a something like “background: #000080” to the CSS on each column. But this ends up rendering background only behind the text, not the entire bulleted area.
2. Put a div class wrapper, but it turns out that box created by the wrapper does not align with the javascripted-generated list. The list goes past the box, even though there are no dimension parameters for the wrapper. One needs to add several breaks after script in order to get the box the right size. However, if the list is dynamic, this method doesn’t work.
Might you all have some ideas on how to add a background or box around the bulleted list?
Thanks, again, and kind regards,
Alec
08/18/07 2:20AM
@alec:
The bullets appear outside of the box because the default position for the bullet is “outside”. You can do one of two things: 1. set list-style-position: inside; 2. Add about 1.5em padding to the left side of the lists.
Either of those will bring your bullets inside the block. Remember that setting the list-style-position to inside effectively moves the bullets inline, so if you have a list item that wraps at the end of the line, the bullet will be flush with the start of the line.
08/18/07 6:35AM
This section seems really excessive:
var itemsLength = items.length/2;
for (i = 0; i
{
leftList.appendChild(items[0]);
}
itemsLength = items.length;
for (i = 0; i
{
rightList.appendChild(items[0]);
}
container.appendChild(leftList);
container.appendChild(rightList);
So, you’re moving items to one list until the halfway point, then starting moving what’s left into the second list. But you only really need to move the second half of the list, right? In that case, it seems like this ought to work - start moving items beginning with the midpoint until you’ve reached the size of the original list.
var itemsLength = items.length/2;
for (i = itemsLength; i
{
rightList.appendChild(items[0]);
}
container.appendChild(rightList);
This might have implications for the rest of the code as well. Thanks for posting it though, I yanked the above and rewrote what I needed in jQuery (no OL/UL logic though).
12/14/07 3:20PM
Hey great post man,thanks a lot…
08/14/08 9:49AM
Add a Comment
Have something to say about what I wrote here? Let’s hear it!
- Your name and email address are required, but your email will not be displayed on the site
- If you provide a URL, a link to your site will appear
- You may use the following HTML:
<strong>bold</strong><em>italic</em><a href="http://url">links</a>
- Double line breaks will be converted to paragraphs.
- As you type, you should get a nice little preview of your comment directly below the text box.
- I reserve the right to edit any comment for any reason (I’ll be reasonable).
Recently Played on iTunes
-
“Abierto”
Soul Food Taqueria
Tommy Guerrero
10/06/08 16:28 -
“Hit Or Miss”
Punk Rock Days: The Best Of DBL
Down By Law
10/06/08 16:25 -
“Start!”
Compact Snap
The Jam
10/06/08 16:22
Everett Lindsay:
this was, by far, my favorite solution to the split list problem. additionally, it’s the first justifiable application i’ve seen for “building” client-side html on the fly. tried to hack this so’s it would split lists at my beck (based on a <li> id), however…i ran into a setback. this technique only works on lists that aren’t nested.
i know that javascript has some functions for parsing literal lines of code. but that stuff is pretty obscure. (at least in my circles…) this would seem the way to go for a more complex, unique list when preserving markup semantics is your motivation.
04/08/05 3:07PM