3 namespace Drupal\system\Plugin\Block;
5 use Drupal\Core\Block\BlockBase;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Menu\MenuLinkTreeInterface;
9 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
10 use Symfony\Component\DependencyInjection\ContainerInterface;
13 * Provides a generic Menu block.
16 * id = "system_menu_block",
17 * admin_label = @Translation("Menu"),
18 * category = @Translation("Menus"),
19 * deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock",
21 * "settings_tray" = "\Drupal\system\Form\SystemMenuOffCanvasForm",
25 class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {
28 * The menu link tree service.
30 * @var \Drupal\Core\Menu\MenuLinkTreeInterface
35 * Constructs a new SystemMenuBlock.
37 * @param array $configuration
38 * A configuration array containing information about the plugin instance.
39 * @param string $plugin_id
40 * The plugin_id for the plugin instance.
41 * @param array $plugin_definition
42 * The plugin implementation definition.
43 * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
44 * The menu tree service.
46 public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuLinkTreeInterface $menu_tree) {
47 parent::__construct($configuration, $plugin_id, $plugin_definition);
48 $this->menuTree = $menu_tree;
54 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
59 $container->get('menu.link_tree')
66 public function blockForm($form, FormStateInterface $form_state) {
67 $config = $this->configuration;
69 $defaults = $this->defaultConfiguration();
70 $form['menu_levels'] = [
72 '#title' => $this->t('Menu levels'),
73 // Open if not set to defaults.
74 '#open' => $defaults['level'] !== $config['level'] || $defaults['depth'] !== $config['depth'],
75 '#process' => [[get_class(), 'processMenuLevelParents']],
78 $options = range(0, $this->menuTree->maxDepth());
81 $form['menu_levels']['level'] = [
83 '#title' => $this->t('Initial visibility level'),
84 '#default_value' => $config['level'],
85 '#options' => $options,
86 '#description' => $this->t('The menu is only visible if the menu item for the current page is at this level or below it. Use level 1 to always display this menu.'),
90 $options[0] = $this->t('Unlimited');
92 $form['menu_levels']['depth'] = [
94 '#title' => $this->t('Number of levels to display'),
95 '#default_value' => $config['depth'],
96 '#options' => $options,
97 '#description' => $this->t('This maximum number includes the initial level.'),
105 * Form API callback: Processes the menu_levels field element.
107 * Adjusts the #parents of menu_levels to save its children at the top level.
109 public static function processMenuLevelParents(&$element, FormStateInterface $form_state, &$complete_form) {
110 array_pop($element['#parents']);
117 public function blockSubmit($form, FormStateInterface $form_state) {
118 $this->configuration['level'] = $form_state->getValue('level');
119 $this->configuration['depth'] = $form_state->getValue('depth');
125 public function build() {
126 $menu_name = $this->getDerivativeId();
127 $parameters = $this->menuTree->getCurrentRouteMenuTreeParameters($menu_name);
129 // Adjust the menu tree parameters based on the block's configuration.
130 $level = $this->configuration['level'];
131 $depth = $this->configuration['depth'];
132 $parameters->setMinDepth($level);
133 // When the depth is configured to zero, there is no depth limit. When depth
134 // is non-zero, it indicates the number of levels that must be displayed.
135 // Hence this is a relative depth that we must convert to an actual
136 // (absolute) depth, that may never exceed the maximum depth.
138 $parameters->setMaxDepth(min($level + $depth - 1, $this->menuTree->maxDepth()));
141 // For menu blocks with start level greater than 1, only show menu items
142 // from the current active trail. Adjust the root according to the current
143 // position in the menu in order to determine if we can show the subtree.
145 if (count($parameters->activeTrail) >= $level) {
146 // Active trail array is child-first. Reverse it, and pull the new menu
147 // root based on the parent of the configured start level.
148 $menu_trail_ids = array_reverse(array_values($parameters->activeTrail));
149 $menu_root = $menu_trail_ids[$level - 1];
150 $parameters->setRoot($menu_root)->setMinDepth(1);
152 $parameters->setMaxDepth(min($level - 1 + $depth - 1, $this->menuTree->maxDepth()));
160 $tree = $this->menuTree->load($menu_name, $parameters);
162 ['callable' => 'menu.default_tree_manipulators:checkAccess'],
163 ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
165 $tree = $this->menuTree->transform($tree, $manipulators);
166 return $this->menuTree->build($tree);
172 public function defaultConfiguration() {
182 public function getCacheTags() {
183 // Even when the menu block renders to the empty string for a user, we want
184 // the cache tag for this menu to be set: whenever the menu is changed, this
185 // menu block must also be re-rendered for that user, because maybe a menu
186 // link that is accessible for that user has been added.
187 $cache_tags = parent::getCacheTags();
188 $cache_tags[] = 'config:system.menu.' . $this->getDerivativeId();
195 public function getCacheContexts() {
196 // ::build() uses MenuLinkTreeInterface::getCurrentRouteMenuTreeParameters()
197 // to generate menu tree parameters, and those take the active menu trail
198 // into account. Therefore, we must vary the rendered menu by the active
199 // trail of the rendered menu.
200 // Additional cache contexts, e.g. those that determine link text or
201 // accessibility of a menu, will be bubbled automatically.
202 $menu_name = $this->getDerivativeId();
203 return Cache::mergeContexts(parent::getCacheContexts(), ['route.menu_active_trails:' . $menu_name]);