I would have thought this would be a lot easier than it initially was, and in the end, it is really simple, but it took some time to figure it out, so I thought I’d pass along.
Innately WP creates menus that look something like this:
<ul id="menu-top-nav" class="menu">
<li id="menu-item-72" class="menu-item menu-item-type-post_type current-menu-item page_item page-item-58 current_page_item menu-item-72"><a href="about-us">About Us</a></li>
<li id="menu-item-73" class="menu-item menu-item-type-post_type current-menu-item page_item page-item-59 current_page_item menu-item-73"><a href="contact-us">Contact Us</a></li>
<li id="menu-item-74" class="menu-item menu-item-type-post_type current-menu-item page_item page-item-60 current_page_item menu-item-74"><a href="services">Services</a></li>
</ul>
I don’t know about you, but to me, that is not only bloated and ugly, it is also pretty unusable… For anyone that works in both a Development and Production environment, or has ever had to move a WP installation, those numbers will change, making any CSS really hard to write, let along readable/understandable…
What I wanted was something more like:
<ul id="menu-top-nav" class="menu">
<li id="nav-about-us" class="current_page_item"><a href="about-us">About Us</a></li>
<li id="nav-contact-us" class=""><a href="contact-us">Contact Us</a></li>
<li id="nav-services" class=""><a href="services">Services</a></li>
</ul>
Notice how the ID for each <li>
is now the page slug, prefaced with nav-
? Not that’s useful! I did keep the current_page_item
class as that’s really useful too, but otherwise, dump that fluff!
In the end, all I needed were two very small functions, implemented via a filter:
// Reduce nav classes, leaving only 'current-menu-item'
function nav_class_filter( $var ) {
return is_array($var) ? array_intersect($var, array('current-menu-item')) : '';
}
add_filter('nav_menu_css_class', 'nav_class_filter', 100, 1);
// Add page slug as nav IDs
function nav_id_filter( $id, $item ) {
return 'nav-'.cleanname($item->title);
}
add_filter( 'nav_menu_item_id', 'nav_id_filter', 10, 2 );
The nav_id_filter
function makes use of a function I use pretty regularly, to create “codeable” versions of text:
function cleanname($v) {
$v = preg_replace('/[^a-zA-Z0-9s]/', '', $v);
$v = str_replace(' ', '-', $v);
$v = strtolower($v);
return $v;
}
This just removes anything that isn’t alpha-numeric, converts spaces to dashes, and makes everything lowercase; basically converts any string into a “slug”. Probably a better way to write that, but it works…
I’ll likely push this as a Plug-in, again, because it seems really useful, but wanted to make it available to anyone else trying to do the same…
Cheers,
Atg
And it appears that the hooks I finally found are only called on custom menus…
Will try to find hooks that are always called and update this…
Thanks, WP! :-)
Atg
This looks great indeed! Any idea on how to implement this for the “common” wp_list_pages() ?
fantastic solution, thanks for sharing
Awesome! Is it possible to remove empty classes?
Services
instead
<li id="xxx" class="">Services
Thanks!
@Piet: Planning to try to find that, will update this Post if/when I do…
@ Fask: Thanks!
@Nico: Definitely possible, but likely would involve editing the actual WP code, whereas the above can be done via a Theme’s function.php. Will take a look though, updating here if I find a way.
Atg
This is perfect, just made use of it, thanks! but have you found a way to get rid of the empty class? :)
Thanks for posting this Aaron. As a front-end guy that doesn’t touch all that much PHP, this came in very handy!
Just letting everyone know that I have converted this code into a WordPress plug-in, if you’re interested.
Atg
Hi Aaron,
Overall, this plugin is amazing as it cleans up most of the IDs and Classes. Unfortunately, it removes some key Menu Nav classes that are essential for Menu highlighting. Especially, I want the Parent Menu Nav to highlight when its Child Page/Post is selected and currently shown. With this, the needed Classes are:
current-menu-item
current-menu-parent
current-menu-ancestor
I hope that you can exclude these Classes in your next release.
Cheers,
Jason
@Jason:
current-menu-item
(or something similar) should be retained, but, yes, a future enhancement would be to have an admin page with all of the native WP classes listed, allowing the developer to check what they want to keep, uncheck the ones they don’t.One of these days… :-)
Thx for the comment!
Atg
You can add the current-menu-parent and current-menu-ancestor to the array
function nav_class_filter( $var ) {
return is_array($var) ? array_intersect($var, array('current-menu-item', 'current-menu-parent', 'current-menu-ancestor')) : '';
}
add_filter('nav_menu_css_class', 'nav_class_filter', 100, 1);
@Chris: Thanks, yes, I should have pointed out in my reply to @Jason… :-)
Cheers,
Atg
Ugh!! You are my hero for the day!!!
Grand! Very much appreciated, looks so much cleaner already :)
WordPress also comes with a built-in function to generate proper slugs from strings:
See:
http://codex.wordpress.org/Function_Reference/sanitize_title_with_dashes
@Sam: Thanks! I assume that’s in reference to my
cleanname
function? Cool, not crazy that it doesn’t deal with “special accented characters”, though neither does mine, but then, someone could always edit mine if they needed to. Bah. In any case, thanks!Sorry to dig up an old post, but I’m just wondering how to adapt this code to work with two navigations? For example, a header and footer.
In its current form, a website ends up with duplicate IDs if there is more than once instance of a navigation with similar links (even if it is a different menu). Is there a way we can tell it to change ‘nav-‘ to ‘nav2-‘, ‘nav3-‘ and so on?
Thanks!
@trmash: Yes, a known issue, and one I will try to address on the next iteration.
Thanks,
Atg
So, the issue with numerating the navigation menus is two fold:
nav-1
andnav-2
), but then another page, in addtion to those, also has a new middle nav; this middle nav would not getnav-2
and the bottom would becomenav-3
…It could be that simply removing the ID would be the way to go… Still thinking…
Atg
A gentle prod… ;)
Actually, I was just wondering how I might be able to go about stripping the ID from the second menu – is this easily done, or a whole rewrite?
Appreciate your work, thanks.
@trmash: gentle prod sheepishly accepted… :-)
to remove would not be hard, but at this point i would be afraid of breaking CSS that someone already has in place. but, still probably the right way to go. not a complete rewrite, but will require a function edit.
brilliant stuff– exactly what I needed. surprised WP doesn’t do this by default. thanks for your contribution. E
Why is it necessary to check is_array($var)? Can you do without it?
@John: You have to make sure
$var
is an array before you can usearray_intersect
. If you pass a non-array, it will throw an error.Isn’t it always an array though?
erm, I don’t remember off the top of my head, but I’m going to guess that I must have encountered an error at some point that made me do that, but in either case, it is a good practice, and really doesn’t have a downside, aside from one very quick test, so, I’d stick with it…
you see any major issues with having it there?
I saw no issues leaving it out, so I was curious why it was there. But if something screws up I’ll come back and let you know :)
Thank you very much! Useful
Nice approach, however using the itmes title in the id construction, will change the id if the current administrator intercepts with the menu title in menu settings.
Why not using the page id instead?
To add the menu slug will also create an unique id for the same page. Add a third arguments that actually contains the menu information.
add_filter(‘nav_menu_item_id’, ‘nav_id_filter’, 10, 3 );
= function nav_id_filter($id, $item, $args)
and
return ‘nav-‘.$args->menu->slug.’-‘.$item->object_id;
This is what we use:
function nav_class_filter( $var ) {
if(!is_array($var)) return $var;
$pattern = array(
‘current_page_item’,
‘current_page_parent’,
‘current_page_ancestor’,
‘current-cat’,
‘current-menu-item’,
‘current-menu-parent’,
‘current-menu-ancestor’
);
if(!empty(array_intersect($var, $pattern))) return array(‘selected’, ‘selected-onload’);
else return ”;
}
add_filter(‘nav_menu_css_class’, ‘nav_class_filter’, 100, 1);
function nav_id_filter($id, $item, $args) {
return ‘nav-‘.$args->menu->slug.’-‘.$item->object_id;
}
add_filter(‘nav_menu_item_id’, ‘nav_id_filter’, 10, 3 );
Hope this comment doesnt end up to cluttered, I cant find any allowed tags information.
/ Jonas Lundman, Intervik development
Thanks for the note, Jonas.
I guess the part I was trying to avoid was having to know the page’s ID when working in CSS.
Your method doesn’t seem that different to the native WP behavior, where you have to know the menu-item ID…
But, which ever works best for you, is the best method! :-)
Cheers,
Atg