3

I would like to create a walker function, or modify somehow current wp_nav_menus so it can produce a custom output. Let's imagine this structure:

menu item 1
menu item 2
 submenu item a
 submenu item b
menu item 3
 submenu item c
 submenu item d
 submenu item e

The easiest part is to show in one place only main menu items (main menu 1,2,3), and it can be done with wp_nav_menus, so we don't need to code it.

The problematic part is to show in other place submenu items of currently main item... So if user is on 'menu item 1' page, nothing shows. If user is on 'menu item 2' page, our "new" custom menu shows:

submenu item a
submenu item b

Same menu items renders when user is on either of two above pages (menu items).

When user clicks 'menu item 3' and visits its target page, he sees submenu items c,d,e, same as after clicking on any of those submenu items.

If the menu has 3rd level elements (and deeper), custom menu should display all child elements of current most top menu element, beside that top element itself (which will be listed in top part of the site described in first place)

Is there a way of creating such functionality?

The idea is close to: Display a portion/branch of menu just it needs to be dynamic and display child elements of current menu branch/all child elements of top menu branch element if they exist.

AFAIK I can't use get_ancestors, because it works only with hierarchical taxonomies, and here we are not talking about menu created from post/pages hierarchical structure, but with use of menu editor.

SOLVED: Seems I was able to create correct functions kombining few methods:

/**
 * The API function, just a wrap for wp_nav_menu adding filter first and removing after
 */ 
function filtered_nav_menu( $args = array() ) {
    $echo = isset($args['echo']) ? (bool)$args['echo'] : true;
    $args['echo'] = false;
    add_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
    $menu = wp_nav_menu( $args );
    remove_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
    if ( $echo ) echo $menu;
    else return $menu;
}

/**
 * The filter callback, return the filtered elements
 */
function gmfnv_filter( $items ) {
    $found_top_parent_ID = false;
    foreach ($items as $item) {
        if ( ($item->menu_item_parent == 0 && $item->current_item_ancestor == 1) || ($item->menu_item_parent == 0 && $item->current == 1) ) {
            $found_top_parent_ID = $item->ID;
        }
    }
    $children  = submenu_get_children_ids( $found_top_parent_ID, $items );
    foreach ( $items as $key => $item ) {
        if ( ! in_array( $item->ID, $children ) )
            unset($items[$key]);
    }
    return $items;
}

/**
 * Helper function: return children of an element using wp_filter_object_list
 */
function submenu_get_children_ids( $id, $items ) {
    $ids = wp_filter_object_list( $items, array( 'menu_item_parent' => $id ), 'and', 'ID' );
    foreach ( $ids as $id ) {
        $ids = array_merge( $ids, submenu_get_children_ids( $id, $items ) );
    }
    return $ids;
}

What it does - it search for current branch top ancestor by going through each $items element and checking against:

($item->menu_item_parent == 0 && $item->current_item_ancestor == 1) || ($item->menu_item_parent == 0 && $item->current == 1)

This conditional needs to be 2 part, because top level menu element might also be current one.

If top level $item is found, then function submenu_get_children_ids is used to list all its child elements.

9
  • possible duplicate of Display a portion/ branch of the menu tree using wp_nav_menu()
    – Milo
    Oct 13, 2013 at 16:38
  • Well, almost, just I need it to be dynamic and display children of only current brach (either it is a top menu element as a current page or any to its child elements) Oct 13, 2013 at 17:07
  • you can make this solution dynamic by just passing the current page's top most ancestor as the submenu argument. see get_queried_object and get_ancestors.
    – Milo
    Oct 14, 2013 at 23:33
  • I don't think it can be done with use of get_ancestors, as it works only on hierarchical elements, and we are talking about editable menu structure, that don't connect with posts/page hierarchical, or am I wrong? Oct 22, 2013 at 16:22
  • Any reason you wouldn't just control what is shown / hidden using css and the rich class naming that WP provides for it's menus? Oct 23, 2013 at 15:07

1 Answer 1

5
+100

The idea

Instead of create a custom walker I thought was easier filter the items using the filter wp_nav_menu_objects hook.

This hook is defined in /wp-includes/nav-menu-templates.php and whe is fired, it pass to functions hooking into it an array $sorted_menu_items that contains all the elements of the menu being printed, if the function alter that array, adding or removing items, the resulting menu will be altered.

The method

If the filter to wp_nav_menu_objects is applyed direclty, all menus will be filtered, so I thought was better create a function that wrap wp_nav_menu adding the filter before calling wp_nav_menu and remove it after: in this way only the wanted menu is filtered.

The filter workflow

  1. Cycle all items passed by wp_nav_menu_objects filter hook
  2. Create 2 helper arrays: one only of parent ids, one with items like $itemid => $parentid
  3. While looping items, also check it item url match the current url
  4. if url not match return only parent items
  5. if url match, using the helper arrays cretaed, return the wanted elements

The code

The code I wrote for that, make use of 5 functions, so I create a plugin to contains all that, here the code:

<?php
/**
 * Plugin Name: Filtered Nav Menus
 * Author: Giuseppe  Mazzapica
 * Plugin URI: http://wordpress.stackexchange.com/questions/118720/
 * Author URI: http://wordpress.stackexchange.com/users/35541/
 */

/**
 * The API function, just a wrap for wp_nav_menu adding filter first and removing after
 */ 
function filtered_nav_menu( $args = array() ) {
  $echo = isset($args['echo']) ? (bool)$args['echo'] : true;
  $args['echo'] = false;
  add_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
  $menu = wp_nav_menu( $args );
  remove_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
  if ( $echo ) echo $menu;
  else return $menu;
}

/**
 * The filter callback, return the filtered elements
 */
function gmfnv_filter( $items ) {
  $found = false;
  $parents = $items_tree = $allowed = array();
  $all_items = $items;
  while ( ! empty( $items ) ) {
    $item = array_shift( $items );
    $items_tree[$item->ID] = $item->menu_item_parent;
    if ( (int) $item->menu_item_parent == 0 ) $parents[] = $item->ID;
    if ( isset($item->current) && $item->current ) $found = $item->ID;
  }
  if ( ! $found ) {
    $ids = $parents;
  } else {
    $tree = gmfnv_get_tree( $found, $all_items, $items_tree );
    $ids = array_merge( $parents, $tree );
  }
  foreach ( $all_items as $item ) {
    if ( in_array( $item->ID, $ids ) ) $allowed[] = $item;
  }
  return $allowed;
}


/**
 * Helper function: take the matched element if and the helper array and
 * return the item ancestors by gmfnv_get_parents,
 * and the children of these ancestors returned by gmfnv_get_parents
 * using gmfnv_get_parents
 */
function gmfnv_get_tree( $test, $items, $tree ) {
  $parents = gmfnv_get_parents( $test, $items );
  $parents[] = $test;
  $n = array();
  foreach ( $parents as $parent ) {
    $n = array_merge( $n, gmfnv_get_childrens( $parent, $tree ) );
  }
  return array_unique( $n );
}


/**
 * Helper function: return ancestors of an element using the helper array
 */
function gmfnv_get_parents( $test, $items ) {
  $parents = array();
  foreach( $items as $item ) {
      if (
        (isset($item->current_item_ancestor) && $item->current_item_ancestor)
        || (isset($item->current_item_ancestor) && $item->current_item_ancestor)
      ) $parents[] = $item->ID;
  }
  return $parents;
}


/**
 * Helper function: return children of an element using the helper array
 */
function gmfnv_get_childrens( $test, $tree ) {
  $children = array();
  foreach ( $tree as $child => $parent ) {
    if ( $parent == $test ) $children[] = $child;
  }
  return $children;
}

How To

Create a file containing this plugin put in plugins folder and activate.

When you want filter a menu as required, instead using wp_nav_menu( $args ) use

filtered_nav_menu( $args );

Disclaimer

Code provided as is, no warranty, but just quickly tested on PHP 5.4, WP 3.7 with twentytherteen theme active, and no other plugins: it worked in this case.

12
  • Ok, it seems it doesn't work as expected. With simple menu structure: {MainitemA, MainitemB: {subitem1, subitem2, subitem3}} it displays only MainitemA and MainitemB whenever I go. Oct 26, 2013 at 13:07
  • Trying debuggind the code I have found, that in gmfnv_filter( $items ), $found is alway false, even when the item $item->menu_item_parent != 0 (there is a parent to the item). Oct 26, 2013 at 13:19
  • You are genius. Thanks so much for the help. Ill spend hours on figuring out how this work, but hopefully will get it to the bare bones :). Again, great thanks. Oct 26, 2013 at 20:27
  • PS - I strongly suggest posting this solution as a plugin to WP repository :). Oct 26, 2013 at 20:28
  • Hmm... Somehow after few days of break code don't work anymore :/. And as I cant seem to understand the code completely - I don't know where the problem lies :( Nov 12, 2013 at 18:06

Your Answer

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

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