diff --git a/core/includes/common.inc b/core/includes/common.inc index 8f60fe4..502052d 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -3527,7 +3527,7 @@ function drupal_pre_render_link($element) { * Pre-render callback: Collects child links into a single array. * * This function can be added as a pre_render callback for a renderable array, - * usually one which will be themed by theme_links(). It iterates through all + * usually one which will be themed by links.html.twig. It iterates through all * unrendered children of the element, collects any #links properties it finds, * merges them into the parent element's #links array, and prevents those * children from being rendered separately. @@ -3548,21 +3548,21 @@ function drupal_pre_render_link($element) { * '#theme' => 'links__node__comment', * '#links' => array( * // An array of links associated with node comments, suitable for - * // passing in to theme_links(). + * // passing in to links.html.twig. * ), * ), * 'statistics' => array( * '#theme' => 'links__node__statistics', * '#links' => array( * // An array of links associated with node statistics, suitable for - * // passing in to theme_links(). + * // passing in to links.html.twig. * ), * ), * 'translation' => array( * '#theme' => 'links__node__translation', * '#links' => array( * // An array of links associated with node translation, suitable for - * // passing in to theme_links(). + * // passing in to links.html.twig. * ), * ), * ); diff --git a/core/includes/form.inc b/core/includes/form.inc index 5867a36..0885aeb 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -1422,7 +1422,7 @@ function form_pre_render_actions_dropbutton(array $element) { } // Add this button to the corresponding dropbutton. // @todo Change #type 'dropbutton' to be based on theme_item_list() - // instead of theme_links() to avoid this preemptive rendering. + // instead of links.html.twig to avoid this preemptive rendering. $button = drupal_render($element[$key]); $dropbuttons[$dropbutton]['#links'][$key] = array( 'title' => $button, diff --git a/core/includes/menu.inc b/core/includes/menu.inc index a8a30b6..618f16e 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -1936,7 +1936,7 @@ function menu_navigation_links($menu_name, $level = 0) { if ($item['link']['href'] == $router_item['tab_root_href'] && $item['link']['href'] != current_path()) { $l['attributes']['class'][] = 'active'; } - // Keyed with the unique mlid to generate classes in theme_links(). + // Keyed with the unique mlid to generate classes in links.html.twig. $links['menu-' . $item['link']['mlid'] . $class] = $l; } } @@ -2186,6 +2186,114 @@ function _menu_get_legacy_tasks($router_item, &$data, &$root_path) { } /** + * Retrieves contextual links for a path based on registered local tasks. + * + * This leverages the menu system to retrieve the first layer of registered + * local tasks for a given system path. All local tasks of the tab type + * MENU_CONTEXT_INLINE are taken into account. + * + * For example, when considering the following registered local tasks: + * - node/%node/view (default local task) with no 'context' defined + * - node/%node/edit with context: MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE + * - node/%node/revisions with context: MENU_CONTEXT_PAGE + * - node/%node/report-as-spam with context: MENU_CONTEXT_INLINE + * + * If the path "node/123" is passed to this function, then it will return the + * links for 'edit' and 'report-as-spam'. + * + * @param $module + * The name of the implementing module. This is used to prefix the key for + * each contextual link, which is transformed into a CSS class during + * rendering by links.html.twig. For example, if $module is 'block' and the + * retrieved local task path argument is 'edit', then the resulting CSS class + * will be 'block-edit'. + * @param $parent_path + * The static menu router path of the object to retrieve local tasks for, for + * example 'node' or 'admin/structure/block/manage'. + * @param $args + * A list of dynamic path arguments to append to $parent_path to form the + * fully-qualified menu router path; for example, array(123) for a certain + * node or array('system', 'tools') for a certain block. + * + * @return + * A list of menu router items that are local tasks for the passed-in path. + * + * @see contextual_links_preprocess() + * @see hook_menu() + */ +function menu_contextual_links($module, $parent_path, $args) { + static $path_empty = array(); + + $links = array(); + // Performance: In case a previous invocation for the same parent path did not + // return any links, we immediately return here. + if (isset($path_empty[$parent_path]) && strpos($parent_path, '%') !== FALSE) { + return $links; + } + // Construct the item-specific parent path. + $path = $parent_path . '/' . implode('/', $args); + + // Get the router item for the given parent link path. + $router_item = menu_get_item($path); + if (!$router_item || !$router_item['access']) { + $path_empty[$parent_path] = TRUE; + return $links; + } + $data = &drupal_static(__FUNCTION__, array()); + $root_path = $router_item['path']; + + // Performance: For a single, normalized path (such as 'node/%') we only query + // available tasks once per request. + if (!isset($data[$root_path])) { + // Get all contextual links that are direct children of the router item and + // not of the tab type 'view'. + $data[$root_path] = db_select('menu_router', 'm') + ->fields('m') + ->condition('tab_parent', $router_item['tab_root']) + ->condition('context', MENU_CONTEXT_NONE, '<>') + ->condition('context', MENU_CONTEXT_PAGE, '<>') + ->orderBy('weight') + ->orderBy('title') + ->execute() + ->fetchAllAssoc('path', PDO::FETCH_ASSOC); + } + $parent_length = drupal_strlen($root_path) + 1; + $map = $router_item['original_map']; + foreach ($data[$root_path] as $item) { + // Extract the actual "task" string from the path argument. + $key = drupal_substr($item['path'], $parent_length); + + // Denormalize and translate the contextual link. + _menu_translate($item, $map, TRUE); + if (!$item['access']) { + continue; + } + + // If this item is a default local task, rewrite the href to link to its + // parent item. + if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) { + $item['href'] = $item['tab_parent_href']; + } + + // All contextual links are keyed by the actual "task" path argument, + // prefixed with the name of the implementing module. + $links[$module . '-' . $key] = $item; + } + + // Allow modules to alter contextual links. + drupal_alter('menu_contextual_links', $links, $router_item, $root_path); + + // Performance: If the current user does not have access to any links for this + // router path and no other module added further links, we assign FALSE here + // to skip the entire process the next time the same router path is requested. + if (empty($links)) { + $path_empty[$parent_path] = TRUE; + } + + return $links; +} + +/** * Returns the rendered local tasks at the top level. */ function menu_primary_local_tasks() { diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 069b497..213b2f8 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1271,9 +1271,11 @@ function template_preprocess_status_messages(&$variables) { } /** - * Returns HTML for a set of links. + * Prepares variables for links templates. * - * @param $variables + * Default template: links.html.twig. + * + * @param array $variables * An associative array containing: * - links: An associative array of links to be themed. The key for each link * is used as its CSS class. Each link should be itself an array, with the @@ -1307,11 +1309,10 @@ function template_preprocess_status_messages(&$variables) { * http://juicystudio.com/article/screen-readers-display-none.php and * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. */ -function theme_links($variables) { +function template_preprocess_links(&$variables) { + $language_url = language(Language::TYPE_URL); $links = $variables['links']; - $attributes = $variables['attributes']; - $heading = $variables['heading']; - $output = ''; + $heading = &$variables['heading']; if (!empty($links)) { // Prepend the heading to the list, if any. @@ -1329,14 +1330,11 @@ function theme_links($variables) { if (isset($heading['class'])) { $heading['attributes']['class'] = $heading['class']; } - - $output .= '<' . $heading['level'] . new Attribute($heading['attributes']) . '>'; - $output .= String::checkPlain($heading['text']); - $output .= ''; + // Convert the attributes array into an attributes object. + $heading['attributes'] = new Attribute($heading['attributes']); + $heading['text'] = String::checkPlain($heading['text']); } - $output .= ''; - $num_links = count($links); $i = 0; $active = \Drupal::linkGenerator()->getActive(); @@ -1362,69 +1360,45 @@ function theme_links($variables) { if ($i == $num_links) { $class[] = 'last'; } - - $link_element = array( - '#type' => 'link', - '#title' => $link['title'], - '#options' => array_diff_key($link, MapArray::copyValuesToKeys(array('title', 'href', 'route_name', 'route_parameters'))), - '#href' => $link['href'], - '#route_name' => $link['route_name'], - '#route_parameters' => $link['route_parameters'], - ); - - // @todo Reconcile Views usage of 'ajax' as a boolean with the rest of - // core's usage of it as an array. - if (isset($link['ajax']) && is_array($link['ajax'])) { - $link_element['#ajax'] = $link['ajax']; - } - - // Handle links and ensure that the active class is added on the LIs. - if (isset($link['route_name'])) { - $variables = array( - 'options' => array(), - ); - if (!empty($link['language'])) { - $variables['options']['language'] = $link['language']; - } - - if (($link['route_name'] == $active['route_name']) - // The language of an active link is equal to the current language. - && (empty($variables['options']['language']) || ($variables['options']['language']->id == $active['language'])) - && ($link['route_parameters'] == $active['parameters'])) { - $class[] = 'active'; - } - - $item = drupal_render($link_element); - } - elseif (isset($link['href'])) { + // Handle links. + if (isset($link['href'])) { $is_current_path = ($link['href'] == current_path() || ($link['href'] == '' && drupal_is_front_page())); $is_current_language = (empty($link['language']) || $link['language']->id == $language_url->id); if ($is_current_path && $is_current_language) { $class[] = 'active'; } - $item = drupal_render($link_element); - } - // Handle title-only text items. - else { - // Merge in default array properties into $link. - $link += array( - 'html' => FALSE, + // @todo Reconcile Views usage of 'ajax' as a boolean with the rest of + // core's usage of it as an array. + $item = array( + '#type' => 'link', + '#title' => $link['title'], + '#href' => $link['href'], ); - $item = ($link['html'] ? $link['title'] : String::checkPlain($link['title'])); - if (isset($link['attributes'])) { - $item = '' . $item . ''; + if (isset($link['ajax']) && is_array($link['ajax'])) { + $item += array( + '#ajax' => $link['ajax'], + '#options' => array_diff_key($link, drupal_map_assoc(array('title', 'href', 'ajax'))), + ); } + else { + $item += array( + '#options' => array_diff_key($link, drupal_map_assoc(array('title', 'href'))), + ); + } + $variables['links'][$key]['link'] = $item; } - $output .= ' $class)) . '>'; - $output .= $item; - $output .= ''; - } + // Handle text. + $text = (!empty($link['html']) ? $link['title'] : String::checkPlain($link['title'])); + $variables['links'][$key]['text'] = $text; + if (isset($link['attributes'])) { + $variables['links'][$key]['text_attributes'] = new Attribute($link['attributes']); + } - $output .= ''; + // Handle list item attributes. + $variables['links'][$key]['attributes'] = new Attribute(array('class' => $class)); + } } - - return $output; } /** @@ -2695,6 +2669,7 @@ function drupal_common_theme() { ), 'links' => array( 'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array()), + 'template' => 'links', ), 'dropbutton_wrapper' => array( 'variables' => array('children' => NULL), diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index e405f20..447a5ee 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -274,7 +274,7 @@ function contextual_pre_render_links($element) { $items += $contextual_links_manager->getContextualLinksArrayByGroup($group, $args['route_parameters'], $args['metadata']); } - // Transform contextual links into parameters suitable for theme_links(). + // Transform contextual links into parameters suitable for links.html.twig. $links = array(); foreach ($items as $class => $item) { $class = drupal_html_class($class); @@ -283,6 +283,7 @@ function contextual_pre_render_links($element) { 'route_name' => isset($item['route_name']) ? $item['route_name'] : '', 'route_parameters' => isset($item['route_parameters']) ? $item['route_parameters'] : array(), ); + // @todo links.html.twig should *really* use the same parameters as l(). $item['localized_options'] += array('query' => array()); $item['localized_options']['query'] += drupal_get_destination(); $links[$class] += $item['localized_options']; diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 284df1d..7fbd720 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -2865,7 +2865,7 @@ protected function assertNoTitle($title, $message = '', $group = 'Other') { * Asserts themed output. * * @param $callback - * The name of the theme function to invoke; e.g. 'links' for theme_links(). + * The name of the theme function to invoke; e.g. 'links' for links.html.twig. * @param $variables * An array of variables to pass to the theme function. * @param $expected diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css index c6ed012..1fb5cc6 100644 --- a/core/modules/system/css/system.theme.css +++ b/core/modules/system/css/system.theme.css @@ -397,7 +397,7 @@ ul.menu a.active { } /** - * Markup generated by theme_links(). + * Markup generated by links.html.twig. */ ul.inline, ul.links.inline { diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php index d1930c9..b8128bc 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php @@ -146,7 +146,7 @@ function testItemList() { } /** - * Tests theme_links(). + * Tests links.html.twig. */ function testLinks() { // Verify that empty variables produce no output. @@ -164,6 +164,9 @@ function testLinks() { // because the current path is different when running tests manually via // simpletest.module ('batch') and via the testing framework (''). _current_path(\Drupal::config('system.site')->get('page.front')); + // Release the static variable because it's not getting released before + // the tests run. + drupal_static_reset('drupal_is_front_page'); // Verify that a list of links is properly rendered. $variables = array(); diff --git a/core/modules/system/templates/links.html.twig b/core/modules/system/templates/links.html.twig new file mode 100644 index 0000000..e895feb --- /dev/null +++ b/core/modules/system/templates/links.html.twig @@ -0,0 +1,62 @@ +{# +/** + * @file + * Default theme implementation for a set of links. + * + * Available variables: + * - attributes: Attributes for the UL containing the list of links. + * - links: Links to be output. + * Each link will have the following elements: + * - title: The link text. + * - href: The link URL. If omitted, the 'title' is shown as a plain text + * item in the links list. If 'href' is supplied, the entire link is passed + * to l() as its $options parameter. + * - html: (optional) Whether or not 'title' is HTML. If set, the title will + * not be passed through check_plain(). + * - attributes: (optional) HTML attributes for the anchor, or for the + * tag if no 'href' is supplied. + * - heading: (optional) A heading to precede the links. May be an associative + * array or a string. + * - 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_links() + * + * @ingroup themeable + */ +#} +{% spaceless %} + {% if links %} + {% if heading %} + {% if heading.level %} + <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }} + {% else %} + {{ heading.text }} + {% endif %} + {% endif %} + + {% for item in links %} + + {%- if item.link -%} + {{ item.link }} + {%- elseif item.text_attributes -%} + {{ item.text }} + {%- else -%} + {{ item.text }} + {%- endif -%} + + {% endfor %} + + {% endif %} +{% endspaceless %} diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module index f42844f..170e44b 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.module +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module @@ -110,7 +110,7 @@ function ajax_test_dialog() { ), ); - // Dialog behavior applied to links rendered by theme_links(). + // Dialog behavior applied to links rendered by links.html.twig. $build['links'] = array( '#theme' => 'links', '#links' => array(