diff --git a/core/includes/menu.inc b/core/includes/menu.inc index de98776..eedd3a2 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -1858,14 +1858,13 @@ function menu_navigation_links($menu_name, $level = 0) { * * @return * An array containing - * - tabs: Local tasks for the requested level: - * - count: The number of local tasks. - * - output: The themed output of local tasks. - * - actions: Action links for the requested level: - * - count: The number of action links. - * - output: The themed output of action links. + * - tabs: Local tasks for the requested level. + * - actions: Action links for the requested level. * - root_path: The router path for the current page. If the current page is * a default local task, then this corresponds to the parent tab. + * + * @see hook_menu_local_tasks() + * @see hook_menu_local_tasks_alter() */ function menu_local_tasks($level = 0) { $data = &drupal_static(__FUNCTION__); @@ -1957,7 +1956,7 @@ function menu_local_tasks($level = 0) { if ($link['href'] != current_path()) { $link['localized_options']['attributes']['class'][] = 'active'; } - $tabs_current[] = array( + $tabs_current[$link['href']] = array( '#theme' => 'menu_local_task', '#link' => $link, '#active' => TRUE, @@ -1971,7 +1970,7 @@ function menu_local_tasks($level = 0) { // MENU_IS_LOCAL_ACTION before checking. if (($item['type'] & MENU_IS_LOCAL_ACTION) == MENU_IS_LOCAL_ACTION) { // The item is an action, display it as such. - $actions_current[] = array( + $actions_current[$link['href']] = array( '#theme' => 'menu_local_action', '#link' => $link, '#weight' => isset($link['weight']) ? $link['weight'] : NULL, @@ -1980,7 +1979,7 @@ function menu_local_tasks($level = 0) { } else { // Otherwise, it's a normal tab. - $tabs_current[] = array( + $tabs_current[$link['href']] = array( '#theme' => 'menu_local_task', '#link' => $link, '#weight' => isset($link['weight']) ? $link['weight'] : NULL, @@ -2034,7 +2033,7 @@ function menu_local_tasks($level = 0) { if ($link['href'] != current_path()) { $link['localized_options']['attributes']['class'][] = 'active'; } - $tabs_current[] = array( + $tabs_current[$link['href']] = array( '#theme' => 'menu_local_task', '#link' => $link, '#active' => TRUE, @@ -2046,7 +2045,7 @@ function menu_local_tasks($level = 0) { } } else { - $tabs_current[] = array( + $tabs_current[$link['href']] = array( '#theme' => 'menu_local_task', '#link' => $link, '#weight' => isset($link['weight']) ? $link['weight'] : NULL, @@ -2194,7 +2193,7 @@ function menu_contextual_links($module, $parent_path, $args) { function menu_primary_local_tasks() { $links = menu_local_tasks(0); // Do not display single tabs. - return count($links['tabs']) > 1 ? $links['tabs'] : ''; + return count(element_get_visible_children($links['tabs'])) > 1 ? $links['tabs'] : ''; } /** @@ -2203,7 +2202,7 @@ function menu_primary_local_tasks() { function menu_secondary_local_tasks() { $links = menu_local_tasks(1); // Do not display single tabs. - return count($links['tabs']) > 1 ? $links['tabs'] : ''; + return count(element_get_visible_children($links['tabs'])) > 1 ? $links['tabs'] : ''; } /** @@ -2226,11 +2225,12 @@ function menu_tab_root_path() { * Returns a renderable element for the primary and secondary tabs. */ function menu_local_tabs() { - return array( + $build = array( '#theme' => 'menu_local_tasks', '#primary' => menu_primary_local_tasks(), '#secondary' => menu_secondary_local_tasks(), ); + return !empty($build['#primary']) ? $build : array(); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/LocalTasksTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/LocalTasksTest.php new file mode 100644 index 0000000..5f695c0 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/LocalTasksTest.php @@ -0,0 +1,143 @@ + 'Local tasks', + 'description' => 'Tests local tasks derived from router and added/altered via hooks.', + 'group' => 'Menu', + ); + } + + /** + * Tests appearance of local tasks. + * + * @see menu_test_menu() + * @see menu_test_menu_local_tasks() + * @see menu_test_menu_local_tasks_alter() + */ + function testLocalTasks() { + // Verify that there is no local tasks markup if none are defined in the + // router and no module adds any dynamically. + $this->drupalGet('menu-test/tasks/empty'); + $this->assertNoRaw('tabs'); + $this->drupalGet('menu-test/tasks/default'); + $this->assertNoRaw('tabs'); + + // Verify that local tasks appear as defined in the router. + $this->drupalGet('menu-test/tasks/tasks'); + $this->assertLocalTasks(array( + // MENU_DEFAULT_LOCAL_TASK is expected to get a default weight of -10 + // (without having to define it manually), so it should appear first, + // despite that its label is "View". + 'menu-test/tasks/tasks', + 'menu-test/tasks/tasks/edit', + 'menu-test/tasks/tasks/settings', + )); + + // Enable addition of tasks in menu_test_menu_local_tasks(). + config('menu_test.settings')->set('tasks.add', TRUE)->save(); + + // Verify that the added tasks appear even if there are no tasks normally. + $this->drupalGet('menu-test/tasks/empty'); + $this->assertLocalTasks(array( + 'task/foo', + 'task/bar', + )); + + // Verify that the default local task appears before the added tasks. + $this->drupalGet('menu-test/tasks/default'); + $this->assertLocalTasks(array( + 'menu-test/tasks/default', + 'task/foo', + 'task/bar', + )); + + // Verify that the added tasks appear within normal tasks. + $this->drupalGet('menu-test/tasks/tasks'); + $this->assertLocalTasks(array( + 'menu-test/tasks/tasks', + // The Edit task defines no weight, which is expected to sort as 0. + 'menu-test/tasks/tasks/edit', + 'task/foo', + 'task/bar', + 'menu-test/tasks/tasks/settings', + )); + + // Enable manipulation of tasks in menu_test_menu_local_tasks_alter(). + config('menu_test.settings')->set('tasks.alter', TRUE)->save(); + + // Verify that the added tasks appear even if there are no tasks normally. + $this->drupalGet('menu-test/tasks/empty'); + $this->assertLocalTasks(array( + 'task/bar', + 'task/foo', + )); + $this->assertNoText('Show it'); + $this->assertText('Advanced settings'); + + // Verify that the default local task appears before the added tasks. + $this->drupalGet('menu-test/tasks/default'); + $this->assertLocalTasks(array( + 'menu-test/tasks/default', + 'task/bar', + 'task/foo', + )); + $this->assertText('Show it'); + $this->assertText('Advanced settings'); + + // Verify that the added tasks appear within normal tasks. + $this->drupalGet('menu-test/tasks/tasks'); + $this->assertLocalTasks(array( + 'menu-test/tasks/tasks', + 'menu-test/tasks/tasks/edit', + 'task/bar', + 'menu-test/tasks/tasks/settings', + 'task/foo', + )); + $this->assertText('Show it'); + $this->assertText('Advanced settings'); + } + + /** + * Asserts local tasks in the page output. + * + * @param array $hrefs + * A list of expected link hrefs of local tasks to assert on the page (in + * the given order). + * @param int $level + * (optional) The local tasks level to assert; 0 for primary, 1 for + * secondary. Defaults to 0. + */ + protected function assertLocalTasks(array $hrefs, $level = 0) { + $elements = $this->xpath('//*[contains(@class, :class)]//a', array( + ':class' => $level == 0 ? 'tabs primary' : 'tabs secondary', + )); + $this->assertTrue(count($elements), 'Local tasks found.'); + foreach ($hrefs as $index => $element) { + $expected = url($hrefs[$index]); + $method = ($elements[$index]['href'] == $expected ? 'pass' : 'fail'); + $this->{$method}(format_string('Task @number href @value equals @expected.', array( + '@number' => $index + 1, + '@value' => (string) $elements[$index]['href'], + '@expected' => $expected, + ))); + } + } + +} diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 5dd8534..f646c70 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -1054,10 +1054,10 @@ function hook_menu_link_delete($link) { * * @param array $data * An associative array containing: - * - actions: A list of of actions, each one being an associative array - * as described above. - * - tabs: A list of (up to 2) tab levels that contain a list of of tabs, each - * one being an associative array as described above. + * - actions: A list of of actions keyed by their href, each one being an + * associative array as described above. + * - tabs: A list of (up to 2) tab levels that contain a list of of tabs keyed + * by their href, each one being an associative array as described above. * @param array $router_item * The menu system router item of the page. * @param string $root_path @@ -1065,7 +1065,7 @@ function hook_menu_link_delete($link) { */ function hook_menu_local_tasks(&$data, $router_item, $root_path) { // Add an action linking to node/add to all pages. - $data['actions'][] = array( + $data['actions']['node/add'] = array( '#theme' => 'menu_local_action', '#link' => array( 'title' => t('Add new content'), @@ -1079,7 +1079,7 @@ function hook_menu_local_tasks(&$data, $router_item, $root_path) { ); // Add a tab linking to node/add to all pages. - $data['tabs'][0][] = array( + $data['tabs'][0]['node/add'] = array( '#theme' => 'menu_local_task', '#link' => array( 'title' => t('Example tab'), @@ -1090,9 +1090,7 @@ function hook_menu_local_tasks(&$data, $router_item, $root_path) { ), ), ), - // Define whether this link is active. This can be omitted for - // implementations that add links to pages outside of the current page - // context. + // Define whether this link is active. This can usually be omitted. '#active' => ($router_item['path'] == $root_path), ); } diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module index b0bcdc5..1b0ada5 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.module +++ b/core/modules/system/tests/modules/menu_test/menu_test.module @@ -236,6 +236,38 @@ function menu_test_menu() { 'type' => MENU_LOCAL_TASK, ) + $base; + // Menu local tasks tests. + // @see Drupal\system\Tests\Menu\LocalTasksTest + $items['menu-test/tasks'] = array( + 'title' => 'Local tasks', + ) + $base; + $items['menu-test/tasks/empty'] = array( + 'title' => 'Empty', + ) + $base; + $items['menu-test/tasks/default'] = array( + 'title' => 'Default only', + ) + $base; + $items['menu-test/tasks/default/view'] = array( + 'title' => 'View', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ) + $base; + $items['menu-test/tasks/tasks'] = array( + 'title' => 'With tasks', + ) + $base; + $items['menu-test/tasks/tasks/view'] = array( + 'title' => 'View', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ) + $base; + $items['menu-test/tasks/tasks/edit'] = array( + 'title' => 'Edit', + 'type' => MENU_LOCAL_TASK, + ) + $base; + $items['menu-test/tasks/tasks/settings'] = array( + 'title' => 'Settings', + 'type' => MENU_LOCAL_TASK, + 'weight' => 100, + ) + $base; + // Menu trail tests. // @see MenuTrailTestCase $items['menu-test/menu-trail'] = array( @@ -364,6 +396,56 @@ function menu_test_menu() { } /** + * Implements hook_menu_local_tasks(). + */ +function menu_test_menu_local_tasks(&$data, $router_item, $root_path) { + if (!config('menu_test.settings')->get('tasks.add')) { + return; + } + if (strpos($router_item['tab_root'], 'menu-test/tasks/') === 0) { + $data['tabs'][0]['foo'] = array( + '#theme' => 'menu_local_task', + '#link' => array( + 'title' => 'Task 1', + 'href' => 'task/foo', + ), + '#weight' => 10, + ); + $data['tabs'][0]['bar'] = array( + '#theme' => 'menu_local_task', + '#link' => array( + 'title' => 'Task 2', + 'href' => 'task/bar', + ), + '#weight' => 20, + ); + } +} + +/** + * Implements hook_menu_local_tasks_alter(). + */ +function menu_test_menu_local_tasks_alter(&$data, $router_item, $root_path) { + if (!config('menu_test.settings')->get('tasks.alter')) { + return; + } + if (strpos($router_item['tab_root'], 'menu-test/tasks/') === 0) { + // Rename the default local task from 'View' to 'Show'. + // $data['tabs'] is expected to be keyed by link hrefs. + // The default local task always links to its parent path, which means that + // if the tab root path appears as key in $data['tabs'], then that key is + // the default local task. + $key = $router_item['tab_root']; + if (isset($data['tabs'][0][$key])) { + $data['tabs'][0][$key]['#link']['title'] = 'Show it'; + } + // Rename the 'foo' task to "Advanced settings" and put it last. + $data['tabs'][0]['foo']['#link']['title'] = 'Advanced settings'; + $data['tabs'][0]['foo']['#weight'] = 110; + } +} + +/** * Dummy argument loader for hook_menu() to point to. */ function menu_test_argument_load($arg1) { diff --git a/core/themes/seven/template.php b/core/themes/seven/template.php index b92d064..4921748 100644 --- a/core/themes/seven/template.php +++ b/core/themes/seven/template.php @@ -34,7 +34,7 @@ function seven_preprocess_page(&$vars) { unset($vars['primary_local_tasks']['#secondary']); $vars['secondary_local_tasks'] = array( '#theme' => 'menu_local_tasks', - '#secondary' => $vars['tabs']['#secondary'], + '#secondary' => isset($vars['tabs']['#secondary']) ? $vars['tabs']['#secondary'] : '', ); }