Sunday, April 19, 2009

Styling first and last list items (and Wordpress)

Sometimes a design calls for the first and last items in a list to be styled differently for the others, for example a line or column of overlapping tabs.

For most modern browsers this is trivial and can be done using the list-class:first-child and list-class:last-child psuedoclasses. It's just that Internet Explorer doesn't support these very well.

If you are hardcoding the list or writing the generation routine it is also simple to generate 'first-item' and 'last-item' classes in the appropriate place.

<ul id="list-id">
<li class="ie-list-first list-type-item">Stuff</li>
<li class="list-type-item">Stuff</li>

<li class="list-type-item">Stuff</li>
<li class="ie-list-last-last list-type-item">Stuff</li>
</ul>

When templating for an existing CMS it may not be desirable to dig deeply enough to customise the code. For example WordPress's wp_list_pages() function (with appropriate parameters) returns something like this:
<li class="page_item page-item-4 current_page_item">Page link</li>
<li class="page_item page-item-9">Page link</li>

<li class="page_item page-item-13">Page link</li>
<li class="page_item page-item-15">Page link</li>
<li class="page_item page-item-34">Page link></li>
<li class="page_item page-item-25">Page link</li>

I spent some time following the function trail before deciding it was better to add the classes after the string was generated. Fortunately if you set 'echo=0' wp_list_pages() will return the string to a variable. After that it's just a matter of string substitution - first and last incidence of class=" . (This might get a bit trickier if you're using nested lists, but I try and keep depth=1 if possible).

It turned out the easiest way to do this was with a preg_replace - but it still requires a little trickery because you can't tell preg_replace to start from the end of a string. Instead you have to reverse everything, do the replacement, and then reverse everything again.
//get li string
$pages = wp_list_pages('title_li=&depth=1&echo=0' );

//add styling identifiers for first item
$pages = preg_replace('/class="/','class="ie-list-first ', $pages, 1); //note space on end of replacement string

//add styling identifiers for last item
$reversedString = strrev($pages);
$reversedSearch = '/'.strrev('class="').'/';
$endClass = strrev('class="ie-list-last '); //note space on end

$pages = strrev(preg_replace($reversedSearch,$endClass, $reversedString, 1));

//output
echo $pages;

I make sure the ie-list class is first because it makes it easier to call in your IE-specific stylesheet without worrying that another CMS generated class will get precedence.

19 comments:

sean.afk said...

I think you're missing a double quote:


$reversedSearch = '/'.strrev('class="').'/';

Robert said...

By jove, I think you might be right. Edited to fix, thank you very much :)

Simon said...

Hi Robert

This piece of code was just what I was looking for to style the first and last elements of my wordpress menu but I'm getting an error on this line:

$endClass = strrev('class='"ie-list-last '); //note space on end

Everything after that in the editor is not showing the correct syntax highlighting which indicates that something is not quite right and the browser is throwing a "Parse error" at that line in the code. If I comment it out everything works fine but no last item class... any idea's/help would be greatly appreciated :-)

Thanks.

Robert said...

Hi Simon,

you're right, a close inspection shows I've managed to stick an extra single quote in that line right next to the double quote.

replace it with

$endClass = strrev('class="ie-list-last '); ; //note space on end

[fixes post]

Simon said...

Thanks Robert, that fixed the error :-) but now I'm getting a different issue... the class being applied to the last list item is ending up like this:

<li class="last " page-item-13="" page_item="">

page-item-13 and page-item are the existing classes but it looks like the quotes are still out of sync.

Also, a question: what would I need to change (or is it even viable) to add the first and last class to the a tag withing the list element rather than the list element itself?

Cheers :-)

Robert said...

Hi Simon,

I think the problem is now with this line:

$reversedSearch = '/'.strrev('class=').'/';

should be

$reversedSearch = '/'.strrev('class="').'/';

If not I'll have to go dig up the project I was working on at the time and see what I was actually doing. Obviously I did not manage to copy the code perfectly into blogger :-/

It's feasible to change the <a> tag as long as you can identify a constant string around the class to work with

- if it doesn't already have a class changing out the search string for /<a/ and the replace string for /<a class="[class-name]"/ ought to do it

- I'd leave it as is and apply css as eg:
li.first a:link, li.first a:visited {[style]} myself

Simon said...

You're a legend! Thank you so much :-)

I had to make a couple tweeks to get the link class working but it now works a treat! The reason I want the class on the a tag rather than the list item is so that I can style the linked based on the currently selected item:

.current-page-item { default list item style }
.current-page-item .first { first list item style }
.current-page-item .last{ last list item style }

this way I can add menu ends that are current page aware. :-)

I'll flick you a link to the finished site when it rolls if you like.

Robert said...

Glad to be of service :) Please do send over the link when you are done.

You know you can chain-reference classnames within the same class attribute by omitting the space between them? So for example

<li class="current-page-item"><a class="first">
.current-page-item .first

is equivalent to

<li class="current-page-item first"><a>
.current-page-item.first a

This is more useful when you have multiple elements to style within your first or last item :)

Simon said...

Wow... I didn't know that, very handy! How browser safe is chaining though? I know we all hate IE6 but it's often a pain we have to bear ;-)

Another option that came to mind was utilising jQuery's :first-child and :last-child selectors.

Robert said...

Chaining is IE6 safe in my experience.

jQuery and other javascript solutions analyse the DOM to get around IE's lack of support for :first-child, :last-child (and now :nth-child). I prefer to use css if possible because more people have javascript disabled than css, and more non-visual - actually make that non-PC (eg mobile) - browsers are css-aware than javascript aware.

stein™ said...

Great tip!

Is it possible to use this on list_categories?

I have this string;
wp_list_categories('orderby=id&show_count=0&title_li=&use_desc_for_title=1&child_of='.$this_category->cat_ID);

but i can't get it to add to the class names.

Robert said...

Hi stein™

as with wp_list_pages you need to tell wp_list categories to return the html as a string for further processing rather than echoing it to the browser directly. Use 'echo=0' for this.

$categories =
wp_list_categories('orderby=id&show_count=0&title_li=&use_desc_for_title=1&child_of='.$this_category->cat_ID&echo=0);

See http://codex.wordpress.org/Template_Tags/wp_list_categories for a list of all the available parameters.

Robert said...

Oops. Missed the concatenation.

wp_list_categories('orderby=id&show_count=0&title_li=&use_desc_for_title=1&child_of='.$this_category->cat_ID.'&echo=0');

John said...

Perfect!

xSEOn said...

The perfect solution! Thank you Robert! After hours on trying different plugins and other bul..ts your simple script gave me exactly what I needed to create a working multilingual overlapping tabbed menu in half an hour.

Randy said...

I needed to remove the border-right off my last list item in the footer, and I could not for the life of me figure out why I was having trouble manipulating the returned value of the wp_list_pages() function. I tried assigning it to a variable so I could search the $output string with other functions, I tried while/if statements, I tried CSS :last-child and adjacent selectors... nothing. Before I read your solution I did try using the echo=0 argument a few times but the Codex doesn't make it clear enough what it actually does (prevents automatic echo of $output), so I stopped using it until I read this. Thank you for this solution, this problem kept me up for many hours!

squidz said...

I too have been trying to conquer this little lingering item. I can see that the code here works. However, I don't quite get how to make it apply to my existing pages list menu. When I put the code in my functions.php file, it the creates its own list at the very top of my page. My normal menu remains with the styles unapplied.

What am I missing here?

Robert said...

Hi Squidz,

You need to put at least the line

//output
echo $pages;

in your template in place of the current page generation function (wherever it calls wp_list_pages() normally, that's header.php in the site I'm looking at right now).

I personally put the entire chunk of code at that location to make the transformation obvious.

ben said...

great bit of code cheers :)