Skip to content
Snippets Groups Projects
MenuBlock.php 14.2 KiB
Newer Older
bcweaver's avatar
bcweaver committed
<?php

namespace Drupal\menu_block\Plugin\Block;

use Drupal\Core\Form\FormStateInterface;
use Drupal\system\Entity\Menu;
use Drupal\system\Plugin\Block\SystemMenuBlock;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Menu\MenuActiveTrailInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
bcweaver's avatar
bcweaver committed

/**
 * Provides an extended Menu block.
 *
 * @Block(
 *   id = "menu_block",
 *   admin_label = @Translation("Menu block"),
 *   category = @Translation("Menus"),
 *   deriver = "Drupal\menu_block\Plugin\Derivative\MenuBlock"
 * )
 */
class MenuBlock extends SystemMenuBlock {

  /**
   * Constant definition options for block label type.
   */
  const LABEL_BLOCK = 'block';
  const LABEL_MENU = 'menu';
  const LABEL_ACTIVE_ITEM = 'active_item';
  const LABEL_PARENT = 'parent';
  const LABEL_ROOT = 'root';
  const LABEL_FIXED_PARENT = 'fixed_parent';

  /**
   * Entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The active menu trail service.
   *
   * @var \Drupal\Core\Menu\MenuActiveTrailInterface
   */
  protected $menuActiveTrail;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('menu.link_tree'),
      $container->get('menu.active_trail'),
      $container->get('entity_type.manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, array $plugin_definition, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $active_trail, EntityTypeManagerInterface $entity_type_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $menu_tree);
    $this->menuActiveTrail = $active_trail;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfiguration() {
    $label = $this->getBlockLabel() ?: $this->label();
    $this->setConfigurationValue('label', $label);
    return $this->configuration;
  }

bcweaver's avatar
bcweaver committed
  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $config = $this->configuration;
    $defaults = $this->defaultConfiguration();

    $form = parent::blockForm($form, $form_state);

    $form['advanced'] = [
      '#type' => 'details',
      '#title' => $this->t('Advanced options'),
      '#open' => FALSE,
      '#process' => [[get_class(), 'processMenuBlockFieldSets']],
    ];

    $form['advanced']['expand'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('<strong>Expand all menu links</strong>'),
      '#default_value' => $config['expand'],
      '#description' => $this->t('All menu links that have children will "Show as expanded".'),
    ];

    $menu_name = $this->getDerivativeId();
    $menus = Menu::loadMultiple(array($menu_name));
    $menus[$menu_name] = $menus[$menu_name]->label();

    /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
    $menu_parent_selector = \Drupal::service('menu.parent_form_selector');
    $form['advanced']['parent'] = $menu_parent_selector->parentSelectElement($config['parent'], '', $menus);

    $form['advanced']['parent'] += [
      '#title' => $this->t('Fixed parent item'),
      '#description' => $this->t('Alter the options in “Menu levels” to be relative to the fixed parent item. The block will only contain children of the selected menu link.'),
    ];

    $form['advanced']['label_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Use as title'),
      '#description' => $this->t('Replace the block title with an item from the menu.'),
      '#options' => [
        self::LABEL_BLOCK => $this->t('Block title'),
        self::LABEL_MENU => $this->t('Menu title'),
        self::LABEL_FIXED_PARENT => $this->t("Fixed parent item's title"),
        self::LABEL_ACTIVE_ITEM => $this->t("Active item's title"),
        self::LABEL_PARENT => $this->t("Active trail's parent title"),
        self::LABEL_ROOT => $this->t("Active trail's root title"),
      ],
      '#default_value' => $config['label_type'],
      '#states' => [
        'visible' => [
          ':input[name="settings[label_display]"]' => ['checked' => TRUE],
        ],
      ],
    ];

bcweaver's avatar
bcweaver committed
    $form['style'] = [
      '#type' => 'details',
      '#title' => $this->t('HTML and style options'),
      '#open' => FALSE,
      '#process' => [[get_class(), 'processMenuBlockFieldSets']],
    ];

    $form['style']['suggestion'] = [
      '#type' => 'machine_name',
      '#title' => $this->t('Theme hook suggestion'),
      '#default_value' => $config['suggestion'],
      '#field_prefix' => '<code>menu__</code>',
      '#description' => $this->t('A theme hook suggestion can be used to override the default HTML and CSS classes for menus found in <code>menu.html.twig</code>.'),
      '#machine_name' => [
        'error' => $this->t('The theme hook suggestion must contain only lowercase letters, numbers, and underscores.'),
      ],
    ];

    // Open the details field sets if their config is not set to defaults.
    foreach(['menu_levels', 'advanced', 'style'] as $fieldSet) {
      foreach (array_keys($form[$fieldSet]) as $field) {
        if (isset($defaults[$field]) && $defaults[$field] !== $config[$field]) {
          $form[$fieldSet]['#open'] = TRUE;
        }
      }
    }

    return $form;
  }

  /**
   * Form API callback: Processes the elements in field sets.
   *
   * Adjusts the #parents of field sets to save its children at the top level.
   */
  public static function processMenuBlockFieldSets(&$element, FormStateInterface $form_state, &$complete_form) {
    array_pop($element['#parents']);
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['level'] = $form_state->getValue('level');
    $this->configuration['depth'] = $form_state->getValue('depth');
    $this->configuration['expand'] = $form_state->getValue('expand');
    $this->configuration['parent'] = $form_state->getValue('parent');
    $this->configuration['suggestion'] = $form_state->getValue('suggestion');
    $this->configuration['label_type'] = $form_state->getValue('label_type');
bcweaver's avatar
bcweaver committed
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $menu_name = $this->getDerivativeId();
    $parameters = $this->menuTree->getCurrentRouteMenuTreeParameters($menu_name);

    // Adjust the menu tree parameters based on the block's configuration.
    $level = $this->configuration['level'];
    $depth = $this->configuration['depth'];
    $expand = $this->configuration['expand'];
    $parent = $this->configuration['parent'];
    $suggestion = $this->configuration['suggestion'];

    $parameters->setMinDepth($level);
    // When the depth is configured to zero, there is no depth limit. When depth
    // is non-zero, it indicates the number of levels that must be displayed.
    // Hence this is a relative depth that we must convert to an actual
    // (absolute) depth, that may never exceed the maximum depth.
    if ($depth > 0) {
      $parameters->setMaxDepth(min($level + $depth - 1, $this->menuTree->maxDepth()));
    }

    // For menu blocks with start level greater than 1, only show menu items
    // from the current active trail. Adjust the root according to the current
    // position in the menu in order to determine if we can show the subtree.
    // If we're using a fixed parent item, we'll skip this step.
    $fixed_parent_menu_link_id = str_replace($menu_name . ':', '', $parent);
    if ($level > 1 && !$fixed_parent_menu_link_id) {
      if (count($parameters->activeTrail) >= $level) {
        // Active trail array is child-first. Reverse it, and pull the new menu
        // root based on the parent of the configured start level.
        $menu_trail_ids = array_reverse(array_values($parameters->activeTrail));
        $menu_root = $menu_trail_ids[$level - 1];
        $parameters->setRoot($menu_root)->setMinDepth(1);
        if ($depth > 0) {
          $max_depth = min($level - 1 + $depth - 1, $this->menuTree->maxDepth());
          $parameters->setMaxDepth($max_depth);
        }
      }
      else {
        return array();
      }
    }

bcweaver's avatar
bcweaver committed
    // If expandedParents is empty, the whole menu tree is built.
    if ($expand) {
      $parameters->expandedParents = array();
    }
    // When a fixed parent item is set, root the menu tree at the given ID.
    if ($fixed_parent_menu_link_id) {
      $parameters->setRoot($fixed_parent_menu_link_id);
bcweaver's avatar
bcweaver committed

      // If the starting level is 1, we always want the child links to appear,
      // but the requested tree may be empty if the tree does not contain the
      // active trail.
      if ($level === 1 || $level === '1') {
        // Check if the tree contains links.
        $tree = $this->menuTree->load(NULL, $parameters);
        if (empty($tree)) {
          // Change the request to expand all children and limit the depth to
          // the immediate children of the root.
          $parameters->expandedParents = array();
          $parameters->setMinDepth(1);
          $parameters->setMaxDepth(1);
          // Re-load the tree.
          $tree = $this->menuTree->load(NULL, $parameters);
        }
      }
    }

    // Load the tree if we haven't already.
    if (!isset($tree)) {
      $tree = $this->menuTree->load($menu_name, $parameters);
    }
    $manipulators = array(
      array('callable' => 'menu.default_tree_manipulators:checkAccess'),
      array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
    );
    $tree = $this->menuTree->transform($tree, $manipulators);
    $build = $this->menuTree->build($tree);

    if (!empty($build['#theme'])) {
      // Add the configuration for use in menu_block_theme_suggestions_menu().
      $build['#menu_block_configuration'] = $this->configuration;
      // Remove the menu name-based suggestion so we can control its precedence
      // better in menu_block_theme_suggestions_menu().
      $build['#theme'] = 'menu';
    }

    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'level' => 1,
      'depth' => 0,
      'expand' => 0,
      'parent' => $this->getDerivativeId() . ':',
      'suggestion' => strtr($this->getDerivativeId(), '-', '_'),
      'label_type' => self::LABEL_BLOCK,
bcweaver's avatar
bcweaver committed
    ];
  }

  /**
   * Get the configured block label.
   *
   * @return string
   *   The configured label.
   */
  public function getBlockLabel() {
    switch ($this->configuration['label_type']) {
      case self::LABEL_MENU:
        return $this->getMenuTitle();

      case self::LABEL_ACTIVE_ITEM:
        return $this->getActiveItemTitle();

      case self::LABEL_PARENT:
        return $this->getActiveTrailParentTitle();

      case self::LABEL_ROOT:
        return $this->getActiveTrailRootTitle();

      case self::LABEL_FIXED_PARENT:
        return $this->getFixedParentItemTitle();

      default:
        return $this->label();
    }
  }

  /**
   * Get the label of the configured menu.
   *
   * @return string|null
   *   Menu label or null if no menu exists.
   */
  protected function getMenuTitle() {
    try {
      $menu = $this->entityTypeManager->getStorage('menu')
        ->load($this->getDerivativeId());
    }
    catch (\Exception $e) {
      return NULL;
    }

    return $menu ? $menu->label() : NULL;
  }

  /**
   * @return string
   */
  protected function getFixedParentItemTitle() {
    $parent = $this->configuration['parent'];

    if ($parent) {
      $fixed_parent_menu_link_id = str_replace($this->getDerivativeId() . ':', '', $parent);
      return $this->getLinkTitleFromLink($fixed_parent_menu_link_id);
    }
  }

  /**
   * Get the active menu item's title.
   *
   * @return string
   *   Current menu item title.
   */
  protected function getActiveItemTitle() {
    $active_trail_ids = $this->getDerivativeActiveTrailIds();
    if ($active_trail_ids) {
      return $this->getLinkTitleFromLink(reset($active_trail_ids));
    }
  }

  /**
   * Get the current menu item's parent menu title.
   *
   * @return string
   *   The menu item title.
   */
  protected function getActiveTrailParentTitle() {
    $active_trail_ids = $this->getDerivativeActiveTrailIds();

    if ($active_trail_ids) {
      if (count($active_trail_ids) === 1) {
        return $this->getActiveItemTitle();
      }
      return $this->getLinkTitleFromLink(next($active_trail_ids));
    }
  }

  /**
   * Get the current menu item's root menu item title.
   *
   * @return string
   *   The menu item title.
   */
  protected function getActiveTrailRootTitle() {
    $active_trail_ids = $this->getDerivativeActiveTrailIds();

    if ($active_trail_ids) {
      return $this->getLinkTitleFromLink(end($active_trail_ids));
    }
  }

  /**
   * Get an array of the active trail menu link items.
   *
   * @return array
   *   The active trail.
   */
  protected function getDerivativeActiveTrailIds() {
    $menu_id = $this->getDerivativeId();
    return array_filter($this->menuActiveTrail->getActiveTrailIds($menu_id));
  }

  /**
   * Given a menu item ID, get that item's title.
   *
   * @param string $link_id
   *   Menu Item ID.
   *
   * @return string
   *   The menu item title.
   */
  protected function getLinkTitleFromLink($link_id) {
    $parameters = new MenuTreeParameters();
    $menu = $this->menuTree->load($this->getDerivativeId(), $parameters);
    if ($link = $this->findLinkInTree($menu, $link_id)) {
      return $link->link->getTitle();
    }
  }

  /**
   * Find and return the menu link item from the menu tree.
   *
   * @param array $menu_tree
   *   Associative array containing the menu link tree data.
   * @param string $link_id
   *   Menu link id to find.
   *
   * @return \Drupal\Core\Menu\MenuLinkTreeElement
   *   The link element from the given menu tree.
   */
  protected function findLinkInTree(array $menu_tree, $link_id) {
    if (isset($menu_tree[$link_id])) {
      return $menu_tree[$link_id];
    }
    /** @var \Drupal\Core\Menu\MenuLinkTreeElement $link */
    foreach ($menu_tree as $link) {
      if ($link = $this->findLinkInTree($link->subtree, $link_id)) {
        return $link;
      }
    }
  }

bcweaver's avatar
bcweaver committed
}