Newer
Older
<?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;
/**
* 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 {
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 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;
}
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* {@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],
],
],
];
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
$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');
}
/**
* {@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()));
}
Brian Canini
committed
// 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();
}
}
// 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.
Brian Canini
committed
if ($fixed_parent_menu_link_id) {
$parameters->setRoot($fixed_parent_menu_link_id);
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// 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,
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
/**
* 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;
}
}
}