Custom Section in the Backend Navigation

Access to MetaModel data entry is often best placed in a dedicated section in the backend navigation. To do this, a corresponding group must be created, to which the desired model(s) can be assigned in the input mask properties under “Backend section”.

img_be-section

This requires an SVG icon and an assignment via the Contao MenuEvent — it is planned that this will be configurable via an entry in config.yaml in the future.

SVG icons can be downloaded from e.g. material.io — the width, height, and fill colour should be adjusted as shown in the example using a text editor:

1 <svg xmlns="http://www.w3.org/2000/svg" fill="#91979c" width="15" height="15" viewBox="0 0 24 24">
2     <path d="...."/>
3 </svg>

The file can be saved e.g. at files/backend/group_icon_mm-test.svg (make the folder public).

An event listener is also required to create the entry — the group can be configured via the following parameters (lines 30 to 34):

  • $nodeName — alias of the entry

  • $nodeTitle — title

  • $nodeIcon — path to the icon

  • $targetNode — search for an existing entry such as “content” for content items

  • $targetType — whether the entry should appear before or after the “targetNode”

Place the listener at src/EventListener/BackendMenuListener.php and run composer install.

 1<?php
 2
 3namespace App\EventListener;
 4
 5use Contao\CoreBundle\Event\MenuEvent;
 6use Knp\Menu\Util\MenuManipulator;
 7use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
 8use Symfony\Component\HttpFoundation\RequestStack;
 9use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
10
11#[AsEventListener(priority: -1)]
12class BackendMenuListener
13{
14    private array $targetTypes = ['before' => 0, 'after' => 1];
15
16    public function __construct(
17        private readonly RequestStack $requestStack,
18    ) {
19    }
20
21    public function __invoke(MenuEvent $event): void
22    {
23        $factory = $event->getFactory();
24        $tree    = $event->getTree();
25
26        if ('mainMenu' !== $tree->getName()) {
27            return;
28        }
29
30        $nodeName   = 'mm-test';
31        $nodeTitle  = 'My MM Category';
32        $nodeIcon   = '/files/backend/group_icon_mm-test.svg';
33        $targetNode = 'content';
34        $targetType = 'after';
35
36        $categoryNode = $tree->getChild($nodeName);
37        if (!$categoryNode) {
38            $sessionBag  = $this->requestStack->getSession()->getBag('contao_backend');
39            $status      = ($sessionBag instanceof AttributeBagInterface) ? $sessionBag->get('backend_modules') : [];
40            $isCollapsed = ($status[$nodeName] ?? 1) < 1;
41
42            $categoryNode = $factory
43                ->createItem($nodeName)
44                ->setLabel($nodeTitle)
45                ->setUri('/contao?mtg=' . $nodeName)
46                ->setLinkAttribute('class', 'group-' . $nodeName)
47                ->setLinkAttribute('title', $nodeTitle)
48                ->setLinkAttribute('data-action', 'contao--toggle-navigation#toggle:prevent')
49                ->setLinkAttribute('data-contao--toggle-navigation-category-param', $nodeName)
50                ->setLinkAttribute('aria-controls', $nodeName)
51                ->setLinkAttribute('aria-expanded', $isCollapsed ? 'false' : 'true')
52                ->setChildrenAttribute('id', $nodeName)
53                ->setLinkAttribute('style', \sprintf('background: url(%s) 3px 2px no-repeat;', $nodeIcon))
54                ->setExtra('translation_domain', false);
55
56            if ($isCollapsed) {
57                $categoryNode->setAttribute('class', 'collapsed');
58            }
59
60            $tree->addChild($categoryNode);
61
62            $targetPosition = \array_search($targetNode, \array_keys($tree->getChildren()), true);
63            $targetPosition = false === $targetPosition ? 0 : $targetPosition + $this->targetTypes[$targetType];
64            $manipulator    = new MenuManipulator();
65            $manipulator->moveToPosition($categoryNode, $targetPosition);
66        }
67    }
68}
69?>

The listener and a dummy SVG are available here for download.