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 .= '' . $heading['level'] . '>';
+ // 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 }}{{ heading.level }}>
+ {% 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(