function ActiveLinkResponseFilter::setLinkActiveClass

Same name and namespace in other branches
  1. 9 core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter::setLinkActiveClass()
  2. 8.9.x core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter::setLinkActiveClass()
  3. 10 core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter::setLinkActiveClass()

Sets the "is-active" class and aria-current attribute on relevant links.

This is a PHP implementation of the drupal.active-link JavaScript library.

@todo Once a future version of PHP supports parsing HTML5 properly (i.e. doesn't fail on https://www.drupal.org/comment/7938201#comment-7938201) then we can get rid of this manual parsing and use DOMDocument instead.

Parameters

string $html_markup: The HTML markup to update.

string $current_path: The system path of the currently active page.

bool $is_front: Whether the current page is the front page (which implies the current path might also be <front>).

string $url_language: The language code of the current URL.

array $query: The query string for the current URL.

Return value

string The updated HTML markup.

2 calls to ActiveLinkResponseFilter::setLinkActiveClass()
ActiveLinkResponseFilter::onResponse in core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php
Sets the 'is-active' class on links.
ActiveLinkResponseFilterTest::testSetLinkActiveClass in core/tests/Drupal/Tests/Core/EventSubscriber/ActiveLinkResponseFilterTest.php
Tests setLinkActiveClass().

File

core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php, line 136

Class

ActiveLinkResponseFilter
Subscribes to filter HTML responses, to set attributes on active links.

Namespace

Drupal\Core\EventSubscriber

Code

public static function setLinkActiveClass($html_markup, $current_path, $is_front, $url_language, array $query) {
    $search_key_current_path = 'data-drupal-link-system-path="' . $current_path . '"';
    $search_key_front = 'data-drupal-link-system-path="&lt;front&gt;"';
    // Receive the query in a standardized manner.
    ksort($query);
    $offset = 0;
    // There are two distinct conditions that can make a link be marked active:
    // 1. A link has the current path in its 'data-drupal-link-system-path'
    //    attribute.
    // 2. We are on the front page and a link has the special '<front>' value in
    //    its 'data-drupal-link-system-path' attribute.
    while (str_contains(substr($html_markup, $offset), $search_key_current_path) || $is_front && str_contains(substr($html_markup, $offset), $search_key_front)) {
        $pos_current_path = strpos($html_markup, $search_key_current_path, $offset);
        // Only look for links with the special '<front>' system path if we are
        // actually on the front page.
        $pos_front = $is_front ? strpos($html_markup, $search_key_front, $offset) : FALSE;
        // Determine which of the two values is the next match: the exact path, or
        // the <front> special case.
        $pos_match = NULL;
        if ($pos_front === FALSE) {
            $pos_match = $pos_current_path;
        }
        elseif ($pos_current_path === FALSE) {
            $pos_match = $pos_front;
        }
        elseif ($pos_current_path < $pos_front) {
            $pos_match = $pos_current_path;
        }
        else {
            $pos_match = $pos_front;
        }
        // Find beginning and ending of opening tag.
        $pos_tag_start = NULL;
        for ($i = $pos_match; $pos_tag_start === NULL && $i > 0; $i--) {
            if ($html_markup[$i] === '<') {
                $pos_tag_start = $i;
            }
        }
        $pos_tag_end = NULL;
        for ($i = $pos_match; $pos_tag_end === NULL && $i < strlen($html_markup); $i++) {
            if ($html_markup[$i] === '>') {
                $pos_tag_end = $i;
            }
        }
        // Get the HTML: this will be the opening part of a single tag, e.g.:
        // <a href="/" data-drupal-link-system-path="&lt;front&gt;">
        $tag = substr($html_markup, $pos_tag_start ?? 0, $pos_tag_end - $pos_tag_start + 1);
        // Parse it into a DOMDocument so we can reliably read and modify
        // attributes.
        $dom = new \DOMDocument();
        @$dom->loadHTML('<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $tag . '</body></html>');
        $node = $dom->getElementsByTagName('body')
            ->item(0)->firstChild;
        // Ensure we don't set the "active" class twice on the same element.
        $class = $node->getAttribute('class');
        $add_active = !in_array('is-active', explode(' ', $class));
        // The language of an active link is equal to the current language.
        if ($add_active && $url_language) {
            if ($node->hasAttribute('hreflang') && $node->getAttribute('hreflang') !== $url_language) {
                $add_active = FALSE;
            }
        }
        // The query parameters of an active link are equal to the current
        // parameters.
        if ($add_active) {
            if ($query) {
                if (!$node->hasAttribute('data-drupal-link-query') || $node->getAttribute('data-drupal-link-query') !== Json::encode($query)) {
                    $add_active = FALSE;
                }
            }
            else {
                if ($node->hasAttribute('data-drupal-link-query')) {
                    $add_active = FALSE;
                }
            }
        }
        // Only if the path, the language and the query match, we set the
        // "is-active" class and add aria-current="page".
        if ($add_active) {
            if (strlen($class) > 0) {
                $class .= ' ';
            }
            $class .= 'is-active';
            $node->setAttribute('class', $class);
            $node->setAttribute('aria-current', 'page');
            // Get the updated tag.
            $updated_tag = $dom->saveXML($node, LIBXML_NOEMPTYTAG);
            // saveXML() added a closing tag, remove it.
            $updated_tag = substr($updated_tag, 0, strrpos($updated_tag, '<'));
            $html_markup = str_replace($tag, $updated_tag, $html_markup);
            // Ensure we only search the remaining HTML.
            $offset = $pos_tag_end - strlen($tag) + strlen($updated_tag);
        }
        else {
            // Ensure we only search the remaining HTML.
            $offset = $pos_tag_end + 1;
        }
    }
    return $html_markup;
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.