Commit 5d4d6b1c authored by catch's avatar catch

Issue #1979468 by Wim Leers, nod_, pwolanin, longwave, thedavidmeister,...

Issue #1979468 by Wim Leers, nod_, pwolanin, longwave, thedavidmeister, jessebeach, larowlan | catch: .active from linkGenerator(), l() and theme_links() forces an upper limit of per-page caching for all content containing links.
parent 34a95532
This software is dedicated to the public domain. No warranty is expressed or implied.
Use this software at your own risk.
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"
/*
* 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));
}
/*! @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<o;p++){if(p in this&&this[p]===q){return p}}return -1},n=function(o,p){this.name=o;this.code=DOMException[o];this.message=p},g=function(p,o){if(o===""){throw new n("SYNTAX_ERR","An invalid or illegal string was specified")}if(/\s/.test(o)){throw new n("INVALID_CHARACTER_ERR","String contains an invalid character")}return c.call(p,o)},d=function(s){var r=k.call(s.getAttribute("class")),q=r?r.split(/\s+/):[],p=0,o=q.length;for(;p<o;p++){this.push(q[p])}this._updateClassName=function(){s.setAttribute("class",this.toString())}},e=d[f]=[],i=function(){return new d(this)};n[f]=Error[f];e.item=function(o){return this[o]||null};e.contains=function(o){o+="";return g(this,o)!==-1};e.add=function(){var s=arguments,r=0,p=s.length,q,o=false;do{q=s[r]+"";if(g(this,q)===-1){this.push(q);o=true}}while(++r<p);if(o){this._updateClassName()}};e.remove=function(){var t=arguments,s=0,p=t.length,r,o=false;do{r=t[s]+"";var q=g(this,r);if(q!==-1){this.splice(q,1);o=true}}while(++s<p);if(o){this._updateClassName()}};e.toggle=function(p,q){p+="";var o=this.contains(p),r=o?q!==true&&"remove":q!==false&&"add";if(r){this[r](p)}return !o};e.toString=function(){return this.join(" ")};if(b.defineProperty){var l={get:i,enumerable:true,configurable:true};try{b.defineProperty(m,a,l)}catch(h){if(h.number===-2146823252){l.enumerable=false;b.defineProperty(m,a,l)}}}else{if(b[f].__defineGetter__){m.__defineGetter__(a,i)}}}(self))};
......@@ -276,7 +276,7 @@ services:
- [setContext, ['@?router.request_context']]
link_generator:
class: Drupal\Core\Utility\LinkGenerator
arguments: ['@url_generator', '@module_handler', '@language_manager']
arguments: ['@url_generator', '@module_handler', '@language_manager', '@path.alias_manager.cached']
calls:
- [setRequest, ['@?request']]
router.dynamic:
......
......@@ -1214,12 +1214,24 @@ function drupal_http_header_attributes(array $attributes = array()) {
* 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). This element is also used by url().
* - 'set_active_class': Whether l() should compare the $path, language and
* query options to the current URL to determine whether the link is
* "active". Defaults to FALSE. If TRUE, 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.
* 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().
* - Additional $options elements used by the url() function.
*
* @return string
* An HTML string containing a link to the given path.
*
* @see url()
* @see system_page_build()
*/
function l($text, $path, array $options = array()) {
// Start building a structured representation of our link to be altered later.
......@@ -1235,6 +1247,7 @@ function l($text, $path, array $options = 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
......@@ -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 == '<front>' && $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()->getCurrentLanguage(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.
......
......@@ -1700,6 +1700,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 '<li' . new Attribute($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
}
......@@ -1735,6 +1736,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']);
......@@ -1766,6 +1769,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 = '<li>';
// @todo Remove this check and the call to l() when all pages are converted to
......
......@@ -1194,6 +1194,19 @@ 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, as well as the link itself. 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 and contained links, 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:
......@@ -1209,6 +1222,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'];
......@@ -1240,9 +1266,6 @@ function theme_links($variables) {
$output .= '<ul' . new Attribute($attributes) . '>';
$active = \Drupal::linkGenerator()->getActive();
$language_url = \Drupal::languageManager()->getCurrentLanguage(Language::TYPE_URL);
foreach ($links as $key => $link) {
$link += array(
'href' => NULL,
......@@ -1251,9 +1274,9 @@ 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);
$link_element = array(
'#type' => 'link',
......@@ -1265,30 +1288,38 @@ 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 (($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';
}
// Also enable set_active_class for the contained link.
$link_element['#options']['set_active_class'] = TRUE;
$item = drupal_render($link_element);
}
elseif (isset($link['href'])) {
$is_current_path = ($link['href'] == current_path() || ($link['href'] == '<front>' && 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 (!empty($link['language'])) {
$li_attributes['hreflang'] = $link['language']->id;
}
// 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);
}
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.
......@@ -1303,7 +1334,7 @@ function theme_links($variables) {
}
}
$output .= '<li' . new Attribute(array('class' => $class)) . '>';
$output .= '<li' . new Attribute($li_attributes) . '>';
$output .= $item;
$output .= '</li>';
}
......@@ -2227,7 +2258,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'])) {
......@@ -2237,7 +2269,8 @@ function template_preprocess_page(&$variables) {
'#heading' => array(
'text' => t('Secondary menu'),
'class' => array('visually-hidden'),
)
),
'#set_active_class' => TRUE,
);
}
......@@ -2567,7 +2600,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),
......
......@@ -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\LanguageManagerInterface;
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;
......@@ -21,13 +24,6 @@
*/
class LinkGenerator implements LinkGeneratorInterface {
/**
* Stores some information about the current request, like the language.
*
* @var array
*/
protected $active;
/**
* The url generator.
*
......@@ -49,6 +45,13 @@ class LinkGenerator implements LinkGeneratorInterface {
*/
protected $languageManager;
/**
* The path alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* Constructs a LinkGenerator instance.
*
......@@ -58,11 +61,14 @@ class LinkGenerator implements LinkGeneratorInterface {
* The module handler.
* @param \Drupal\Core\Language\LanguageManagerInterface $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, LanguageManagerInterface $language_manager) {
public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManagerInterface $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()
......
......@@ -36,8 +36,8 @@ interface LinkGeneratorInterface {
* @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,12 @@ interface LinkGeneratorInterface {
* 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': Whether this method should compare the $route_name,
* $parameters, language and query options to the current URL to determine
* whether the link is "active". Defaults to FALSE. If TRUE, 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.
......
/**
* @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 <front> path as well.
if (path.isFront) {
originalSelectors.push('[data-drupal-link-system-path="<front>"]');
}
// 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