1

I need to load a full menu for navigation in a custom theme with active trail, and can't find any way to do it, but that can't be right. I can't imagine such an obvious and widespread use-case having no core support, but I've scoured menu.inc and found nothing. The functions offered range from inadequate (no active trail) to completely unusable (menu is missing children for the CSS dropdowns/submenus outside the active trail, and even empty when needed most, such as on broken links).

I've tried using the following functions in my THEME_preprocess_page hook:

  • menu_tree (only renders siblings of the active trail--the navbar can even end up empty)
  • menu_tree_page_data (same result)
  • menu_tree_all_data (contrary to the documentation--luckily--this method does not return hidden items, but there's no active trail)

So far as I can tell, my only options are to copy and rewrite several functions from menu.inc, or to load two separate menus and apply deep analysis/merging to find and duplicate the active trail from the incomplete menu into the complete one--and this is just as bad because the data structures don't match. Is there no other way to get a full menu with active trail?

Also, what if I do go with the latter option using menu_tree_all_data? Will I end up exposing links that aren't properly going through access checks, or have similar issues? How the heck does Drupal itself load a full menu, like the Navigation menu which doesn't go away while visiting other parts of the site?

I did find another nearly identical question but no one answered it and that was 9 months ago. This is for a project that's already running well behind schedule due pretty much exclusively to unforseen Drupal-related issues, so any pointers are much appreciated.

EDIT: Whoops...tried to be thorough and missed a major detail :P I'm on Drupal 7.

1
  • Hi there, please, could you add a Drupal version?
    – Ambidex
    Commented Aug 15, 2012 at 6:15

3 Answers 3

0

First of all, I have to say, the menu system that is implemented in Drupal is, in my eyes, really dodgy. You'll encounter stuff you simply cannot realize with the core implementation, which you would think is basic use cases.

Depending on which Drupal major you are using, I'd suggest taking a look at the Menu Settings per Content Type (CTM) when your at D6. This module will enable you to setup menu settings per content-type, which contains an option to "expand items by default".

On D7 this feature is built-in, when you create a menu item under admin/structure/menu you will find a checkbox Show as expanded, which will do the same.

Edit:

Menu block module

I was just thinking about it again, and remembered that menu_block module also will help you out in this situation. You can create a block which has a lot more options then al the built-in menu functionalities. Also there is an option: Expand all children of this tree., I think that'll be your ticket out of this problem, and should leave the burdon for your clients to actually manually expand them

5
  • Can "show as expanded" be made to apply by default or triggered in code (in some Drupal-'proper' way)? Clients will be creating content that they put in the menu so the procedure needs to be kept straightforward without hidden steps. Commented Aug 15, 2012 at 16:15
  • It looks like I'll have to write my own "menu_tree_full_data" function based on "menu_tree_page_data." I'm awarding the answer since the "show as expanded" hint is what finally put me on the right track. Commented Aug 16, 2012 at 3:00
  • @HonoredMule, I've edited my original answer. Please take a look at it, I think this will sort out your problems, without the need of hooking PHP up in the core module.
    – Ambidex
    Commented Aug 16, 2012 at 8:34
  • I'm actually already using menu_block for other things, but because I need to inject the menu into a menu bar area and not a region, this wouldn't work. It's possible I might have been able to turn the menu bar into a region, but my efforts in that direction went quite badly, screwing up the surrounding header layout. I now have a solution I'm very happy with, and I'll post it shortly since it's very reusable. Commented Aug 16, 2012 at 15:53
  • Turning the menu bar into a region would have been pretty wonky anyway, since no block would ever be able to go there without the layout going pear-shaped. Commented Aug 16, 2012 at 16:34
2

I just want to add my experience to the lot. I had similar issues using Drupal menu. The solution I used is:

  • Set all menu items with children to "Expanded". This as to be done manually: Structure > Menus > List links, edit links which has children, check the Show as expanded checkbox.
  • Use menu_tree_page_data function (example: $menu_tree = menu_tree_page_data('main-menu')) to get the menu, with all its children AND with active trail.
  • Create a renderable array using menu_tree_output function (example: $renderable_array = menu_tree_output($menu_tree)) to prepare the menu for rendering.
  • Use the renderable array as content for my custom block, but you could call drupal_render($renderable_array) to get the rendered HTML representing the menu.
2
  • This does it perfectly - no additional modules or templating. One small edit use main-menu in code rather than main_menu Commented Jun 17, 2020 at 12:43
  • 1
    Thanks Scott Lawrence, I have edited my answer. Commented Jun 19, 2020 at 0:56
1

I ended up writing my own "menu_tree_full_data" function that builds the menu, still using all the appropriate cache facilities, etc. Also take note any Drupal Core developers around: this is how you write a function that isn't incomprehensibly nested 9 blocks deep.

function menu_tree_full_data($menu_name) {
    $tree = &drupal_static(__FUNCTION__, array());

    // Check if the active trail has been overridden for this menu tree.
    $active_path = menu_tree_get_path($menu_name);

    // Generate a cache ID(cid) specific for this page
    $item = menu_get_item($active_path);
    $cid = "links:$menu_name:full:{$item['href']}:{$GLOBALS['language']->language}";

    // Did we already build this menu during this request?
    if(isset($tree[$cid]))
        return $tree[$cid];

    // If the static variable doesn't have the data, check {cache_menu}.
    $cache = cache_get($cid, 'cache_menu');
    if($cache && isset($cache->data)) {
        $tree_params = $cache->data;
        if(isset($tree_params))
            return $tree[$cid] = menu_build_tree($menu_name, $tree_params);
    }

    $tree_params = array(
        'min_depth' => 1,
        'max_depth' => null,
    );
    // Parent mlids; used both as key and value to ensure uniqueness.
    // We always want all the top-level links with plid == 0.
    $active_trail = array(0 => 0);

    // Find a menu link corresponding to the current path. If $active_path
    // is NULL, let menu_link_get_preferred() determine the path.
    $active_link = menu_link_get_preferred($active_path, $menu_name);
    // The active link may only be taken into account to build the
    // active trail, if it resides in the requested menu. Otherwise,
    // we'd needlessly re-run _menu_build_tree() queries for every menu
    // on every page.
    if(@$active_link['menu_name'] == $menu_name) {
        // Use all the coordinates, except the last one because there
        // can be no child beyond the last column.
        for($i = 1; $i < MENU_MAX_DEPTH; $i++) {
            if($active_link['p' . $i])
                $active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
        }
    }

    $parents = $active_trail;
    do {
        $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
            ->fields('menu_links', array('mlid'))
            ->condition('menu_name', $menu_name)
            //->condition('expanded', 1)
            ->condition('has_children', 1)
            ->condition('plid', $parents, 'IN')
            ->condition('mlid', $parents, 'NOT IN')
            ->execute();
        $num_rows = FALSE;
        foreach($result as $item) {
            $parents[$item['mlid']] = $item['mlid'];
            $num_rows = TRUE;
        }
    } while($num_rows);
    $tree_params['expanded'] = $parents;
    $tree_params['active_trail'] = $active_trail;

    // Cache the tree building parameters using the page-specific cid.
    cache_set($cid, $tree_params, 'cache_menu');

    // Build the tree using the parameters; the resulting tree will be cached by _menu_build_tree().
    return $tree[$cid] = menu_build_tree($menu_name, $tree_params);
}

Aside from changing the cache key, commenting out the expanded condition, dropping the no-longer-relevant $max_depth and $only_active_trail arguments, and the branching on $item['access'] (whether the user can access current page), this is logically equivalent to menu_tree_page_data. 4 of the extra 5 levels of nested code blocks could be just as easily removed from the original function as well.

Now I can finally just load the menu I need as I need it with no mess or fuss, like so:

function THEME_preprocess_page(&$vars) {

    if(@$vars['main_menu']) {
        $menu = variable_get('menu_main_links_source', 'main-menu');
        $vars['main_menu'] = menu_tree_output(menu_tree_full_data($menu));
    }
}

And use it in my theme like so:

<?= render($main_menu); ?>

Or something like so (this is how a lot of other themes seem to handle it, though it just makes a mess for me):

<?php if ($main_menu): ?>
    <nav id="primary-menu" role="navigation">
        <?php print theme('links__system_primary_menu', array(
            'links' => $main_menu,
            'attributes' => array(
                'class' => array('links', 'inline', 'clearfix'),
            )
        )); ?>
    </nav>
<?php endif; ?>

Of course you still have to define or include the menu_tree_full_data function somewhere as well. For me, it goes in a "Shorthand" module I maintain with various simplified object-oriented API wrappers, where it'll also get turned into a method in SH_Menu class and get loaded on demand with shorthand_load_menu(); (Note you've gotta switch __FUNCTION__ for 'menu_tree_full_data' if you do something similar, or the caching won't work correctly.)

0

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.