View source
<?php
namespace Drupal\menu_block\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountInterface;
use Drupal\system\Entity\Menu;
use Drupal\system\Plugin\Block\SystemMenuBlock;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MenuBlock extends SystemMenuBlock {
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 = 'fixed';
protected $entityTypeManager;
protected $menuParentFormSelector;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->menuParentFormSelector = $container
->get('menu.parent_form_selector');
$instance->entityTypeManager = $container
->get('entity_type.manager');
return $instance;
}
public function blockForm($form, FormStateInterface $form_state) {
$config = $this->configuration;
$defaults = $this
->defaultConfiguration();
$form = parent::blockForm($form, $form_state);
if (isset($config['expand'])) {
$form['menu_levels']['expand_all_items']['#default_value'] = $config['expand'];
}
$form['advanced'] = [
'#type' => 'details',
'#title' => $this
->t('Advanced options'),
'#open' => FALSE,
'#process' => [
[
get_class(),
'processMenuBlockFieldSets',
],
],
];
$menu_name = $this
->getDerivativeId();
$menus = Menu::loadMultiple([
$menu_name,
]);
$menus[$menu_name] = $menus[$menu_name]
->label();
$form['advanced']['parent'] = $this->menuParentFormSelector
->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 => $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,
],
],
],
];
$form['advanced']['label_link'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Link the title?'),
'#default_value' => $config['label_link'],
'#states' => [
'visible' => [
':input[name="settings[label_display]"]' => [
'checked' => TRUE,
],
':input[name="settings[label_type]"]' => [
[
'value' => self::LABEL_ACTIVE_ITEM,
],
[
'value' => self::LABEL_PARENT,
],
[
'value' => self::LABEL_ROOT,
],
[
'value' => self::LABEL_FIXED,
],
],
],
],
];
$form['style'] = [
'#type' => 'details',
'#title' => $this
->t('HTML and style options'),
'#open' => FALSE,
'#process' => [
[
get_class(),
'processMenuBlockFieldSets',
],
],
];
$form['advanced']['follow'] = [
'#type' => 'checkbox',
'#title' => $this
->t('<strong>Make the initial visibility level follow the active menu item.</strong>'),
'#default_value' => $config['follow'],
'#description' => $this
->t('If the active menu item is deeper than the initial visibility level set above, the initial visibility level will be relative to the active menu item. Otherwise, the initial visibility level of the tree will remain fixed.'),
];
$form['advanced']['follow_parent'] = [
'#type' => 'radios',
'#title' => $this
->t('Initial visibility level will be'),
'#description' => $this
->t('When following the active menu item, select whether the initial visibility level should be set to the active menu item, or its children.'),
'#default_value' => $config['follow_parent'],
'#options' => [
'active' => $this
->t('Active menu item'),
'child' => $this
->t('Children of active menu item'),
],
'#states' => [
'visible' => [
':input[name="settings[follow]"]' => [
'checked' => TRUE,
],
],
],
];
$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.'),
'exists' => [
$this,
'suggestionExists',
],
],
];
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;
}
public static function processMenuBlockFieldSets(&$element, FormStateInterface $form_state, &$complete_form) {
array_pop($element['#parents']);
return $element;
}
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['follow'] = $form_state
->getValue('follow');
$this->configuration['follow_parent'] = $form_state
->getValue('follow_parent');
$this->configuration['level'] = $form_state
->getValue('level');
$this->configuration['depth'] = $form_state
->getValue('depth');
$this->configuration['expand_all_items'] = (bool) $form_state
->getValue('expand_all_items');
if (isset($this->configuration['expand'])) {
unset($this->configuration['expand']);
}
$this->configuration['parent'] = $form_state
->getValue('parent');
$this->configuration['suggestion'] = $form_state
->getValue('suggestion');
$this->configuration['label_type'] = $form_state
->getValue('label_type');
$this->configuration['label_link'] = $form_state
->getValue('label_link');
}
public function build() {
$menu_name = $this
->getDerivativeId();
$parameters = $this->menuTree
->getCurrentRouteMenuTreeParameters($menu_name);
$level = $this->configuration['level'];
$depth = $this->configuration['depth'];
$expand_all_items = $this->configuration['expand'] ?? $this->configuration['expand_all_items'];
$parent = $this->configuration['parent'];
$follow = $this->configuration['follow'];
$follow_parent = $this->configuration['follow_parent'];
$following = FALSE;
$parameters
->setMinDepth($level);
if ($follow && count($parameters->activeTrail) > $level) {
$level = count($parameters->activeTrail);
$following = TRUE;
}
if ($depth > 0) {
$parameters
->setMaxDepth(min($level + $depth - 1, $this->menuTree
->maxDepth()));
}
$fixed_parent_menu_link_id = str_replace($menu_name . ':', '', $parent);
if ($following || $level > 1 && !$fixed_parent_menu_link_id) {
if (count($parameters->activeTrail) >= $level) {
$menu_trail_ids = array_reverse(array_values($parameters->activeTrail));
$offset = $following && $follow_parent == 'active' ? 2 : 1;
$menu_root = $menu_trail_ids[$level - $offset];
$parameters
->setRoot($menu_root)
->setMinDepth(1);
if ($depth > 0) {
$parameters
->setMaxDepth(min($depth, $this->menuTree
->maxDepth()));
}
}
else {
return [];
}
}
if ($expand_all_items) {
$parameters->expandedParents = [];
}
if ($fixed_parent_menu_link_id) {
$fixed_parameters = clone $parameters;
$fixed_parameters
->setRoot($fixed_parent_menu_link_id);
$tree = $this->menuTree
->load($menu_name, $fixed_parameters);
if (empty($tree)) {
if ($this->configuration['level'] === 1 || $this->configuration['level'] === '1') {
$fixed_parameters->expandedParents = [];
$fixed_parameters
->setMinDepth(1);
$fixed_parameters
->setMaxDepth(1);
$tree = $this->menuTree
->load($menu_name, $fixed_parameters);
}
}
elseif ($following) {
unset($tree);
}
}
if (!isset($tree)) {
$tree = $this->menuTree
->load($menu_name, $parameters);
}
$manipulators = [
[
'callable' => 'menu.default_tree_manipulators:checkAccess',
],
[
'callable' => 'menu.default_tree_manipulators:generateIndexAndSort',
],
];
$tree = $this->menuTree
->transform($tree, $manipulators);
$build = $this->menuTree
->build($tree);
$label = $this
->getBlockLabel() ?: $this
->label();
$build['#title'] = [
'#markup' => $label,
];
if (!empty($build['#theme'])) {
$build['#menu_block_configuration'] = $this->configuration;
$build['#menu_block_configuration']['label'] = $label;
$build['#theme'] = 'menu';
}
$build['#contextual_links']['menu'] = [
'route_parameters' => [
'menu' => $menu_name,
],
];
return $build;
}
public function blockAccess(AccountInterface $account) {
$build = $this
->build();
if (empty($build['#items'])) {
return AccessResult::forbidden();
}
return parent::blockAccess($account);
}
public function defaultConfiguration() {
return [
'follow' => 0,
'follow_parent' => 'child',
'level' => 1,
'depth' => 0,
'expand_all_items' => FALSE,
'parent' => $this
->getDerivativeId() . ':',
'suggestion' => strtr($this
->getDerivativeId(), '-', '_'),
'label_type' => self::LABEL_BLOCK,
'label_link' => FALSE,
];
}
public function suggestionExists() {
return FALSE;
}
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:
return $this
->getFixedMenuItemTitle();
default:
return $this
->label();
}
}
protected function getMenuTitle() {
try {
$menu = $this->entityTypeManager
->getStorage('menu')
->load($this
->getDerivativeId());
} catch (\Exception $e) {
return NULL;
}
return $menu ? $menu
->label() : NULL;
}
protected function getFixedMenuItemTitle() {
$parent = $this->configuration['parent'];
if ($parent) {
$fixed_menu_link_id = str_replace($this
->getDerivativeId() . ':', '', $parent);
return $this
->getLinkTitleFromLink($fixed_menu_link_id);
}
}
protected function getActiveItemTitle() {
$active_trail_ids = $this
->getDerivativeActiveTrailIds();
if ($active_trail_ids) {
return $this
->getLinkTitleFromLink(reset($active_trail_ids));
}
}
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));
}
}
protected function getActiveTrailRootTitle() {
$active_trail_ids = $this
->getDerivativeActiveTrailIds();
if ($active_trail_ids) {
return $this
->getLinkTitleFromLink(end($active_trail_ids));
}
}
protected function getDerivativeActiveTrailIds() {
$menu_id = $this
->getDerivativeId();
return array_filter($this->menuActiveTrail
->getActiveTrailIds($menu_id));
}
protected function getLinkTitleFromLink($link_id) {
$parameters = new MenuTreeParameters();
$menu = $this->menuTree
->load($this
->getDerivativeId(), $parameters);
$link = $this
->findLinkInTree($menu, $link_id);
if ($link) {
if ($this->configuration['label_link']) {
$block_link = Link::fromTextAndUrl($link->link
->getTitle(), $link->link
->getUrlObject())
->toString();
return Markup::create($block_link);
}
return $link->link
->getTitle();
}
}
protected function findLinkInTree(array $menu_tree, $link_id) {
if (isset($menu_tree[$link_id])) {
return $menu_tree[$link_id];
}
foreach ($menu_tree as $link) {
$link = $this
->findLinkInTree($link->subtree, $link_id);
if ($link) {
return $link;
}
}
}
}