4

I've seen a lot of code on how to add a first and last class to a WordPress menu, but in my instance I would like to add a last class to the last li in the sub-menu that WP generates. Here's what I would like to achieve in it's simplest form.

<ul>
    <li>Menu item one</li>
    <li class="has-sub-menu">Menu item two
        <ul class="sub-menu">
            <li>Sub menu item one</li>
            <li>Sub menu item two</li>
            <li class="last">Sub menu item three</li>
        </ul>
    </li>
    <li>Menu item three</li>
</ul>

And here is what I currently have, that adds first and last classes to only the parent menu items. Could this be adapted?

function add_first_and_last($items) {
    $items[1]->classes[] = 'first';
    $items[count($items)]->classes[] = 'last';
    return $items;
}

add_filter('wp_nav_menu_objects', 'add_first_and_last');

I would like to stick with php for this and not use either :last-child (want it to work in ie7 & 8) or jQuery.

2
  • Could you link to the code that you've seen how to add a first and last class to a WordPress menu? It seems that this could just be adapted slightly for your purpose. Failing that a custom walker should be able to do the trick. Commented Apr 14, 2012 at 10:22
  • Hi Stephen, code is posted in my question. Not too clued up with custom walkers, any ideas? thanks
    – Andrew
    Commented Apr 14, 2012 at 10:24

3 Answers 3

5

Put the following in your functions.php

class SH_Last_Walker extends Walker_Nav_Menu{

   function display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output ) {

        $id_field = $this->db_fields['id'];

       //If the current element has children, add class 'sub-menu'
       if( isset($children_elements[$element->$id_field]) ) { 
            $classes = empty( $element->classes ) ? array() : (array) $element->classes;
            $classes[] = 'has-sub-menu';
            $element->classes =$classes;
       }
        // We don't want to do anything at the 'top level'.
        if( 0 == $depth )
            return parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );

        //Get the siblings of the current element
        $parent_id_field = $this->db_fields['parent'];      
        $parent_id = $element->$parent_id_field;
        $siblings = $children_elements[ $parent_id ] ;

        //No Siblings?? 
        if( ! is_array($siblings) )
            return parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );

        //Get the 'last' of the siblings.
        $last_child = array_pop($siblings);
        $id_field = $this->db_fields['id'];

            //If current element is the last of the siblings, add class 'last'
        if( $element->$id_field == $last_child->$id_field ){
            $classes = empty( $element->classes ) ? array() : (array) $element->classes;
            $classes[] = 'last';
            $element->classes =$classes;
        }

        return parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
    }
}

Usage

Where you use wp_nav_menu and you wish to have the 'last' class added, specify the walker above:

wp_nav_menu( array('theme_location'=>'primary','walker' => new SH_Last_Walker(),'depth' => 0) ); 

theme_location and depth can be whatever you like, the important part is the walker attribute.

Side remarks

This code can be improved - potentially by un-setting each element in the $children_elements[$parent_id] array when its called and checking when it's down to the last element (i.e. when it contains only one element). I think wp_list_filter would be suitable here. This might not improve efficiency, but might be neater.

Also, you can hard-code the parent and id fields - I've not done this for portability. The following class could be able to extend any of the provided extensions of the Walker Class (page lists, category lists etc).

6
  • Thank you Stephen, appreciate it. Worked great, although I don't understand enough about walkers to understand what is going on in the above code. Will do some further reading on walkers. On another note, I added some code into your walker that I had somewhere else to add a has-sub-menu class to the parent menu item. It works fine, but am wondering if there is anything wrong with the implentation. I added the below code just inside the walker function.
    – Andrew
    Commented Apr 14, 2012 at 22:00
  • $id = $this->db_fields['id']; if(isset($children_elements[$element->$id])) { $element->classes[] = 'has-sub-menu'; return parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output ); }
    – Andrew
    Commented Apr 14, 2012 at 22:01
  • I don't think you want/need to return parent::display_element. I'll edit the answer to reflect what I think you are after. Commented Apr 14, 2012 at 22:05
  • Hi thanks for your update. It seems to be adding the class in a weird place (within the submenu), and not on the top level menu item. I've updated my original question to reflect where I would like the class name. I did catch the misspelling of your class name (slightly different to mine) but this didn't work either.
    – Andrew
    Commented Apr 14, 2012 at 22:26
  • I've edited the answer again. All I've done is move the extra checks to the top, so the class should always be added to the <li> tag of an element which has children. I've tested it on TwentyEleven and it works as expected. :) Commented Apr 14, 2012 at 22:37
3

The jQuery would look something like this;

jQuery(document).ready( function($) {
    $('.sub-menu > li:last-child').addClass('last-item');

} );

But as for the PHP, please take a look at this answer provided by Rarst HERE and see if this helps you at all.

UPDATE

Rart' solution from the above link does it fact work and the code snippet is;

add_filter( 'wp_nav_menu_items', 'first_last_class' );

function first_last_class( $items ) {

    $first = strpos( $items, 'class=' );

    if( false !== $first )
         $items = substr_replace( $items, 'first ', $first+7, 0 );

    $last = strripos( $items, 'class=');

    if( false !== $last )
         $items = substr_replace( $items, 'last ', $last+7, 0 );

    return $items;
}

The output gave me something like this;

<ul>
    <li class="first">Menu item one</li>
    <li>Menu item two
        <ul class="sub-menu">
            <li class="first">Sub menu item one</li>
            <li>Sub menu item two</li>
            <li class="last">Sub menu item three</li>
        </ul>
    </li>
    <li class="last">Menu item three</li>
</ul>

Which then means you can style your ul ul li element via CSS selectors successfully.

11
  • thanks but as per question I want to avoid using jQuery. I've seen that answer from Rarst but unfortunately it only adds the classes to the parent menu items and not the sub-menu items
    – Andrew
    Commented Apr 14, 2012 at 10:25
  • Sorry Andrew I read that incorrectly. I'll attempt to run a test and see if I can solve this.
    – Adam
    Commented Apr 14, 2012 at 10:27
  • @Andrew I just tested Rarst' solution and it worked for me.
    – Adam
    Commented Apr 14, 2012 at 10:29
  • it worked on the 2nd level .sub-menu? Doesn't seem to work for me, but does work correctly on the first level. I'll have another look...
    – Andrew
    Commented Apr 14, 2012 at 10:32
  • No luck here, only affects the top level and not the .sub-menu. Tried on a new install of Twenty Eleven.
    – Andrew
    Commented Apr 14, 2012 at 10:39
2

something to consider: while IE7 and IE8 don't support last-child, they DO support first-child. so depending on what CSS you need to apply differently to the last item, you could reverse it.

also, FWIW, your theme is already using jQuery in other areas by default (comment section, for example) so that would be a quicker and cleaner option.

5
  • Thanks for your input. I know about :first-child support in IE7+ but :last-child was important in this instance. I'm not a big fan of relying on jQuery for adding classes like this.
    – Andrew
    Commented Apr 14, 2012 at 22:08
  • Just to note, this is exactly what jQuery is meant for.
    – Wyck
    Commented Apr 14, 2012 at 22:15
  • I guess I'm thinking that if I can do it without relying on jQuery then I might as well. If JS was not turned on it would affect the overall design. Thanks though, I see your point.
    – Andrew
    Commented Apr 14, 2012 at 22:24
  • I can't find the exact numbers, but something like less than 2% of users have JS turned off. If they do, then they are most likely accustomed to not seeing much of anything on the web.
    – Norcross
    Commented Apr 15, 2012 at 2:08
  • 2
    The Mozilla survey is from 2010 , the average amount of users with js disabled (worldwide) was 1%. Doing this type of stuff in php is just silly.
    – Wyck
    Commented Apr 15, 2012 at 6:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.