WordPress 菜单客户化定制

wordpress-menu-customization

WordPress 系统核心提供了一套基础的菜单框架,使用该框架所提供的功能可以在主题中实现并建立与用户交互的菜单系统。这种标准的用户菜单系统就是常用的基于文本的下拉菜单,支持多级子菜单,并通过 CSS 建立一些不同样式的菜单。如果要定制比较复杂的菜单系统,比如大型的网页样式菜单(Mega Menu,菜单里包括文本、图片、动图等),就需要借助 WordPress 核心菜单系统所提供的接口做定制,本篇文章就详细介绍相应的方法。

在介绍具体的实现方法之前,首先要明确一下 WordPress 菜单框架核心中涉及的几个基本概念和接口函数,然后再用一个例子来说明图像菜单的实现方法。

一、菜单的显示位置

这个比较好理解,就是定义菜单元素在界面布局中的显示位置,这个菜单位置是一个逻辑概念,就是为了关联具体的菜单,比如在后台管理工具中创建的菜单。

菜单显示位置是通过调用函数 register_nav_menus 来注册的,一般是放置在主题的 functions.php 中,当注册成功后,在后台工具的菜单管理界面中就可以看到了(界面的下方)。注册好的菜单位置可以通过函数 get_nav_menu_locations 获取。

根据需求可以注册多个菜单显示位置,然后使用后台菜单管理工具创建多套菜单并分别与相应的位置进行关联。关联了显示位置的菜单可以调用函数 wp_nav_menu 来显示出来,在调用这个函数时才会确定菜单在界面上具体的显示位置。

二、菜单

指实际的与用户交互的菜单内容,比如主菜单、子菜单。这些菜单项可以通过后台管理工具配置,也可以通过程序动态生成。

前面提到配置好的菜单可以与显示位置做关联,然后通过 wp_nav_menu 显示出来。另外这个函数也接受具体的菜单参数,后台配置好的菜单创建时都有一个名称,把这个名称作为参数传递给 wp_nav_menu 也可以显示对应的菜单,这时可以不使用菜单显示位置参数。

配置好的菜单可以通过在程序中调用函数 wp_get_nav_menus 来获取菜单对象,通过函数 wp_get_nav_menu_items 获取菜单中的每一项数据。

三、菜单项遍历

正常情况下,如果对菜单不做进一步的定制,调用 wp_nav_menu 就会把配置好的菜单显示出来,也就是最普通的下拉菜单,当然通过 CSS 可以做一些不同的样式。

WordPress 菜单系统核心还提供了灵活的机制,可以对标准菜单做复杂的定制,这个机制就是当在后台组织菜单的时候,可以对菜单的每一项做遍历并重新设置要显示的布局和样式,从而实现非常复杂的下拉菜单(Mega Menu)。

这个机制的核心就是 WordPress 菜单遍历类 Walker_Nav_Menu,当菜单系统在组织菜单项的显示内容时,会调用该类中相应的方法,对每一个菜单项做设置。而菜单显示函数 wp_nav_menu 提供了一个接口,可以使用客户化的菜单遍历类替换系统原有的遍历类,实现对标准菜单项做客户化修改。

Walker_Nav_Menu 类(class-walker-nav-menu.php)的基本结构如下:

class Walker_Nav_Menu extends Walker {
	......
	/**
	 * 设置菜单的 `<ul>` 标签及其属性
	 *
	 * 参数 $output(string)  菜单项的 Html
	 * 参数 $depth(int)      菜单项的层级
	 * 参数 $args(stdClass)  函数 wp_nav_menu() 传入的 args 参数
	 */
	public function start_lvl( &$output, $depth = 0, $args = null ) {
		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$indent = str_repeat( $t, $depth );

		// Default class.
		$classes = array( 'sub-menu' );

		/**
		 * 设置菜单 ul 标签的类属性
		 *
		 * 参数 $classes(string[]) 加到 `<ul>` 中的 CSS 样式
		 * 参数 $args(stdClass)  函数 wp_nav_menu() 传入的 args 参数
		 * 参数 $depth(int)      菜单项的层级
		 */
		$class_names = implode( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
		$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

		$output .= "{$n}{$indent}<ul$class_names>{$n}";
	}

	/**
	 * 设置菜单的`</ul>`标签
	 *
	 * 参数 $output(string)  菜单项的 Html
	 * 参数 $depth(int)      菜单项的层级
	 * 参数 $args(stdClass)  函数 wp_nav_menu() 传入的 args 参数
	 */
	public function end_lvl( &$output, $depth = 0, $args = null ) {
		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$indent  = str_repeat( $t, $depth );
		$output .= "$indent</ul>{$n}";
	}

	/**
	 * 设置菜单项的`<li>`标签属性及内容
	 *
	 * 参数 $output(string)  菜单项的 Html
	 * 参数 $item(WP_Post)   菜单项的对象数据
	 * 参数 $depth(int)      菜单项的层级
	 * 参数 $args(stdClass)  函数 wp_nav_menu() 传入的 args 参数
	 * 参数 $id(int)         当前菜单项的 ID
	 */
	public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$indent = ( $depth ) ? str_repeat( $t, $depth ) : '';

		$classes   = empty( $item->classes ) ? array() : (array) $item->classes;
		$classes[] = 'menu-item-' . $item->ID;

		/**
		 * 菜单项数据修改建立用户接口(钩链)
		 */
		$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );

		/**
		 * 菜单项 li CSS 样式(类属性)的修改建立用户接口(钩链)
		 */
		$class_names = implode( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
		$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

		/**
		 * 菜单项 li id 属性的修改建立用户接口(钩链)
		 */
		$id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
		$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

		$output .= $indent . '<li' . $id . $class_names . '>';

		/* 获取菜单项的属性,比如标题,链接等 */
		$atts           = array();
		$atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
		$atts['target'] = ! empty( $item->target ) ? $item->target : '';
		if ( '_blank' === $item->target && empty( $item->xfn ) ) {
			$atts['rel'] = 'noopener';
		} else {
			$atts['rel'] = $item->xfn;
		}
		$atts['href']         = ! empty( $item->url ) ? $item->url : '';
		$atts['aria-current'] = $item->current ? 'page' : '';

		/**
		 * 菜单项内`<a>`标签属性及内容的修改建立用户接口(钩链)
		 *
		 * 参数 $atts(array) {
		 *     `<a>` 标签的属性参数
		 *
		 *     参数 $title(string)        title 属性
		 *     参数 $target(string)       target 属性
		 *     参数 $rel(string)          rel 属性
		 *     参数 $href(string)         href 属性
		 *     参数 $aria_current(string) aria-current 属性
		 * }
		 * 参数 $item(WP_Post)   菜单项的对象数据
		 * 参数 $args(stdClass)  函数 wp_nav_menu() 传入的 args 参数
		 * 参数 $depth(int)      菜单项的层级
		 */
		$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );

		$attributes = '';
		foreach ( $atts as $attr => $value ) {
			if ( is_scalar( $value ) && '' !== $value && false !== $value ) {
				$value       = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
				$attributes .= ' ' . $attr . '="' . $value . '"';
			}
		}

		/* 菜单项标题的修改建立用户接口(钩链)*/
		$title = apply_filters( 'the_title', $item->title, $item->ID );

		/**
		 * 菜单项`<a>`标签标题属性的修改建立用户接口(钩链)
		 */
		$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );

		$item_output  = $args->before;
		$item_output .= '<a' . $attributes . '>';
		$item_output .= $args->link_before . $title . $args->link_after;
		$item_output .= '</a>';
		$item_output .= $args->after;

		/**
		 * 菜单项内容输出的修改建立用户接口(钩链)
		 * 包括<before>标签、<a>标签属性及内容、</a>标签、<after>标签
		 */
		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}

	/**
	 * 设置菜单项的`</li>`标签
	 */
	public function end_el( &$output, $item, $depth = 0, $args = null ) {
		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$output .= "</li>{$n}";
	}

}

所以对菜单项做客户化定制就是从 Walker_Nav_Menu 类派生新的遍历类,重载并修改相应的方法,然后调用 wp_nav_menu 传入用户的菜单遍历对象。

四、wp_nav_menu 简单说明

之前说过把配置好的菜单显示出来,需要调用 wp_nav_menu 函数,具体的调用时点决定了菜单的显示位置,比如在<header>标签内调用,把菜单显示在界面的上方。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
......
</head>
<body>
  <header id="masthead" class="site-header" role="banner">
......
    <div id="site-header-menu" class="site-header-menu">
......
       <nav id="site-navigation" class="main-navigation">
	<?php
          wp_nav_menu(array(
           'theme_location' => 'primary',
	   'menu_class' => 'primary-menu',
           'walker'	=> new my_walker_menu(),
		            )
			);
	?>
       </nav>
......
     </div>
   </header>
</body>
</html>

wp_nav_menu 参数的说明:

wp_nav_menu( array(
    'theme_location'  => '', //菜单显示位置名称,参见第一部分的说明
    'menu'   => '', //菜单,可以是名称、ID、对象
    'menu_class'   => 'menu', //ul标签class,缺省是menu
    'menu_id'   => '',  //ul标签的id,缺省是菜单的slug
    'container'  => 'div', //菜单的容器标签,封装 ul 标签,缺省是div
    'container_class' => '', //容器标签的class
    'container_id'  => '', //容器标签的id
    'fallback_cb' => '', //菜单不存在时调用的回调函数,缺省是wp_page_menu
    'before' => '', //{before}{link_before}链接文本{link_after}{after}
    'after'  => '',
    'link_before'  => '',
    'link_after'  => '',
    'echo'  => true, //是否显示菜单,缺省是true
    'depth' => 0,  //菜单层级,缺省是0
    'walker' => '' //菜单遍历walker
    'items_wrap'  => '', //如何建立ul标签,使用printf的格式,缺省是<ul id="%1$s" class="%2$s">%3$s</ul>,如果是%3$s,ul标签就会去除
    'item_spacing' => 'preserve' //是否去除菜单Html中的空格,缺省是保留
  ) );
五、定制带有图像的菜单项

下面用一个例子说明如何定制图像菜单,为了可以选择哪个菜单项显示图像,需要给菜单项建立一个标志开关,比较简单的方式就是使用 CSS 的 class 属性。首先到后台工具的菜单管理界面,点开上边的 “显示选项”,然后勾选 “CSS 类”,这样每一个菜单项的配置界面都会出现 CSS 类的文本输入框,对需要显示图像菜单项的 CSS 类输入比如 “menu-image-item”。

接着就是新建菜单遍历类,然后把遍历类对象传入 wp_nav_menu。

class my_walker_menu extends Walker_Nav_Menu
{
    public function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0)
    {
        $indent = ($depth) ? str_repeat("\t", $depth) : '';

        $classes = empty($item->classes) ? array() : (array) $item->classes;

        ......

        $item_output .= '<a'.$attributes.'>';
        // 在菜单项的 <a>标签内增加 <img>标签
        // 检查菜单项的 menu-image-item class
        $has_image_class = array_search('menu-image-item', $classes);
	if ($has_image_class !== false) {
            $postID = url_to_postid( $item->url );
            $item_output .= "<img alt=\"" . esc_attr($item->attr_title) . "\" src=\"" . get_the_post_thumbnail_url( $postID ) . "\"/>";
        }

        ......

        $output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
    }
}

可以把上面的菜单遍历类代码放到一个单独的文件中,并把这个文件存到当前主题的目录下,然后在主题的 functions.php 文件里引用一下新的遍历类。

if ( ! file_exists( get_stylesheet_directory() . '/my_walker_menu.php' ) ) {
	return new WP_Error( 'my_walker_menu_missing', __( 'The my_walker_menu.php file don't exist.', 'kflyo' ) );
} else {
    require_once get_stylesheet_directory() . '/my_walker_menu.php';
}

除了显示图片菜单项外,使用文章中介绍的方法,还可以在菜单容器中增加更多的界面布局标签,比如<div><ul><li>等,并通过 CSS 样式实现更丰富的菜单界面。

发表评论

邮箱地址不会被公开。 必填项已用*标注