Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I'm trying to create a mega menu walker. Unfortunately, walkers completely escape my coding knowledge. I could really use some help getting it working. Here's the features I need:

  • Wrap the second-level <ul> in <section>. [COMPLETE]
  • When a user sets the class "break" on an <li> in the second level <ul>, make that <li> the start a new <ul>. If it's the first <li> in the list, don't do anything, to prevent the formation of empty unordered lists. [COMPLETE]
  • When a user sets the class "widget" on an <li> in the first level that has a sub <ul>, append a widget to the end of that <ul>. [COMPLETE]
  • Add class mega-menu-columns-# to first level <li> elements that contain drop downs with multiple columns and/or a widget. The # represents the number of <ul> elements, +1 for the widget if it exists. [COMPLETE]

I have a bit of code to do some of this, but not all of it. There are cut-out sections below:

Wrap second level <ul> in <section>:

function start_lvl(&$output, $depth = 0, $args = array()) {
    if ($depth == 0) {
        $output .= "<section>";
    }
    $output .= "<ul class=\"sub-menu\">";
}
function end_lvl(&$output, $depth = 0, $args = array()) {
    $output .= "</ul>";
    if ($depth == 0) {
        $output .= "</section>\n";
    }
}

Generate the widget HTML:

ob_start();
dynamic_sidebar("Navigation Callout");
$widget = ob_get_contents();
ob_end_clean();

The output HTML would be:

<ul>
    <li id="menu-item-1" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-has-children menu-item-1 mega-menu-columns-2">
        <a href="http://www.example.com/about/">
            About Us
        </a>
        <section>
            <ul class="sub-menu">
                <li id="menu-item-2" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-2">
                    <a href="http://www.example.com/about/company-profile/">
                        Company Profile
                    </a>
                </li>
                <li id="menu-item-3" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-3">
                    <a href="http://www.example.com/about/leadership-team/">
                        Leadership Team
                    </a>
                </li>
                <li id="menu-item-4" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-4">
                    <a href="http://www.example.com/about/professional-affiliations/">
                        Professional Affiliations
                    </a>
                </li>
            </ul>
            <ul class="sub-menu">
                <li id="menu-item-5" class="break menu-item menu-item-type-post_type menu-item-object-page menu-item-5">
                    <a href="http://www.example.com/about/clients/">
                        Clients
                    </a>
                </li>
                <li id="menu-item-6" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-6">
                    <a href="http://www.example.com/about/partnerships/">
                        Partnerships
                    </a>
                </li>
            </ul>
        </section>
    </li>
    <li id="menu-item-7" class="widget menu-item menu-item-type-post_type menu-item-object-page menu-item-has-children menu-item-7 mega-menu-columns-3">
        <a href="http://www.example.com/services/">
            Services
        </a>
        <section>
            <ul class="sub-menu">
                <li id="menu-item-8" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-8">
                    <a href="http://www.example.com/services/civil-engineering/">
                        Civil Engineering
                    </a>
                </li>
                <li id="menu-item-9" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-9">
                    <a href="http://www.example.com/services/land-planning/">
                        Land Planning
                    </a>
                </li>
                <li id="menu-item-10" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-10">
                    <a href="http://www.example.com/services/surveying/">
                        Surveying
                    </a>
                </li>
            </ul>
            <ul class="sub-menu">
                <li id="menu-item-11" class="break menu-item menu-item-type-post_type menu-item-object-page menu-item-11">
                    <a href="http://www.example.com/services/information-technology/">
                        Information Technology
                    </a>
                </li>
                <li id="menu-item-12" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-12">
                    <a href="http://www.example.com/services/subsurface-utility-engineering/">
                        Subsurface Utility Engineering
                    </a>
                </li>
            </ul>
            <aside>
                <h6>Widget Title</h6>
                <p>Maecenas quis semper arcu. Quisque consequat risus nisi. Sed venenatis urna porta eros malesuada euismod. Nulla sollicitudin fringilla posuere. Nulla et tellus eu nisi sodales convallis non vel tellus.</p>
            </aside>
        </section>
    </li>
    <li id="menu-item-13" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-15">
        <a href="http://www.example.com/contact/">
            Contact Us
        </a>
    </li>
</ul>

UPDATE: My counters are giving me grief. They're only counting after the sub menu's been generated, which doesn't help me. See this screenshot to understand what I mean:

The top numbers are being pulled in start_el. The bottom numbers are being pulled in end_el. As you can see, the top numbers aren't counting my .breaks as they should. They count the widget class because those are being counted in $depth = 0. Someone please save me from this horribleness!

// mega menu walker
/*
    ONE REMAINING BUG:
    - Need to add class to LI containing mega-menu-columns-#
*/
class megaMenuWalker extends Walker_Nav_Menu {
    private $column_limit = 3; /* needs to be set for each site */
    private $show_widget = false;
    private $column_count = 0;
    static $li_count = 0;
    function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
        $classes = empty($item->classes) ? array() : (array) $item->classes;
        $item_id = $item->ID;
        if ($depth == 0) self::$li_count = 0;
        if ($depth == 0 && in_array("widget", $classes)) {
            $this->show_widget = true;
            $this->column_count++;
        }
        if ($depth == 1 && self::$li_count == 1) {
            $this->column_count++;
        }
        if ($depth == 1 && in_array("break", $classes) && self::$li_count != 1 && $this->column_count < $this->column_limit) {
            $output .= "</ul><ul class=\"sub-menu\">";
            $this->column_count++;
        }
        if ($depth == 0 && $this->column_count > 0) {
            $mega_menu_class = " mega-menu-columns-" . $this->column_count;
        }
        $class_names = join(" ", apply_filters("nav_menu_css_class", array_filter($classes), $item));
        $class_names = " class=\"" . esc_attr($class_names . $mega_menu_class) . "\"";
        $output .= sprintf(
            "<li id=\"menu-item-%s\"%s><a href=\"%s\">%s</a>",
            $item_id,
            $class_names,
            $item->url,
            $item->title
        );
        self::$li_count++;
    }
    function start_lvl(&$output, $depth = 0, $args = array()) {
        if ($depth == 0) {
            $output .= "<section>";
        }
        $output .= "<ul class=\"sub-menu\">";
    }
    function end_lvl(&$output, $depth = 0, $args = array()) {
        $output .= "</ul>";
        if ($depth == 0) {
            if ($this->show_widget) {
                ob_start();
                dynamic_sidebar("Navigation Callout");
                $widget = ob_get_contents();
                ob_end_clean();
                $output .= $widget;
                $this->show_widget = false;
            }
            $output .= "</section>";
        }
    }
    function end_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
        if ($depth == 0 && $this->column_count > 0) {
            /* needs to be added to opening level 0 li */
            $column_count_class = " mega-menu-columns-" . $this->column_count;
            $output .= $column_count_class;
            /* end */
            $this->column_count = 0;
        }
        $output .= "</li>";
    }
}   

UPDATE 2: Here's an example of output with comments describing how the mega-menu-columns- class should count things:

<ul>
    <!-- +1 because this has a class of "widget" -->
    <li id="menu-item-1" class="widget menu-item menu-item-type-post_type menu-item-object-page menu-item-has-children menu-item-1 mega-menu-columns-3">
        <a href="http://www.example.com/about/">
            About Us
        </a>
        <!-- +1 because a drop down exists -->
        <!-- gets added by my walker -->
        <section>
        <!-- end gets added by my walker -->
            <ul class="sub-menu">
                <!-- +0 because this "break" is the first child -->
                <li id="menu-item-2" class="break menu-item menu-item-type-post_type menu-item-object-page menu-item-2">
                    <a href="http://www.example.com/about/company-profile/">
                        Company Profile
                    </a>
                    <ul>
                        <!-- +0 because this "break" is in level 2 -->
                        <li id="menu-item-3" class="break menu-item menu-item-type-post_type menu-item-object-page menu-item-3">
                            <a href="http://www.example.com/about/our-team/">
                                Our Team
                            </a>
                        </li>
                    </ul>
                </li>
                <li id="menu-item-4" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-4">
                    <a href="http://www.example.com/about/leadership-team/">
                        Leadership Team
                    </a>
                </li>
                <li id="menu-item-5" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-5">
                    <a href="http://www.example.com/about/professional-affiliations/">
                        Professional Affiliations
                    </a>
                </li>
            <!-- gets added by my walker -->
            </ul>
            <ul class="sub-menu">
            <!-- end gets added by my walker -->
                <!-- +1 because this "break" is in level 1 and not the first child -->
                <li id="menu-item-6" class="break menu-item menu-item-type-post_type menu-item-object-page menu-item-6">
                    <a href="http://www.example.com/about/clients/">
                        Clients
                    </a>
                </li>
                <li id="menu-item-7" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-7">
                    <a href="http://www.example.com/about/partnerships/">
                        Partnerships
                    </a>
                </li>
            </ul>
            <!-- gets added by my walker for .widget -->
            <section>
                <header>
                    <h1>Widget Title</h1>
                </header>
                <p>This is a widget. It was hard to make appear!</p>
            </section>
            <!-- end gets added by my walker for .widget -->
        <!-- gets added by my walker -->
        </section>
        <!-- end gets added by my walker -->
    </li>
</ul>

UPDATE: Here's my final Walker and Functions. This does exactly what I wanted it to. Thanks for the help!

// mega menu walker
class megaMenuWalker extends Walker_Nav_Menu {
    private $column_limit = 3;
    private $show_widget = false;
    private $column_count = 0;
    static $li_count = 0;
    function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
        $classes = empty($item->classes) ? array() : (array) $item->classes;
        $item_id = $item->ID;
        if ($depth == 0) {
            self::$li_count = 0;
        }
        if ($depth == 0 && in_array("widget", $classes)) {
            $this->show_widget = true;
            $this->column_count++;
        }
        if ($depth == 1 && self::$li_count == 1) {
            $this->column_count++;
        }
        if ($depth == 1 && in_array("break", $classes) && self::$li_count != 1 && $this->column_count < $this->column_limit) {
            $output .= "</ul><ul class=\"sub-menu\">";
            $this->column_count++;
        }
        $class_names = join(" ", apply_filters("nav_menu_css_class", array_filter($classes), $item)); // set up the classes array to be added as classes to each li
        $class_names = " class=\"" . esc_attr($class_names) . "\"";
        $output .= sprintf(
            "<li id=\"menu-item-%s\"%s><a href=\"%s\">%s</a>",
            $item_id,
            $class_names,
            $item->url,
            $item->title
        );
        self::$li_count++;
    }
    function start_lvl(&$output, $depth = 0, $args = array()) {
        if ($depth == 0) {
            $output .= "<section>";
        }
        $output .= "<ul class=\"sub-menu\">";
    }
    function end_lvl(&$output, $depth = 0, $args = array()) {
        $output .= "</ul>";
        if ($depth == 0) {
            if ($this->show_widget) {
                ob_start();
                dynamic_sidebar("Navigation Callout");
                $widget = ob_get_contents();
                ob_end_clean();
                $output .= $widget;
                $this->show_widget = false;
            }
            $output .= "</section>";
        }
    }
    function end_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
        if ($depth == 0 && $this->column_count > 0) {
            $this->column_count = 0;
        }
        $output .= "</li>";
    }
}

// add mega-menu-columns-# classes
function add_column_number($items, $args) {
    static $column_limit = 3;
    static $post_id = 0;
    static $x_key = 0;
    static $column_count = 0;
    static $li_count = 0;
    $tmp = array();
    foreach($items as $key => $item) {
        if (0 == $item->menu_item_parent) {
            $x_key = $key;
            $post_id = $item->ID;
            $column_count = 0;
            $li_count = 0;
            if (in_array("widget", $item->classes, 1)) {
                $column_count++;
            }
        }
        if ($post_id == $item->menu_item_parent) {
            $li_count++;
            if ($column_count < $column_limit && $li_count == 1) {
                $column_count++;
            }
            if (in_array("break", $item->classes, 1) && $li_count > 1 && $column_count < $column_limit) {
                $column_count++;
            }
            $tmp[$x_key] = $column_count;
        }
    }
    foreach($tmp as $key => $value) {
        $items[$key]->classes[] = sprintf("mega-menu-columns-%d", $value);
    }
    unset($tmp);
    return $items;
};

// add the column classes
add_filter("wp_nav_menu_args", function($args) {
    if ($args["walker"] instanceof megaMenuWalker) {
        add_filter("wp_nav_menu_objects", "add_column_number");
    }
    return $args;
});

// stop the column classes function
add_filter("wp_nav_menu", function( $nav_menu ) {
    remove_filter("wp_nav_menu_objects", "add_column_number");
    return $nav_menu;
});
share|improve this question
1  
This is super amazing and exactly what I have been wanting to build for a long time! I was not sure how to handle the creation of new columns and such so I really love the idea of just setting a class name on items to create new columns and also the ability to add widget areas, just genius! I am however having some trouble with your finished code...it outputs the corrects HTML however I am getting this PHP error... Warning: Missing argument 2 for add_column_number() Did you encounter this problem? –  jasondavis Jul 12 at 23:00
    
I don't think so, it seems to be working fine on the two sites I've used it on so far. I've updated the above code, in case I changed something when implementing it. Try it again and let me know if it still doesn't work; I'm not very good with PHP but I'd be happy to try and help :) –  Rev Jul 14 at 19:40
1  
What I had to do was change this function add_column_number($items, $args) { into this function add_column_number($items) { removing the $args and it works fine for me with that change, pretty strange! THanks for sharing your code though it is just what I have needed for a long time –  jasondavis Jul 14 at 20:57
    
No problem, I know how difficult it is to do this, which is why I shared my full code :) Glad it helped! –  Rev Jul 15 at 14:51
add comment

1 Answer 1

up vote 4 down vote accepted
+100

If I understand the problem setup correctly, you could try to do the break and widget class counting within the wp_nav_menu_objects filter.

Here's an updated example, it's rather expanded because of the extra debug part:

add_filter( 'wp_nav_menu_objects', 
    function( $items, $args  ) {

        // Only apply this for the 'primary' menu:    
        if( 'primary' !== $args->theme_location ) 
            return $items;

        // Here "x_" means the latest root li (depth 0)
        static $x_pid           = 0;      // post ID of the latest root li (depth 0) 
        static $x_key           = 0;      // array key of the latest root li (depth 0)
        static $x_cols          = 0;      // n breaks or widgets gives n+1 columns  
        static $x_has_dropdown  = false;  // if the latest root li (depth 0) has dropdown

        // Internals:
        $tmp            = array();  
        $debug_string   = '';
        $show_debug     = true;  // Edit this to your needs:

        foreach( $items as $key => $item )
        {
            // Debug:
            $debug                              = array();
            $debug['ID']                        = $item->ID;
            $debug['title']                     = $item->title;
            $debug['key']                       = $key;
            $debug['x_key']                     = $x_key;
            $debug['depth']                     = '';
            $debug['menu_item_parent']          = $item->menu_item_parent;
            $debug['has_widget_class']          = 0;
            $debug['is_depth_1_first_child']    = 0;
            $debug['x_has_dropdown']            = 0;
            $debug['has_break_class']           = 0;
            $debug['x_cols_increase']           = 0;

            // Collect columns increaments:
            $inc = 0;

            // Depth 0:
            if( 0 == $item->menu_item_parent )
            {
                $debug['depth'] = 0;

                // Resets:
                $x_key          = $key;
                $x_pid          = $item->ID;                            
                $x_cols         = 0;
                $x_has_dropdown = false;

                // If widget class exists:
                if( in_array( 'widget', $item->classes, 1 ) )
                {
                    $debug['has_widget_class'] = '1';
                    $inc++; 
                }       
            }

            // Depth 1:
            if( $x_pid == $item->menu_item_parent )
            {   
                $debug['depth'] = 1;

                // Increase the columns count for an existing dropdown:
                if( ! $x_has_dropdown )
                {
                    $inc++;
                    $x_has_dropdown = true;
                }

                // Check for the 'break' class: 
                if( in_array( 'break', $item->classes, 1 ) )
                {
                    $debug['x_has_break_class'] = 1;

                    // First li child:
                    if( $x_key+1 == $key+0 )
                    {
                        $debug['is_depth_1_first_child'] = 1;
                    }
                    else
                    {
                        $debug['is_depth_1_first_child'] = 0;
                        $inc++; 
                    }
                }

                $t[$x_key] = $x_cols;
            }           

            $debug['x_has_dropdown'] = (int) $x_has_dropdown;

            // Increase the columns count:
            $debug['x_cols_increase'] = $inc;           
            $x_cols += $inc;
            $debug['x_cols'] = $x_cols;

            // Collect the debug:
            $debug_string .= print_r( $debug, 1 );
        } // end foreach

        // Show debug info:
        if( $show_debug ) 
            printf( "<!-- debug: %s -->", $debug_string );

        // Insert the new 'mega menu' class to the corresponding menu object:
        foreach( $t as $key => $value )
        {
            $items[$key]->classes[] = sprintf( 'mega-menu-columns-%d', $value );
        }

        return $items;

    }
, PHP_INT_MAX, 2 );

With your current menu tree, I get this debug information:

<!-- debug: Array
(
    [ID] => 3316
    [title] => About Us
    [key] => 1
    [x_key] => 0
    [depth] => 0
    [menu_item_parent] => 0
    [has_widget_class] => 1
    [is_depth_1_first_child] => 0
    [x_has_dropdown] => 0
    [has_break_class] => 0
    [x_cols_increase] => 1
    [x_cols] => 1
)
Array
(
    [ID] => 3317
    [title] => Company Profile
    [key] => 2
    [x_key] => 1
    [depth] => 1
    [menu_item_parent] => 3316
    [has_widget_class] => 0
    [is_depth_1_first_child] => 1
    [x_has_dropdown] => 1
    [has_break_class] => 0
    [x_cols_increase] => 1
    [x_has_break_class] => 1
    [x_cols] => 2
)
Array
(
    [ID] => 3318
    [title] => Our Team
    [key] => 3
    [x_key] => 1
    [depth] => 
    [menu_item_parent] => 3317
    [has_widget_class] => 0
    [is_depth_1_first_child] => 0
    [x_has_dropdown] => 1
    [has_break_class] => 0
    [x_cols_increase] => 0
    [x_cols] => 2
)
Array
(
    [ID] => 3319
    [title] => Leadership Team
    [key] => 4
    [x_key] => 1
    [depth] => 1
    [menu_item_parent] => 3316
    [has_widget_class] => 0
    [is_depth_1_first_child] => 0
    [x_has_dropdown] => 1
    [has_break_class] => 0
    [x_cols_increase] => 0
    [x_cols] => 2
)
Array
(
    [ID] => 3320
    [title] => Professional Affiliations
    [key] => 5
    [x_key] => 1
    [depth] => 1
    [menu_item_parent] => 3316
    [has_widget_class] => 0
    [is_depth_1_first_child] => 0
    [x_has_dropdown] => 1
    [has_break_class] => 0
    [x_cols_increase] => 0
    [x_cols] => 2
)
Array
(
    [ID] => 3321
    [title] => Clients
    [key] => 6
    [x_key] => 1
    [depth] => 1
    [menu_item_parent] => 3316
    [has_widget_class] => 0
    [is_depth_1_first_child] => 0
    [x_has_dropdown] => 1
    [has_break_class] => 0
    [x_cols_increase] => 1
    [x_has_break_class] => 1
    [x_cols] => 3
)
Array
(
    [ID] => 3322
    [title] => Partnerships
    [key] => 7
    [x_key] => 1
    [depth] => 1
    [menu_item_parent] => 3316
    [has_widget_class] => 0
    [is_depth_1_first_child] => 0
    [x_has_dropdown] => 1
    [has_break_class] => 0
    [x_cols_increase] => 0
    [x_cols] => 3
)
 -->

If you want to check if the walker object is of the megaMenuWalker class, you can use:

if( ! is_object( $args->walker ) || ! is_a( $args->walker, 'megaMenuWalker' ) )
    return $items;

instead of

if( 'primary' !== $args->theme_location ) 
            return $items;

I hope this helps.

share|improve this answer
    
Strangely this is outputting 1 as the number. See hfracquetandfitness.myweblinx.net (inspect the first li in the top navigation). EDIT -- I think I know why. Break gets added to direct child <li> elements, not the current <li>. It needs to check that it's a direct child, and not the first child that has the class of break as well. –  Rev Jun 21 at 13:44
    
I updated the answer, I had a typo (&& instead of ||). –  birgire Jun 21 at 13:47
    
Ah, that helped a bit, but I see it's counting if the current list item has the class of break. It actually needs to count if direct children have the class of break, not itself, and exclude the first child li in the child ul, if that makes sense. –  Rev Jun 21 at 13:51
    
I assume the root <li> items are at depth 0, and then I count the break/widget classes of the sub <li> items at depth 1. I think I should start the $x_cols at 1 instead of 0, since n breaks gives n+1 columns. –  birgire Jun 21 at 14:15
1  
You're welcome. I updated the answer with an example how you can check the walker class instead of the theme location. Best of luck with your project. –  birgire Jun 23 at 15:37
show 6 more comments

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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