Навигационное меню это один из 6-и типов навигации представленных в вордпресс. Поскольку 3 это часть “архитектурного ансамбля” других элементов – расскажу о них в будущих статьях; оставшиеся 2 типа – это виджеты со списками последних записей и различных архивов. О них, я считаю, и говорить не стоит…
Содержание
- Подготовительные работы
- Вывод меню
- Класс Walker. Кастомизация базового шаблона меню
- Глянем фильтры
- Создание своего шаблона меню
- Заключение
- Источники
Подготовительные работы
Прежде, чем вызывать в шаблоне менюху, нужно зарегистрировать меню в системе. Да, в любом шаблоне это уже сделано, но ведь я рассматриваю случай с разработкой собственной! Итак, код регистрации(в 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
Все, следующий шаг – это, непосредственно, вывод самого меню.
Вывод меню
Вывод всего дерева меню производится вызовом одной функции(неожиданно, да?). Вот он:
<?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; }
как при кастомном классе, но используя базовый. Кстати, только заметил, что полного сходства добиться невозможно.