Verified Commit ad606a8f authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3370946 by kunal.sachdev, lauriii, tim.plunkett, omkar.podey, alexpott,...

Issue #3370946 by kunal.sachdev, lauriii, tim.plunkett, omkar.podey, alexpott, ckrina, smustgrave, larowlan, rkoller, hooroomoo, duadua: Page title should contextualize the local navigation
parent 1707169f
Loading
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -1073,6 +1073,14 @@ services:
    class: Drupal\Core\Utility\LinkGenerator
    arguments: ['@url_generator', '@module_handler', '@renderer']
  Drupal\Core\Utility\LinkGeneratorInterface: '@link_generator'
  request_generator:
    class: Drupal\Core\Utility\RequestGenerator
    arguments: ['@path_processor_manager', '@path.current', '@router']
  Drupal\Core\Utility\RequestGenerator: '@request_generator'
  base_route_title_resolver:
    class: Drupal\Core\Controller\BaseRouteTitleResolver
    arguments: ['@url_generator', '@title_resolver', '@plugin.manager.menu.local_task', '@router.route_provider', '@request_generator']
  Drupal\Core\Utility\BaseRouteTitleResolver: '@base_route_title_resolver'
  router:
    class: Drupal\Core\Routing\AccessAwareRouter
    arguments: ['@router.no_access_checks', '@access_manager', '@current_user']
+25 −5
Original line number Diff line number Diff line
@@ -1319,11 +1319,31 @@ function template_preprocess_html(&$variables) {
    $variables['page']['#title'] = (string) \Drupal::service('renderer')->render($variables['page']['#title']);
  }
  if (!empty($variables['page']['#title'])) {
    $head_title = [
    $head_title = [];

    // Marking the title as safe since it has had the tags stripped.
      'title' => Markup::create(trim(strip_tags($variables['page']['#title']))),
      'name' => $site_config->get('name'),
    ];
    $head_title['title'] = Markup::create(trim(strip_tags($variables['page']['#title'])));

    $maintenance_mode = defined('MAINTENANCE_MODE') || \Drupal::state()->get('system.maintenance_mode');
    $is_admin_route = (bool) \Drupal::routeMatch()->getRouteObject()?->getOption('_admin_route');
    if (!$maintenance_mode && $is_admin_route) {
      $base_route_title = \Drupal::service('base_route_title_resolver')->getTitle(\Drupal::requestStack()->getCurrentRequest(), \Drupal::routeMatch()->getRouteObject());
      if (is_array($base_route_title)) {
        $base_route_title = (string) \Drupal::service('renderer')->render($base_route_title);
      }
      if ($base_route_title) {
        // Marking the title as safe since it has had the tags stripped.
        $base_route_title = Markup::create(trim(strip_tags($base_route_title)));

        // If the base route title is not the same as the current title, append it
        // to the head title.
        if ((string) $base_route_title !== (string) $head_title['title']) {
          $head_title['base_route_title'] = $base_route_title;
        }
      }
    }

    $head_title['name'] = $site_config->get('name');
  }
  // @todo Remove once views is not bypassing the view subscriber anymore.
  //   @see https://www.drupal.org/node/2068471
+142 −3
Original line number Diff line number Diff line
@@ -2,9 +2,15 @@

namespace Drupal\Core\Block\Plugin\Block;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\TitleBlockPluginInterface;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
@@ -17,7 +23,7 @@
    'settings_tray' => FALSE,
  ]
)]
class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {
class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface, ContainerFactoryPluginInterface {

  /**
   * The page title: a string (plain title) or a render array (formatted title).
@@ -26,6 +32,58 @@ class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {
   */
  protected $title = '';

  /**
   * Constructs a new PageTitleBlock.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin ID for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Controller\TitleResolverInterface|null $titleResolver
   *   The title resolver.
   * @param \Drupal\Core\Routing\RouteMatchInterface|null $routeMatch
   *   The route match.
   * @param \Symfony\Component\HttpFoundation\RequestStack|null $requestStack
   *   The request stack.
   * @param \Drupal\Core\Controller\TitleResolverInterface|null $baseRouteTitleResolver
   *   The base route title resolver.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    protected ?TitleResolverInterface $titleResolver,
    protected ?RouteMatchInterface $routeMatch,
    protected ?RequestStack $requestStack,
    protected ?TitleResolverInterface $baseRouteTitleResolver,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    if (!$this->titleResolver || !$this->routeMatch || !$this->requestStack || !$this->baseRouteTitleResolver) {
      @trigger_error('Calling PageTitleBlock::__construct() without the $titleResolver, $routeMatch, $requestStack, and $baseRouteTitleResolver arguments is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3397210', E_USER_DEPRECATED);
      $this->titleResolver = \Drupal::service('title_resolver');
      $this->routeMatch = \Drupal::service('current_route_match');
      $this->requestStack = \Drupal::service('request_stack');
      $this->baseRouteTitleResolver = \Drupal::service('base_route_title_resolver');
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('title_resolver'),
      $container->get('current_route_match'),
      $container->get('request_stack'),
      $container->get('base_route_title_resolver'),
    );
  }

  /**
   * {@inheritdoc}
   */
@@ -38,17 +96,98 @@ public function setTitle($title) {
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return ['label_display' => FALSE];
    return [
      'label_display' => FALSE,
      'base_route_title' => FALSE,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $title = $this->title;
    if ($this->configuration['base_route_title']) {
      $base_route_title = $this->getTitleBasedOnBaseRoute();
      if (!is_null($base_route_title)) {
        $title = $base_route_title;
      }
    }
    return [
      '#type' => 'page_title',
      '#title' => $this->title,
      '#title' => $title,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) : array {
    $form['base_route_title'] = [
      '#type' => 'radios',
      '#title' => $this->t('Title to be displayed'),
      '#options' => [
        0 => $this->t('Current page title'),
        1 => $this->t('Section page title'),
      ],
      '#default_value' => (int) $this->configuration['base_route_title'],
      '#description' => $this->t('Choose whether to display the title of the current page or the current section. The section page title is preferred if the title is displayed before local tasks and if it is displayed after local tasks then the current page title is preferred.'),
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) : void {
    $this->configuration['base_route_title'] = (bool) $form_state->getValue('base_route_title');
  }

  /**
   * Gets title based on base route.
   *
   * @return array|string|null|\Stringable
   *   The title based on base route.
   */
  private function getTitleBasedOnBaseRoute(): array|string|null|\Stringable {
    $controller_title = $this->titleResolver->getTitle($this->requestStack->getCurrentRequest(), $this->routeMatch->getRouteObject());

    // Controller render arrays using `#title` take precedent over the title
    // resolvers.
    if ((string) $this->titleToString($controller_title) !== (string) $this->titleToString($this->title)) {
      return $this->title;
    }

    $base_route_title = $this->baseRouteTitleResolver->getTitle($this->requestStack->getCurrentRequest(), $this->routeMatch->getRouteObject());
    if (!is_null($base_route_title)) {
      // If the titles are equal, return the original title.
      if ((string) $this->titleToString($base_route_title) === (string) $this->titleToString($this->title)) {
        return $this->title;
      }

      return $this->t('@section_title<span class="visually-hidden">: @current_title</span>', [
        '@section_title' => $this->titleToString($base_route_title),
        '@current_title' => $this->titleToString($this->title),
      ]);
    }

    return $this->title;
  }

  /**
   * Converts title to string.
   *
   * @param array|string|null|\Stringable $title
   *   A title that could be an array, string or stringable object.
   *
   * @return string|\Stringable
   */
  private function titleToString(array|string|null|\Stringable $title): string|\Stringable {
    if (is_array($title)) {
      $title = \Drupal::service('renderer')->render($title);
    }

    return $title ?? '';
  }

}
+68 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Controller;

use Drupal\Core\Menu\LocalTaskManager;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Utility\RequestGenerator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Route;

/**
 * Provides a class which gets the title from the current local-task base route.
 */
class BaseRouteTitleResolver implements TitleResolverInterface {

  /**
   * Constructs a RequestGenerator object.
   *
   * @param \Drupal\Core\Routing\UrlGeneratorInterface $urlGenerator
   *   The url generator.
   * @param \Drupal\Core\Controller\TitleResolverInterface $titleResolver
   *   The title resolver.
   * @param \Drupal\Core\Menu\LocalTaskManager $localTaskManager
   *   The local task manager.
   * @param \Drupal\Core\Routing\RouteProviderInterface $routeProvider
   *   The route provider.
   * @param \Drupal\Core\Utility\RequestGenerator $requestGenerator
   *   The request generator.
   */
  public function __construct(
    protected UrlGeneratorInterface $urlGenerator,
    protected TitleResolverInterface $titleResolver,
    protected LocalTaskManager $localTaskManager,
    protected RouteProviderInterface $routeProvider,
    protected RequestGenerator $requestGenerator,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public function getTitle(Request $request, Route $route) : array|string|\Stringable|null {
    $route_match = RouteMatch::createFromRequest($request);
    $base_route_names = $this->localTaskManager->getBaseRouteNames($route_match->getRouteName());
    $title = NULL;
    if ($base_route_names) {
      $base_route_name = reset($base_route_names);
      if ($base_route_name !== $route_match->getRouteName()) {
        try {
          $path = $this->urlGenerator->getPathFromRoute($base_route_name, $route_match->getRawParameters()->all());
        }
        catch (RouteNotFoundException | InvalidParameterException) {
          return NULL;
        }
        $route_request = $this->requestGenerator->generateRequestForPath($path, []);
        if ($route_request) {
          $title = $this->titleResolver->getTitle($route_request, $this->routeProvider->getRouteByName($base_route_name));
        }
      }
    }
    return $title;
  }

}
+81 −52
Original line number Diff line number Diff line
@@ -197,58 +197,11 @@ public function getDefinitions() {
  public function getLocalTasksForRoute($route_name) {
    if (!isset($this->instances[$route_name])) {
      $this->instances[$route_name] = [];
      if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) {
        $base_routes = $cache->data['base_routes'];
        $parents = $cache->data['parents'];
        $children = $cache->data['children'];
      }
      else {
        $definitions = $this->getDefinitions();
        // We build the hierarchy by finding all tabs that should
        // appear on the current route.
        $base_routes = [];
        $parents = [];
        $children = [];
        foreach ($definitions as $plugin_id => $task_info) {
          // Fill in the base_route from the parent to insure consistency.
          if (!empty($task_info['parent_id']) && !empty($definitions[$task_info['parent_id']])) {
            $task_info['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
            // Populate the definitions we use in the next loop. Using a
            // reference like &$task_info causes bugs.
            $definitions[$plugin_id]['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
          }
          if ($route_name == $task_info['route_name']) {
            if (!empty($task_info['base_route'])) {
              $base_routes[$task_info['base_route']] = $task_info['base_route'];
            }
            // Tabs that link to the current route are viable parents
            // and their parent and children should be visible also.
            // @todo - this only works for 2 levels of tabs.
            // instead need to iterate up.
            $parents[$plugin_id] = TRUE;
            if (!empty($task_info['parent_id'])) {
              $parents[$task_info['parent_id']] = TRUE;
            }
          }
        }
        if ($base_routes) {
          // Find all the plugins with the same root and that are at the top
          // level or that have a visible parent.
          foreach ($definitions as $plugin_id => $task_info) {
            if (!empty($base_routes[$task_info['base_route']]) && (empty($task_info['parent_id']) || !empty($parents[$task_info['parent_id']]))) {
              // Concat '> ' with root ID for the parent of top-level tabs.
              $parent = empty($task_info['parent_id']) ? '> ' . $task_info['base_route'] : $task_info['parent_id'];
              $children[$parent][$plugin_id] = $task_info;
            }
          }
        }
        $data = [
          'base_routes' => $base_routes,
          'parents' => $parents,
          'children' => $children,
        ];
        $this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, Cache::PERMANENT, $this->cacheTags);
      }
      $data = $this->getLocalTasksDataForRoute($route_name);
      $base_routes = $data['base_routes'];
      $children = $data['children'];
      $parents = $data['parents'];

      // Create a plugin instance for each element of the hierarchy.
      foreach ($base_routes as $base_route) {
        // Convert the tree keyed by plugin IDs into a simple one with
@@ -411,4 +364,80 @@ protected function isRouteActive($current_route_name, $route_name, $route_parame
    return $active;
  }

  /**
   * {@inheritdoc}
   */
  public function getBaseRouteNames(string $route_name): array {
    $data = $this->getLocalTasksDataForRoute($route_name);
    return $data['base_routes'] ?? [];
  }

  /**
   * Gets the local task data for the given route.
   *
   * @param string $route_name
   *   The route name.
   *
   * @return array
   *   The local task data with the following keys:
   *   - base_routes: An array of base route names of the given route.
   *   - children: An array of the route's child local task definitions keyed by
   *     local task ID.
   *   - parents: An array of the route's parent local task definitions.
   */
  protected function getLocalTasksDataForRoute(string $route_name): array {
    if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) {
      $data = $cache->data;
    }
    else {
      $definitions = $this->getDefinitions();
      // We build the hierarchy by finding all tabs that should
      // appear on the current route.
      $base_routes = [];
      $parents = [];
      $children = [];
      foreach ($definitions as $plugin_id => $task_info) {
        // Fill in the base_route from the parent to insure consistency.
        if (!empty($task_info['parent_id']) && !empty($definitions[$task_info['parent_id']])) {
          $task_info['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
          // Populate the definitions we use in the next loop. Using a
          // reference like &$task_info causes bugs.
          $definitions[$plugin_id]['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
        }
        if ($route_name == $task_info['route_name']) {
          if (!empty($task_info['base_route'])) {
            $base_routes[$task_info['base_route']] = $task_info['base_route'];
          }
          // Tabs that link to the current route are viable parents
          // and their parent and children should be visible also.
          // @todo - this only works for 2 levels of tabs.
          // instead need to iterate up.
          $parents[$plugin_id] = TRUE;
          if (!empty($task_info['parent_id'])) {
            $parents[$task_info['parent_id']] = TRUE;
          }
        }
      }
      if ($base_routes) {
        // Find all the plugins with the same root and that are at the top
        // level or that have a visible parent.
        foreach ($definitions as $plugin_id => $task_info) {
          if (!empty($base_routes[$task_info['base_route']]) && (empty($task_info['parent_id']) || !empty($parents[$task_info['parent_id']]))) {
            // Concat '> ' with root ID for the parent of top-level tabs.
            $parent = empty($task_info['parent_id']) ? '> ' . $task_info['base_route'] : $task_info['parent_id'];
            $children[$parent][$plugin_id] = $task_info;
          }
        }
      }
      $data = [
        'base_routes' => $base_routes,
        'parents' => $parents,
        'children' => $children,
      ];
      $this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, Cache::PERMANENT, $this->cacheTags);
    }

    return $data;
  }

}
Loading