433 lines
15 KiB
PHP
433 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Backend\Template\Components;
|
|
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
|
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbContext;
|
|
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbFactory;
|
|
use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
|
|
use TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton;
|
|
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
|
|
use TYPO3\CMS\Core\Resource\ResourceInterface;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
|
|
/**
|
|
* Document header component for backend modules.
|
|
*
|
|
* This component manages the header area of backend module views, providing:
|
|
* - Breadcrumb navigation (via BreadcrumbContext)
|
|
* - Button bar for action buttons (save, close, delete, etc.)
|
|
* - Drop-down menus for module-specific actions
|
|
*
|
|
* The component can be enabled or disabled to control visibility of the entire
|
|
* document header. It integrates with the ModuleTemplate to provide a consistent
|
|
* header across all backend modules.
|
|
*
|
|
* Usage in a controller:
|
|
*
|
|
* ```
|
|
* public function __construct(
|
|
* protected readonly ComponentFactory $componentFactory,
|
|
* ) {}
|
|
*
|
|
* public function myAction(): ResponseInterface
|
|
* {
|
|
* $view = $this->moduleTemplateFactory->create($request);
|
|
* $docHeader = $view->getDocHeaderComponent();
|
|
*
|
|
* // Set breadcrumb for a page
|
|
* $docHeader->setPageBreadcrumb($pageInfo);
|
|
*
|
|
* // Add action buttons using ComponentFactory
|
|
* $buttonBar = $docHeader->getButtonBar();
|
|
* $saveButton = $this->componentFactory->createSaveButton('editform');
|
|
* $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
|
* }
|
|
* ```
|
|
*/
|
|
#[Autoconfigure(public: true)]
|
|
class DocHeaderComponent
|
|
{
|
|
/**
|
|
* Button bar component for managing action buttons.
|
|
*/
|
|
protected ButtonBar $buttonBar;
|
|
|
|
/**
|
|
* Breadcrumb component for rendering navigation trails.
|
|
*/
|
|
protected Breadcrumb $breadcrumb;
|
|
|
|
/**
|
|
* Context information for breadcrumb rendering.
|
|
*
|
|
* Contains the main context (page, record, or resource) and optional suffix nodes
|
|
* for additional navigation elements.
|
|
*/
|
|
protected ?BreadcrumbContext $breadcrumbContext = null;
|
|
|
|
/**
|
|
* Whether the document header is enabled and should be rendered.
|
|
*/
|
|
protected bool $enabled = true;
|
|
|
|
/**
|
|
* Language selector component.
|
|
*/
|
|
protected ?ComponentInterface $languageSelector = null;
|
|
|
|
/**
|
|
* The automatic shortcut button instance, if configured.
|
|
*/
|
|
protected ?ShortcutButton $automaticShortcutButton = null;
|
|
|
|
/**
|
|
* Whether the automatic reload button should be added.
|
|
*/
|
|
protected bool $automaticReloadButton = true;
|
|
|
|
public function __construct(
|
|
protected readonly MenuRegistry $menuRegistry,
|
|
protected readonly BreadcrumbFactory $breadcrumbFactory,
|
|
protected readonly ComponentFactory $componentFactory,
|
|
) {
|
|
$this->buttonBar = GeneralUtility::makeInstance(ButtonBar::class);
|
|
$this->breadcrumb = GeneralUtility::makeInstance(Breadcrumb::class);
|
|
}
|
|
|
|
/**
|
|
* Sets the breadcrumb context for rendering.
|
|
*
|
|
* This is the main API for providing breadcrumb information.
|
|
*
|
|
* For common scenarios, use the convenience methods instead:
|
|
* - setPageBreadcrumb() for page records
|
|
* - setRecordBreadcrumb() for any record
|
|
* - setResourceBreadcrumb() for files or folders
|
|
*
|
|
* @param BreadcrumbContext|null $breadcrumbContext The breadcrumb context
|
|
*/
|
|
public function setBreadcrumbContext(?BreadcrumbContext $breadcrumbContext): void
|
|
{
|
|
$this->breadcrumbContext = $breadcrumbContext;
|
|
}
|
|
|
|
/**
|
|
* Sets breadcrumb from a page record array.
|
|
*
|
|
* Example:
|
|
* $view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);
|
|
*
|
|
* @param array $pageRecord The page record array (must contain 'uid')
|
|
*/
|
|
public function setPageBreadcrumb(array $pageRecord): void
|
|
{
|
|
$this->breadcrumbContext = $this->breadcrumbFactory->forPageArray($pageRecord);
|
|
}
|
|
|
|
/**
|
|
* Sets breadcrumb for editing a record.
|
|
*
|
|
* Example:
|
|
* $view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);
|
|
*
|
|
* @param string $table The table name
|
|
* @param int $uid The record UID
|
|
*/
|
|
public function setRecordBreadcrumb(string $table, int $uid): void
|
|
{
|
|
$this->breadcrumbContext = $this->breadcrumbFactory->forEditAction($table, $uid);
|
|
}
|
|
|
|
/**
|
|
* Sets breadcrumb for any resource (file or folder).
|
|
*
|
|
* Example:
|
|
* $view->getDocHeaderComponent()->setResourceBreadcrumb($file);
|
|
* $view->getDocHeaderComponent()->setResourceBreadcrumb($folder);
|
|
*
|
|
* @param ResourceInterface $resource The resource (file or folder)
|
|
*/
|
|
public function setResourceBreadcrumb(ResourceInterface $resource): void
|
|
{
|
|
$this->breadcrumbContext = $this->breadcrumbFactory->forResource($resource);
|
|
}
|
|
|
|
/**
|
|
* Adds a suffix node to the current breadcrumb context.
|
|
*
|
|
* Suffix nodes are appended after the main breadcrumb trail and are useful for:
|
|
* - Indicating "Create New" actions
|
|
* - Showing "Edit Multiple" states
|
|
* - Adding custom contextual information
|
|
*
|
|
* Example:
|
|
*
|
|
* $docHeader->setPageBreadcrumb($pageInfo);
|
|
* $docHeader->addBreadcrumbSuffixNode(
|
|
* new BreadcrumbNode(
|
|
* identifier: 'new',
|
|
* label: 'Create New Content Element',
|
|
* icon: 'actions-add'
|
|
* )
|
|
* );
|
|
*
|
|
* Note: This creates or modifies the breadcrumb context. If you need to build
|
|
* a complete context, use BreadcrumbFactory instead.
|
|
*
|
|
* @param BreadcrumbNode $node The node to append
|
|
*/
|
|
public function addBreadcrumbSuffixNode(BreadcrumbNode $node): void
|
|
{
|
|
if ($this->breadcrumbContext === null) {
|
|
$this->breadcrumbContext = new BreadcrumbContext(null, [$node]);
|
|
} else {
|
|
// Create new context with added suffix node
|
|
$existingSuffixNodes = $this->breadcrumbContext->suffixNodes;
|
|
$existingSuffixNodes[] = $node;
|
|
$this->breadcrumbContext = new BreadcrumbContext(
|
|
$this->breadcrumbContext->mainContext,
|
|
$existingSuffixNodes
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the menu registry for adding drop-down menus to the document header.
|
|
*/
|
|
public function getMenuRegistry(): MenuRegistry
|
|
{
|
|
return $this->menuRegistry;
|
|
}
|
|
|
|
/**
|
|
* Returns the button bar for adding action buttons to the document header.
|
|
*
|
|
* The button bar supports multiple button positions (left, right) and groups
|
|
* to organize buttons logically.
|
|
*/
|
|
public function getButtonBar(): ButtonBar
|
|
{
|
|
return $this->buttonBar;
|
|
}
|
|
|
|
/**
|
|
* Determines whether this component is enabled and should be rendered.
|
|
*
|
|
* When disabled, the entire document header (including breadcrumbs, buttons,
|
|
* and menus) will not be displayed in the backend module.
|
|
*/
|
|
public function isEnabled(): bool
|
|
{
|
|
return $this->enabled;
|
|
}
|
|
|
|
/**
|
|
* Enables this component for rendering.
|
|
*/
|
|
public function enable(): void
|
|
{
|
|
$this->enabled = true;
|
|
}
|
|
|
|
/**
|
|
* Disables this component to prevent rendering.
|
|
*/
|
|
public function disable(): void
|
|
{
|
|
$this->enabled = false;
|
|
}
|
|
|
|
public function setLanguageSelector(?ComponentInterface $component): void
|
|
{
|
|
$this->languageSelector = $component;
|
|
}
|
|
|
|
public function getLanguageSelector(): ?ComponentInterface
|
|
{
|
|
return $this->languageSelector;
|
|
}
|
|
|
|
/**
|
|
* Sets the context for the automatic shortcut button.
|
|
*
|
|
* Controllers can use this method to provide shortcut information without
|
|
* manually creating and adding the shortcut button. The button will be
|
|
* automatically added to the button bar in the correct position.
|
|
*
|
|
* Example:
|
|
*
|
|
* $docHeader->setShortcutContext('site_configuration.edit', sprintf('Edit site: %s', $siteIdentifier), ['site' => $siteIdentifier]);
|
|
*
|
|
* @param string $routeIdentifier The route identifier for the shortcut
|
|
* @param string $displayName The display name shown in the bookmark list
|
|
* @param array $arguments Optional arguments to include in the shortcut URL
|
|
*/
|
|
public function setShortcutContext(string $routeIdentifier, string $displayName, array $arguments = []): void
|
|
{
|
|
$this->automaticShortcutButton = $this->componentFactory->createShortcutButton()
|
|
->setRouteIdentifier($routeIdentifier)
|
|
->setDisplayName($displayName)
|
|
->setArguments($arguments);
|
|
}
|
|
|
|
/**
|
|
* Disables the automatic reload button for this module.
|
|
*
|
|
* Use this if your module needs custom reload behavior or should not
|
|
* have a reload button at all.
|
|
*/
|
|
public function disableAutomaticReloadButton(): void
|
|
{
|
|
$this->automaticReloadButton = false;
|
|
}
|
|
|
|
/**
|
|
* Disables the automatic shortcut button for this module.
|
|
*
|
|
* Use this if your module should not have a shortcut button.
|
|
*/
|
|
public function disableAutomaticShortcutButton(): void
|
|
{
|
|
$this->automaticShortcutButton = null;
|
|
}
|
|
|
|
/**
|
|
* Returns the complete document header content as an array for rendering.
|
|
*
|
|
* This method aggregates all components (buttons, breadcrumbs) into
|
|
* a structured array that can be consumed by the Fluid template rendering
|
|
* the backend module layout.
|
|
*
|
|
* The returned array structure:
|
|
* - 'enabled': Whether the document header should be rendered
|
|
* - 'buttons': Array of button configurations from the button bar
|
|
* - 'breadcrumb': Breadcrumb trail data from the breadcrumb context
|
|
* - 'languageSelector': Language Selector
|
|
*/
|
|
public function docHeaderContent(?ServerRequestInterface $request): array
|
|
{
|
|
// Process MenuRegistry and add any menus as dropdown buttons to the button bar
|
|
$moduleMenuButton = $this->processMenuRegistry();
|
|
if ($moduleMenuButton !== null) {
|
|
$this->buttonBar->addButton($moduleMenuButton, ButtonBar::BUTTON_POSITION_LEFT, 0);
|
|
}
|
|
|
|
// Add automatic buttons (reload, shortcut)
|
|
$this->addAutomaticButtons($request);
|
|
|
|
return [
|
|
'enabled' => $this->isEnabled(),
|
|
'buttons' => $this->buttonBar->getButtons($request),
|
|
'breadcrumb' => $this->breadcrumb->getBreadcrumb($request, $this->breadcrumbContext),
|
|
'languageSelector' => $this->getLanguageSelector(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Adds automatic reload and shortcut buttons to the button bar.
|
|
*
|
|
* This method is called automatically by docHeaderContent() and handles:
|
|
* - Adding automatic reload button (if enabled)
|
|
* - Adding automatic shortcut button (if configured)
|
|
*
|
|
* The buttons are added to groups 90 and 91 on the right side, which are conventionally
|
|
* used for these system buttons. This ensures they appear at the end of the button bar
|
|
* while still allowing PSR-14 event listeners to modify or remove them via ModifyButtonBarEvent.
|
|
*/
|
|
private function addAutomaticButtons(?ServerRequestInterface $request): void
|
|
{
|
|
if ($request === null) {
|
|
return;
|
|
}
|
|
|
|
// Add automatic reload button if enabled
|
|
if ($this->automaticReloadButton) {
|
|
$reloadButton = $this->componentFactory->createReloadButton(
|
|
$request->getAttribute('normalizedParams')->getRequestUri()
|
|
);
|
|
// Add to group 90 on the right (conventionally second-to-last position)
|
|
$this->buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 90);
|
|
}
|
|
|
|
// Add automatic shortcut button if configured
|
|
if ($this->automaticShortcutButton !== null) {
|
|
// Add to group 91 on the right (conventionally last position)
|
|
$this->buttonBar->addButton($this->automaticShortcutButton);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes registered menus from the MenuRegistry into a dropdown button component.
|
|
*
|
|
* Takes the first registered menu from the MenuRegistry and creates a dropdown button
|
|
* component that can be added to the button bar.
|
|
*
|
|
* @return ButtonInterface|null The dropdown button, or null if no menus registered
|
|
*/
|
|
private function processMenuRegistry(): ?ButtonInterface
|
|
{
|
|
$menus = $this->menuRegistry->getMenus();
|
|
|
|
if ($menus === []) {
|
|
return null;
|
|
}
|
|
|
|
if (count($menus) > 1) {
|
|
throw new \RuntimeException('The menuRegistry should only contain one menu. '
|
|
. 'Multiple DocHeaderComponents can not be displayed - prefer to add distinct dropdown '
|
|
. 'buttons to add more view possibilities, or create actual submodules instead of secondary menus.', 1783447740);
|
|
}
|
|
|
|
// Use the first menu (most controllers only register one menu)
|
|
$menu = reset($menus);
|
|
|
|
// Hide menu if it's either empty or offers only one item
|
|
if (count($menu->getMenuItems()) < 2) {
|
|
return null;
|
|
}
|
|
|
|
$label = $menu->getLabel();
|
|
$dropdownButton = $this->componentFactory->createDropDownButton()
|
|
->setShowActiveLabelText(true)
|
|
->setShowLabelText(true);
|
|
|
|
foreach ($menu->getMenuItems() as $menuItem) {
|
|
if ($label === '') {
|
|
// Previously, the menu was rendered as a <select>, which meant the first or
|
|
// currently selected <option> acted as the visible label. The menu itself had
|
|
// no separate label. As a fallback, we now use the first menu item title as the
|
|
// button label, ensuring the DropDownButton is valid. The button will still
|
|
// always display the active item, because setShowActiveLabelText(true) is set.
|
|
$label = $menuItem->getTitle();
|
|
}
|
|
$dropdownItem = $this->componentFactory->createDropDownRadio()
|
|
->setHref($menuItem->getHref())
|
|
->setLabel($menuItem->getTitle())
|
|
->setActive($menuItem->isActive());
|
|
|
|
$dropdownButton->addItem($dropdownItem);
|
|
}
|
|
|
|
$dropdownButton->setLabel($label);
|
|
|
|
return $dropdownButton;
|
|
}
|
|
}
|