--- /dev/null
+<?php
+
+namespace Drupal\system\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Menu\MenuActiveTrailInterface;
+use Drupal\Core\Menu\MenuLinkTreeInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a generic Menu block.
+ *
+ * @Block(
+ * id = "system_menu_block",
+ * admin_label = @Translation("Menu"),
+ * category = @Translation("Menus"),
+ * deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock"
+ * )
+ */
+class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * The menu link tree service.
+ *
+ * @var \Drupal\Core\Menu\MenuLinkTreeInterface
+ */
+ protected $menuTree;
+
+ /**
+ * The active menu trail service.
+ *
+ * @var \Drupal\Core\Menu\MenuActiveTrailInterface
+ */
+ protected $menuActiveTrail;
+
+ /**
+ * Constructs a new SystemMenuBlock.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin_id for the plugin instance.
+ * @param array $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
+ * The menu tree service.
+ * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail
+ * The active menu trail service.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $menu_active_trail) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->menuTree = $menu_tree;
+ $this->menuActiveTrail = $menu_active_trail;
+ }
+
+ /**
+ * {@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')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockForm($form, FormStateInterface $form_state) {
+ $config = $this->configuration;
+
+ $defaults = $this->defaultConfiguration();
+ $form['menu_levels'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Menu levels'),
+ // Open if not set to defaults.
+ '#open' => $defaults['level'] !== $config['level'] || $defaults['depth'] !== $config['depth'],
+ '#process' => [[get_class(), 'processMenuLevelParents']],
+ ];
+
+ $options = range(0, $this->menuTree->maxDepth());
+ unset($options[0]);
+
+ $form['menu_levels']['level'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Initial visibility level'),
+ '#default_value' => $config['level'],
+ '#options' => $options,
+ '#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.'),
+ '#required' => TRUE,
+ ];
+
+ $options[0] = $this->t('Unlimited');
+
+ $form['menu_levels']['depth'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Number of levels to display'),
+ '#default_value' => $config['depth'],
+ '#options' => $options,
+ '#description' => $this->t('This maximum number includes the initial level.'),
+ '#required' => TRUE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Form API callback: Processes the menu_levels field element.
+ *
+ * Adjusts the #parents of menu_levels to save its children at the top level.
+ */
+ public static function processMenuLevelParents(&$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');
+ }
+
+ /**
+ * {@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'];
+ $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()));
+ }
+
+ $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);
+ return $this->menuTree->build($tree);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'level' => 1,
+ 'depth' => 0,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ // Even when the menu block renders to the empty string for a user, we want
+ // the cache tag for this menu to be set: whenever the menu is changed, this
+ // menu block must also be re-rendered for that user, because maybe a menu
+ // link that is accessible for that user has been added.
+ $cache_tags = parent::getCacheTags();
+ $cache_tags[] = 'config:system.menu.' . $this->getDerivativeId();
+ return $cache_tags;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheContexts() {
+ // ::build() uses MenuLinkTreeInterface::getCurrentRouteMenuTreeParameters()
+ // to generate menu tree parameters, and those take the active menu trail
+ // into account. Therefore, we must vary the rendered menu by the active
+ // trail of the rendered menu.
+ // Additional cache contexts, e.g. those that determine link text or
+ // accessibility of a menu, will be bubbled automatically.
+ $menu_name = $this->getDerivativeId();
+ return Cache::mergeContexts(parent::getCacheContexts(), ['route.menu_active_trails:' . $menu_name]);
+ }
+
+}