core/includes/menu.inc | 108 ++++++++------------- core/includes/theme.inc | 27 ++---- .../Core/Menu/DefaultMenuLinkTreeManipulators.php | 29 +++++- core/modules/system/templates/menu-tree.html.twig | 40 ++++++++ .../user/src/Tests/UserAccountLinksTests.php | 4 +- .../Menu/DefaultMenuLinkTreeManipulatorsTest.php | 59 +++++++++++ core/themes/bartik/bartik.theme | 22 +++-- 7 files changed, 189 insertions(+), 100 deletions(-) diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 24d9033..e9a3dba 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -101,26 +101,42 @@ */ /** - -/** * Implements template_preprocess_HOOK() for theme_menu_tree(). */ function template_preprocess_menu_tree(&$variables) { - $variables['tree'] = $variables['tree']['#children']; -} + if (isset($variables['tree']['#heading'])) { + $variables['heading'] = $variables['tree']['#heading']; + $heading = &$variables['heading']; + // Convert a string heading into an array, using a H2 tag by default. + if (is_string($heading)) { + $heading = array('text' => $heading); + } + // Merge in default array properties into $heading. + $heading += array( + 'level' => 'h2', + 'attributes' => array(), + ); + // @todo Remove backwards compatibility for $heading['class']. + if (isset($heading['class'])) { + $heading['attributes']['class'] = $heading['class']; + } + // Convert the attributes array into an Attribute object. + $heading['attributes'] = new Attribute($heading['attributes']); + $heading['text'] = String::checkPlain($heading['text']); + } -/** - * Returns HTML for a wrapper for a menu sub-tree. - * - * @param $variables - * An associative array containing: - * - tree: An HTML string containing the tree's items. - * - * @see template_preprocess_menu_tree() - * @ingroup themeable - */ -function theme_menu_tree($variables) { - return ''; + if (isset($variables['tree']['#attributes'])) { + $variables['attributes'] = new Attribute($variables['tree']['#attributes']); + } + else { + $variables['attributes'] = new Attribute(); + } + if (!isset($variables['attributes']['class'])) { + $variables['attributes']['class'] = array(); + } + $variables['attributes']['class'][] = 'menu'; + + $variables['tree'] = $variables['tree']['#children']; } /** @@ -282,23 +298,17 @@ function _menu_get_links_source($name, $default) { } /** - * Returns an array of links for a navigation menu. + * Builds a renderable array for a navigation menu. * - * @param $menu_name + * @param string $menu_name * The name of the menu. - * @param $level + * @param int $level * Optional, the depth of the menu to be returned. * - * @return - * An array of links of the specified menu and level. + * @return array + * A renderable array. */ function menu_navigation_links($menu_name, $level = 0) { - // Don't even bother querying the menu table if no menu is specified. - if (empty($menu_name)) { - return array(); - } - - // Get the menu hierarchy for the current page. /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = \Drupal::service('menu.link_tree'); $parameters = $menu_tree->buildPageDataTreeParameters($menu_name, $level + 1); @@ -306,50 +316,10 @@ function menu_navigation_links($menu_name, $level = 0) { $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + array('callable' => 'menu.default_tree_manipulators:extractSubtreeOfActiveTrail', 'args' => array($level)), ); $tree = $menu_tree->transform($tree, $manipulators); - - // Go down the active trail until the right level is reached. - while ($level-- > 0 && $tree) { - // Loop through the current level's items until we find one that is in trail. - while ($item = array_shift($tree)) { - if ($item['in_active_trail']) { - // If the item is in the active trail, we continue in the subtree. - $tree = empty($item['below']) ? array() : $item['below']; - break; - } - } - } - - // Create a single level of links. - $links = array(); - foreach ($tree as $item) { - /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ - $link = $item['link']; - - if ($link->isHidden()) { - continue; - } - - $class = ''; - $url = $link->getUrlObject(); - $l = $url->getOptions(); - $l['title'] = $link->getTitle(); - if ($url->isExternal()) { - $l['href'] = $url->getPath(); - } - else { - $l['route_name'] = $url->getRouteName(); - $l['route_parameters'] = $url->getRouteParameters(); - } - if ($item['in_active_trail']) { - $class = ' active-trail'; - $l['attributes']['class'][] = 'active-trail'; - } - // Keyed with the unique ID to generate classes in links.html.twig. - $links['menu-' . $link->getPluginId() . $class] = $l; - } - return $links; + return $menu_tree->render($tree); } /** diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 03d0df0..2cefe31 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2109,27 +2109,17 @@ function template_preprocess_page(&$variables) { // Pass the main menu and secondary menu to the template as render arrays. if (!empty($variables['main_menu'])) { - $variables['main_menu'] = array( - '#theme' =>'links__system_main_menu', - '#links' => $variables['main_menu'], - '#heading' => array( - 'text' => t('Main menu'), - 'class' => array('visually-hidden'), - 'attributes' => array('id' => 'links__system_main_menu'), - ), - '#set_active_class' => TRUE, + $variables['main_menu']['#heading'] = array( + 'text' => t('Main menu'), + 'class' => array('visually-hidden'), + 'attributes' => array('id' => 'links__system_main_menu'), ); } if (!empty($variables['secondary_menu'])) { - $variables['secondary_menu'] = array( - '#theme' =>'links__system_secondary_menu', - '#links' => $variables['secondary_menu'], - '#heading' => array( - 'text' => t('Secondary menu'), - 'class' => array('visually-hidden'), - 'attributes' => array('id' => 'links__system_secondary_menu'), - ), - '#set_active_class' => TRUE, + $variables['secondary_menu']['#heading'] = array( + 'text' => t('Secondary menu'), + 'class' => array('visually-hidden'), + 'attributes' => array('id' => 'links__system_secondary_menu'), ); } @@ -2599,6 +2589,7 @@ function drupal_common_theme() { ), 'menu_tree' => array( 'render element' => 'tree', + 'template' => 'menu-tree', ), 'menu_local_task' => array( 'render element' => 'element', diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php index a216ef3..9e3c484 100644 --- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php @@ -131,7 +131,7 @@ public function generateIndexAndSort(array $tree) { * @return array * The manipulated menu tree. */ - public function flatten($tree) { + public function flatten(array $tree) { foreach ($tree as $key => $item) { if ($tree[$key]['below']) { $tree += $this->flatten($tree[$key]['below']); @@ -141,4 +141,31 @@ public function flatten($tree) { return $tree; } + /** + * Tree manipulator that extracts a subtree of the active trail. + * + * @param array $tree + * The menu tree to manipulate. + * @param int $level + * The level in the active trail to extract. + * + * @return array + * The manipulated menu tree. + */ + public function extractSubtreeOfActiveTrail(array $tree, $level) { + // Go down the active trail until the right level is reached. + while ($level-- > 0 && $tree) { + // Loop through the current level's items until we find one that is in the + // active trail. + while ($item = array_shift($tree)) { + if (isset($item['in_active_trail']) && $item['in_active_trail']) { + // If the item is in the active trail, we continue in the subtree. + $tree = empty($item['below']) ? array() : $item['below']; + break; + } + } + } + return $tree; + } + } diff --git a/core/modules/system/templates/menu-tree.html.twig b/core/modules/system/templates/menu-tree.html.twig new file mode 100644 index 0000000..1d47cd9 --- /dev/null +++ b/core/modules/system/templates/menu-tree.html.twig @@ -0,0 +1,40 @@ +{# +/** + * @file + * Default theme implementation for a menu tree. + * + * Available variables: + * - attributes: Attributes for the UL containing the tree of links. + * - tree: Menu tree to be output. + * - heading: (optional) A heading to precede the links. + * - text: The heading text. + * - level: The heading level (e.g. 'h2', 'h3'). + * - attributes: (optional) A keyed list of attributes for the heading. + * If the heading is a string, it will be used as the text of the heading and + * the level will default to 'h2'. + * + * Headings should be used on navigation menus and any list of links that + * consistently appears on multiple pages. To make the heading invisible use + * the 'visually-hidden' CSS class. Do not use 'display:none', which + * removes it from screen-readers and assistive technology. Headings allow + * screen-reader and keyboard only users to navigate to or skip the links. + * See http://juicystudio.com/article/screen-readers-display-none.php and + * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. + * + * @see template_preprocess_menu_tree() + * + * @ingroup themeable + */ +#} +{% if tree -%} + {%- if heading -%} + {%- if heading.level -%} + <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }} + {%- else -%} + {{ heading.text }} + {%- endif -%} + {%- endif -%} + + {{ tree }} + +{%- endif %} diff --git a/core/modules/user/src/Tests/UserAccountLinksTests.php b/core/modules/user/src/Tests/UserAccountLinksTests.php index 47c5db1..451d9b3 100644 --- a/core/modules/user/src/Tests/UserAccountLinksTests.php +++ b/core/modules/user/src/Tests/UserAccountLinksTests.php @@ -49,14 +49,14 @@ function testSecondaryMenu() { // For a logged-in user, expect the secondary menu to have links for "My // account" and "Log out". $link = $this->xpath('//ul[@class=:menu_class]/li/a[contains(@href, :href) and text()=:text]', array( - ':menu_class' => 'links', + ':menu_class' => 'menu', ':href' => 'user', ':text' => 'My account', )); $this->assertEqual(count($link), 1, 'My account link is in secondary menu.'); $link = $this->xpath('//ul[@class=:menu_class]/li/a[contains(@href, :href) and text()=:text]', array( - ':menu_class' => 'links', + ':menu_class' => 'menu', ':href' => 'user/logout', ':text' => 'Log out', )); diff --git a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php index 1e1f688..88068c7 100644 --- a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php @@ -202,4 +202,63 @@ public function testFlatten() { $this->assertEquals(array(1, 2, 5, 6, 8, 3, 4, 7), array_keys($tree)); } + /** + * Tests the extractSubtreeOfActiveTrail() tree manipulator. + * + * @covers ::extractSubtreeOfActiveTrail + */ + public function testExtractSubtreeOfActiveTrail() { + // No link in the active trail. + $this->mockTree(); + // Get level 0. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0); + $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree)); + // Get level 1. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1); + $this->assertEquals(array(), array_keys($tree)); + // Get level 2. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1); + $this->assertEquals(array(), array_keys($tree)); + + // Link 5 in the active trail. + $this->mockTree(); + $this->originalTree[5]['in_active_trail'] = TRUE; + // Get level 0. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0); + $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree)); + // Get level 1. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1); + $this->assertEquals(array(7), array_keys($tree)); + // Get level 2. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 2); + $this->assertEquals(array(), array_keys($tree)); + + // Link 2 in the active trail. + $this->mockTree(); + $this->originalTree[2]['in_active_trail'] = TRUE; + // Get level 0. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0); + $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree)); + // Get level 1. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1); + $this->assertEquals(array(3), array_keys($tree)); + // Get level 2. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 2); + $this->assertEquals(array(), array_keys($tree)); + + // Links 2 and 3 in the active trail. + $this->mockTree(); + $this->originalTree[2]['in_active_trail'] = TRUE; + $this->originalTree[2]['below'][3]['in_active_trail'] = TRUE; + // Get level 0. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0); + $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree)); + // Get level 1. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1); + $this->assertEquals(array(3), array_keys($tree)); + // Get level 2. + $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 2); + $this->assertEquals(array(4), array_keys($tree)); + } + } diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme index db70a55..6fb840c 100644 --- a/core/themes/bartik/bartik.theme +++ b/core/themes/bartik/bartik.theme @@ -56,18 +56,15 @@ function bartik_preprocess_page(&$variables) { // Store back the classes to the htmlpage object. $attributes['class'] = $classes; - // Pass the main menu and secondary menu to the template as render arrays. + // Set additional attributes on the primary and secondary navigation menus. if (!empty($variables['main_menu'])) { $variables['main_menu']['#attributes']['id'] = 'main-menu-links'; - $variables['main_menu']['#attributes']['class'] = array('links', 'clearfix'); + $variables['main_menu']['#attributes']['class'][] = 'links'; } if (!empty($variables['secondary_menu'])) { $variables['secondary_menu']['#attributes']['id'] = 'secondary-menu-links'; - $variables['secondary_menu']['#attributes']['class'] = array( - 'links', - 'inline', - 'clearfix', - ); + $variables['secondary_menu']['#attributes']['class'][] = 'links'; + $variables['secondary_menu']['#attributes']['class'][] = 'inline'; } // Set the options that apply to both page and maintenance page. @@ -139,10 +136,15 @@ function bartik_preprocess_block(&$variables) { } /** - * Implements THEME_menu_tree(). + * Implements hook_preprocess_HOOK() for menu-tree.html.twig. + * + * @see template_preprocess_menu_tree() */ -function bartik_menu_tree($variables) { - return ''; +function bartik_preprocess_menu_tree(&$variables) { + if (!isset($variables['attributes']['class'])) { + $variables['attributes']['class'] = array(); + } + $variables['attributes']['class'][] = 'clearfix'; } /**