Contenu principal

Dupliquer un sous-menu de WordPress vers une barre latérale

Lors d’un projet récent j’ai eu besoin de dupliquer le sous-menu (du menu principal du site) correspondant à la page courante vers une zone déportée, telle qu’une barre latérale. Aujourd’hui je vais vous exposer la technique que j’ai employée pour réaliser ceci assez simplement.

Situation

Le menu principal de votre site comporte des pages, des catégories, etc., le tout organisé avec des sous-menus. Lorsque vous vous trouvez sur une page, le sous-menu correspondant à cette page doit être affiché dans une sidebar. On duplique donc le « sous-menu courant ».

L’idée

L’idée de base est très simple : plutôt que de vouloir aller chercher les items de menu correspondants et en quelques sortes, réinventer la roue, pourquoi ne pas commencer avec l’outil le plus adapté pour afficher ces items de menu ? Oui, je parle bien de la fonction wp_nav_menu(), la même fonction qui a normalement été utilisée précédemment dans votre thème. Il « suffirait » ensuite de filtrer les items à afficher.

La pratique

Nous avons notre menu principal avec quelques paramètres :

123456789

$menu_args = array(
	'theme_location'  => 'primary',
	'container'       => 'nav',
	'container_id'    => 'header-menu-container',
	'container_class' => 'menu-container clearfix',
	'menu_class'      => 'clearfix nav site-navigation',
	'menu_id'         => 'site-navigation',
);
wp_nav_menu( $menu_args );

Tous ces paramètres ne sont pas importants aujourd’hui, sauf un : 'theme_location'.
A l’endroit où nous mettrons le sous-menu dupliqué (sidebar.php ?), il nous faudra donc utiliser wp_nav_menu() avec le même paramètre 'theme_location'. Ceci nous permet de récupérer les mêmes items que dans notre menu principal.

12345678

$menu_args = array(
	'theme_location'  => 'primary',
	'container_id'    => 'header-menu-container-sub',
	'container_class' => 'menu-container',
	'menu_class'      => 'nav site-navigation',
	'menu_id'         => 'site-navigation-sub',
);
wp_nav_menu( $menu_args );

A ce stade, nous dupliquons entièrement notre menu. Les autres paramètres de mise en forme, comme les classes CSS et id, peuvent être modifiés.

Il nous faut maintenant ajouter des filtres dans le fichier functions.php afin de ne garder que les items de menu qui nous intéressent, et ça tombe bien…

123456

add_filter( 'wp_nav_menu_objects', 'minime_nav_menu_objects', 10, 2 );

function minime_nav_menu_objects( $items, $args ) {
	// Do magic
	return $items;
}

Ici, $items contient les items de menu sous forme d’objet, et $args contient les paramètres que l’on a passés au menu.

Première chose à faire, savoir si l’on est dans le menu original ou dans son « clone ». Pour cela on utilise une variable static qui, pour chaque menu (on peut avoir d’autres menus que ‘primary’), nous dira si ce menu a déjà été exécuté une fois. Si oui, cela veut dire que c’est un « clone ».

010203040506070809101112

add_filter( 'wp_nav_menu_objects', 'minime_nav_menu_objects', 10, 2 );

function minime_nav_menu_objects( $items, $args ) {
	static $duplicate = array();

	if ( ! empty( $duplicate[ $args->theme_location ] ) ) {
		// Virer les indésirables
	}
	$duplicate[ $args->theme_location ] = 1;

	return $items;
}

Une seule contrainte ici, il faut que notre « clone » soit exécuté APRÈS le menu original dans la page. Par exemple, si vous souhaitiez dupliquer un menu situé en pied de page, c’est celui-ci qui serait réduit au sous-menu courant.
Il y a bien sûr un moyen de contourner le problème, en indiquant soi-même quel menu est le « clone » : au lieu d’utiliser la variable $duplicate, nous passerions un paramètre personnalisé 'is_duplicate' => true à wp_nav_menu(), et côté filtre, si ce paramètre existe et est à true, on ne retourne que les items du sous-menu courant. Ça enlève juste un peu la côté automatique, mais au moins on garde le contrôle.

0102030405060708091011121314151617181920212223

// Exemple avec un paramètre personnalisé "is_duplicate"
// sidebar.php
$menu_args = array(
	'theme_location'  => 'primary',
	'container_id'    => 'header-menu-container-sub',
	'container_class' => 'menu-container',
	'menu_class'      => 'nav site-navigation',
	'menu_id'         => 'site-navigation-sub',
	'is_duplicate'    => true,
);
wp_nav_menu( $menu_args );

// functions.php
add_filter( 'wp_nav_menu_objects', 'minime_nav_menu_objects', 10, 2 );

function minime_nav_menu_objects( $items, $args ) {

	if ( isset( $args->is_duplicate ) && $args->is_duplicate ) {
		// Virer les indésirables
	}

	return $items;
}

La prochaine chose à faire est de savoir où nous sommes, c’est à dire trouver l’item courant. Enfin, pas tout à fait, il nous faut trouver l’item qui n’a pas de parent (à la racine du menu) et dont l’item courant est un fils de celui-ci (vous avez suivi ?) :

0102030405060708091011121314151617181920

add_filter( 'wp_nav_menu_objects', 'minime_nav_menu_objects', 10, 2 );

function minime_nav_menu_objects( $items, $args ) {
	static $duplicate = array();

	if ( ! empty( $duplicate[ $args->theme_location ] ) ) {
		$current = 0;
		$new_items = array();
		foreach ( $items as $item ) {
			if ( ! $item->menu_item_parent && ( $item->current || $item->current_item_ancestor ) ) {
				$current = $item;
				break;
			}
		}
		// La suite...
	}
	$duplicate[ $args->theme_location ] = 1;

	return $items;
}

Dans le if situé à l’intérieur de la boucle foreach, !$item->menu_item_parent signifie que l’on recherche un item situé à la racine du menu. Ensuite nous cherchons un item qui a le statut « courant » ou « ancêtre de l’item courant ». Nous le rangeons alors dans une variable $current et nous sortons de la boucle.
Maintenant il nous faut trouver les items fils de notre item $current.

01020304050607080910111213141516171819202122232425262728

add_filter( 'wp_nav_menu_objects', 'minime_nav_menu_objects', 10, 2 );

function minime_nav_menu_objects( $items, $args ) {
	static $duplicate = array();

	if ( ! empty( $duplicate[ $args->theme_location ] ) ) {
		$current = 0;
		$new_items = array();
		foreach ( $items as $item ) {
			if ( ! $item->menu_item_parent && ( $item->current || $item->current_item_ancestor ) ) {
				$current = $item;
				break;
			}
		}

		if ( $current ) {
			foreach ( $items as $item ) {
				if ( $item->menu_item_parent === $current->ID ) {
					$new_items[] = $item;
				}
			}
		}
		// La suite...
	}
	$duplicate[ $args->theme_location ] = 1;

	return $items;
}

Nous trouvons là une limite à notre script : avec if ( $item->menu_item_parent === $current->ID ) nous cherchons un item dont $current est le parent direct. Si vous avez plusieurs sous-menus imbriqués, ceux-ci ne seront pas affichés. Pour cela il faudrait certainement une boucle supplémentaire qui répèterait notre foreach jusqu’à ce que tous les items « petit-fils » soient trouvés, en limitant leur profondeur avec $args->depth si celui-ci est fourni en paramètre du menu. Vu la lourdeur, ce n’est pas une voie que j’ai souhaité explorer.

Dernière étape (ou presque), ajouter $current aux items à retourner, et ranger tout ce petit monde dans $items.

010203040506070809101112131415161718192021222324252627282930

add_filter( 'wp_nav_menu_objects', 'minime_nav_menu_objects', 10, 2 );

function minime_nav_menu_objects( $items, $args ) {
	static $duplicate = array();

	if ( ! empty( $duplicate[ $args->theme_location ] ) ) {
		$current = 0;
		$new_items = array();
		foreach ( $items as $item ) {
			if ( ! $item->menu_item_parent && ( $item->current || $item->current_item_ancestor ) ) {
				$current = $item;
				break;
			}
		}

		if ( $current ) {
			foreach ( $items as $item ) {
				if ( $item->menu_item_parent === $current->ID ) {
					$new_items[] = $item;
				}
			}
		}

		array_unshift( $new_items, $current );
		$items = $new_items;
	}
	$duplicate[ $args->theme_location ] = 1;

	return $items;
}

Voilà, notre sous-menu dupliqué est presque prêt. Tel quel, il fonctionne, reste un détail : qu’arrive t’il si le visiteur se trouve sur une page sans sous-menu ou non listée dans le menu ? Le sous-menu ne sortira aucun item (et provoquera une notice php car $current est égal alors à 0), mais imprimera tout de même ce qui va autour (les conteneurs) : les balises <ul> et <div> ou <nav>.
Comment faire ?
Si aucun item $current n’a été trouvé, nous allons prendre le premier item qui nous tombe sous la main (pour avoir un item valide à sortir) et lui donner une url spéciale facilement repérable lors d’une recherche de caractères.

01020304050607080910111213141516171819202122232425262728293031323334

add_filter( 'wp_nav_menu_objects', 'minime_nav_menu_objects', 10, 2 );

function minime_nav_menu_objects( $items, $args ) {
	static $duplicate = array();

	if ( ! empty( $duplicate[ $args->theme_location ] ) ) {
		$current = 0;
		$new_items = array();
		foreach ( $items as $item ) {
			if ( ! $item->menu_item_parent && ( $item->current || $item->current_item_ancestor ) ) {
				$current = $item;
				break;
			}
		}

		if ( $current ) {
			foreach ( $items as $item ) {
				if ( $item->menu_item_parent === $current->ID ) {
					$new_items[] = $item;
				}
			}
		}
		else {
			$current = reset( $items );
			$current->url = '#empty_items##';
		}

		array_unshift( $new_items, $current );
		$items = $new_items;
	}
	$duplicate[ $args->theme_location ] = 1;

	return $items;
}

Ensuite, il nous faut filtrer la sortie du menu et retourner une chaine vide si notre url spéciale est trouvée.

3637383940414243

add_filter( 'wp_nav_menu', 'minime_nav_menu', 10, 2 );

function minime_nav_menu( $nav_menu, $args ) {
	if ( strpos( $nav_menu, '"#empty_items##"' ) !== false ) {
		return '';
	}
	return $nav_menu;
}

Hop, terminé.
See ya!