Drupal - Drupal 8 - Taxonomy Menu with Hierarchy
I needed the same thing. Unfortunately there is nothing for it yet so I had to create something for time being. It is not pretty but it works so I don't care.
/**
* Returns renderable array of taxonomy terms from Categories vocabulary in
* hierarchical structure ready to be rendered as html list.
*
* @param int $parent
* The ID of the parent taxonomy term.
*
* @param int $max_depth
* The max depth up to which to look up children.
*
* @param string $route_name
* The name of the route to be used for link generation.
* Taxonomy term(ID) will be provided as route parameter.
*
* @return array
*/
function mymodule_categories_tree($parent = 0, $max_depth = NULL, $route_name = 'mymodule.category.view') {
// Load terms
$tree = \Drupal::entityManager()->getStorage('taxonomy_term')->loadTree('categories', $parent, $max_depth);
// Make sure there are terms to work with.
if (empty($tree)) {
return [];
}
// Sort tree by depth so we can easily find out the deepest level
uasort($tree, function($a, $b) {
// Change objects to array
return \Drupal\Component\Utility\SortArray::sortByKeyInt((array) $a, (array) $b, 'depth');
});
// Get the value of the deepest term
$deepest = end($tree);
$deepest = $deepest->depth;
// Create a structured array
$list = [
$parent => [
'items' => [],
'depth' => -1
]
];
foreach ($tree AS $term) {
$list[$term->tid] = (array) $term;
}
// See if we're on a node page and if so open the menu
// on the proper position.
$node = \Drupal::request()->attributes->get('node');
if ($node) {
$categories = $node->get('categories')->getValue();
// Go through each category to find out the least deep one.
// That one will be the one we'll open.
$open_category = $parent;
foreach ($categories AS $target) {
$tid = $target['target_id'];
if ($list[$tid]['depth'] > $list[$open_category]['depth']) {
$open_category = $tid;
}
}
} else {
// See if we're on a term page and set the corresponding item
// as active so we don't have to rely on JS.
$term = \Drupal::request()->attributes->get('taxonomy_term');
if ($term) {
$open_term = $term->id();
}
}
for ($i = $deepest; $i >= 0; $i--) {
foreach ($list AS $term) {
if ($term['depth'] == $i) {
$item = [
'#type' => 'link',
'#weight' => $term['weight'],
'#title' => $term['name'],
'#url' => Url::fromRoute($route_name, ['taxonomy_term' => $term['tid']]),
'#options' => [
'set_active_class' => TRUE
]
];
// If we're on a node page and this category was chosen
// as active, set the link's class.
if (isset($open_category) && $open_category == $term['tid']) {
$item['#attributes']['class'][] = 'active';
}
// If we're on term page, set the link's class to 'active'
// and if this item is a parent, open it.
if (isset($open_term) && $open_term == $term['tid']) {
$item['#attributes']['class'][] = 'active';
if (!empty($term['items'])) {
$item['#wrapper_attributes']['class'][] = 'open';
}
}
// If this item has children
if (!empty($term['items'])) {
$item['items'] = $term['items'];
$item['#wrapper_attributes']['class'][] = 'parent';
$item['#prefix'] = '<span></span>';
// If any of the child items has 'active' class,
// or is also a parent and has 'open' class
// add the 'open' class to this wrapper too.
foreach ($item['items'] AS $child) {
if (
isset($child['#attributes']['class']) && in_array('active', $child['#attributes']['class'])
|| isset($child['#wrapper_attributes']['class']) && in_array('open', $child['#wrapper_attributes']['class'])
) {
$item['#wrapper_attributes']['class'][] = 'open';
break;
}
}
}
foreach ($term['parents'] AS $pid) {
$list[$pid]['items'][$term['tid']] = $item;
}
unset($list[$term['tid']]);
}
}
}
return [
'#theme' => 'item_list',
'#items' => $list[$parent]['items'],
'#attributes' => [
'class' => ['categories-tree']
],
'#attached' => [
'library' => [
'mymodule/categories_tree'
]
]
];
}