0

I am conditionally showing menu items. All is fine but want to remove parent item if returning no child items.

For example if I have a parent menu (it will always be a custom link with #) called More and it has multiple child items (mostly one level). Now if I am hiding child items based on user roles and for some roles no child items available, in that case, I want to remove More menu items also since it has no child.

I am trying with the following code but since $item is an object, array_search won't work. So how can I check for child-parent and remove the menu item if it has no child?

array_search( $item[ 'ID' ], array_column( $item, 'menu_item_parent' ) )

Below is the working code that hides menu item based on user roles.

public static function exclude_menu_items( $items, $menu, $args ) {

    if ( current_user_can( 'administrator' ) ) {
        return $items;
    }

    foreach ( $items as $key => $item ) {

        if ( $page = get_post( $item->object_id ) ) {
            if ( $page->post_type == 'page' ) {
                $template  = get_post_meta( $page->ID, '_wp_page_template', TRUE );
                $post_type = self::get_cpt_for_template( $template );

                if ( $post_type && ( ! current_user_can( 'cp_access_' . $post_type ) || ! ( new self() )->is_current_user_granted_for_module( $post_type ) ) ) {
                    unset( $items[ $key ] );
                }
            }
        }
    }

    return $items;
}

2 Answers 2

1

If you want to use the wp_get_nav_menu_items hook, then try the function below which should be hooked after the exclude_menu_items function:

// Example when NOT using a class: (you already defined the exclude_menu_items function)
add_filter( 'wp_get_nav_menu_items', 'exclude_menu_items', 10, 3 );
add_filter( 'wp_get_nav_menu_items', 'exclude_menu_items2' );

function exclude_menu_items2( $items ) {
    if ( current_user_can( 'administrator' ) ) {
        return $items;
    }

    $mores   = [];
    $parents = [];
    foreach ( $items as $key => $item ) {
        if ( '#' === $item->url && 'More' === $item->title ) {
            $mores[] = $item->ID;
        }

        if ( ! in_array( $item->menu_item_parent, $parents ) ) {
            $parents[] = $item->menu_item_parent;
        }
    }

    $mores = array_diff( $mores, $parents );
    foreach ( $items as $i => $item ) {
        if ( in_array( $item->ID, $mores ) ) {
            unset( $items[ $i ] );
        }
    }

    return $items;
}

Just make sure to run the appropriate validation so that the menu items don't get messed, e.g. on the Menus admin screen.

And the two functions can also be combined:

function exclude_menu_items( $items, $menu, $args ) {
    if ( current_user_can( 'administrator' ) ) {
        return $items;
    }

    $mores   = [];
    $parents = [];
    foreach ( $items as $key => $item ) {
        if ( $page = get_post( $item->object_id ) ) {
            // ... your code here.
        }

        if ( ! empty( $items[ $key ] ) ) {
            if ( '#' === $item->url && 'More' === $item->title ) {
                $mores[] = $item->ID;
            }

            if ( ! in_array( $item->menu_item_parent, $parents ) ) {
                $parents[] = $item->menu_item_parent;
            }
        }
    }

    $mores = array_diff( $mores, $parents );
    foreach ( $items as $i => $item ) {
        if ( in_array( $item->ID, $mores ) ) {
            unset( $items[ $i ] );
        }
    }

    return $items;
}

And actually — when wp_nav_menu() is called — with the wp_nav_menu_objects hook, it's simpler to remove the "More" items which have no children:

add_filter( 'wp_nav_menu_objects', function ( $items ) {
    if ( is_admin() ) { // sample validation..
        return $items;
    }

    return array_filter( $items, function ( $item ) {
        return ! ( '#' === $item->url && 'More' === $item->title &&
            ! in_array( 'menu-item-has-children', $item->classes )
        );
    } );
} );
2
  • This looks great to me. Let me try and get back to you. Thanks a lot and appreciated. Commented Jun 3, 2020 at 4:46
  • Sure, take your time. :)
    – Sally CJ
    Commented Jun 3, 2020 at 5:55
0

Here you may find a more in depth approach on dealing with the limitations of the wp_get_nav_menu_items

It uses SQL to determine the children but provides far more flexibility in order to achieve full control of what you are actually want to do:

   //this query produces all you need to have for the children at any depth. It is explained on the readme and will spare you the add on walkers.   
                            $query = "SELECT e.id, e.post_title, CASE WHEN c.meta_value='custom' THEN ( d.meta_value) ELSE concat ('/', e.post_name ,'/') END as linkurl, b.post_id 
                                                FROM wp_postmeta a FORCE INDEX (post_id)
                                                JOIN wp_postmeta b FORCE INDEX (post_id) ON a.post_id=b.post_id
                                                JOIN wp_postmeta c FORCE INDEX (post_id) ON c.post_id=b.post_id
                                                JOIN wp_postmeta d FORCE INDEX (post_id) ON d.post_id=b.post_id
                                                JOIN wp_posts e ON e.id=b.meta_value
                                                WHERE d.meta_key='_menu_item_url' AND c.meta_key='_menu_item_object' AND b.meta_key='_menu_item_object_id' 
                                                AND a.meta_key='_menu_item_menu_item_parent' AND a.meta_value='" . $submenu->ID . "'";

                            $resultsData = $wpdb->get_results($query);

                            foreach ($resultsData as $childrenData) {

                                $menu_html_s .=   '<!-- Lets print each of the children  -->';
                                $menu_html_s .=   '<div class="menu-sub menu-sub-accordion menu-active-bg">';
                                $menu_html_s .=   '<div class="menu-item">';
                                $menu_html_s .=   '<a class="menu-link" href="' . $childrenData->linkurl . '">';
                                $menu_html_s .=   '<span class="menu-bullet">';
                                $menu_html_s .=   '<span class="bullet bullet-dot"></span>';
                                $menu_html_s .=   '</span>';
                                $menu_html_s .=   '<span class="menu-title">' . $childrenData->post_title . '</span>';
                                $menu_html_s .=   '</a>';
                                $menu_html_s .=   '</div>';
                                $menu_html_s .=   '</div>';

More details here:

https://github.com/exonianp/wp-bootstrap5-accordeon-vertical-menu

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.