diff --git a/core/assets/vendor/classList/LICENSE.md b/core/assets/vendor/classList/LICENSE.md new file mode 100644 index 0000000..cb518af --- /dev/null +++ b/core/assets/vendor/classList/LICENSE.md @@ -0,0 +1,2 @@ +This software is dedicated to the public domain. No warranty is expressed or implied. +Use this software at your own risk. diff --git a/core/assets/vendor/classList/README.md b/core/assets/vendor/classList/README.md new file mode 100644 index 0000000..dd7359c --- /dev/null +++ b/core/assets/vendor/classList/README.md @@ -0,0 +1,7 @@ +classList.js is a cross-browser JavaScript shim that fully implements `element.classList`. Refer to [the MDN page on `element.classList`][1] for more information. + + +![Tracking image](https://in.getclicky.com/212712ns.gif) + + + [1]: https://developer.mozilla.org/en/DOM/element.classList "MDN / DOM / element.classList" diff --git a/core/assets/vendor/classList/classList.js b/core/assets/vendor/classList/classList.js new file mode 100644 index 0000000..1faaec8 --- /dev/null +++ b/core/assets/vendor/classList/classList.js @@ -0,0 +1,179 @@ +/* + * classList.js: Cross-browser full element.classList implementation. + * 2012-11-15 + * + * By Eli Grey, http://eligrey.com + * Public Domain. + * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + */ + +/*global self, document, DOMException */ + +/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ + +if ("document" in self && !( + "classList" in document.createElement("_") && + "classList" in document.createElementNS("http://www.w3.org/2000/svg", "svg") + )) { + +(function (view) { + +"use strict"; + +if (!('Element' in view)) return; + +var + classListProp = "classList" + , protoProp = "prototype" + , elemCtrProto = view.Element[protoProp] + , objCtr = Object + , strTrim = String[protoProp].trim || function () { + return this.replace(/^\s+|\s+$/g, ""); + } + , arrIndexOf = Array[protoProp].indexOf || function (item) { + var + i = 0 + , len = this.length + ; + for (; i < len; i++) { + if (i in this && this[i] === item) { + return i; + } + } + return -1; + } + // Vendors: please allow content code to instantiate DOMExceptions + , DOMEx = function (type, message) { + this.name = type; + this.code = DOMException[type]; + this.message = message; + } + , checkTokenAndGetIndex = function (classList, token) { + if (token === "") { + throw new DOMEx( + "SYNTAX_ERR" + , "An invalid or illegal string was specified" + ); + } + if (/\s/.test(token)) { + throw new DOMEx( + "INVALID_CHARACTER_ERR" + , "String contains an invalid character" + ); + } + return arrIndexOf.call(classList, token); + } + , ClassList = function (elem) { + var + trimmedClasses = strTrim.call(elem.getAttribute("class") || "") + , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] + , i = 0 + , len = classes.length + ; + for (; i < len; i++) { + this.push(classes[i]); + } + this._updateClassName = function () { + elem.setAttribute("class", this.toString()); + }; + } + , classListProto = ClassList[protoProp] = [] + , classListGetter = function () { + return new ClassList(this); + } +; +// Most DOMException implementations don't allow calling DOMException's toString() +// on non-DOMExceptions. Error's toString() is sufficient here. +DOMEx[protoProp] = Error[protoProp]; +classListProto.item = function (i) { + return this[i] || null; +}; +classListProto.contains = function (token) { + token += ""; + return checkTokenAndGetIndex(this, token) !== -1; +}; +classListProto.add = function () { + var + tokens = arguments + , i = 0 + , l = tokens.length + , token + , updated = false + ; + do { + token = tokens[i] + ""; + if (checkTokenAndGetIndex(this, token) === -1) { + this.push(token); + updated = true; + } + } + while (++i < l); + + if (updated) { + this._updateClassName(); + } +}; +classListProto.remove = function () { + var + tokens = arguments + , i = 0 + , l = tokens.length + , token + , updated = false + ; + do { + token = tokens[i] + ""; + var index = checkTokenAndGetIndex(this, token); + if (index !== -1) { + this.splice(index, 1); + updated = true; + } + } + while (++i < l); + + if (updated) { + this._updateClassName(); + } +}; +classListProto.toggle = function (token, forse) { + token += ""; + + var + result = this.contains(token) + , method = result ? + forse !== true && "remove" + : + forse !== false && "add" + ; + + if (method) { + this[method](token); + } + + return !result; +}; +classListProto.toString = function () { + return this.join(" "); +}; + +if (objCtr.defineProperty) { + var classListPropDesc = { + get: classListGetter + , enumerable: true + , configurable: true + }; + try { + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } catch (ex) { // IE 8 doesn't support enumerable:true + if (ex.number === -0x7FF5EC54) { + classListPropDesc.enumerable = false; + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } + } +} else if (objCtr[protoProp].__defineGetter__) { + elemCtrProto.__defineGetter__(classListProp, classListGetter); +} + +}(self)); + +} diff --git a/core/assets/vendor/classList/classList.min.js b/core/assets/vendor/classList/classList.min.js new file mode 100644 index 0000000..fa98825 --- /dev/null +++ b/core/assets/vendor/classList/classList.min.js @@ -0,0 +1,2 @@ +/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ +if("document" in self&&!("classList" in document.createElement("_")&&"classList" in document.createElementNS("http://www.w3.org/2000/svg","svg"))){(function(j){"use strict";if(!("Element" in j)){return}var a="classList",f="prototype",m=j.Element[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p array(), 'html' => FALSE, 'language' => NULL, + 'set_active_class' => FALSE, ); // Add a hreflang attribute if we know the language of this link's url and @@ -1243,35 +1256,21 @@ function l($text, $path, array $options = array()) { $variables['options']['attributes']['hreflang'] = $variables['options']['language']->id; } - // Because l() is called very often we statically cache values that require an - // extra function call. - static $drupal_static_fast; - if (!isset($drupal_static_fast['active'])) { - $drupal_static_fast['active'] = &drupal_static(__FUNCTION__); - } - $active = &$drupal_static_fast['active']; - if (!isset($active)) { - $active = array( - 'path' => current_path(), - 'front_page' => drupal_is_front_page(), - 'language' => language(Language::TYPE_URL)->id, - 'query' => \Drupal::service('request')->query->all(), - ); - } - - // Determine whether this link is "active', meaning that it links to the - // current page. It is important that we stop checking "active" conditions if - // we know the link is not active. This helps ensure that l() remains fast. - // An active link's path is equal to the current path. - $variables['url_is_active'] = ($path == $active['path'] || ($path == '' && $active['front_page'])) - // The language of an active link is equal to the current language. - && (empty($variables['options']['language']) || $variables['options']['language']->id == $active['language']) - // The query parameters of an active link are equal to the current parameters. - && ($variables['options']['query'] == $active['query']); + // Set the "active" class if the 'set_active_class' option is not empty. + if (!empty($variables['options']['set_active_class'])) { + // Add a "data-drupal-link-query" attribute to let the drupal.active-link + // library know the query in a standardized manner. + if (!empty($variables['options']['query'])) { + $query = $variables['options']['query']; + ksort($query); + $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query); + } - // Add the "active" class if appropriate. - if ($variables['url_is_active']) { - $variables['options']['attributes']['class'][] = 'active'; + // Add a "data-drupal-link-system-path" attribute to let the + // drupal.active-link library know the path in a standardized manner. + if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) { + $variables['options']['attributes']['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getSystemPath($path); + } } // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags() @@ -2149,6 +2148,7 @@ function drupal_add_js($data = NULL, $options = NULL) { // @todo Make this less hacky: http://drupal.org/node/1547376. $scriptPath = $GLOBALS['script_path']; $pathPrefix = ''; + $current_query = \Drupal::service('request')->query->all(); url('', array('script' => &$scriptPath, 'prefix' => &$pathPrefix)); $current_path = current_path(); $current_path_is_admin = FALSE; @@ -2156,13 +2156,20 @@ function drupal_add_js($data = NULL, $options = NULL) { if (!(defined('MAINTENANCE_MODE') && MAINTENANCE_MODE === 'update')) { $current_path_is_admin = path_is_admin($current_path); } - $javascript['settings']['data'][] = array( + $path = array( 'basePath' => base_path(), 'scriptPath' => $scriptPath, 'pathPrefix' => $pathPrefix, 'currentPath' => $current_path, 'currentPathIsAdmin' => $current_path_is_admin, + 'isFront' => drupal_is_front_page(), + 'currentLanguage' => \Drupal::languageManager()->getLanguage(Language::TYPE_URL)->id, ); + if (!empty($current_query)) { + ksort($current_query); + $path['currentQuery'] = (object) $current_query; + } + $javascript['settings']['data'][] = array('path' => $path); } // All JavaScript settings are placed in the header of the page with // the library weight so that inline scripts appear afterwards. diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 856c424..208ef9b 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -1704,6 +1704,7 @@ function theme_menu_link(array $variables) { if ($element['#below']) { $sub_menu = drupal_render($element['#below']); } + $element['#localized_options']['set_active_class'] = TRUE; $output = l($element['#title'], $element['#href'], $element['#localized_options']); return '' . $output . $sub_menu . "\n"; } @@ -1739,6 +1740,8 @@ function theme_menu_local_task($variables) { $link['localized_options']['html'] = TRUE; $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active)); } + $link['localized_options']['set_active_class'] = TRUE; + if (!empty($link['href'])) { // @todo - remove this once all pages are converted to routes. $a_tag = l($link_text, $link['href'], $link['localized_options']); @@ -1770,6 +1773,7 @@ function theme_menu_local_action($variables) { ); $link['localized_options']['attributes']['class'][] = 'button'; $link['localized_options']['attributes']['class'][] = 'button-action'; + $link['localized_options']['set_active_class'] = TRUE; $output = '
  • '; // @todo Remove this check and the call to l() when all pages are converted to diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 37f04f9..65349a3 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1188,6 +1188,18 @@ function template_preprocess_status_messages(&$variables) { * l() as its $options parameter. * - attributes: A keyed array of attributes for the UL containing the * list of links. + * - set_active_class: (optional) Whether theme_links() should compare the + * route_name + route_parameters or href (path), language and query options + * to the current URL for each of the links, to determine whether the link + * is "active". If so, an "active" class will be applied to the list item + * containing the link. It is important to use this sparingly since it is + * usually unnecessary and requires extra processing. + * For anonymous users, the "active" class will be calculated on the server, + * because most sites serve each anonymous user the same cached page anyway. + * For authenticated users, the "active" class will be calculated on the + * client (through JavaScript), only data- attributes are added to list + * items to prevent breaking the render cache. The JavaScript is added in + * system_page_build(). * - heading: (optional) A heading to precede the links. May be an * associative array or a string. If it's an array, it can have the * following elements: @@ -1203,6 +1215,19 @@ function template_preprocess_status_messages(&$variables) { * 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. + * + * theme_links() unfortunately duplicates the "active" class handling of l() and + * LinkGenerator::generate() because it needs to be able to set the "active" + * class not on the links themselves ("a" tags), but on the list items ("li" + * tags) that contain the links. This is necessary for CSS to be able to style + * list items differently when the link is active, since CSS does not yet allow + * one to style list items only if it contains a certain element with a certain + * class. I.e. we cannot yet convert this jQuery selector to a CSS selector: + * jQuery('li:has("a.active")') + * + * @see l() + * @see \Drupal\Core\Utility\LinkGenerator::generate() + * @see system_page_build() */ function theme_links($variables) { $links = $variables['links']; @@ -1236,8 +1261,7 @@ function theme_links($variables) { $num_links = count($links); $i = 0; - $active = \Drupal::linkGenerator()->getActive(); - $language_url = \Drupal::languageManager()->getLanguage(Language::TYPE_URL); + $active_route = \Drupal::linkGenerator()->getActive(); foreach ($links as $key => $link) { $i++; @@ -1249,16 +1273,16 @@ function theme_links($variables) { 'ajax' => NULL, ); - $class = array(); + $li_attributes = array('class' => array()); // Use the array key as class name. - $class[] = drupal_html_class($key); + $li_attributes['class'][] = drupal_html_class($key); // Add odd/even, first, and last classes. - $class[] = ($i % 2 ? 'odd' : 'even'); + $li_attributes['class'][] = ($i % 2 ? 'odd' : 'even'); if ($i == 1) { - $class[] = 'first'; + $li_attributes['class'][] = 'first'; } if ($i == $num_links) { - $class[] = 'last'; + $li_attributes['class'][] = 'last'; } $link_element = array( @@ -1271,30 +1295,34 @@ function theme_links($variables) { '#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']; - } + // Handle links and ensure that the active class is added on the LIs, but + // only if the 'set_active_class' option is not empty. + if (isset($link['href']) || isset($link['route_name'])) { + if (!empty($variables['set_active_class'])) { + if (!empty($link['language'])) { + $li_attributes['hreflang'] = $link['language']->id; + } - 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'; - } + // Add a "data-drupal-link-query" attribute to let the + // drupal.active-link library know the query in a standardized manner. + if (!empty($link['query'])) { + $query = $link['query']; + ksort($query); + $li_attributes['data-drupal-link-query'] = Json::encode($query); + } - $item = drupal_render($link_element); - } - elseif (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'; + if (isset($link['route_name'])) { + $path = \Drupal::service('url_generator')->getPathFromRoute($link['route_name'], $link['route_parameters']); + } + else { + $path = $link['href']; + } + + // Add a "data-drupal-link-system-path" attribute to let the + // drupal.active-link library know the path in a standardized manner. + $li_attributes['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager.cached')->getSystemPath($path); } + $item = drupal_render($link_element); } // Handle title-only text items. @@ -1309,7 +1337,7 @@ function theme_links($variables) { } } - $output .= ' $class)) . '>'; + $output .= ''; $output .= $item; $output .= '
  • '; } @@ -2243,7 +2271,8 @@ function template_preprocess_page(&$variables) { '#heading' => array( 'text' => t('Main menu'), 'class' => array('visually-hidden'), - ) + ), + '#set_active_class' => TRUE, ); } if (!empty($variables['secondary_menu'])) { @@ -2253,7 +2282,8 @@ function template_preprocess_page(&$variables) { '#heading' => array( 'text' => t('Secondary menu'), 'class' => array('visually-hidden'), - ) + ), + '#set_active_class' => TRUE, ); } @@ -2583,7 +2613,7 @@ function drupal_common_theme() { 'template' => 'status-messages', ), 'links' => array( - 'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array()), + 'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array(), 'set_active_class' => FALSE), ), 'dropbutton_wrapper' => array( 'variables' => array('children' => NULL), diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php index 3f63620..ba2b06e 100644 --- a/core/lib/Drupal/Core/Utility/LinkGenerator.php +++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php @@ -7,12 +7,15 @@ namespace Drupal\Core\Utility; +use Drupal\Component\Utility\Json; use Drupal\Component\Utility\String; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Path\AliasManagerInterface; use Drupal\Core\Template\Attribute; use Drupal\Core\Routing\UrlGeneratorInterface; +use Drupal\Core\Session\AccountInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; @@ -22,13 +25,6 @@ class LinkGenerator implements LinkGeneratorInterface { /** - * Stores some information about the current request, like the language. - * - * @var array - */ - protected $active; - - /** * The url generator. * * @var \Drupal\Core\Routing\UrlGeneratorInterface @@ -50,6 +46,13 @@ class LinkGenerator implements LinkGeneratorInterface { protected $languageManager; /** + * The path alias manager. + * + * @var \Drupal\Core\Path\AliasManagerInterface + */ + protected $aliasManager; + + /** * Constructs a LinkGenerator instance. * * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator @@ -58,11 +61,14 @@ class LinkGenerator implements LinkGeneratorInterface { * The module handler. * @param \Drupal\Core\Language\LanguageManager $language_manager * The language manager. + * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager + * The path alias manager. */ - public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManager $language_manager) { + public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManager $language_manager, AliasManagerInterface $alias_manager) { $this->urlGenerator = $url_generator; $this->moduleHandler = $module_handler; $this->languageManager = $language_manager; + $this->aliasManager = $alias_manager; } /** @@ -93,6 +99,15 @@ public function getActive() { /** * {@inheritdoc} + * + * For anonymous users, the "active" class will be calculated on the server, + * because most sites serve each anonymous user the same cached page anyway. + * For authenticated users, the "active" class will be calculated on the + * client (through JavaScript), only data- attributes are added to links to + * prevent breaking the render cache. The JavaScript is added in + * system_page_build(). + * + * @see system_page_build() */ public function generate($text, $route_name, array $parameters = array(), array $options = array()) { // Start building a structured representation of our link to be altered later. @@ -110,30 +125,31 @@ public function generate($text, $route_name, array $parameters = array(), array 'query' => array(), 'html' => FALSE, 'language' => NULL, + 'set_active_class' => FALSE, ); + // Add a hreflang attribute if we know the language of this link's url and // hreflang has not already been set. if (!empty($variables['options']['language']) && !isset($variables['options']['attributes']['hreflang'])) { $variables['options']['attributes']['hreflang'] = $variables['options']['language']->id; } - // This is only needed for the active class. The generator also combines - // the parameters and $options['query'] and adds parameters that are not - // path slugs as query strings. - $full_parameters = $parameters + (array) $variables['options']['query']; - - // Determine whether this link is "active", meaning that it has the same - // URL path and query string as the current page. Note that this may be - // removed from l() in https://drupal.org/node/1979468 and would be removed - // or altered here also. - $variables['url_is_active'] = $route_name == $this->active['route_name'] - // The language of an active link is equal to the current language. - && (empty($variables['options']['language']) || $variables['options']['language']->id == $this->active['language']) - && $full_parameters == $this->active['parameters']; - - // Add the "active" class if appropriate. - if ($variables['url_is_active']) { - $variables['options']['attributes']['class'][] = 'active'; + // Set the "active" class if the 'set_active_class' option is not empty. + if (!empty($variables['options']['set_active_class'])) { + // Add a "data-drupal-link-query" attribute to let the + // drupal.active-link library know the query in a standardized manner. + if (!empty($variables['options']['query'])) { + $query = $variables['options']['query']; + ksort($query); + $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query); + } + + // Add a "data-drupal-link-system-path" attribute to let the + // drupal.active-link library know the path in a standardized manner. + if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) { + $path = $this->urlGenerator->getPathFromRoute($route_name, $parameters); + $variables['options']['attributes']['data-drupal-link-system-path'] = $this->aliasManager->getSystemPath($path); + } } // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags() diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php index 8bc7eb6..b832873 100644 --- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php +++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php @@ -36,8 +36,8 @@ * @param array $options * (optional) An associative array of additional options. Defaults to an * empty array. It may contain the following elements: - * - 'query': An array of query key/value-pairs (without any URL-encoding) to - * append to the URL. + * - 'query': An array of query key/value-pairs (without any URL-encoding) + * to append to the URL. * - absolute: Whether to force the output to be an absolute link (beginning * with http:). Useful for links that will be displayed outside the site, * such as in an RSS feed. Defaults to FALSE. @@ -55,6 +55,11 @@ * internal to the site, $options['language'] is used to determine whether * the link is "active", or pointing to the current page (the language as * well as the path must match). + * - 'set_active_class' (default FALSE): Whether this method should compare + * the $route_name, $parameters, language and query options to the current + * URL to determine whether the link is "active". If so, an "active" class + * will be applied to the link. It is important to use this sparingly + * since it is usually unnecessary and requires extra processing. * * @return string * An HTML string containing a link to the given route and parameters. diff --git a/core/misc/active-link.js b/core/misc/active-link.js new file mode 100644 index 0000000..6054faa --- /dev/null +++ b/core/misc/active-link.js @@ -0,0 +1,63 @@ +/** + * @file + * Attaches behaviors for Drupal's active link marking. + */ + +(function (Drupal, drupalSettings) { + +"use strict"; + +/** + * Append active class. + * + * The link is only active if its path corresponds to the current path, the + * language of the linked path is equal to the current language, and if the + * query parameters of the link equal those of the current request, since the + * same request with different query parameters may yield a different page + * (e.g. pagers, exposed View filters). + * + * Does not discriminate based on element type, so allows you to set the active + * class on any element: a, li… + */ +Drupal.behaviors.activeLinks = { + attach: function (context) { + // Start by finding all potentially active links. + var path = drupalSettings.path; + var queryString = JSON.stringify(path.currentQuery); + var querySelector = path.currentQuery ? "[data-drupal-link-query='" + queryString + "']" : ':not([data-drupal-link-query])'; + var originalSelectors = ['[data-drupal-link-system-path="' + path.currentPath + '"]']; + var selectors; + + // If this is the front page, we have to check for the path as well. + if (path.isFront) { + originalSelectors.push('[data-drupal-link-system-path=""]'); + } + + // Add language filtering. + selectors = [].concat( + // Links without any hreflang attributes (most of them). + originalSelectors.map(function (selector) { return selector + ':not([hreflang])';}), + // Links with hreflang equals to the current language. + originalSelectors.map(function (selector) { return selector + '[hreflang="' + path.currentLanguage + '"]';}) + ); + + // Add query string selector for pagers, exposed filters. + selectors = selectors.map(function (current) { return current + querySelector; }); + + // Query the DOM. + var activeLinks = context.querySelectorAll(selectors.join(',')); + for (var i = 0, il = activeLinks.length; i < il; i += 1) { + activeLinks[i].classList.add('active'); + } + }, + detach: function (context, settings, trigger) { + if (trigger === 'unload') { + var activeLinks = context.querySelectorAll('[data-drupal-link-system-path].active'); + for (var i = 0, il = activeLinks.length; i < il; i += 1) { + activeLinks[i].classList.remove('active'); + } + } + } +}; + +})(Drupal, drupalSettings); diff --git a/core/misc/ajax.js b/core/misc/ajax.js index 6ecde95..3754af6 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -180,7 +180,7 @@ Drupal.ajax = function (base, element, element_settings) { // If there isn't a form, jQuery.ajax() will be used instead, allowing us to // bind Ajax to links as well. if (this.element.form) { - this.form = $(this.element.form); + this.$form = $(this.element.form); } // If no Ajax callback URL was given, use the link href or form action. @@ -189,7 +189,7 @@ Drupal.ajax = function (base, element, element_settings) { this.url = $(element).attr('href'); } else if (element.form) { - this.url = this.form.attr('action'); + this.url = this.$form.attr('action'); // @todo If there's a file input on this form, then jQuery will submit the // AJAX response with a hidden Iframe rather than the XHR object. If the @@ -200,7 +200,7 @@ Drupal.ajax = function (base, element, element_settings) { // elements that submit to the same URL as the form when there's a file // input. For example, this means the Delete button on the edit form of // an Article node doesn't open its confirmation form in a dialog. - if (this.form.find(':file').length) { + if (this.$form.find(':file').length) { return; } } @@ -327,7 +327,7 @@ Drupal.ajax.prototype.eventResponse = function (element, event) { } try { - if (ajax.form) { + if (ajax.$form) { // If setClick is set, we must set this to ensure that the button's // value is passed. if (ajax.setClick) { @@ -338,7 +338,7 @@ Drupal.ajax.prototype.eventResponse = function (element, event) { element.form.clk = element; } - ajax.form.ajaxSubmit(ajax.options); + ajax.$form.ajaxSubmit(ajax.options); } else { ajax.beforeSerialize(ajax.element, ajax.options); @@ -362,12 +362,12 @@ Drupal.ajax.prototype.eventResponse = function (element, event) { Drupal.ajax.prototype.beforeSerialize = function (element, options) { // Allow detaching behaviors to update field values before collecting them. // This is only needed when field values are added to the POST data, so only - // when there is a form such that this.form.ajaxSubmit() is used instead of + // when there is a form such that this.$form.ajaxSubmit() is used instead of // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize() - // isn't called, but don't rely on that: explicitly check this.form. - if (this.form) { + // isn't called, but don't rely on that: explicitly check this.$form. + if (this.$form) { var settings = this.settings || drupalSettings; - Drupal.detachBehaviors(this.form, settings, 'serialize'); + Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize'); } // Prevent duplicate HTML ids in the returned markup. @@ -421,7 +421,7 @@ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) { // to the form to submit the values in options.extraData. There is no simple // way to know which submission mechanism will be used, so we add to extraData // regardless, and allow it to be ignored in the former case. - if (this.form) { + if (this.$form) { options.extraData = options.extraData || {}; // Let the server know when the IFRAME submission mechanism is used. The @@ -491,9 +491,9 @@ Drupal.ajax.prototype.success = function (response, status) { // attachBehaviors() called on the new content from processing the response // commands is not sufficient, because behaviors from the entire form need // to be reattached. - if (this.form) { + if (this.$form) { var settings = this.settings || drupalSettings; - Drupal.attachBehaviors(this.form, settings); + Drupal.attachBehaviors(this.$form.get(0), settings); } // Remove any response-specific settings so they don't get used on the next @@ -544,9 +544,9 @@ Drupal.ajax.prototype.error = function (response, uri) { // Re-enable the element. $(this.element).removeClass('progress-disabled').prop('disabled', false); // Reattach behaviors, if they were detached in beforeSerialize(). - if (this.form) { + if (this.$form) { var settings = response.settings || this.settings || drupalSettings; - Drupal.attachBehaviors(this.form, settings); + Drupal.attachBehaviors(this.$form.get(0), settings); } throw new Drupal.AjaxError(response, uri); }; @@ -597,7 +597,7 @@ Drupal.AjaxCommands.prototype = { case 'empty': case 'remove': settings = response.settings || ajax.settings || drupalSettings; - Drupal.detachBehaviors(wrapper, settings); + Drupal.detachBehaviors(wrapper.get(0), settings); } // Add the new content to the page. @@ -625,7 +625,7 @@ Drupal.AjaxCommands.prototype = { if (new_content.parents('html').length > 0) { // Apply any settings from the returned JSON if available. settings = response.settings || ajax.settings || drupalSettings; - Drupal.attachBehaviors(new_content, settings); + Drupal.attachBehaviors(new_content.get(0), settings); } }, @@ -634,8 +634,10 @@ Drupal.AjaxCommands.prototype = { */ remove: function (ajax, response, status) { var settings = response.settings || ajax.settings || drupalSettings; - Drupal.detachBehaviors($(response.selector), settings); - $(response.selector).remove(); + $(response.selector).each(function() { + Drupal.detachBehaviors(this, settings); + }) + .remove(); }, /** diff --git a/core/misc/drupal.js b/core/misc/drupal.js index 6c04130..e994ec6 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -267,7 +267,7 @@ Drupal.t = function (str, args, options) { * Returns the URL to a Drupal page. */ Drupal.url = function (path) { - return drupalSettings.basePath + drupalSettings.scriptPath + path; + return drupalSettings.path.basePath + drupalSettings.path.scriptPath + path; }; /** diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index ae2dec0..e78d693 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -1069,9 +1069,15 @@ Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) { * DOM element what will be swapped with the row group. */ Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) { - Drupal.detachBehaviors(this.group, drupalSettings, 'move'); + // Makes sure only DOM object are passed to Drupal.detachBehaviors(). + this.group.forEach(function (row) { + Drupal.detachBehaviors(row, drupalSettings, 'move'); + }); $(row)[position](this.group); - Drupal.attachBehaviors(this.group, drupalSettings); + // Makes sure only DOM object are passed to Drupal.attachBehaviors()s. + this.group.forEach(function (row) { + Drupal.attachBehaviors(row, drupalSettings); + }); this.changed = true; this.onSwap(row); }; diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php index aa54d89..c8f205f 100644 --- a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php +++ b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDisplayTest.php @@ -113,7 +113,7 @@ function _testImageFieldFormatters($scheme) { '#width' => 40, '#height' => 20, ); - $default_output = l($image, 'node/' . $nid, array('html' => TRUE, 'attributes' => array('class' => 'active'))); + $default_output = l($image, 'node/' . $nid, array('html' => TRUE)); $this->drupalGet('node/' . $nid); $this->assertRaw($default_output, 'Image linked to content formatter displaying correctly on full node view.'); diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc index f87284d..f59a94e 100644 --- a/core/modules/language/language.negotiation.inc +++ b/core/modules/language/language.negotiation.inc @@ -399,6 +399,7 @@ function language_switcher_url($type, $path) { 'title' => $language->name, 'language' => $language, 'attributes' => array('class' => array('language-link')), + 'set_active_class' => TRUE, ); } diff --git a/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php b/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php index f53c81b..d97d49f 100644 --- a/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php +++ b/core/modules/language/lib/Drupal/language/Plugin/Block/LanguageBlock.php @@ -47,6 +47,7 @@ public function build() { "language-switcher-{$links->method_id}", ), ), + '#set_active_class' => TRUE, ); } return $build; diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php index 4613f43..b4c4616 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php @@ -42,8 +42,12 @@ function setUp() { * Functional tests for the language switcher block. */ function testLanguageBlock() { - // Enable the language switching block. - $block = $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array('id' => 'test_language_block')); + // Enable the language switching block.. + $block = $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array( + 'id' => 'test_language_block', + // Ensure a 2-byte UTF-8 sequence is in the tested output. + 'label' => $this->randomName(8) . '×', + )); // Add language. $edit = array( @@ -55,9 +59,70 @@ function testLanguageBlock() { $edit = array('language_interface[enabled][language-url]' => '1'); $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); + $this->doTestLanguageBlockAuthenticated($block->label()); + $this->doTestLanguageBlockAnonymous($block->label()); + } + + /** + * For authenticated users, the "active" class is set by JavaScript. + * + * @param string $block_label + * The label of the language switching block. + * + * @see testLanguageBlock() + */ + protected function doTestLanguageBlockAuthenticated($block_label) { + // Assert that the language switching block is displayed on the frontpage. + $this->drupalGet(''); + $this->assertText($block_label, 'Language switcher block found.'); + + // Assert that each list item and anchor element has the appropriate data- + // attributes. + list($language_switcher) = $this->xpath('//div[@id=:id]/div[contains(@class, "content")]', array(':id' => 'block-test-language-block')); + $list_items = array(); + $anchors = array(); + foreach ($language_switcher->ul->li as $list_item) { + $classes = explode(" ", (string) $list_item['class']); + list($langcode) = array_intersect($classes, array('en', 'fr')); + $list_items[] = array( + 'langcode_class' => $langcode, + 'data-drupal-link-system-path' => (string) $list_item['data-drupal-link-system-path'], + ); + $anchors[] = array( + 'hreflang' => (string) $list_item->a['hreflang'], + 'data-drupal-link-system-path' => (string) $list_item->a['data-drupal-link-system-path'], + ); + } + $expected_list_items = array( + 0 => array('langcode_class' => 'en', 'data-drupal-link-system-path' => 'user/2'), + 1 => array('langcode_class' => 'fr', 'data-drupal-link-system-path' => 'user/2'), + ); + $this->assertIdentical($list_items, $expected_list_items, 'The list items have the correct attributes that will allow the drupal.active-link library to mark them as active.'); + $expected_anchors = array( + 0 => array('hreflang' => 'en', 'data-drupal-link-system-path' => 'user/2'), + 1 => array('hreflang' => 'fr', 'data-drupal-link-system-path' => 'user/2'), + ); + $this->assertIdentical($anchors, $expected_anchors, 'The anchors have the correct attributes that will allow the drupal.active-link library to mark them as active.'); + $settings = $this->drupalGetSettings(); + $this->assertIdentical($settings['path']['currentPath'], 'user/2', 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.'); + $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.'); + $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.'); + } + + /** + * For anonymous users, the "active" class is set by PHP. + * + * @param string $block_label + * The label of the language switching block. + * + * @see testLanguageBlock() + */ + protected function doTestLanguageBlockAnonymous($block_label) { + $this->drupalLogout(); + // Assert that the language switching block is displayed on the frontpage. $this->drupalGet(''); - $this->assertText($block->label(), 'Language switcher block found.'); + $this->assertText($block_label, 'Language switcher block found.'); // Assert that only the current language is marked as active. list($language_switcher) = $this->xpath('//div[@id=:id]/div[contains(@class, "content")]', array(':id' => 'block-test-language-block')); @@ -104,7 +169,80 @@ function testLanguageLinkActiveClass() { $edit = array('language_interface[enabled][language-url]' => '1'); $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); + $this->doTestLanguageLinkActiveClassAuthenticated(); + $this->doTestLanguageLinkActiveClassAnonymous(); + } + + /** + * For authenticated users, the "active" class is set by JavaScript. + * + * @see testLanguageLinkActiveClass() + */ + protected function doTestLanguageLinkActiveClassAuthenticated() { + $function_name = '#type link'; + $path = 'language_test/type-link-active-class'; + + // Test links generated by l() on an English page. + $current_language = 'English'; + $this->drupalGet($path); + + // Language code 'none' link should be active. + $langcode = 'none'; + $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', array(':id' => 'no_lang_link', ':path' => $path)); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Language code 'en' link should be active. + $langcode = 'en'; + $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'en_link', ':lang' => 'en', ':path' => $path)); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Language code 'fr' link should not be active. + $langcode = 'fr'; + $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'fr_link', ':lang' => 'fr', ':path' => $path)); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Verify that drupalSettings contains the correct values. + $settings = $this->drupalGetSettings(); + $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.'); + $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.'); + $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.'); + + // Test links generated by l() on a French page. + $current_language = 'French'; + $this->drupalGet('fr/language_test/type-link-active-class'); + + // Language code 'none' link should be active. + $langcode = 'none'; + $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', array(':id' => 'no_lang_link', ':path' => $path)); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Language code 'en' link should not be active. + $langcode = 'en'; + $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'en_link', ':lang' => 'en', ':path' => $path)); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Language code 'fr' link should be active. + $langcode = 'fr'; + $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', array(':id' => 'fr_link', ':lang' => 'fr', ':path' => $path)); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Verify that drupalSettings contains the correct values. + $settings = $this->drupalGetSettings(); + $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.'); + $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.'); + $this->assertIdentical($settings['path']['currentLanguage'], 'fr', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.'); + } + + /** + * For anonymous users, the "active" class is set by PHP. + * + * @see testLanguageLinkActiveClass() + */ + protected function doTestLanguageLinkActiveClassAnonymous() { $function_name = '#type link'; + $path = 'language_test/type-link-active-class'; + + $this->drupalLogout(); // Test links generated by l() on an English page. $current_language = 'English'; diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php index 7c037b5..19744c2 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php @@ -411,6 +411,11 @@ function testUrlLanguageFallback() { // Enable the language switcher block. $this->drupalPlaceBlock('language_block:' . Language::TYPE_INTERFACE, array('id' => 'test_language_block')); + // Log out, because for anonymous users, the "active" class is set by PHP + // (which means we can easily test it here), whereas for authenticated users + // it is set by JavaScript. + $this->drupalLogout(); + // Access the front page without specifying any valid URL language prefix // and having as browser language preference a non-default language. $http_header = array("Accept-Language: $langcode_browser_fallback;q=1"); @@ -464,7 +469,7 @@ function testLanguageDomain() { $italian_url = url('admin', array('language' => $languages['it'], 'script' => '')); $url_scheme = $this->request->isSecure() ? 'https://' : 'http://'; $correct_link = $url_scheme . $link; - $this->assertTrue($italian_url == $correct_link, format_string('The url() function returns the right URL (@url) in accordance with the chosen language', array('@url' => $italian_url))); + $this->assertEqual($italian_url, $correct_link, format_string('The url() function returns the right URL (@url) in accordance with the chosen language', array('@url' => $italian_url))); // Test HTTPS via options. $this->settingsSet('mixed_mode_sessions', TRUE); diff --git a/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php b/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php index 001fe39..781af06 100644 --- a/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php +++ b/core/modules/language/tests/language_test/lib/Drupal/language_test/Controller/LanguageTestController.php @@ -58,6 +58,7 @@ public function typeLinkActiveClass() { 'attributes' => array( 'id' => 'no_lang_link', ), + 'set_active_class' => TRUE, ), ), 'fr' => array( @@ -69,6 +70,7 @@ public function typeLinkActiveClass() { 'attributes' => array( 'id' => 'fr_link', ), + 'set_active_class' => TRUE, ), ), 'en' => array( @@ -80,6 +82,7 @@ public function typeLinkActiveClass() { 'attributes' => array( 'id' => 'en_link', ), + 'set_active_class' => TRUE, ), ), ); diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php index 6145a11..a51ae40 100644 --- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php +++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php @@ -7,6 +7,7 @@ namespace Drupal\system\Controller; +use Drupal\Component\Utility\Json; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\Query\QueryFactory; @@ -161,4 +162,69 @@ public function themeSetDefault() { return system_theme_default(); } + /** + * #post_render_cache callback; sets the "active" class on relevant links. + * + * This is a PHP implementation of the drupal.active-link JavaScript library. + * + * @param array $element + * A renderable array with the following keys: + * - #markup + * - #attached + * @param array $context + * An array with the following keys: + * - path: the system path of the currently active page + * - front: whether the current page is the front page (which implies the + * current path might also be ) + * - language: the language code of the currently active page + * - query: the query string for the currently active page + * + * @return array + * The updated renderable array. + */ + public static function setLinkActiveClass(array $element, array $context) { + // If none of the HTML in the current page contains even just the current + // page's attribute, return early. + if (strpos($element['#markup'], 'data-drupal-link-system-path="' . $context['path'] . '"') === FALSE && (!$context['front'] || strpos($element['#markup'], 'data-drupal-link-system-path="<front>"') === FALSE)) { + return $element; + } + + // Build XPath query to find links that should get the "active" class. + $query = "//*["; + // An active link's path is equal to the current path. + $query .= "@data-drupal-link-system-path='" . $context['path'] . "'"; + if ($context['front']) { + $query .= " or @data-drupal-link-system-path=''"; + } + // The language of an active link is equal to the current language. + if ($context['language']) { + $query .= " and (not(@hreflang) or @hreflang='" . $context['language'] . "')"; + } + // The query parameters of an active link are equal to the current + // parameters. + if ($context['query']) { + $query .= " and @data-drupal-link-query='" . Json::encode($context['query']) . "'"; + } + else { + $query .= " and not(@data-drupal-link-query)"; + } + $query .= "]"; + + // Set the "active" class on all matching HTML elements. + $dom = new \DOMDocument(); + @$dom->loadHTML($element['#markup']); + $xpath = new \DOMXPath($dom); + foreach ($xpath->query($query) as $node) { + $class = $node->getAttribute('class'); + if (strlen($class) > 0) { + $class .= ' '; + } + $class .= 'active'; + $node->setAttribute('class', $class); + } + $element['#markup'] = $dom->saveHTML(); + + return $element; + } + } diff --git a/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php b/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php index 11204fe..8450b9c 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Batch/ProcessingTest.php @@ -250,28 +250,28 @@ function _resultMessages($id) { switch ($id) { case 'batch_0': - $messages[] = 'results for batch 0
    none'; + $messages[] = 'results for batch 0
    none'; break; case 'batch_1': - $messages[] = 'results for batch 1
    op 1: processed 10 elements'; + $messages[] = 'results for batch 1
    op 1: processed 10 elements'; break; case 'batch_2': - $messages[] = 'results for batch 2
    op 2: processed 10 elements'; + $messages[] = 'results for batch 2
    op 2: processed 10 elements'; break; case 'batch_3': - $messages[] = 'results for batch 3
    op 1: processed 10 elements
    op 2: processed 10 elements'; + $messages[] = 'results for batch 3
    op 1: processed 10 elements
    op 2: processed 10 elements'; break; case 'batch_4': - $messages[] = 'results for batch 4
    op 1: processed 10 elements'; + $messages[] = 'results for batch 4
    op 1: processed 10 elements'; $messages = array_merge($messages, $this->_resultMessages('batch_2')); break; case 'batch_5': - $messages[] = 'results for batch 5
    op 5: processed 10 elements'; + $messages[] = 'results for batch 5
    op 5: processed 10 elements'; break; case 'chained': diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php index 3f584aa..888a699 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php @@ -83,7 +83,7 @@ function testAddSetting() { drupal_add_library('system', 'drupalSettings'); $javascript = drupal_add_js(); $last_settings = reset($javascript['settings']['data']); - $this->assertTrue(array_key_exists('currentPath', $last_settings), 'The current path JavaScript setting is set correctly.'); + $this->assertTrue(array_key_exists('currentPath', $last_settings['path']), 'The current path JavaScript setting is set correctly.'); $javascript = drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting'); $last_settings = end($javascript['settings']['data']); 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..97335d2 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/FunctionsTest.php @@ -2,12 +2,15 @@ /** * @file - * Definition of Drupal\system\Tests\Theme\FunctionsTest. + * Contains \Drupal\system\Tests\Theme\FunctionsTest. */ namespace Drupal\system\Tests\Theme; +use Drupal\Core\Session\UserSession; use Drupal\simpletest\WebTestBase; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Symfony\Component\HttpFoundation\Request; /** * Tests for common theme functions. @@ -159,12 +162,6 @@ function testLinks() { $expected = ''; $this->assertThemeOutput('links', $variables, $expected, 'Empty %callback with heading generates no output.'); - // Set the current path to the front page path. - // Required to verify the "active" class in expected links below, and - // 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')); - // Verify that a list of links is properly rendered. $variables = array(); $variables['attributes'] = array('id' => 'somelinks'); @@ -191,7 +188,7 @@ function testLinks() { $expected_links .= ''; @@ -224,11 +221,24 @@ function testLinks() { $expected_links .= ''; $expected = $expected_heading . $expected_links; $this->assertThemeOutput('links', $variables, $expected); + + // Verify the data- attributes for setting the "active" class on links. + $this->container->set('current_user', new UserSession(array('uid' => 1))); + $variables['set_active_class'] = TRUE; + $expected_links = ''; + $expected_links .= ''; + $expected = $expected_heading . $expected_links; + $this->assertThemeOutput('links', $variables, $expected); } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 1a0be69..85788dd 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -877,6 +877,20 @@ function system_library_info() { ), ); + // Drupal's active link marking. + $libraries['drupal.active-link'] = array( + 'title' => 'Drupal active link marking', + 'version' => \Drupal::VERSION, + 'js' => array( + 'core/misc/active-link.js' => array(), + ), + 'dependencies' => array( + array('system', 'drupal'), + array('system', 'drupalSettings'), + array('system', 'classList'), + ), + ); + // Drupal's Ajax framework. $libraries['drupal.ajax'] = array( 'title' => 'Drupal AJAX', @@ -1123,6 +1137,20 @@ function system_library_info() { ), ); + // IE9 classList polyfill. + $libraries['classList'] = array( + 'title' => 'classList.js', + 'website' => 'https://github.com/eligrey/classList.js', + 'version' => 'master', + 'js' => array( + 'core/assets/vendor/classList/classList.min.js' => array( + 'group' => JS_LIBRARY, + 'weight' => -21, + 'browsers' => array('IE' => 'lte IE 9', '!IE' => FALSE), + ), + ), + ); + // jQuery. $libraries['jquery'] = array( 'title' => 'jQuery', @@ -2089,6 +2117,7 @@ function system_filetransfer_info() { * Implements hook_page_build(). * * @see template_preprocess_maintenance_page() + * @see \Drupal\system\Controller\SystemController::setLinkActiveClass() */ function system_page_build(&$page) { // Ensure the same CSS is loaded in template_preprocess_maintenance_page(). @@ -2108,6 +2137,28 @@ function system_page_build(&$page) { 'weight' => CSS_COMPONENT - 10, ); } + + // Handle setting the "active" class on links by: + // - loading the active-link library if the current user is authenticated; + // - applying a post-render cache callback if the current user is anonymous. + // @see l() + // @see \Drupal\Core\Utility\LinkGenerator::generate() + // @see theme_links() + // @see \Drupal\system\Controller\SystemController::setLinkActiveClass + if (\Drupal::currentUser()->isAuthenticated()) { + $page['#attached']['library'][] = array('system', 'drupal.active-link'); + } + else { + $page['#post_render_cache']['\Drupal\system\Controller\SystemController::setLinkActiveClass'] = array( + // Collect the current state that determines whether a link is active. + array( + 'path' => current_path(), + 'front' => drupal_is_front_page(), + 'language' => language(\Drupal\Core\Language\Language::TYPE_URL)->id, + 'query' => \Drupal::service('request')->query->all(), + ) + ); + } } /** diff --git a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc index 6d9a9a6..ca1ea6f 100644 --- a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc +++ b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc @@ -94,7 +94,7 @@ function _batch_test_finished_helper($batch_id, $success, $results, $operations) $messages[] = t('An error occurred while processing @op with arguments:
    @args', array('@op' => $error_operation[0], '@args' => print_r($error_operation[1], TRUE))); } - drupal_set_message(implode('
    ', $messages)); + drupal_set_message(implode('
    ', $messages)); } /** diff --git a/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php b/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php index 12d287f..ffce7dc 100644 --- a/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php +++ b/core/modules/system/tests/modules/common_test/lib/Drupal/common_test/Controller/CommonTestController.php @@ -35,6 +35,9 @@ public function typeLinkActiveClass() { '#type' => 'link', '#title' => t('Link with no query string'), '#href' => current_path(), + '#options' => array( + 'set_active_class' => TRUE, + ), ), 'with_query' => array( '#type' => 'link', @@ -45,6 +48,7 @@ public function typeLinkActiveClass() { 'foo' => 'bar', 'one' => 'two', ), + 'set_active_class' => TRUE, ), ), 'with_query_reversed' => array( @@ -56,6 +60,7 @@ public function typeLinkActiveClass() { 'one' => 'two', 'foo' => 'bar', ), + 'set_active_class' => TRUE, ), ), ); diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php index 6b7334f..eec43a6 100644 --- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php +++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php @@ -43,7 +43,6 @@ class LinkGeneratorTest extends UnitTestCase { protected $moduleHandler; /** - * * The mocked language manager. * * @var \PHPUnit_Framework_MockObject_MockObject @@ -51,12 +50,20 @@ class LinkGeneratorTest extends UnitTestCase { protected $languageManager; /** + * The mocked path alias manager. + * + * @var \Drupal\Core\Path\AliasManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $aliasManager; + + /** * Contains the LinkGenerator default options. */ protected $defaultOptions = array( 'query' => array(), 'html' => FALSE, 'language' => NULL, + 'set_active_class' => FALSE, ); /** @@ -80,8 +87,9 @@ protected function setUp() { $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE); $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManager'); + $this->aliasManager = $this->getMock('\Drupal\Core\Path\AliasManagerInterface'); - $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->languageManager); + $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->languageManager, $this->aliasManager); } /** @@ -312,19 +320,31 @@ public function testGenerateWithHtml() { * service. */ public function testGenerateActive() { - $this->urlGenerator->expects($this->exactly(7)) + $this->urlGenerator->expects($this->exactly(8)) ->method('generateFromRoute') ->will($this->returnValueMap(array( array('test_route_1', array(), FALSE, '/test-route-1'), - array('test_route_1', array(), FALSE, '/test-route-1'), - array('test_route_1', array(), FALSE, '/test-route-1'), - array('test_route_1', array(), FALSE, '/test-route-1'), - array('test_route_3', array(), FALSE, '/test-route-3'), array('test_route_3', array(), FALSE, '/test-route-3'), array('test_route_4', array('object' => '1'), FALSE, '/test-route-4/1'), ))); - $this->moduleHandler->expects($this->exactly(7)) + $this->urlGenerator->expects($this->exactly(7)) + ->method('getPathFromRoute') + ->will($this->returnValueMap(array( + array('test_route_1', array(), 'test-route-1'), + array('test_route_3', array(), 'test-route-3'), + array('test_route_4', array('object' => '1'), 'test-route-4/1'), + ))); + + $this->aliasManager->expects($this->exactly(7)) + ->method('getSystemPath') + ->will($this->returnValueMap(array( + array('test-route-1', NULL, 'test-route-1'), + array('test-route-3', NULL, 'test-route-3'), + array('test-route-4/1', NULL, 'test-route-4/1'), + ))); + + $this->moduleHandler->expects($this->exactly(8)) ->method('alter'); $this->setUpLanguageManager(); @@ -332,10 +352,10 @@ public function testGenerateActive() { // Render a link with a path different from the current path. $request = new Request(array(), array(), array('system_path' => 'test-route-2')); $this->linkGenerator->setRequest($request); - $result = $this->linkGenerator->generate('Test', 'test_route_1'); - $this->assertNotTag(array( + $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE)); + $this->assertTag(array( 'tag' => 'a', - 'attributes' => array('class' => 'active'), + 'attributes' => array('data-drupal-link-system-path' => 'test-route-1'), ), $result); // Render a link with the same path as the current path. @@ -345,17 +365,31 @@ public function testGenerateActive() { $raw_variables = new ParameterBag(); $request->attributes->set('_raw_variables', $raw_variables); $this->linkGenerator->setRequest($request); - $result = $this->linkGenerator->generate('Test', 'test_route_1'); + $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE)); $this->assertTag(array( 'tag' => 'a', - 'attributes' => array('class' => 'active'), + 'attributes' => array('data-drupal-link-system-path' => 'test-route-1'), + ), $result); + + // Render a link with the same path as the current path, but with the + // set_active_class option disabled. + $request = new Request(array(), array(), array('system_path' => 'test-route-1', RouteObjectInterface::ROUTE_NAME => 'test_route_1')); + // This attribute is expected to be set in a Drupal request by + // \Drupal\Core\ParamConverter\ParamConverterManager + $raw_variables = new ParameterBag(); + $request->attributes->set('_raw_variables', $raw_variables); + $this->linkGenerator->setRequest($request); + $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => FALSE)); + $this->assertNotTag(array( + 'tag' => 'a', + 'attributes' => array('data-drupal-link-system-path' => 'test-route-1'), ), $result); // Render a link with the same path and language as the current path. - $result = $this->linkGenerator->generate('Test', 'test_route_1'); + $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array('set_active_class' => TRUE)); $this->assertTag(array( 'tag' => 'a', - 'attributes' => array('class' => 'active'), + 'attributes' => array('data-drupal-link-system-path' => 'test-route-1'), ), $result); // Render a link with the same path but a different language than the current @@ -364,11 +398,17 @@ public function testGenerateActive() { 'Test', 'test_route_1', array(), - array('language' => new Language(array('id' => 'de'))) + array( + 'language' => new Language(array('id' => 'de')), + 'set_active_class' => TRUE, + ) ); - $this->assertNotTag(array( + $this->assertTag(array( 'tag' => 'a', - 'attributes' => array('class' => 'active'), + 'attributes' => array( + 'data-drupal-link-system-path' => 'test-route-1', + 'hreflang' => 'de', + ), ), $result); // Render a link with the same path and query parameter as the current path. @@ -380,11 +420,17 @@ public function testGenerateActive() { 'Test', 'test_route_3', array(), - array('query' => array('value' => 'example_1') - )); + array( + 'query' => array('value' => 'example_1'), + 'set_active_class' => TRUE, + ) + ); $this->assertTag(array( 'tag' => 'a', - 'attributes' => array('class' => 'active'), + 'attributes' => array( + 'data-drupal-link-system-path' => 'test-route-3', + 'data-drupal-link-query' => 'regexp:/.*value.*example_1.*/', + ), ), $result); // Render a link with the same path but a different query parameter than the @@ -393,12 +439,19 @@ public function testGenerateActive() { 'Test', 'test_route_3', array(), - array('query' => array('value' => 'example_2')) + array( + 'query' => array('value' => 'example_2'), + 'set_active_class' => TRUE, + ) ); - $this->assertNotTag(array( + $this->assertTag(array( 'tag' => 'a', - 'attributes' => array('class' => 'active'), + 'attributes' => array( + 'data-drupal-link-system-path' => 'test-route-3', + 'data-drupal-link-query' => 'regexp:/.*value.*example_2.*/', + ), ), $result); + // Render a link with the same path and query parameter as the current path. $request = new Request(array('value' => 'example_1'), array(), array('system_path' => 'test-route-4/1', RouteObjectInterface::ROUTE_NAME => 'test_route_4')); $raw_variables = new ParameterBag(array('object' => '1')); @@ -408,11 +461,17 @@ public function testGenerateActive() { 'Test', 'test_route_4', array('object' => '1'), - array('query' => array('value' => 'example_1')) + array( + 'query' => array('value' => 'example_1'), + 'set_active_class' => TRUE, + ) ); $this->assertTag(array( 'tag' => 'a', - 'attributes' => array('class' => 'active'), + 'attributes' => array( + 'data-drupal-link-system-path' => 'test-route-4/1', + 'data-drupal-link-query' => 'regexp:/.*value.*example_1.*/', + ), ), $result); }