Adding first and last class to Menu on top level only

I’m using a WordPress Menu to create a navigation structured like this:

  • Home
  • About
  • Contact
    • Sub Page

Using this code from kuroi’s response here I’m able to add first-menu-item and last-menu-item classes to the list items above:

function add_first_and_last($output) {
    $output = preg_replace('/class="menu-item/', 'class="first-menu-item menu-item', $output, 1);
    $output = substr_replace($output, 'class="last-menu-item menu-item', strripos($output, 'class="menu-item'), strlen('class="menu-item'));
    return $output;
}
add_filter('wp_nav_menu', 'add_first_and_last');

However the last-menu-item class is being added to the Sub Page list item (because it’s the last) rather than to the Contact list item.

Question: How can I make this function apply only to the top level items of a menu?

Thanks!

Solutions Collecting From Web of "Adding first and last class to Menu on top level only"

I would lean very much towards a custom walker for this but I think I’ve managed to make it work using part of that code and some of my own.

function add_position_classes_wpse_100781($classes, $item, $args) {
  static $fl;
  if (0 == $item->menu_item_parent) {
    $fl = (empty($fl)) ? 'first' : 'middle';
    $classes[] = $fl.'-menu-item';
  } 
  return $classes;
}
add_filter('nav_menu_css_class','add_position_classes_wpse_100781',1,3);

function replace_class_on_last_occurance_wpse_100781($output) {
    $output = substr_replace(
      $output, 
      'last-menu-item ', 
      strripos($output, 'middle-menu-item'), 
      strlen('middle-menu-item')
    );
    return $output;
}
add_filter('wp_nav_menu', 'replace_class_on_last_occurance_wpse_100781');

What I did was add first-menu-item and middle-menu-item to top level items only with the first filter on nav_menu_css_class. Then with the second filter I replaced the last occurrence of middle-menu-item with last-menu-item.

It works for the few test cases I tried.

I have little fix on Bainternet code, because this code not work if last item has sub item

function wpb_first_and_last_menu_class($items) {
    $items[1]->classes[] = 'first-menu-item'; // add first class

    $cnt = count($items);
    while($items[$cnt--]->post_parent != 0); // find last li item
    $items[$cnt+1]->classes[] = 'last-menu-item'; // last item class
    return $items;
}
add_filter('wp_nav_menu_objects', 'wpb_first_and_last_menu_class'); //filter to iterate each menu

something like this:

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

add_filter('wp_nav_menu_objects', 'add_first_and_last_classes_wpa100781');

I would recommend:

1) skip adding the first menu item class. That’s extra php that you don’t need. Using ul > li:first-child will let you target the first menu item in either CSS or as a jQuery selector. :first-child has great browser support (more than :last-child, so I would add a class for the last menu item).

2) Find the last top-level menu item and add a class to it. Drop this code into your theme’s functions.php file and it will add a class to only the last top-level navigation item instead of the very last navigation item like many of the code snippets in the other answers here.

// Add a class to the last top-level menu item
function fancy_last_menu_item_class($items) {
    // Create an empty array to hold the array ID's of top level menu items
    $topLevelMenuItemIDs = array();

    // Loop through all of the menu $items
    for($i=0,$count=count($items);$i<$count;$i++){
        // Check if the 'menu_item_parent' property is set to '0'
        if($items[$i]->menu_item_parent == 0){   
            // This item has no parent menu items, so it's top-level.
            // Add its array ID to the top level menu item IDs array.
            $topLevelMenuItemIDs[] = $i;
        }
    }

    // Count how many top level nav items were found, so you can target the last one
    $topLevelMenuItemCount = count($topLevelMenuItemIDs);

    // Add a class to the last top level navigation item.
    $lastMenuItemClass = 'last-menu-item';
    $items[$topLevelMenuItemIDs[--$topLevelMenuItemCount]]->classes[] = $lastMenuItemClass;

    // Return the items with the altered last item
    return $items;
}
// Hook the last menu item class to the wp_nav_menu_objects filter
add_filter('wp_nav_menu_objects', 'fancy_last_menu_item_class');