Ссылки Назад и Вперёд для древовидных страниц
Задача:
Имеется иерархическая ctp=wiki, где родительские страницы (0 уровень вложенности) являются архивными, а внутренними - документы. В шаблоне есть кнопки "Предыдущая" и "Следующая", которые надо "оживить" - сделать ссылками на предыдущий и следующий пост (страницу). Пользователь, благодаря им, должен иметь возможность пройти от родительской страницы до самого конца, учитывая любой уровень вложенности (древовидности).
Читайте также:
- Получаем несколько соседних записей одним запросом.
- Для плоских постов (типа записи post) используйте функцию get_adjacent_post().
/** * Получает объекты предыдущей и следующей страницы документации относительно текущей. * * @return Wiki_Item[]|null[] */ public function get_adjacent_pages() { global $chapter_ids; $walker = new class extends \Walker_Page { public function start_el( &$output, $data_object, $depth = 0, $args = [], $current_object_id = 0 ) { global $chapter_ids; $chapter_ids[] = (int) $data_object->ID; } }; // Добавляем в начало списка id индексной страницы, который туда саа не попадёт при работе wp_list_pages() $index_page_id = $this->get_index_page()->get_id(); $chapter_ids = [ $index_page_id ]; wp_list_pages( [ 'post_type' => $this->get_post_type(), 'child_of' => $index_page_id, 'echo' => false, 'walker' => $walker, ] ); $data = [ 'prev' => null, 'next' => null, ]; foreach ( $chapter_ids as $index => $chapter_id ) { if ( $chapter_id === $this->get_id() ) { $data['prev'] = isset( $chapter_ids[ $index - 1 ] ) ? new self( $chapter_ids[ $index - 1 ] ) : null; $data['next'] = isset( $chapter_ids[ $index + 1 ] ) ? new self( $chapter_ids[ $index + 1 ] ) : null; break; } } unset( $chapter_ids ); return $data; }
Использование в шаблоне:
<div class="documents__nav-buttons"> <div class="documents-nav"> <?php $wiki_item = \Wiki_Item( get_the_ID() ); $wiki_items = $wiki_item->get_adjacent_pages(); $prev_url = $wiki_items['prev'] ? $wiki_items['prev']->get_url() : null; $next_url = $wiki_items['next'] ? $wiki_items['next']->get_url() : null; ?> <a class="documents-nav__item documents-nav__item--prev <?= $prev_url ? '' : 'documents-nav__item--disabled' ?>" href="<?= $prev_url ?>"> <svg class="documents-nav__icon" xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12" fill="none"> <path d="M6.88462 1L1.5 6M1.5 6L6.88462 11M1.5 6H15.5"/> </svg> </a> <a class="documents-nav__item documents-nav__item--next <?= $next_url ? '' : 'documents-nav__item--disabled' ?>" href="<?= $next_url ?>"> <svg class="documents-nav__icon" xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12" fill="none"> <path d="M9.11538 1L14.5 6M14.5 6L9.11538 11M14.5 6H0.499999"/> </svg> </a> </div> </div>
Выглядит так:
Тот же код, но немного унифицированный
class Adjacent_Pages { /** Данные страниц по порядку пагинации */ private array $items_data; public function __construct( string $post_type, int $child_of = 0 ) { $this->set_items_data( $post_type, $child_of ); } /** * Получает объекты предыдущей и следующей страницы относительно текущей. * Учитывает древовидную вложенность. * * @return array{0:WP_Post|null, 1:WP_Post|null} Предыдущий и следующий пост. */ public function get_prev_next( ?WP_Post $current_post = null ): array { if( ! $current_post ){ $current_post = $GLOBALS['post']; } foreach ( $this->items_data as $index => $item ) { if ( $item->ID !== $current_post->ID ) { continue; } $prev_item = $this->items_data[ $index - 1 ] ?? null; $next_item = $this->items_data[ $index + 1 ] ?? null; break; } return [ isset( $prev_item ) ? get_post( $prev_item->ID ) : null, isset( $next_item ) ? get_post( $next_item->ID ) : null, ]; } private function set_items_data( string $post_type, int $child_of ): void { $walker = new class extends \Walker_Page { public array $items_data = []; public function start_el( &$output, $data_object, $depth = 0, $args = [], $current_object_id = 0 ) { $this->items_data[] = (object) [ 'ID' => (int) $data_object->ID, 'post_parent' => (int) $data_object->post_parent, 'post_title' => $data_object->post_title, ]; } }; wp_list_pages( [ 'post_type' => $post_type, 'child_of' => $child_of, 'echo' => false, 'sort_order' => 'ASC', 'sort_column' => 'menu_order, post_title', 'walker' => $walker, ] ); $this->items_data = $walker->items_data; unset( $walker ); // just in case } }
Использование:
$chapter_post_id = 654; [ $prev, $next ] = ( new Adjacent_Pages( 'wiki', $chapter_post_id ) )->get_prev_next(); $prev_url = $prev ? get_permalink( $prev ) : ''; $next_url = $next ? get_permalink( $next ) : '';
Аналогичный код только без использования wp_list_pages()
Для этого нужно заменить метод set_items_data() из класса выше Adjacent_Pages:
class Native_Adjacent_Pages extends Adjacent_Pages { protected function set_items_data( string $post_type, int $child_of ): void { $pages = get_pages( [ 'post_type' => $post_type, 'child_of' => $child_of, 'hierarchical' => false, 'sort_order' => 'ASC', 'sort_column' => 'menu_order, post_title', ] ); $this->items_data = ( new Kama_Make_Pages_Tree( $pages ) )->get_tree_flat_data(); } }
Используем также:
$chapter_post_id = 654; [ $prev, $next ] = ( new Native_Adjacent_Pages( 'wiki', $chapter_post_id ) )->get_prev_next(); $prev_url = $prev ? get_permalink( $prev ) : ''; $next_url = $next ? get_permalink( $next ) : '';
Также нам понадобится класс, который будет работать с деревом:
/** * Gets a flat array of hierarchical posts {@see get_pages()}, then * creates a nested tree from them with `children` elements, then * creates flat array in the order of that tree elements. */ final class Kama_Make_Pages_Tree { public array $parent_groups; public array $top_parents; private array $wp_post_fields = [ 'ID' => 'int', 'post_parent' => 'int', 'post_title' => 'string', ]; /** * @param \WP_Post[] $pages An array of pages objects obtained with {@see get_pages()}. */ public function __construct( $pages ){ $pages = $this->prepare_pages( $pages ); $this->parent_groups = self::group_by_parent_id( $pages ); $this->top_parents = $this->parent_groups[0]; // zero level posts } public function get_tree_flat_data(): array { return $this->collect_flat_data( $this->get_tree() ); } public function get_tree(): array { $tree = $this->top_parents; $this->fill_tree_recursively( $tree ); return $tree; } private static function group_by_parent_id( array $pages ){ $groups = []; foreach( $pages as $p ){ $parent_id = (int) $p->post_parent; $groups[ $parent_id ][] = $p; } return $groups; } private function collect_flat_data( $pages_tree ): array { $flat_data = []; foreach( $pages_tree as $obj ){ $flat_data[] = $obj; if( ! empty( $obj->children ) ){ foreach( $this->collect_flat_data( $obj->children ) as $_obj ){ $flat_data[] = $_obj; } } } return $flat_data; } /** * Fill each passed item's `children` field based on existing $parent_groups. * * @param object[] $parents */ private function fill_tree_recursively( & $parents ): void { foreach( $parents as & $parent ){ if( empty( $this->parent_groups[ $parent->ID ] ) ){ continue; } $parent->children = $this->parent_groups[ $parent->ID ]; $this->fill_tree_recursively( $this->parent_groups[ $parent->ID ] ); } unset( $parent ); // just in case } /** * @param \WP_Post[] $pages */ private function prepare_pages( $pages ): array { $unset_fields = array_diff_key( (array) reset( $pages ), $this->wp_post_fields ); $unset_fields = array_keys( $unset_fields ); foreach( $pages as & $p ){ $p = (object) (array) $p; // cast to stdClass foreach( $unset_fields as $name ){ unset( $p->$name ); } foreach( $this->wp_post_fields as $name => $type ){ settype( $p->$name, $type ); } } return $pages; } }