Навигационное меню в WordPress

Навигационное меню это один из 6-и типов навигации представленных в вордпресс. Поскольку 3 это часть “архитектурного ансамбля” других элементов – расскажу о них в будущих статьях; оставшиеся 2 типа – это виджеты со списками последних записей и различных архивов. О них, я считаю, и говорить не стоит…

Содержание

Подготовительные работы

Прежде, чем вызывать в шаблоне менюху, нужно зарегистрировать меню в системе. Да, в любом шаблоне это уже сделано, но ведь я рассматриваю случай с разработкой собственной! Итак, код регистрации(в functions.php темы)

<?php
add_action( 'after_setup_theme', 'theme_register_nav_menu' );
function theme_register_nav_menu() {
	register_nav_menu( 'primary', 'Primary Menu' );
}

Данным кодом я регнул локацию для вывода меню с идентификатором “primary” и именем “Primary Menu”. В случаи регистрации множества локаций для меню, код будет таким

<?php
add_action( 'after_setup_theme', 'theme_register_nav_menus' );
function theme_register_nav_menus(){
	register_nav_menus( [
		'primary' => 'Primary Menu',
		'secondary' => 'Secondary Menu'
	] );
} );

Результатом регистрации будет появление дополнительного чекбокса в зоне Display location в Menu Settings на странице Appearance > Menus

Display location в настройках меню

Все, следующий шаг – это, непосредственно, вывод самого меню.

Вывод меню

Вывод всего дерева меню производится вызовом одной функции(неожиданно, да?). Вот он:

<?php
wp_nav_menu( [
	'theme_location'  => '',
	'menu'            => '', 
	'container'       => 'div', 
	'container_class' => '', 
	'container_id'    => '',
	'container_aria_label' => '',
	'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
	'menu_class'      => 'menu', 
	'menu_id'         => '',
	'echo'            => true,
	'fallback_cb'     => 'wp_page_menu',
	'before'          => '',
	'after'           => '',
	'link_before'     => '',
	'link_after'      => '',
	'depth'           => 0,
	'walker'          => '',
] );

массив аргументов необязателен, но тогда будет выведено первое непустое меню.

Теперь по аргументам… theme_location идентификатор зоны регистрации меню(если равно primary из примера реги, будет выведено меню, закреплённое за этой зоной в админке – выбран чекбокс). В menu будет слаг, название или id созданного меню.

Ключи container, container_class, container_id и container_aria_label – это тег обертки меню и ее атрибуты class, id и aria-label соответственно. В items_wrap, menu_class, menu_id хранятся разметка для обертки списка и ее атрибуты. Если ты используешь свой шаблон обертки – обязательно делай плейсхолдер %3$s, поскольку именно он будет заменен на пункты списка.

Каждый пункт меню – это ссылка, тег а; before и after – это содержимое перед и после ссылки. link_before и link_after – содержимое до и после текста ссылки(между открывающим и закрывающим тегами а).

Также, есть настройки, не связанные непосредственно с постройкой дерева – fallback_cb, depth и walker. Первая – это название функции, которая будет вызвана при отсутствии меню в БД; вторая – разрешенная глубина меню, третья – класс, который будет строить само меню(нужно указывать объект класса, а не строку). О нем то мы и поговорим более подробно.

Класс Walker. Кастомизация базового шаблона меню

По умолчанию, функция wp_nav_menu использует родной класс Walker_Nav_Menu расширяющий класс Walker. По сути, класс Walker_Nav_Menu содержит методы-шаблону постройки пунктов меню и оберток для них, а родительский класс содержит всю логику посстроения скелета по шаблонам ребенка. Для кастомизации шаблонов, в нем(классе), как и во всем вордпресс, предусмотрены фильтры и экшны; предлагая их кратко рассмотреть.

<?php
class Walker_Nav_Menu extends Walker {
	/**
	 * What the class handles.
	 *
	 * @since 3.0.0
	 * @var string
	 *
	 * @see Walker::$tree_type
	 */
	public $tree_type = array( 'post_type', 'taxonomy', 'custom' );

	/**
	 * Database fields to use.
	 *
	 * @since 3.0.0
	 * @todo Decouple this.
	 * @var array
	 *
	 * @see Walker::$db_fields
	 */
	public $db_fields = array(
		'parent' => 'menu_item_parent',
		'id'     => 'db_id',
	);

	/**
	 * Starts the list before the elements are added.
	 *
	 * @since 3.0.0
	 *
	 * @see Walker::start_lvl()
	 *
	 * @param string   $output Used to append additional content (passed by reference).
	 * @param int      $depth  Depth of menu item. Used for padding.
	 * @param stdClass $args   An object of wp_nav_menu() arguments.
	 */
	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' );

		/**
		 * Filters the CSS class(es) applied to a menu list element.
		 *
		 * @since 4.8.0
		 *
		 * @param string[] $classes Array of the CSS classes that are applied to the menu `<ul>` element.
		 * @param stdClass $args    An object of `wp_nav_menu()` arguments.
		 * @param int      $depth   Depth of menu item. Used for padding.
		 */
		$class_names = join( ' ', 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}";
	}

	/**
	 * Ends the list of after the elements are added.
	 *
	 * @since 3.0.0
	 *
	 * @see Walker::end_lvl()
	 *
	 * @param string   $output Used to append additional content (passed by reference).
	 * @param int      $depth  Depth of menu item. Used for padding.
	 * @param stdClass $args   An object of wp_nav_menu() arguments.
	 */
	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}";
	}

	/**
	 * Starts the element output.
	 *
	 * @since 3.0.0
	 * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
	 *
	 * @see Walker::start_el()
	 *
	 * @param string   $output Used to append additional content (passed by reference).
	 * @param WP_Post  $item   Menu item data object.
	 * @param int      $depth  Depth of menu item. Used for padding.
	 * @param stdClass $args   An object of wp_nav_menu() arguments.
	 * @param int      $id     Current item 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;

		/**
		 * Filters the arguments for a single nav menu item.
		 *
		 * @since 4.4.0
		 *
		 * @param stdClass $args  An object of wp_nav_menu() arguments.
		 * @param WP_Post  $item  Menu item data object.
		 * @param int      $depth Depth of menu item. Used for padding.
		 */
		$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );

		/**
		 * Filters the CSS classes applied to a menu item's list item element.
		 *
		 * @since 3.0.0
		 * @since 4.1.0 The `$depth` parameter was added.
		 *
		 * @param string[] $classes Array of the CSS classes that are applied to the menu item's `<li>` element.
		 * @param WP_Post  $item    The current menu item.
		 * @param stdClass $args    An object of wp_nav_menu() arguments.
		 * @param int      $depth   Depth of menu item. Used for padding.
		 */
		$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
		$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

		/**
		 * Filters the ID applied to a menu item's list item element.
		 *
		 * @since 3.0.1
		 * @since 4.1.0 The `$depth` parameter was added.
		 *
		 * @param string   $menu_id The ID that is applied to the menu item's `<li>` element.
		 * @param WP_Post  $item    The current menu item.
		 * @param stdClass $args    An object of wp_nav_menu() arguments.
		 * @param int      $depth   Depth of menu item. Used for padding.
		 */
		$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 noreferrer';
		} else {
			$atts['rel'] = $item->xfn;
		}
		$atts['href']         = ! empty( $item->url ) ? $item->url : '';
		$atts['aria-current'] = $item->current ? 'page' : '';

		/**
		 * Filters the HTML attributes applied to a menu item's anchor element.
		 *
		 * @since 3.6.0
		 * @since 4.1.0 The `$depth` parameter was added.
		 *
		 * @param array $atts {
		 *     The HTML attributes applied to the menu item's `<a>` element, empty strings are ignored.
		 *
		 *     @type string $title        Title attribute.
		 *     @type string $target       Target attribute.
		 *     @type string $rel          The rel attribute.
		 *     @type string $href         The href attribute.
		 *     @type string $aria_current The aria-current attribute.
		 * }
		 * @param WP_Post  $item  The current menu item.
		 * @param stdClass $args  An object of wp_nav_menu() arguments.
		 * @param int      $depth Depth of menu item. Used for padding.
		 */
		$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 . '"';
			}
		}

		/** This filter is documented in wp-includes/post-template.php */
		$title = apply_filters( 'the_title', $item->title, $item->ID );

		/**
		 * Filters a menu item's title.
		 *
		 * @since 4.4.0
		 *
		 * @param string   $title The menu item's title.
		 * @param WP_Post  $item  The current menu item.
		 * @param stdClass $args  An object of wp_nav_menu() arguments.
		 * @param int      $depth Depth of menu item. Used for padding.
		 */
		$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;

		/**
		 * Filters a menu item's starting output.
		 *
		 * The menu item's starting output only includes `$args->before`, the opening `<a>`,
		 * the menu item's title, the closing `</a>`, and `$args->after`. Currently, there is
		 * no filter for modifying the opening and closing `<li>` for a menu item.
		 *
		 * @since 3.0.0
		 *
		 * @param string   $item_output The menu item's starting HTML output.
		 * @param WP_Post  $item        Menu item data object.
		 * @param int      $depth       Depth of menu item. Used for padding.
		 * @param stdClass $args        An object of wp_nav_menu() arguments.
		 */
		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}

И еще… мало не забыл… немного о методах. Для строительства скелета, есть 4 метода: start_lvl и end_lvl – строят открывающий и закрывающий теги обертки вложенных меню; start_el и end_el – открывающий и закрывающий теги пункта меню.

Глянем фильтры

Буду описывать их по порядку встречи в коде. Первым мы встречаем фильтр “nav_menu_submenu_css_class”. Он расположен в методе start_lvl и отвечает за фильтрацию классов оберток дочерних оберток списков. Функции-фильтру передаются 3 параметра – массив классов, вложенность строящейся обертки и массив, переданный при вызове wp_nav_menu. В этом методе хуков больше нет, перейду к следующему.

Метод end_lvl в принципе не может содержать ничего меняющегося, соответственно – хуков он не содержит. Глянем следующий метод – start_el – он то уж содержит много хуков.

Первый хук который мы здесь видим – фильтр nav_menu_item_args. Как можно понять с его кода, предназначен он для фильтрации массива с функции строителя(wp_nav_menu). Фильтр принимает 3 аргумента: массив для фильтрации, объект данных о строящемся пункте меню и его вложенность. Следующий, тоже фильтр, nav_menu_css_class фильтрует классы у тега li, принимает уже 4 аргумента – массив классов, объект пункта, массив из строителя и вложенность. Следующим, идет фильтр атрибута id тега li. Он принимает те же аргументы, что и предыдущий фильтр с той лишь разницей, что вместо массива классов – строка с текущим id(строка, поскольку class`ов может быть много, а id один).

Следующий фильтр nav_menu_link_attributes фильтрует атрибуты тега а. Принимает он те же аргументы, что и фильтр выше, с разницей в место строки id массив атрибутов тега а. Затем идет общий фильтр the_title, которым можно изменить текст ссылки, исходя из текущего текста и id пункта меню. После общего фильтра, идет местный nav_menu_item_title фильтр анкора ссылки. Если честно – не знаю и не понимаю, зачем прилепили общий, ведь этот получает данные для фильтрации, включающие предыдущие; а именно те же, что и с классами, только в место массива классов текст ссылки.

Ну и последним фильтром класса(метод end_el как и end_lvl не содержит никаких данных) является фильтр всего пункта меню. Он, вполне ожидаемо, принимает те же аргументы, что предыдущий, только вместо строки текста передана строка с HTML разметкой пункта.

Создание своего шаблона меню

В принципе, я не знаю что нужно поместить в меню, чего б не можно было сделать фильтрами в дефолтном классе; может, чтоб не наращивать код и с точки зрения оптимизации – выиграть какую-то долю процента…

Но ладно, раз уж это сделать можно – почему об этом не рассказать?… Первым делом – идем в файл wp-includes/class-walker-nav-menu.php, копируем к нам в файл соответствующий класс и переименовываем его. Именно копируем – я то знаю, где-то в буковке ошибся и все – ошибка, ничего не работает… хотя, впрочем, можете и с нуля набрать… только прочти до конца!

Итак, допустим, я решил создать walker-класс который будет строить только такую разметку

<ul class="main-menu">
  <li class="menu-item">POINT 1
    <ul class="sub-menu depth1">
      <li class="menu-item">
        <img src="path/to/image.png" alt="">
        <a href="#">POINT 1.1</a>
      </li>
    </ul>
  </li>
  <li class="menu-item">
    <a href="#">POINT 2</a>
  </li>
</ul>

Первое, что можно заметить – наличие картинки, возможность выбора коей с админки пока не существует.

Безусловно, можно сделать кнопку у каждого пункта меню, которая бы открывала фрейм с медиатекой(как у миниатюры поста, к примеру); с целью укорочения этой статьи, я приделаю ввод id картинки с медиатеки

<?php
add_action('wp_nav_menu_item_custom_fields', 'action_function_name_6494', 10, 5);
function action_function_name_6494($item_id, $item, $depth, $args, $id)
{
    ?>
	<p class="field-thumbnail description description-thin">
		<label for="edit-menu-item-thumbnail-<?php echo $item_id; ?>">
			<?php _e('Thumbnail'); ?><br />
			<input type="number" id="edit-menu-item-thumbnail-<?php echo $item_id; ?>" class="widefat code edit-menu-item-thumbnail" name="menu-item-thumbnail[<?php echo $item_id; ?>]" value="<?php echo get_post_meta($item_id, '_item_thumbnail', 1); ?>" />
		</label>
	</p>
	<?php
}

Этот код выведет под каждым пунктом меню следующее

новое поле для ввода

но это еще не все. Для того, чтоб эту настройку сохранить – нужен дополнительный код. Я знаю, что каждый пункт меню хранится как запись с типом поста nav_menu_item; следовательно, настройка мета данное поста и и для его сохраниния нужно функцию повесить на экшн save_post_nav_menu_item(save_post_{post_type} в общем случаи)

<?php
add_action('save_post_nav_menu_item', 'save_nav_menu_item');

function save_nav_menu_item()
{
    if (isset($_POST['menu-item-thumbnail'])) {
        foreach ($_POST['menu-item-thumbnail'] as $key => $value) {
            add_post_meta($key, '_item_thumbnail', $value);
        }
    }
}

здесь я убрал проверки на возможности пользователя, типы данных и прочее, чтоб не убивать читабельность кода; но ты в своих проектах о них не забывай… Еще одна вещь – все это(настройку, но не меню) можно было бы сделать используя плагины.

Теперь время непосредственно walker-класса. Я не буду делать возможности фильтрации данных и максимально захардкоджу код класса в целях экономии места

<?php
class Walker_Nav_Menu_Thumb extends Walker
{
    public $tree_type = array( 'post_type', 'taxonomy', 'custom' );

    public $db_fields = array(
        'parent' => 'menu_item_parent',
        'id'     => 'db_id',
    );

    public function start_lvl(&$output, $depth = 0, $args = null)
    {
        $classes = join(' ', ['sub-menu', 'depth' . $depth]);
        $output .= '<ul class="' . $classes . '">';
    }

    public function end_lvl(&$output, $depth = 0, $args = null)
    {
        $output .= "</ul>";
    }

    public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0)
    {
        $url = ! empty($item->url) ? $item->url : '';
        $link = $this->has_children ? $item->title : '<a href="' . $url . '">' . $item->title . '</a>';
        $img = get_post_meta($item->ID, '_item_thumbnail', 1) ? '<img src="' . wp_get_attachment_url(get_post_meta($item->id, '_item_thumbnail', 1)) . '"/>' : '';
        $output .= '<li class="menu-item">' . $img . $link;
    }

    public function end_el(&$output, $item, $depth = 0, $args = null)
    {
        $output .= "</li>";
    }
}

Вызов меню, с постройкой ранее указанной разметки, будет выглядеть так

<?php
wp_nav_menu([
	'theme_location' => 'primary',
	'container' => 0,
	'menu_class' => 'main-menu',
	'walker' => new Walker_Nav_Menu_Thumb
]);

Ты наверное заметил, что я “убил” в своем классе часть аргументов функции строителя, для публичных проектов так не делай… никогда.

Заключение

Рассмотрел способ создания кастомного меню. Также был рассмотрен базовый класс, который создает и меню; для примера, наведу код, коим можно сделать такие же изменения в коде

<?php
add_filter('nav_menu_submenu_css_class', 'my_nav_menu_submenu_css_class', 10, 3);
function filter_function_name_8769($classes, $args, $depth)
{
    $classes[] = 'depth' . $depth;

    return $classes;
}

add_filter('nav_menu_item_args', 'my_nav_menu_item_args', 10, 2);
function my_nav_menu_item_args($args, $item)
{
    if ($id = get_post_meta($item->ID, '_item_thumbnail', 1)) {
        $args['before'] = '<img src="' . wp_get_attachment_url($id) . '"/>';
    }

    return $args;
}

add_filter('nav_menu_link_attributes', 'my_nav_menu_link_attributes', 10, 4);
function my_nav_menu_link_attributes($atts, $item, $args, $depth)
{
    $atts = ['href' => in_array('menu-item-has-children', $item->classes) ? '' : $atts['href']];

    return $atts;
}

add_filter('nav_menu_css_class', 'my_nav_menu_css_class', 10, 4);
function my_nav_menu_css_class($classes)
{
    $classes[] = 'menu-item';

    return $classes;
}

как при кастомном классе, но используя базовый. Кстати, только заметил, что полного сходства добиться невозможно.

Источники