TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Base class providing common properties for UI controls in the backend.
|
||||
* Provides standard HTML attributes like title, CSS classes, and data attributes
|
||||
* that are shared across various component types (buttons, menu items, etc.).
|
||||
*
|
||||
* This class is extended by components like:
|
||||
* - AbstractButton
|
||||
* - MenuItem
|
||||
*
|
||||
* Example (inherited in MenuItem):
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* $menuItem = $this->componentFactory->createMenuItem()
|
||||
* ->setTitle('My Item') // From AbstractControl
|
||||
* ->setClasses('custom-class') // From AbstractControl
|
||||
* ->setDataAttributes([ // From AbstractControl
|
||||
* 'action' => 'do-something'
|
||||
* ])
|
||||
* ->setHref('/target'); // MenuItem-specific
|
||||
* ```
|
||||
*/
|
||||
class AbstractControl
|
||||
{
|
||||
/**
|
||||
* CSS classes to apply to the rendered element
|
||||
*/
|
||||
protected string $classes = '';
|
||||
|
||||
/**
|
||||
* Title/label text for the control
|
||||
*/
|
||||
protected string $title = '';
|
||||
|
||||
/**
|
||||
* HTML data-* attributes for the control
|
||||
*
|
||||
* @var array<string, string> Key-value pairs (e.g., ['action' => 'save'])
|
||||
*/
|
||||
protected array $dataAttributes = [];
|
||||
|
||||
public function getClasses(): string
|
||||
{
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getDataAttributes(): array
|
||||
{
|
||||
return $this->dataAttributes;
|
||||
}
|
||||
|
||||
public function setClasses(string $classes): static
|
||||
{
|
||||
$this->classes = $classes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDataAttributes(array $dataAttributes): static
|
||||
{
|
||||
$this->dataAttributes = $dataAttributes;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Defines groups for record list / file list actions.
|
||||
* Currently, there are only two of them, which can be used to move Buttons between the Primary Group and the Secondary Group via Events.
|
||||
*/
|
||||
enum ActionGroup
|
||||
{
|
||||
case primary;
|
||||
case secondary;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?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 Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbContext;
|
||||
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbProviderInterface;
|
||||
use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
|
||||
|
||||
/**
|
||||
* Breadcrumb component, building breadcrumbs for the backend doc header.
|
||||
*
|
||||
* This component uses a provider-based architecture:
|
||||
* - BreadcrumbProviderInterface implementations generate breadcrumb nodes
|
||||
* - Providers are selected based on the context type (record, resource, etc.)
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not part of the TYPO3's Core API.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class Breadcrumb
|
||||
{
|
||||
/**
|
||||
* @param iterable<BreadcrumbProviderInterface> $providers
|
||||
*/
|
||||
public function __construct(
|
||||
private iterable $providers,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates breadcrumb nodes from a breadcrumb context.
|
||||
*
|
||||
* @param ServerRequestInterface|null $request The current request for module detection
|
||||
* @param BreadcrumbContext|null $context The breadcrumb context containing main entity and suffix nodes
|
||||
* @return BreadcrumbNode[] Array of breadcrumb nodes
|
||||
*/
|
||||
public function getBreadcrumb(?ServerRequestInterface $request, ?BreadcrumbContext $context): array
|
||||
{
|
||||
// Generate nodes from providers (works for both null and non-null context)
|
||||
$nodes = $this->generateNodesFromContext($context, $request);
|
||||
|
||||
// Append suffix nodes only if context is not null
|
||||
if ($context !== null && $context->hasSuffixNodes()) {
|
||||
foreach ($context->suffixNodes as $suffixNode) {
|
||||
$nodes[] = $suffixNode;
|
||||
}
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates breadcrumb nodes from a context using providers.
|
||||
*
|
||||
* @return BreadcrumbNode[]
|
||||
*/
|
||||
private function generateNodesFromContext(?BreadcrumbContext $context, ?ServerRequestInterface $request): array
|
||||
{
|
||||
$provider = $this->findProvider($context);
|
||||
|
||||
if ($provider === null) {
|
||||
$this->logger->warning(
|
||||
'No breadcrumb provider found for context',
|
||||
['context_type' => get_debug_type($context)]
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return $provider->generate($context, $request);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error(
|
||||
'Failed to generate breadcrumb from provider',
|
||||
[
|
||||
'provider' => get_class($provider),
|
||||
'context_type' => get_debug_type($context),
|
||||
'exception' => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the most suitable provider for the given context.
|
||||
*
|
||||
* Providers are checked in priority order (highest first).
|
||||
*/
|
||||
private function findProvider(?BreadcrumbContext $context): ?BreadcrumbProviderInterface
|
||||
{
|
||||
$providers = iterator_to_array($this->providers);
|
||||
|
||||
// Sort by priority (highest first)
|
||||
usort($providers, static fn($a, $b) => $b->getPriority() <=> $a->getPriority());
|
||||
|
||||
foreach ($providers as $provider) {
|
||||
if ($provider->supports($context)) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?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\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\PositionInterface;
|
||||
|
||||
/**
|
||||
* Container for managing buttons in the backend module document header.
|
||||
* The ButtonBar organizes buttons into two positions (left and right) with multiple groups,
|
||||
* allowing precise control over button placement and visual grouping.
|
||||
*
|
||||
* Buttons are organized in a three-level structure:
|
||||
* 1. Position: LEFT or RIGHT side of the header
|
||||
* 2. Group: Numerical groups within each position (1, 2, 3, etc.)
|
||||
* 3. Individual buttons within each group
|
||||
*
|
||||
* Groups are rendered with visual spacing between them, allowing logical grouping
|
||||
* of related actions. Lower group numbers appear first (left-to-right, or top-to-bottom on mobile).
|
||||
*
|
||||
* Example - Using pre-configured buttons from ComponentFactory:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
*
|
||||
* // Use pre-configured save button
|
||||
* $saveButton = $this->componentFactory->createSaveButton('editform');
|
||||
* $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
||||
*
|
||||
* // Use pre-configured back button
|
||||
* $backButton = $this->componentFactory->createBackButton($returnUrl);
|
||||
* $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Example - Creating custom buttons with ComponentFactory:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
*
|
||||
* // Create custom link button
|
||||
* $customButton = $this->componentFactory->createLinkButton()
|
||||
* ->setHref('/custom-action')
|
||||
* ->setTitle('Custom Action')
|
||||
* ->setIcon($iconFactory->getIcon('actions-custom'));
|
||||
* $buttonBar->addButton($customButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Example - Buttons with automatic positioning:
|
||||
*
|
||||
* ```
|
||||
* // ShortcutButton implements PositionInterface and positions itself automatically
|
||||
* $shortcutButton = $this->componentFactory->createShortcutButton()
|
||||
* ->setRouteIdentifier('my_module')
|
||||
* ->setDisplayName('My Module');
|
||||
* $buttonBar->addButton($shortcutButton); // Position and group are automatic
|
||||
* ```
|
||||
*
|
||||
* Example - Dropdown button:
|
||||
*
|
||||
* ```
|
||||
* $dropdownButton = $this->componentFactory->createDropDownButton()
|
||||
* ->setLabel('Actions')
|
||||
* ->setIcon($iconFactory->getIcon('actions-menu'));
|
||||
*
|
||||
* $item1 = $this->componentFactory->createDropDownItem()
|
||||
* ->setLabel('Edit')
|
||||
* ->setHref('/edit');
|
||||
* $dropdownButton->addItem($item1);
|
||||
*
|
||||
* $item2 = $this->componentFactory->createDropDownItem()
|
||||
* ->setLabel('Delete')
|
||||
* ->setHref('/delete');
|
||||
* $dropdownButton->addItem($item2);
|
||||
*
|
||||
* $buttonBar->addButton($dropdownButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
|
||||
* ```
|
||||
*
|
||||
* @phpstan-type Buttons array<self::BUTTON_POSITION_*, array<int, list<ButtonInterface>>>
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class ButtonBar
|
||||
{
|
||||
/**
|
||||
* Position constant for left side of the button bar
|
||||
*/
|
||||
public const BUTTON_POSITION_LEFT = 'left';
|
||||
|
||||
/**
|
||||
* Position constant for right side of the button bar
|
||||
*/
|
||||
public const BUTTON_POSITION_RIGHT = 'right';
|
||||
|
||||
/**
|
||||
* Internal array of all registered buttons
|
||||
*
|
||||
* @var Buttons
|
||||
*/
|
||||
protected array $buttons = [];
|
||||
|
||||
public function __construct(
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add a new button
|
||||
*
|
||||
* Buttons implementing PositionInterface will automatically use their own
|
||||
* predefined position and group, ignoring the $buttonPosition and $buttonGroup
|
||||
* parameters. This ensures buttons like ShortcutButton always appear in their
|
||||
* designated location.
|
||||
*
|
||||
* @param ButtonInterface $button The Button Object to add
|
||||
* @param self::BUTTON_POSITION_* $buttonPosition Position of the button (left/right). Ignored if button implements PositionInterface.
|
||||
* @param int $buttonGroup Buttongroup of the button. Ignored if button implements PositionInterface.
|
||||
*
|
||||
* @throws \InvalidArgumentException In case a button is not valid
|
||||
*/
|
||||
public function addButton(
|
||||
ButtonInterface $button,
|
||||
string $buttonPosition = self::BUTTON_POSITION_LEFT,
|
||||
int $buttonGroup = 1
|
||||
): static {
|
||||
if (!$button->isValid()) {
|
||||
throw new \InvalidArgumentException('Button "' . $button->getType() . '" is not valid', 1441706370);
|
||||
}
|
||||
// Buttons implementing PositionInterface define their own position and group
|
||||
if ($button instanceof PositionInterface) {
|
||||
$buttonPosition = $button->getPosition();
|
||||
$buttonGroup = $button->getGroup();
|
||||
}
|
||||
// We make the button immutable here
|
||||
$this->buttons[$buttonPosition][$buttonGroup][] = clone $button;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array of all buttons in the form of
|
||||
* ButtonPosition > ButtonGroup > Button
|
||||
*/
|
||||
public function getButtons(ServerRequestInterface $request): array
|
||||
{
|
||||
// here we need to call the sorting methods and stuff.
|
||||
foreach ($this->buttons as $position => $_) {
|
||||
ksort($this->buttons[$position]);
|
||||
}
|
||||
|
||||
// Dispatch event for manipulating the docHeaderButtons
|
||||
$this->buttons = $this->eventDispatcher->dispatch(new ModifyButtonBarEvent($this->buttons, $this, $request))->getButtons();
|
||||
|
||||
return $this->buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a button of the specified type has been added to the button bar.
|
||||
*
|
||||
* This is primarily used for backwards compatibility detection to prevent
|
||||
* duplicate automatic buttons when controllers manually add them.
|
||||
*
|
||||
* @param class-string<ButtonInterface> $buttonClassName Fully qualified button class name
|
||||
* @return bool True if a button of this type exists, false otherwise
|
||||
*/
|
||||
public function hasButtonOfType(string $buttonClassName): bool
|
||||
{
|
||||
foreach ($this->buttons as $groups) {
|
||||
foreach ($groups as $groupButtons) {
|
||||
foreach ($groupButtons as $button) {
|
||||
if ($button instanceof $buttonClassName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Backend\Template\Components\AbstractControl;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
|
||||
/**
|
||||
* Base class for all button types in the backend document header.
|
||||
* Provides common functionality for buttons including icon handling, label text display,
|
||||
* and disabled state management.
|
||||
*
|
||||
* This class extends AbstractControl (providing title, classes, data attributes) and
|
||||
* implements ButtonInterface (providing validation, type identification, and rendering).
|
||||
*
|
||||
* @see ButtonInterface
|
||||
* @see AbstractControl
|
||||
*/
|
||||
class AbstractButton extends AbstractControl implements ButtonInterface
|
||||
{
|
||||
/**
|
||||
* Optional icon to display on the button
|
||||
*/
|
||||
protected ?Icon $icon = null;
|
||||
|
||||
/**
|
||||
* Whether to show the button's label text (from title property).
|
||||
* If false, only the icon is shown (label is used for title attribute).
|
||||
*/
|
||||
protected bool $showLabelText = false;
|
||||
|
||||
/**
|
||||
* Whether the button is in disabled state
|
||||
*/
|
||||
protected bool $disabled = false;
|
||||
|
||||
/**
|
||||
* Attributes for the button
|
||||
*/
|
||||
protected array $attributes = [];
|
||||
|
||||
public function getShowLabelText(): bool
|
||||
{
|
||||
return $this->showLabelText;
|
||||
}
|
||||
|
||||
public function setShowLabelText(bool $showLabelText): static
|
||||
{
|
||||
$this->showLabelText = $showLabelText;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIcon(): ?Icon
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function setIcon(?Icon $icon): static
|
||||
{
|
||||
$this->icon = $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDisabled(): bool
|
||||
{
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
public function setDisabled(bool $disabled): static
|
||||
{
|
||||
$this->disabled = $disabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function setAttributes(array $attributes): static
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation from ButtonInterface
|
||||
* This object is an abstract, so no implementation is necessary
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation from ButtonInterface
|
||||
* This object is an abstract, so no implementation is necessary
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation from ButtonInterface
|
||||
* This object is an abstract, so no implementation is necessary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?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\Buttons\Action;
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\Router;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\PositionInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Shortcut button for the DocHeader that enables bookmarking of backend module states.
|
||||
*
|
||||
* This button allows users to create bookmarks to specific module views
|
||||
* with their context (e.g., specific page, record, or configuration). The shortcut
|
||||
* preserves the route and arguments for quick access later.
|
||||
*
|
||||
* The button is automatically added to all backend modules by default when shortcut
|
||||
* context is provided. It's positioned on the right side of the button bar as the
|
||||
* last button (group 91).
|
||||
*
|
||||
* Example - Using automatic bookmark button (recommended):
|
||||
*
|
||||
* ```
|
||||
* public function myAction(ServerRequestInterface $request): ResponseInterface
|
||||
* {
|
||||
* $view = $this->moduleTemplateFactory->create($request);
|
||||
*
|
||||
* // Set shortcut context - button is added automatically
|
||||
* $view->getDocHeaderComponent()->setShortcutContext(
|
||||
* routeIdentifier: 'my_module',
|
||||
* displayName: 'My Module: ' . $pageTitle,
|
||||
* arguments: ['id' => $pageId]
|
||||
* );
|
||||
*
|
||||
* return $view->renderResponse('MyTemplate');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Note: Manually creating and adding a ShortcutButton to the button bar is not supported.
|
||||
* It is no longer detected and does not suppress the automatic shortcut button.
|
||||
* Use DocHeaderComponent::setShortcutContext() instead.
|
||||
*/
|
||||
class ShortcutButton implements ButtonInterface, PositionInterface
|
||||
{
|
||||
protected string $routeIdentifier = '';
|
||||
|
||||
protected string $displayName = '';
|
||||
|
||||
/**
|
||||
* @var array List of parameter/value pairs relevant for this bookmark
|
||||
*/
|
||||
protected array $arguments = [];
|
||||
|
||||
protected bool $copyUrlToClipboard = true;
|
||||
|
||||
protected bool $disabled = false;
|
||||
|
||||
public function getRouteIdentifier(): string
|
||||
{
|
||||
return $this->routeIdentifier;
|
||||
}
|
||||
|
||||
public function setRouteIdentifier(string $routeIdentifier): static
|
||||
{
|
||||
$this->routeIdentifier = $routeIdentifier;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return $this->displayName;
|
||||
}
|
||||
|
||||
public function setDisplayName(string $displayName): static
|
||||
{
|
||||
$this->displayName = $displayName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setArguments(array $arguments): static
|
||||
{
|
||||
$this->arguments = $arguments;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines whether the button should be extended to also allow
|
||||
* copying the current URL to the operating systems' clipboard.
|
||||
*/
|
||||
public function setCopyUrlToClipboard(bool $copyUrlToClipboard): static
|
||||
{
|
||||
$this->copyUrlToClipboard = $copyUrlToClipboard;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDisabled(): bool
|
||||
{
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
public function setDisabled(bool $disabled): static
|
||||
{
|
||||
$this->disabled = $disabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): string
|
||||
{
|
||||
return ButtonBar::BUTTON_POSITION_RIGHT;
|
||||
}
|
||||
|
||||
public function getGroup(): int
|
||||
{
|
||||
return 91;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->displayName !== '' && $this->routeExists($this->routeIdentifier);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$canCreateBookmark = $this->getBackendUser()->mayMakeShortcut();
|
||||
// Early return in case the current user is not allowed to create bookmarks.
|
||||
// Note: This is not checked in isValid(), since it only concerns the current
|
||||
// user and does not mean, the button is not configured properly.
|
||||
if (!$canCreateBookmark && !$this->copyUrlToClipboard) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$routeIdentifier = $this->routeIdentifier;
|
||||
$arguments = $this->arguments;
|
||||
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
|
||||
|
||||
// The route parameter is not needed, since this is already provided with the $routeIdentifier
|
||||
unset($arguments['route']);
|
||||
|
||||
// returnUrl should not be stored in the database
|
||||
unset($arguments['returnUrl']);
|
||||
|
||||
// Encode arguments to be stored in the database
|
||||
$encodedArguments = json_encode($arguments) ?: '';
|
||||
|
||||
if ($canCreateBookmark && !$this->copyUrlToClipboard) {
|
||||
// Load the bookmark button custom element
|
||||
$pageRenderer->loadJavaScriptModule('@typo3/backend/bookmark/element/bookmark-button-element.js');
|
||||
|
||||
return $this->renderBookmarkButton($routeIdentifier, $encodedArguments);
|
||||
}
|
||||
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$dropdownItems = [];
|
||||
$componentFactory = GeneralUtility::makeInstance(ComponentFactory::class);
|
||||
|
||||
// Bookmark Button
|
||||
if ($canCreateBookmark) {
|
||||
// Load the bookmark button custom element
|
||||
$pageRenderer->loadJavaScriptModule('@typo3/backend/bookmark/element/bookmark-button-element.js');
|
||||
|
||||
$bookmarkItem = $componentFactory->createDropDownGeneric();
|
||||
$bookmarkItem->setTag('typo3-backend-bookmark-button');
|
||||
$bookmarkItem->setAttributes([
|
||||
'route' => $routeIdentifier,
|
||||
'arguments' => $encodedArguments,
|
||||
'display-name' => $this->displayName,
|
||||
]);
|
||||
$dropdownItems[] = $bookmarkItem;
|
||||
}
|
||||
|
||||
// Clipboard Button
|
||||
if ($this->copyUrlToClipboard) {
|
||||
$pageRenderer->loadJavaScriptModule('@typo3/backend/copy-to-clipboard.js');
|
||||
$clipboardItem = $componentFactory->createDropDownItem();
|
||||
$clipboardItem->setTag('typo3-copy-to-clipboard');
|
||||
$clipboardItem->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.copyCurrentUrl'));
|
||||
$clipboardItem->setAttributes([
|
||||
'text' => (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
|
||||
$routeIdentifier,
|
||||
$arguments,
|
||||
UriBuilder::SHAREABLE_URL
|
||||
),
|
||||
]);
|
||||
$clipboardItem->setIcon($iconFactory->getIcon('actions-link', IconSize::SMALL));
|
||||
$dropdownItems[] = $clipboardItem;
|
||||
}
|
||||
|
||||
$dropdownButton = $componentFactory->createDropDownButton();
|
||||
$dropdownButton->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.share'));
|
||||
$dropdownButton->setIcon($iconFactory->getIcon('actions-share-alt', IconSize::SMALL));
|
||||
$dropdownButton->setDisabled($this->isDisabled());
|
||||
foreach ($dropdownItems as $dropdownItem) {
|
||||
$dropdownButton->addItem($dropdownItem);
|
||||
}
|
||||
|
||||
return (string)$dropdownButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the bookmark button custom element.
|
||||
*/
|
||||
protected function renderBookmarkButton(string $routeIdentifier, string $encodedArguments): string
|
||||
{
|
||||
$attributes = [
|
||||
'class' => 'btn btn-default btn-sm',
|
||||
'route' => $routeIdentifier,
|
||||
'arguments' => $encodedArguments,
|
||||
'display-name' => $this->displayName,
|
||||
'hide-label-text' => 'true',
|
||||
];
|
||||
|
||||
return '<typo3-backend-bookmark-button ' . GeneralUtility::implodeAttributes($attributes, true) . '></typo3-backend-bookmark-button>';
|
||||
}
|
||||
|
||||
protected function routeExists(string $routeIdentifier): bool
|
||||
{
|
||||
return GeneralUtility::makeInstance(Router::class)->hasRoute($routeIdentifier);
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentInterface;
|
||||
|
||||
/**
|
||||
* Interface for buttons in the document header.
|
||||
*
|
||||
* All button types (LinkButton, InputButton, DropDownButton, etc.) must implement
|
||||
* this interface to be added to the ButtonBar.
|
||||
*
|
||||
* This interface extends ComponentInterface, which provides the common contract
|
||||
* for all renderable backend components.
|
||||
*/
|
||||
interface ButtonInterface extends ComponentInterface {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Buttons;
|
||||
|
||||
enum ButtonSize: string
|
||||
{
|
||||
case SMALL = 'btn-sm';
|
||||
case MEDIUM = '';
|
||||
// large does not exist on purpose, see styleguide
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Base class for all dropdown menu items that can be added to DropDownButton or SplitButton.
|
||||
* Provides common functionality for dropdown items including icon handling, labels, links,
|
||||
* custom HTML tags, and active state management.
|
||||
*
|
||||
* Implements DropDownItemInterface, providing validation, type identification, and rendering
|
||||
* capabilities for all dropdown items.
|
||||
*/
|
||||
abstract class AbstractDropDownItem implements \Stringable
|
||||
{
|
||||
/**
|
||||
* HTML tag name for the dropdown item element (e.g., 'a', 'button', custom elements)
|
||||
*/
|
||||
protected string $tag = 'a';
|
||||
|
||||
/**
|
||||
* Optional icon displayed before the label
|
||||
*/
|
||||
protected ?Icon $icon = null;
|
||||
|
||||
/**
|
||||
* Text content/label of the dropdown item
|
||||
*/
|
||||
protected ?string $label = null;
|
||||
|
||||
/**
|
||||
* Tooltip/title attribute (defaults to label if not explicitly set)
|
||||
*/
|
||||
protected ?string $title = null;
|
||||
|
||||
/**
|
||||
* URL/href for link items
|
||||
*/
|
||||
protected ?string $href = null;
|
||||
|
||||
/**
|
||||
* Custom HTML attributes for the element
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $attributes = [];
|
||||
|
||||
/**
|
||||
* Whether this item is currently active/selected
|
||||
*/
|
||||
protected bool $active = false;
|
||||
|
||||
public function setTag(string $tag): static
|
||||
{
|
||||
$this->tag = htmlspecialchars(trim($tag));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
public function getIcon(): ?Icon
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setIcon(?Icon $icon): static
|
||||
{
|
||||
$icon?->setSize(IconSize::SMALL);
|
||||
$this->icon = $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title ?? $this->label;
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHref(): ?string
|
||||
{
|
||||
return $this->href;
|
||||
}
|
||||
|
||||
public function setHref(?string $href): static
|
||||
{
|
||||
$this->href = $href;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function setAttributes(array $attributes): static
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAttribute(string $name, string $value): static
|
||||
{
|
||||
$this->attributes[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function setActive(bool $active): static
|
||||
{
|
||||
$this->active = $active;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->getLabel() !== null && trim($this->getLabel()) !== '';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
protected function getAttributesString(): string
|
||||
{
|
||||
$attributes = $this->getAttributes();
|
||||
$attributes['class'] = rtrim('dropdown-item dropdown-item-spaced ' . ($attributes['class'] ?? ''));
|
||||
if ($this->isActive()) {
|
||||
$attributes['aria-selected'] = 'true';
|
||||
}
|
||||
if ($this->getHref()) {
|
||||
$attributes['href'] = $this->getHref();
|
||||
}
|
||||
if ($this->getTitle()) {
|
||||
$attributes['title'] = $this->getTitle();
|
||||
}
|
||||
|
||||
return GeneralUtility::implodeAttributes($attributes, true);
|
||||
}
|
||||
|
||||
protected function getRenderedIcon(): string
|
||||
{
|
||||
return $this->getIcon()?->render() ?? '';
|
||||
}
|
||||
|
||||
abstract public function render(): string;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
/**
|
||||
* This dropdown item type renders the divider element.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* $item = $this->componentFactory->createDropDownDivider()
|
||||
* $dropDownButton->addItem($item);
|
||||
* ```
|
||||
*/
|
||||
class DropDownDivider implements DropDownItemInterface
|
||||
{
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return '<hr class="dropdown-divider">';
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
/**
|
||||
* Dropdown item with relaxed validation - like DropDownItem but label is optional.
|
||||
*
|
||||
* Use for web components or custom elements that render their own content.
|
||||
*/
|
||||
class DropDownGeneric extends AbstractDropDownItem implements DropDownItemInterface
|
||||
{
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->tag !== '';
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return sprintf(
|
||||
'<%1$s %2$s>%3$s%4$s</%1$s>',
|
||||
$this->getTag(),
|
||||
$this->getAttributesString(),
|
||||
$this->getRenderedIcon(),
|
||||
htmlspecialchars($this->getLabel() ?? '')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
/**
|
||||
* This dropdown item type renders a noninteractive text element
|
||||
* to group items and gives more meaning to a set of options.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* $item = $this->componentFactory->createDropDownHeader()->setLabel('Label');
|
||||
* $dropDownButton->addItem($item);
|
||||
* ```
|
||||
*/
|
||||
class DropDownHeader implements DropDownItemInterface
|
||||
{
|
||||
protected ?string $label = null;
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->getLabel() !== null && trim($this->getLabel()) !== '';
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return '<h6 class="dropdown-header">' . htmlspecialchars(trim($this->getLabel())) . '</h6>';
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
/**
|
||||
* This dropdown item type renders a simple element.
|
||||
* Use this element if you need a link, button.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* $item = $this->componentFactory->createDropDownItem()
|
||||
* ->setTag('a')
|
||||
* ->setHref('#')
|
||||
* ->setLabel('Label')
|
||||
* ->setTitle('Title')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-heart'))
|
||||
* ->setAttributes(['data-value' => '123']);
|
||||
* $dropDownButton->addItem($item);
|
||||
* ```
|
||||
*/
|
||||
class DropDownItem extends AbstractDropDownItem implements DropDownItemInterface
|
||||
{
|
||||
public function render(): string
|
||||
{
|
||||
return sprintf(
|
||||
'<%1$s %2$s>%3$s%4$s</%1$s>',
|
||||
$this->getTag(),
|
||||
$this->getAttributesString(),
|
||||
$this->getIcon()?->render() ?? '',
|
||||
htmlspecialchars($this->getLabel())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentInterface;
|
||||
|
||||
/**
|
||||
* Interface for dropdown items that can be added to a DropDownButton.
|
||||
*
|
||||
* Dropdown items include interactive elements (DropDownItem, DropDownRadio, DropDownToggle)
|
||||
* and structural elements (DropDownHeader, DropDownDivider).
|
||||
*
|
||||
* This interface extends ComponentInterface, which provides the common contract
|
||||
* for all renderable backend components.
|
||||
*/
|
||||
interface DropDownItemInterface extends ComponentInterface {}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This dropdown item type renders an element with an active state.
|
||||
* Use this element to display a radio-like selection of a state.
|
||||
* When set to active, it will show a dot in front of the icon and
|
||||
* text to indicate that this is the current selection.
|
||||
*
|
||||
* At least 2 of these items need to exist within a dropdown button,
|
||||
* so a user has a choice of a state to select.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* $item = $this->componentFactory->createDropDownRadio()
|
||||
* ->setHref('#')
|
||||
* ->setActive(true)
|
||||
* ->setLabel('List')
|
||||
* ->setTitle('List')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-viewmode-list'))
|
||||
* ->setAttributes(['data-type' => 'list']);
|
||||
* $dropDownButton->addItem($item);
|
||||
*
|
||||
* $item = $this->componentFactory->createDropDownRadio()
|
||||
* ->setHref('#')
|
||||
* ->setActive(false)
|
||||
* ->setLabel('Tiles')
|
||||
* ->setTitle('Tiles')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-viewmode-tiles'))
|
||||
* ->setAttributes(['data-type' => 'tiles']);
|
||||
* $dropDownButton->addItem($item);
|
||||
* ```
|
||||
*/
|
||||
class DropDownRadio extends AbstractDropDownItem implements DropDownItemInterface
|
||||
{
|
||||
public function render(): string
|
||||
{
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$statusIcon = $this->isActive()
|
||||
? '<span class="text-primary">' . $iconFactory->getIcon('actions-dot', IconSize::SMALL)->render() . '</span>'
|
||||
: $iconFactory->getIcon('empty-empty', IconSize::SMALL)->render();
|
||||
|
||||
return sprintf(
|
||||
'<%1$s %2$s>%3$s%4$s%5$s</%1$s>',
|
||||
$this->getTag(),
|
||||
$this->getAttributesString(),
|
||||
$statusIcon,
|
||||
$this->getRenderedIcon(),
|
||||
htmlspecialchars($this->getLabel())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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\Buttons\DropDown;
|
||||
|
||||
/**
|
||||
* This dropdown item type renders an element with an active state.
|
||||
* When set to active, it will show a checkmark in front of the icon
|
||||
* and text to indicate the current state.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* $item = $this->componentFactory->createDropDownToggle()
|
||||
* ->setHref('#')
|
||||
* ->setActive(true)
|
||||
* ->setLabel('Label')
|
||||
* ->setTitle('Title')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-heart'))
|
||||
* ->setAttributes(['data-value' => '123']);
|
||||
* $dropDownButton->addItem($item);
|
||||
* ```
|
||||
*/
|
||||
class DropDownToggle extends AbstractDropDownItem implements DropDownItemInterface
|
||||
{
|
||||
public function render(): string
|
||||
{
|
||||
// Status Icon
|
||||
$this->setAttribute('data-dropdowntoggle-status', $this->isActive() ? 'active' : 'inactive');
|
||||
return sprintf(
|
||||
'<%1$s %2$s><span class="dropdown-item-status"></span>%3$s%4$s</%1$s>',
|
||||
$this->getTag(),
|
||||
$this->getAttributesString(),
|
||||
$this->getRenderedIcon(),
|
||||
htmlspecialchars($this->getLabel())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownItemInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownRadio;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This button type is a container for dropdown items.
|
||||
* It will render a dropdown containing all items attached
|
||||
* to it. There are different kinds available, each item
|
||||
* needs to implement the DropDownItemInterface. When this
|
||||
* type contains elements of type DropDownRadio it will use
|
||||
* the icon of the first active item of this type.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
* $dropDownButton = $this->componentFactory->createDropDownButton()
|
||||
* ->setLabel('Dropdown')
|
||||
* ->setTitle('Save')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-heart'))
|
||||
* ->setShowLabelText(true)
|
||||
* ->addItem(
|
||||
* $this->componentFactory->createDropDownItem()
|
||||
* ->setLabel('Item')
|
||||
* ->setHref('#')
|
||||
* );
|
||||
* $buttonBar->addButton($dropDownButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class DropDownButton implements ButtonInterface
|
||||
{
|
||||
protected ?Icon $icon = null;
|
||||
protected string $label = '';
|
||||
protected ?string $title = null;
|
||||
protected array $items = [];
|
||||
protected bool $showLabelText = false;
|
||||
protected bool $showActiveLabelText = false;
|
||||
protected bool $disabled = false;
|
||||
protected ButtonSize $size = ButtonSize::SMALL;
|
||||
|
||||
public function getIcon(): ?Icon
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setIcon(?Icon $icon): static
|
||||
{
|
||||
$icon?->setSize(IconSize::SMALL);
|
||||
$this->icon = $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title ?? $this->label;
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getShowLabelText(): bool
|
||||
{
|
||||
return $this->showLabelText;
|
||||
}
|
||||
|
||||
public function setShowLabelText(bool $showLabelText): static
|
||||
{
|
||||
$this->showLabelText = $showLabelText;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getShowActiveLabelText(): bool
|
||||
{
|
||||
return $this->showActiveLabelText;
|
||||
}
|
||||
|
||||
public function setShowActiveLabelText(bool $showActiveLabelText): static
|
||||
{
|
||||
$this->showActiveLabelText = $showActiveLabelText;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDisabled(): bool
|
||||
{
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
public function setDisabled(bool $disabled): static
|
||||
{
|
||||
$this->disabled = $disabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addItem(DropDownItemInterface $item): static
|
||||
{
|
||||
if (!$item->isValid()) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Only valid items may be assigned to a DropdownButton. "'
|
||||
. $item->getType()
|
||||
. '" did not pass validation',
|
||||
1667645426
|
||||
);
|
||||
}
|
||||
|
||||
$this->items[] = clone $item;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSize(): ButtonSize
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function setSize(ButtonSize $size): DropDownButton
|
||||
{
|
||||
$this->size = $size;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DropDownItemInterface[]
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return !empty($this->getLabel())
|
||||
&& ($this->getShowLabelText() || $this->getIcon())
|
||||
&& !empty($this->getItems());
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$items = $this->getItems();
|
||||
|
||||
/**
|
||||
* @var DropDownRadio|null $activeItem
|
||||
*/
|
||||
$activeItem = null;
|
||||
/**
|
||||
* @var DropDownRadio[] $activeItems
|
||||
*/
|
||||
$activeItems = array_filter($items, static function (DropDownItemInterface $item): bool {
|
||||
return $item instanceof DropDownRadio && $item->isActive();
|
||||
});
|
||||
if (!empty($activeItems)) {
|
||||
$activeItem = array_shift($activeItems);
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'type' => 'button',
|
||||
'class' => 'btn ' . $this->getSize()->value . ' btn-default dropdown-toggle',
|
||||
'data-bs-toggle' => 'dropdown',
|
||||
'aria-expanded' => 'false',
|
||||
];
|
||||
if ($this->isDisabled()) {
|
||||
$attributes['disabled'] = 'disabled';
|
||||
}
|
||||
|
||||
$buttonLabel = '';
|
||||
if ($this->getShowLabelText()) {
|
||||
if ($activeItem !== null && $this->getShowActiveLabelText()) {
|
||||
// Render label with visually-hidden span for screen readers
|
||||
$buttonLabel = '<span class="visually-hidden">' . htmlspecialchars($this->getLabel()) . ':</span> ' . htmlspecialchars($activeItem->getLabel());
|
||||
} else {
|
||||
// No active item or showActiveLabelText is false, just render the label
|
||||
$buttonLabel = htmlspecialchars($this->getLabel());
|
||||
}
|
||||
} else {
|
||||
// Add aria-label and title for accessibility when label text is not shown
|
||||
// Both serve different purposes: aria-label for screen readers, title for visual tooltip
|
||||
if ($activeItem !== null && $this->getShowActiveLabelText()) {
|
||||
$attributes['aria-label'] = htmlspecialchars($this->getTitle() . ': ' . $activeItem->getLabel());
|
||||
$attributes['title'] = htmlspecialchars($this->getTitle() . ': ' . $activeItem->getLabel());
|
||||
} else {
|
||||
$attributes['aria-label'] = $this->getTitle();
|
||||
$attributes['title'] = $this->getTitle();
|
||||
}
|
||||
}
|
||||
|
||||
$icon = $activeItem?->getIcon()?->render() ?? $this->getIcon()?->render() ?? '';
|
||||
$buttonContent = $icon . ($icon !== '' && $buttonLabel !== '' ? ' ' : '') . $buttonLabel;
|
||||
|
||||
return sprintf(
|
||||
'<div class="btn-group"><button %s>%s</button><ul class="dropdown-menu">%s</ul></div>',
|
||||
GeneralUtility::implodeAttributes($attributes, true),
|
||||
$buttonContent,
|
||||
implode('', array_map(static fn(DropDownItemInterface $item) => '<li>' . $item->render() . '</li>', $items))
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?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\Buttons;
|
||||
|
||||
/**
|
||||
* This button type is an intermediate solution for buttons that are rendered
|
||||
* by methods from TYPO3 itself, like the CSH buttons or Bookmark buttons.
|
||||
*
|
||||
* There should be no need to use them, so do yourself a favour and don't.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
* $myButton = $this->componentFactory->createFullyRenderedButton()
|
||||
* ->setHtmlSource('<span class="i-should-not-be-using-this>Foo</span>');
|
||||
* $buttonBar->addButton($myButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class FullyRenderedButton implements ButtonInterface
|
||||
{
|
||||
/**
|
||||
* The full HTML source of the rendered button.
|
||||
* This source will be passed through to the frontend as is, so keep htmlspecialchars() in mind.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $htmlSource = '';
|
||||
|
||||
public function getHtmlSource(): string
|
||||
{
|
||||
return $this->htmlSource;
|
||||
}
|
||||
|
||||
public function setHtmlSource(string $htmlSource): static
|
||||
{
|
||||
$this->htmlSource = $htmlSource;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return trim($this->getHtmlSource()) !== '' && $this->getType() === static::class;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return '<span class="btn btn-sm btn-default">' . $this->getHtmlSource() . '</span>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A flexible button type that allows rendering any HTML tag (button, anchor, or custom
|
||||
* web components) with full control over attributes. Unlike other button types that
|
||||
* extend AbstractButton, GenericButton implements ButtonInterface directly, providing
|
||||
* maximum flexibility for custom implementations.
|
||||
*
|
||||
* Use cases:
|
||||
* - Custom web components (e.g., <typo3-custom-element>)
|
||||
* - Standard buttons with custom attributes
|
||||
* - Links with button styling
|
||||
* - Any HTML element that should appear as a button in the document header
|
||||
*
|
||||
* Key features:
|
||||
* - Configurable HTML tag (button, a, custom elements)
|
||||
* - Full attribute control (href, data attributes, etc.)
|
||||
* - Icon and label support
|
||||
* - Automatic HTML escaping for security
|
||||
*
|
||||
* Example - Standard button:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
* $button = $this->componentFactory->createGenericButton()
|
||||
* ->setTag('button')
|
||||
* ->setLabel('Save')
|
||||
* ->setTitle('Save changes')
|
||||
* ->setIcon($iconFactory->getIcon('actions-save'))
|
||||
* ->setShowLabelText(true)
|
||||
* ->setAttributes(['type' => 'submit', 'form' => 'myForm']);
|
||||
* $buttonBar->addButton($button);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Example - Web component:
|
||||
*
|
||||
* ```
|
||||
* $button = $this->componentFactory->createGenericButton()
|
||||
* ->setTag('typo3-my-action-button')
|
||||
* ->setLabel('Custom Action')
|
||||
* ->setIcon($iconFactory->getIcon('actions-heart'))
|
||||
* ->setAttributes([
|
||||
* 'url' => '/my-action',
|
||||
* 'data-value' => '123'
|
||||
* ]);
|
||||
* $buttonBar->addButton($button);
|
||||
* ```
|
||||
*
|
||||
* Example - Link styled as button:
|
||||
*
|
||||
* ```
|
||||
* $button = $this->componentFactory->createGenericButton()
|
||||
* ->setTag('a')
|
||||
* ->setHref('/target-page')
|
||||
* ->setLabel('View Page')
|
||||
* ->setTitle('Open target page')
|
||||
* ->setIcon($iconFactory->getIcon('actions-view'));
|
||||
* $buttonBar->addButton($button);
|
||||
* ```
|
||||
*/
|
||||
class GenericButton implements ButtonInterface
|
||||
{
|
||||
protected string $tag = 'button';
|
||||
protected ?Icon $icon = null;
|
||||
protected string $label = '';
|
||||
protected ?string $title = null;
|
||||
protected ?string $href = null;
|
||||
protected string $classes = '';
|
||||
protected ButtonSize $size = ButtonSize::SMALL;
|
||||
protected array $attributes = [];
|
||||
protected bool $showLabelText = false;
|
||||
|
||||
public function setTag(string $tag): static
|
||||
{
|
||||
$this->tag = htmlspecialchars(trim($tag));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
public function getIcon(): ?Icon
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setIcon(?Icon $icon): static
|
||||
{
|
||||
$icon?->setSize(IconSize::SMALL);
|
||||
$this->icon = $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title ?? $this->label;
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHref(): ?string
|
||||
{
|
||||
return $this->href;
|
||||
}
|
||||
|
||||
public function setHref(?string $href): static
|
||||
{
|
||||
$this->href = $href;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClasses(): string
|
||||
{
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
public function setClasses(string $classes): static
|
||||
{
|
||||
$this->classes = $classes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSize(): ButtonSize
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function setSize(ButtonSize $size): static
|
||||
{
|
||||
$this->size = $size;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function setAttributes(array $attributes): static
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function getShowLabelText(): bool
|
||||
{
|
||||
return $this->showLabelText;
|
||||
}
|
||||
|
||||
public function setShowLabelText(bool $showLabelText): static
|
||||
{
|
||||
$this->showLabelText = $showLabelText;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return trim($this->getLabel()) !== ''
|
||||
&& $this->getType() === static::class
|
||||
&& $this->getIcon() !== null;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
protected function getAttributesString(): string
|
||||
{
|
||||
$attributes = $this->getAttributes();
|
||||
$attributes['class'] = rtrim('btn ' . $this->getSize()->value . ' btn-default ' . $this->getClasses());
|
||||
if ($this->getHref()) {
|
||||
$attributes['href'] = $this->getHref();
|
||||
}
|
||||
if ($this->getTitle()) {
|
||||
$attributes['title'] = $this->getTitle();
|
||||
}
|
||||
|
||||
return GeneralUtility::implodeAttributes($attributes, true);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return sprintf(
|
||||
'<%1$s %2$s>%3$s%4$s</%1$s>',
|
||||
$this->getTag(),
|
||||
$this->getAttributesString(),
|
||||
$this->getIcon()?->render() ?? '',
|
||||
htmlspecialchars($this->getShowLabelText() ? $this->getLabel() : '')
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This button type renders a HTML tag <button> and takes the HTML attributes
|
||||
* name and value as additional attributes to those defined in AbstractButton.
|
||||
*
|
||||
* Since we no longer want to have any <input type="submit" /> in the TYPO3 core
|
||||
* you should use this button type to send forms
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
* $saveButton = $this->componentFactory->createInputButton()
|
||||
* ->setName('save')
|
||||
* ->setValue('1')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL))
|
||||
* ->setTitle('Save');
|
||||
* $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class InputButton extends AbstractButton
|
||||
{
|
||||
protected string $name = '';
|
||||
protected string $value = '';
|
||||
protected string $form = '';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(string $value): static
|
||||
{
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getForm(): string
|
||||
{
|
||||
return $this->form;
|
||||
}
|
||||
|
||||
public function setForm(string $form): static
|
||||
{
|
||||
$this->form = $form;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return trim($this->getName()) !== ''
|
||||
&& trim($this->getValue()) !== ''
|
||||
&& trim($this->getTitle()) !== ''
|
||||
&& $this->getType() === static::class
|
||||
&& $this->getIcon() !== null;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$attributes = [
|
||||
'name' => $this->getName(),
|
||||
'class' => 'btn btn-sm btn-default ' . $this->getClasses(),
|
||||
'value' => $this->getValue(),
|
||||
'title' => $this->getTitle(),
|
||||
'form' => trim($this->getForm()),
|
||||
];
|
||||
if ($this->isDisabled()) {
|
||||
$attributes['disabled'] = 'disabled';
|
||||
}
|
||||
$labelText = '';
|
||||
if ($this->showLabelText) {
|
||||
$labelText = ' ' . $this->title;
|
||||
}
|
||||
foreach ($this->attributes as $attributeName => $attributeValue) {
|
||||
$attributes[$attributeName] = $attributeValue;
|
||||
}
|
||||
foreach ($this->dataAttributes as $attributeName => $attributeValue) {
|
||||
$attributes['data-' . $attributeName] = $attributeValue;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<button %s>%s%s</button>',
|
||||
GeneralUtility::implodeAttributes($attributes, true),
|
||||
$this->getIcon()?->render() ?? '',
|
||||
htmlspecialchars($labelText)
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Backend\Context\PageContext;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
|
||||
/**
|
||||
* Builds the language selector dropdown for backend modules (Page, List).
|
||||
*
|
||||
* This centralizes the language selection UI logic to ensure consistent behavior
|
||||
* across all modules. It handles both single-select (radio buttons) and multi-select
|
||||
* (toggles/checkboxes) modes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class LanguageSelectorBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private ComponentFactory $componentFactory,
|
||||
private IconFactory $iconFactory,
|
||||
) {}
|
||||
|
||||
public function build(
|
||||
PageContext $pageContext,
|
||||
LanguageSelectorMode $mode,
|
||||
\Closure $urlBuilder,
|
||||
bool $showToggleAll = true,
|
||||
): ButtonInterface {
|
||||
$languageService = $this->getLanguageService();
|
||||
$languageInfo = $pageContext->languageInformation;
|
||||
$selectedLanguages = $pageContext->selectedLanguageIds;
|
||||
$availableLanguages = $languageInfo->availableLanguages;
|
||||
|
||||
// Calculate selector label and icon
|
||||
$selectorIcon = null;
|
||||
$selectedExistingLanguages = array_intersect($selectedLanguages, $languageInfo->getAllExistingLanguageIds());
|
||||
|
||||
if (count($selectedExistingLanguages) === 1 && reset($selectedExistingLanguages) === 0) {
|
||||
// Only default language exists/selected - show the language title and flag
|
||||
$defaultLanguage = $availableLanguages[0] ?? null;
|
||||
if ($defaultLanguage) {
|
||||
$selectorLabel = $defaultLanguage->getTitle();
|
||||
$selectorIcon = $this->iconFactory->getIcon($defaultLanguage->getFlagIdentifier());
|
||||
} else {
|
||||
$selectorLabel = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.language');
|
||||
}
|
||||
} else {
|
||||
// Multiple languages being shown
|
||||
$displayedLanguageCount = count($selectedExistingLanguages);
|
||||
// In comparison mode, default language (0) is always shown
|
||||
// If it's not in the selected list, it was auto-added, so increment the count
|
||||
if (!in_array(0, $selectedExistingLanguages, true)) {
|
||||
$displayedLanguageCount++;
|
||||
}
|
||||
$selectorLabel = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.languages') . ' (' . $displayedLanguageCount . ')';
|
||||
$selectorIcon = $this->iconFactory->getIcon('flags-multiple');
|
||||
}
|
||||
|
||||
$languageDropDownButton = $this->componentFactory->createDropDownButton()
|
||||
->setLabel($selectorLabel)
|
||||
->setShowActiveLabelText(true)
|
||||
->setShowLabelText(true);
|
||||
|
||||
if ($selectorIcon !== null) {
|
||||
$languageDropDownButton->setIcon($selectorIcon);
|
||||
}
|
||||
|
||||
$defaultLanguageItem = null;
|
||||
$existingLanguageItems = [];
|
||||
$newLanguageItems = [];
|
||||
|
||||
foreach ($languageInfo->languageItems as $languageItem) {
|
||||
if (!$languageItem->isAvailable()) {
|
||||
// Skip unavailable languages (no permission to create)
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($languageItem->isCreatable()) {
|
||||
// Language doesn't exist yet - show "Create translation" button
|
||||
$createTranslationHelpText = sprintf(
|
||||
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createTranslationFor'),
|
||||
$languageItem->getTitle()
|
||||
);
|
||||
$item = $this->componentFactory->createDropDownItem()
|
||||
->setTag('typo3-backend-localization-button')
|
||||
->setIcon($this->iconFactory->getIcon($languageItem->getFlagIdentifier()))
|
||||
->setLabel($languageItem->getTitle())
|
||||
->setTitle($createTranslationHelpText)
|
||||
->setAttributes([
|
||||
'record-type' => 'pages',
|
||||
'record-uid' => (string)$pageContext->pageId,
|
||||
'target-language' => (string)$languageItem->getLanguageId(),
|
||||
'aria-label' => $createTranslationHelpText,
|
||||
]);
|
||||
$newLanguageItems[] = $item;
|
||||
} else {
|
||||
// Language exists - build selection item
|
||||
if ($mode === LanguageSelectorMode::MULTI_SELECT) {
|
||||
// Multi-select mode: Use toggles/checkboxes
|
||||
if ($languageItem->getLanguageId() === 0) {
|
||||
// Default language is always selected and disabled in multi-select
|
||||
// Store separately to add divider after it
|
||||
// Add accessibility attributes to explain why it's disabled
|
||||
$defaultLanguageHelpText = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.defaultLanguageAlwaysShown');
|
||||
$defaultLanguageItem = $this->componentFactory->createDropDownToggle()
|
||||
->setActive(true)
|
||||
->setIcon($this->iconFactory->getIcon($languageItem->getFlagIdentifier()))
|
||||
->setHref('#')
|
||||
->setLabel($languageItem->getTitle())
|
||||
->setTitle($defaultLanguageHelpText)
|
||||
->setAttributes([
|
||||
'disabled' => 'disabled',
|
||||
'aria-label' => $languageItem->getTitle() . ' (' . $defaultLanguageHelpText . ')',
|
||||
]);
|
||||
// Don't add to $existingLanguageItems - will be added separately with divider
|
||||
continue;
|
||||
}
|
||||
|
||||
$isSelected = in_array($languageItem->getLanguageId(), $selectedLanguages, true);
|
||||
$newSelectedLanguages = $selectedLanguages;
|
||||
if ($isSelected) {
|
||||
// Deselect this language
|
||||
$newSelectedLanguages = array_values(array_diff($newSelectedLanguages, [$languageItem->getLanguageId()]));
|
||||
// Ensure default language (0) is always present
|
||||
if (!in_array(0, $newSelectedLanguages, true)) {
|
||||
array_unshift($newSelectedLanguages, 0);
|
||||
}
|
||||
} else {
|
||||
// Select this language
|
||||
$newSelectedLanguages = array_unique(array_merge($newSelectedLanguages, [$languageItem->getLanguageId()]));
|
||||
}
|
||||
$item = $this->componentFactory->createDropDownToggle()
|
||||
->setActive($isSelected)
|
||||
->setIcon($this->iconFactory->getIcon($languageItem->getFlagIdentifier()))
|
||||
->setHref($urlBuilder($newSelectedLanguages))
|
||||
->setLabel($languageItem->getTitle());
|
||||
} else {
|
||||
// Single-select mode: Use radio buttons
|
||||
$item = $this->componentFactory->createDropDownRadio()
|
||||
->setActive($languageItem->getLanguageId() === $pageContext->getPrimaryLanguageId())
|
||||
->setIcon($this->iconFactory->getIcon($languageItem->getFlagIdentifier()))
|
||||
->setHref($urlBuilder([$languageItem->getLanguageId()]))
|
||||
->setLabel($languageItem->getTitle());
|
||||
}
|
||||
$existingLanguageItems[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// Add default language first (if in multi-select mode)
|
||||
if ($defaultLanguageItem !== null) {
|
||||
$languageDropDownButton->addItem($defaultLanguageItem);
|
||||
// Add divider after default language to visually separate it from other languages
|
||||
if (!empty($existingLanguageItems)) {
|
||||
$languageDropDownButton->addItem($this->componentFactory->createDropDownDivider());
|
||||
}
|
||||
}
|
||||
|
||||
// Add other existing languages
|
||||
foreach ($existingLanguageItems as $existingItem) {
|
||||
$languageDropDownButton->addItem($existingItem);
|
||||
}
|
||||
|
||||
// Add "Toggle All" option for multi-select mode AFTER the language list
|
||||
// (so users see what they're toggling before the action)
|
||||
if ($mode === LanguageSelectorMode::MULTI_SELECT && $showToggleAll && !empty($languageInfo->existingTranslations)) {
|
||||
$languageDropDownButton->addItem($this->componentFactory->createDropDownDivider());
|
||||
|
||||
$areAllSelected = empty(array_diff(array_keys($languageInfo->existingTranslations), array_diff($selectedLanguages, [0])));
|
||||
$toggleUrl = $urlBuilder($areAllSelected ? [0] : $languageInfo->getAllExistingLanguageIds());
|
||||
|
||||
// Count how many languages can be toggled (excluding default)
|
||||
$toggleableCount = count($languageInfo->existingTranslations);
|
||||
|
||||
if ($areAllSelected) {
|
||||
$toggleLabel = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll') . ' (' . $toggleableCount . ')';
|
||||
$toggleIcon = 'actions-selection-elements-none';
|
||||
$toggleTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deselectAllLanguages');
|
||||
} else {
|
||||
$toggleLabel = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll') . ' (' . $toggleableCount . ')';
|
||||
$toggleIcon = 'actions-selection-elements-all';
|
||||
$toggleTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selectAllLanguages');
|
||||
}
|
||||
|
||||
$toggleItem = $this->componentFactory->createDropDownItem()
|
||||
->setIcon($this->iconFactory->getIcon($toggleIcon))
|
||||
->setHref($toggleUrl)
|
||||
->setLabel($toggleLabel)
|
||||
->setTitle($toggleTitle)
|
||||
->setAttributes([
|
||||
'aria-label' => $toggleTitle,
|
||||
]);
|
||||
$languageDropDownButton->addItem($toggleItem);
|
||||
}
|
||||
|
||||
// Add separator and new languages if any with stronger visual separation
|
||||
if (!empty($newLanguageItems)) {
|
||||
$languageDropDownButton->addItem($this->componentFactory->createDropDownDivider());
|
||||
$languageDropDownButton->addItem(
|
||||
$this->componentFactory->createDropDownHeader()
|
||||
->setLabel($languageService->sL('core.core:labels.new_page_translation'))
|
||||
);
|
||||
foreach ($newLanguageItems as $item) {
|
||||
$languageDropDownButton->addItem($item);
|
||||
}
|
||||
}
|
||||
|
||||
return $languageDropDownButton;
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Buttons;
|
||||
|
||||
/**
|
||||
* Defines how the language selector dropdown should behave.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
enum LanguageSelectorMode
|
||||
{
|
||||
/**
|
||||
* Single language selection using radio buttons.
|
||||
* Used in Page Module's "Layout" view where only one language is displayed at a time.
|
||||
*/
|
||||
case SINGLE_SELECT;
|
||||
|
||||
/**
|
||||
* Multiple language selection using checkboxes/toggles.
|
||||
* Used in Page Module's "Comparison" view and records module where multiple languages can be viewed simultaneously.
|
||||
*/
|
||||
case MULTI_SELECT;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This button type renders a regular anchor tag with TYPO3s way to render a
|
||||
* button control.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
* $saveButton = $this->componentFactory->createLinkButton()
|
||||
* ->setHref('#')
|
||||
* ->setDataAttributes([
|
||||
* 'foo' => 'bar'
|
||||
* ])
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL))
|
||||
* ->setTitle('Save');
|
||||
* $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class LinkButton extends AbstractButton
|
||||
{
|
||||
protected string $href = '';
|
||||
|
||||
protected string $role = 'button';
|
||||
|
||||
protected ButtonSize $size = ButtonSize::SMALL;
|
||||
|
||||
public function getHref(): string
|
||||
{
|
||||
return $this->href;
|
||||
}
|
||||
|
||||
public function setHref(string $href): static
|
||||
{
|
||||
$this->href = $href;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRole(): string
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
public function setRole(string $role): static
|
||||
{
|
||||
$this->role = $role;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSize(): ButtonSize
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function setSize(ButtonSize $size): static
|
||||
{
|
||||
$this->size = $size;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return trim($this->getHref()) !== ''
|
||||
&& trim($this->getTitle()) !== ''
|
||||
&& $this->getType() === static::class
|
||||
&& $this->getIcon() !== null;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$attributes = [
|
||||
'role' => $this->getRole(),
|
||||
'href' => $this->getHref(),
|
||||
// @see SplitButton - hard-coded replacement for this hard-coded class-list
|
||||
'class' => 'btn ' . $this->getSize()->value . ' btn-default ' . $this->getClasses(),
|
||||
'title' => $this->getTitle(),
|
||||
];
|
||||
$labelText = '';
|
||||
if ($this->showLabelText) {
|
||||
$labelText = ' ' . $this->title;
|
||||
}
|
||||
foreach ($this->attributes as $attributeName => $attributeValue) {
|
||||
$attributes[$attributeName] = $attributeValue;
|
||||
}
|
||||
foreach ($this->dataAttributes as $attributeName => $attributeValue) {
|
||||
$attributes['data-' . $attributeName] = $attributeValue;
|
||||
}
|
||||
if ($this->isDisabled()) {
|
||||
$attributes['aria-disabled'] = 'true';
|
||||
$attributes['class'] .= ' disabled';
|
||||
}
|
||||
return sprintf(
|
||||
'<a %s>%s%s</a>',
|
||||
GeneralUtility::implodeAttributes($attributes, true),
|
||||
$this->getIcon()?->render() ?? '',
|
||||
htmlspecialchars($labelText),
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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\Buttons;
|
||||
|
||||
/**
|
||||
* Interface for buttons that define their own fixed position and group.
|
||||
*
|
||||
* Buttons implementing this interface will automatically override the position
|
||||
* and group parameters passed to ButtonBar::addButton(), ensuring they always
|
||||
* appear in their designated location regardless of what the developer specifies.
|
||||
*
|
||||
* This is useful for buttons that should always appear in a consistent location
|
||||
* across the backend, such as the ShortcutButton which always appears in the
|
||||
* top right corner.
|
||||
*
|
||||
* Example implementation:
|
||||
*
|
||||
* ```
|
||||
* class MyButton implements ButtonInterface, PositionInterface
|
||||
* {
|
||||
* public function getPosition(): string
|
||||
* {
|
||||
* return ButtonBar::BUTTON_POSITION_RIGHT;
|
||||
* }
|
||||
*
|
||||
* public function getGroup(): int
|
||||
* {
|
||||
* return 90;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```
|
||||
* $button = $buttonBar->makeMyButton();
|
||||
* // Position and group are ignored - button defines its own
|
||||
* $buttonBar->addButton($button);
|
||||
* ```
|
||||
*/
|
||||
interface PositionInterface
|
||||
{
|
||||
/**
|
||||
* Returns the position where this button should be rendered.
|
||||
*
|
||||
* @return string Either ButtonBar::BUTTON_POSITION_LEFT or ButtonBar::BUTTON_POSITION_RIGHT
|
||||
*/
|
||||
public function getPosition(): string;
|
||||
|
||||
/**
|
||||
* Returns the group number for this button.
|
||||
*
|
||||
* Groups determine the visual grouping and order of buttons within a position.
|
||||
* Lower numbers appear first.
|
||||
*
|
||||
* @return int The group number (e.g., 1, 10, 90, 91)
|
||||
*/
|
||||
public function getGroup(): int;
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
<?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\Buttons;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This button type renders a bootstrap split button.
|
||||
* It takes multiple button objects as parameters.
|
||||
*
|
||||
* The button objects must contain at least one primary
|
||||
* button that is displayed as the main icon, and all other
|
||||
* items will be revealed within a dropdown.
|
||||
*
|
||||
* If a button is of Type "LinkButton" it will not utilize a
|
||||
* HTML `<button>` tag, but instead use `<a>`.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
*
|
||||
* $saveButton = $this->componentFactory->createInputButton()
|
||||
* ->setName('save')
|
||||
* ->setValue('1')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL))
|
||||
* ->setTitle('Save');
|
||||
*
|
||||
* $saveAndCloseButton = $this->componentFactory->createInputButton()
|
||||
* ->setName('save_and_close')
|
||||
* ->setValue('1')
|
||||
* ->setTitle('Save and close')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-document-save-close', IconSize::SMALL));
|
||||
*
|
||||
* $saveAndShowPageButton = $this->componentFactory->createInputButton()
|
||||
* ->setName('save_and_show')
|
||||
* ->setValue('1')
|
||||
* ->setTitle('Save and show')
|
||||
* ->setIcon($this->iconFactory->getIcon('actions-document-save-view', IconSize::SMALL));
|
||||
*
|
||||
* $moduleLink = $this->componentFactory->createLinkButton()
|
||||
* ->setHref((string)$this->uriBuilder->buildUriFromRoute('file_edit', $parameter))
|
||||
* ->setDataAttributes(['customAttribute' => 'customValue'])
|
||||
* ->setShowLabelText(true)
|
||||
* ->setTitle('Edit file')
|
||||
* ->setIcon($this->iconFactory->getIcon('file-edit', IconSize::SMALL));
|
||||
*
|
||||
* $splitButtonElement = $this->componentFactory->createSplitButton()
|
||||
* ->addItem($saveButton, true)
|
||||
* ->addItem($saveAndCloseButton)
|
||||
* ->addItem($moduleLink)
|
||||
* ->addItem($saveAndShowPageButton);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class SplitButton extends AbstractButton
|
||||
{
|
||||
/**
|
||||
* Internal var that determines whether the split button has received any primary actions yet
|
||||
*/
|
||||
protected bool $containsPrimaryAction = false;
|
||||
|
||||
/**
|
||||
* Primary action button
|
||||
*/
|
||||
protected ?AbstractButton $primary = null;
|
||||
|
||||
/**
|
||||
* Array of option buttons for the dropdown
|
||||
*
|
||||
* @var AbstractButton[]
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* Adds an instance of any button to the split button
|
||||
*
|
||||
* @param AbstractButton $item ButtonObject to add
|
||||
* @param bool $primaryAction Is the button the primary action?
|
||||
*
|
||||
* @throws \InvalidArgumentException In case a button is not valid
|
||||
*/
|
||||
public function addItem(AbstractButton $item, bool $primaryAction = false): static
|
||||
{
|
||||
if (!$item->isValid()) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Only valid items may be assigned to a split Button. "'
|
||||
. $item->getType()
|
||||
. '" did not pass validation',
|
||||
1441706330
|
||||
);
|
||||
}
|
||||
if ($primaryAction && $this->containsPrimaryAction) {
|
||||
throw new \InvalidArgumentException('A splitButton may only contain one primary action', 1441706340);
|
||||
}
|
||||
if ($primaryAction) {
|
||||
$this->containsPrimaryAction = true;
|
||||
$this->primary = clone $item;
|
||||
} else {
|
||||
$this->options[] = clone $item;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the split button items as a typed DTO.
|
||||
*
|
||||
* If no primary action was explicitly set, the first option button
|
||||
* becomes the primary action.
|
||||
*
|
||||
* @return SplitButtonItems The typed container with primary and option buttons
|
||||
*/
|
||||
public function getItems(): SplitButtonItems
|
||||
{
|
||||
$primary = $this->primary;
|
||||
$options = $this->options;
|
||||
|
||||
// If no primary action was set, use the first option as primary or thrown an exception if no option exists
|
||||
if ($primary === null) {
|
||||
if (count($options) > 0) {
|
||||
$primary = array_shift($options);
|
||||
} else {
|
||||
throw new \RuntimeException('Split button requires at least one button', 1761311538);
|
||||
}
|
||||
}
|
||||
|
||||
return new SplitButtonItems(primary: $primary, options: $options);
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
try {
|
||||
return $this->getItems()->isValid();
|
||||
} catch (\RuntimeException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML markup of the button
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
$items = $this->getItems();
|
||||
$primary = $items->primary;
|
||||
$options = $items->options;
|
||||
|
||||
$attributes = [
|
||||
'class' => 'btn btn-sm btn-default ' . $primary->getClasses(),
|
||||
];
|
||||
if (method_exists($primary, 'getName')) {
|
||||
$attributes['name'] = $primary->getName();
|
||||
}
|
||||
if (method_exists($primary, 'getValue')) {
|
||||
$attributes['value'] = $primary->getValue();
|
||||
}
|
||||
if (method_exists($primary, 'getForm') && !empty($primary->getForm())) {
|
||||
$attributes['form'] = $primary->getForm();
|
||||
}
|
||||
if ($primary->getAttributes() !== []) {
|
||||
foreach ($primary->getAttributes() as $attributeName => $attributeValue) {
|
||||
$attributes[$attributeName] = $attributeValue;
|
||||
}
|
||||
}
|
||||
if ($primary->getDataAttributes() !== []) {
|
||||
foreach ($primary->getDataAttributes() as $attributeName => $attributeValue) {
|
||||
$attributes['data-' . $attributeName] = $attributeValue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($primary instanceof LinkButton) {
|
||||
// This is needed because the LinkButton can NOT use its ->render() method,
|
||||
// as we want to stick our icon in the result HTML.
|
||||
$attributes['href'] = $primary->getHref();
|
||||
$attributes['role'] = $primary->getRole();
|
||||
$attributes['title'] = $primary->getTitle();
|
||||
}
|
||||
|
||||
$attributesString = GeneralUtility::implodeAttributes($attributes, true);
|
||||
|
||||
if ($primary instanceof LinkButton) {
|
||||
$primaryButtonHTML = '<a ' . $attributesString . '>
|
||||
' . ($primary->getIcon()?->render('inline') ?? '') . '
|
||||
' . htmlspecialchars($primary->getTitle()) . '
|
||||
</a>';
|
||||
} else {
|
||||
$primaryButtonHTML = '<button ' . $attributesString . ' type="submit">
|
||||
' . ($primary->getIcon()?->render('inline') ?? '') . '
|
||||
' . htmlspecialchars($primary->getTitle()) . '
|
||||
</button>';
|
||||
}
|
||||
|
||||
$content = '
|
||||
<div class="btn-group t3js-splitbutton">
|
||||
' . $primaryButtonHTML . '
|
||||
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">';
|
||||
|
||||
foreach ($options as $option) {
|
||||
if ($option instanceof InputButton) {
|
||||
// if the option is an InputButton we have to create a custom rendering
|
||||
$optionAttributes = [
|
||||
'href' => '#',
|
||||
'data-name' => $option->getName(),
|
||||
'data-value' => $option->getValue(),
|
||||
'data-form' => $option->getForm(),
|
||||
];
|
||||
|
||||
if (!empty($option->getClasses())) {
|
||||
$optionAttributes['class'] = $option->getClasses();
|
||||
}
|
||||
$optionAttributes['class'] = implode(' ', [$optionAttributes['class'] ?? '', 'dropdown-item']);
|
||||
$optionAttributesString = '';
|
||||
foreach ($optionAttributes as $key => $value) {
|
||||
$optionAttributesString .= ' ' . htmlspecialchars($key) . '="' . htmlspecialchars($value) . '"';
|
||||
}
|
||||
$html
|
||||
= '<a' . $optionAttributesString . '>'
|
||||
. '<span class="dropdown-item-columns">'
|
||||
. '<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">'
|
||||
. ($option->getIcon()?->render('inline') ?? '')
|
||||
. '</span>'
|
||||
. '<span class="dropdown-item-column dropdown-item-column-title">'
|
||||
. htmlspecialchars($option->getTitle())
|
||||
. '</span>'
|
||||
. '</span>'
|
||||
. '</a>';
|
||||
} else {
|
||||
// for any other kind of button we simply use what comes along (e.g. LinkButton)
|
||||
$html = $option->render();
|
||||
|
||||
if ($option instanceof LinkButton) {
|
||||
// Links inside a dropdown should not be displayed as a button.
|
||||
// Unfortunately, the LinkButton has its class-list
|
||||
// "btn btn-sm btn-default" hard-coded, which makes sense for
|
||||
// its normal context, but not this context. Since it's hard via
|
||||
// CSS to reset the "btn" look, we heuristically remove it here.
|
||||
$html = str_replace('class="btn btn-sm btn-default', 'class="btn-sm btn-default dropdown-item', $html);
|
||||
}
|
||||
}
|
||||
|
||||
$content .= '
|
||||
<li>
|
||||
' . $html . '
|
||||
</li>
|
||||
';
|
||||
}
|
||||
$content .= '
|
||||
</ul>
|
||||
</div>
|
||||
';
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Buttons;
|
||||
|
||||
/**
|
||||
* SplitButtonItems
|
||||
*
|
||||
* Type-safe container for split button items.
|
||||
*
|
||||
* A split button consists of one primary action button and an array of
|
||||
* option buttons shown in a dropdown menu. This DTO ensures type safety
|
||||
* and prevents the use of magic array keys.
|
||||
*
|
||||
* @internal This is a concrete implementation and not part of the TYPO3 Public API.
|
||||
*/
|
||||
final readonly class SplitButtonItems
|
||||
{
|
||||
/**
|
||||
* @param AbstractButton $primary The primary action button
|
||||
* @param AbstractButton[] $options Array of option buttons for the dropdown
|
||||
*/
|
||||
public function __construct(
|
||||
public AbstractButton $primary,
|
||||
public array $options,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks if the split button has a valid configuration.
|
||||
*
|
||||
* @return bool True if primary action exists and at least one option is available
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->primary->isValid() && count($this->options) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
<?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\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownDivider;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownGeneric;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownHeader;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownItem;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownRadio;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownToggle;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDownButton;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\FullyRenderedButton;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\InputButton;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\SplitButton;
|
||||
use TYPO3\CMS\Backend\Template\Components\Event\ModifyPreviewUrlForQrCodeEvent;
|
||||
use TYPO3\CMS\Backend\Template\Components\Menu\Menu;
|
||||
use TYPO3\CMS\Backend\Template\Components\Menu\MenuItem;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Factory for creating backend template components, e.g. buttons.
|
||||
*
|
||||
* This ComponentFactory serves as the central location for all component creation in the backend,
|
||||
* providing both pre-configured components for common patterns and basic component factory methods.
|
||||
*
|
||||
* Currently focused on button creation, but designed to be extensible for other component types
|
||||
* (menus, breadcrumbs, etc.) in the future.
|
||||
*
|
||||
* This reduces boilerplate code and ensures consistent UX across the backend by providing
|
||||
* standardized component configurations for recurring use cases.
|
||||
*
|
||||
* Example - Creating a back button:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
*
|
||||
* // Use pre-configured back button
|
||||
* $backButton = $this->componentFactory->createBackButton($returnUrl);
|
||||
* $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class ComponentFactory
|
||||
{
|
||||
public function __construct(
|
||||
protected IconFactory $iconFactory,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected PageRenderer $pageRenderer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a standardized "back" navigation button.
|
||||
*/
|
||||
public function createBackButton(string|UriInterface $returnUrl): LinkButton
|
||||
{
|
||||
return $this->createLinkButton()
|
||||
->setHref((string)$returnUrl)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
|
||||
->setIcon($this->iconFactory->getIcon('actions-view-go-back', IconSize::SMALL))
|
||||
->setShowLabelText(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized "close" button.
|
||||
*
|
||||
* Similar to back button but uses "actions-close" icon and "Close" label.
|
||||
* Typically used for closing detail views or modal-like overlays.
|
||||
*/
|
||||
public function createCloseButton(string|UriInterface $closeUrl): LinkButton
|
||||
{
|
||||
return $this->createLinkButton()
|
||||
->setHref((string)$closeUrl)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.close'))
|
||||
->setIcon($this->iconFactory->getIcon('actions-close', IconSize::SMALL))
|
||||
->setShowLabelText(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized "reload" button for reloading the current view.
|
||||
*
|
||||
* Uses "actions-refresh" icon without a displayed label.
|
||||
*
|
||||
* Note: As of TYPO3 v14, the reload button is automatically added to all modules
|
||||
* by default. Controllers only need to manually create this button if they need
|
||||
* custom reload behavior. In that case, use DocHeaderComponent::disableAutomaticReloadButton()
|
||||
* to prevent the automatic one from being added.
|
||||
*/
|
||||
public function createReloadButton(string|UriInterface $requestUri): LinkButton
|
||||
{
|
||||
return $this->createLinkButton()
|
||||
->setHref((string)$requestUri)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
|
||||
->setIcon($this->iconFactory->getIcon('actions-refresh', IconSize::SMALL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized "save" button for forms.
|
||||
*
|
||||
* Returns an InputButton configured with:
|
||||
* - "actions-document-save" icon
|
||||
* - Translated "Save" label (shown as text)
|
||||
* - name="_savedok" and value="1"
|
||||
* - Associated with the specified form
|
||||
*
|
||||
* @param string $formName The HTML form ID this button belongs to
|
||||
* @return InputButton Fully configured save button
|
||||
*/
|
||||
public function createSaveButton(string $formName = ''): InputButton
|
||||
{
|
||||
$button = $this->createInputButton()
|
||||
->setName('_savedok')
|
||||
->setValue('1')
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.save') ?: 'Save')
|
||||
->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL))
|
||||
->setShowLabelText(true);
|
||||
|
||||
if ($formName !== '') {
|
||||
$button->setForm($formName);
|
||||
}
|
||||
|
||||
return $button;
|
||||
}
|
||||
|
||||
public function createViewButton(array $previewDataAttributes = []): LinkButton
|
||||
{
|
||||
return $this->createLinkButton()
|
||||
->setHref('#')
|
||||
->setDataAttributes($previewDataAttributes)
|
||||
->setDisabled(!$previewDataAttributes)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
|
||||
->setIcon($this->iconFactory->getIcon('actions-view-page', IconSize::SMALL))
|
||||
->setShowLabelText(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized QR code button that opens a modal with a QR code for the given URL.
|
||||
*
|
||||
* Uses the typo3-qrcode-modal-button web component which displays a scannable QR code
|
||||
* in a modal dialog. The QR code can be downloaded as PNG or SVG.
|
||||
*
|
||||
* @param string|UriInterface $previewUrl The URL to encode in the QR code
|
||||
* @param bool $showCopyUrl Whether to show the URL field with copy button in the modal
|
||||
*/
|
||||
public function createQrCodeButton(string|UriInterface $previewUrl, bool $showCopyUrl = true): GenericButton
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/element/qrcode-modal-button.js');
|
||||
$attributes = [
|
||||
'content' => (string)$previewUrl,
|
||||
'modal-title' => $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:showPageQrCode.modalTitle'),
|
||||
];
|
||||
if ($showCopyUrl) {
|
||||
$attributes['show-url'] = '1';
|
||||
}
|
||||
return $this->createGenericButton()
|
||||
->setTag('typo3-qrcode-modal-button')
|
||||
->setLabel($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:showPageQrCode'))
|
||||
->setTitle($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:showPageQrCode'))
|
||||
->setIcon($this->iconFactory->getIcon('actions-qrcode', IconSize::SMALL))
|
||||
->setAttributes($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a preview URL suitable for QR codes.
|
||||
*
|
||||
* Dispatches the ModifyPreviewUrlForQrCodeEvent to allow extensions (e.g., workspaces)
|
||||
* to provide alternative URLs. For example, the workspaces extension can provide a URL
|
||||
* with ADMCMD_prev parameter that works without backend authentication.
|
||||
*
|
||||
* @param string|UriInterface|null $fallbackUrl Fallback URL if no listener modifies the URL
|
||||
*/
|
||||
public function getPreviewUrlForQrCode(int $pageId, int $languageId, string|UriInterface|null $fallbackUrl = null): ?string
|
||||
{
|
||||
return $this->eventDispatcher->dispatch(
|
||||
new ModifyPreviewUrlForQrCodeEvent($pageId, $languageId, $fallbackUrl !== null ? (string)$fallbackUrl : null)
|
||||
)->getPreviewUrl();
|
||||
}
|
||||
|
||||
public function createGenericButton(): GenericButton
|
||||
{
|
||||
return GeneralUtility::makeInstance(GenericButton::class);
|
||||
}
|
||||
|
||||
public function createInputButton(): InputButton
|
||||
{
|
||||
return GeneralUtility::makeInstance(InputButton::class);
|
||||
}
|
||||
|
||||
public function createSplitButton(): SplitButton
|
||||
{
|
||||
return GeneralUtility::makeInstance(SplitButton::class);
|
||||
}
|
||||
|
||||
public function createDropDownButton(): DropDownButton
|
||||
{
|
||||
return GeneralUtility::makeInstance(DropDownButton::class);
|
||||
}
|
||||
|
||||
public function createDropDownDivider(): DropDownDivider
|
||||
{
|
||||
return GeneralUtility::makeInstance(DropDownDivider::class);
|
||||
}
|
||||
|
||||
public function createDropDownItem(): DropDownItem
|
||||
{
|
||||
return GeneralUtility::makeInstance(DropDownItem::class);
|
||||
}
|
||||
|
||||
public function createDropDownRadio(): DropDownRadio
|
||||
{
|
||||
return GeneralUtility::makeInstance(DropDownRadio::class);
|
||||
}
|
||||
|
||||
public function createDropDownToggle(): DropDownToggle
|
||||
{
|
||||
return GeneralUtility::makeInstance(DropDownToggle::class);
|
||||
}
|
||||
|
||||
public function createDropDownHeader(): DropDownHeader
|
||||
{
|
||||
return GeneralUtility::makeInstance(DropDownHeader::class);
|
||||
}
|
||||
|
||||
public function createDropDownGeneric(): DropDownGeneric
|
||||
{
|
||||
return GeneralUtility::makeInstance(DropDownGeneric::class);
|
||||
}
|
||||
|
||||
public function createLinkButton(): LinkButton
|
||||
{
|
||||
return GeneralUtility::makeInstance(LinkButton::class);
|
||||
}
|
||||
|
||||
public function createFullyRenderedButton(): FullyRenderedButton
|
||||
{
|
||||
return GeneralUtility::makeInstance(FullyRenderedButton::class);
|
||||
}
|
||||
|
||||
public function createShortcutButton(): ShortcutButton
|
||||
{
|
||||
return GeneralUtility::makeInstance(ShortcutButton::class);
|
||||
}
|
||||
|
||||
public function createMenuItem(): MenuItem
|
||||
{
|
||||
return GeneralUtility::makeInstance(MenuItem::class);
|
||||
}
|
||||
|
||||
public function createMenu(): Menu
|
||||
{
|
||||
return GeneralUtility::makeInstance(Menu::class);
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?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;
|
||||
|
||||
final class ComponentGroup
|
||||
{
|
||||
/**
|
||||
* @param array<string, ComponentInterface> $items
|
||||
*/
|
||||
private array $items = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly string $identifier,
|
||||
) {}
|
||||
|
||||
public function get(string $identifier): ?ComponentInterface
|
||||
{
|
||||
return $this->items[$identifier] ?? null;
|
||||
}
|
||||
|
||||
public function remove(string $identifier): true
|
||||
{
|
||||
if (isset($this->items[$identifier])) {
|
||||
unset($this->items[$identifier]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ComponentInterface>
|
||||
*/
|
||||
public function getItems(?ComponentInterface $ifEmptyUseThis = null): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->items as $key => $item) {
|
||||
$result[$key] = $item ?? $ifEmptyUseThis;
|
||||
}
|
||||
return array_filter($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ?ComponentInterface> $items
|
||||
*/
|
||||
public function setItems(array $items): void
|
||||
{
|
||||
$this->items = [];
|
||||
foreach ($items as $identifier => $item) {
|
||||
$this->add($identifier, $item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* $before and $after references the string keys used for $identifier
|
||||
*/
|
||||
public function add(string $identifier, ?ComponentInterface $item, string $before = '', string $after = ''): void
|
||||
{
|
||||
if ($before !== '' && $this->has($before)) {
|
||||
$end = array_splice($this->items, (int)(array_search($before, array_keys($this->items), true)));
|
||||
$this->items = array_merge($this->items, [$identifier => $item], $end);
|
||||
} elseif ($after !== '' && $this->has($after)) {
|
||||
$end = array_splice($this->items, (int)(array_search($after, array_keys($this->items), true)) + 1);
|
||||
$this->items = array_merge($this->items, [$identifier => $item], $end);
|
||||
} else {
|
||||
$this->items[$identifier] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function has(string $identifier): bool
|
||||
{
|
||||
return array_key_exists($identifier, $this->items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Common interface for all renderable backend components (buttons, dropdown items, etc.).
|
||||
*
|
||||
* This interface provides the base contract for components that can be validated,
|
||||
* typed, and rendered as HTML. It is extended by more specific interfaces like
|
||||
* ButtonInterface and DropDownItemInterface.
|
||||
*/
|
||||
interface ComponentInterface extends \Stringable
|
||||
{
|
||||
/**
|
||||
* Validates whether the component is properly configured and can be rendered.
|
||||
*
|
||||
* Each implementing class defines its own validation rules (e.g., required fields).
|
||||
*
|
||||
* @return bool True if the component is valid and can be rendered, false otherwise
|
||||
*/
|
||||
public function isValid(): bool;
|
||||
|
||||
/**
|
||||
* Returns the fully qualified class name as the component type identifier.
|
||||
*
|
||||
* This is used to identify the specific component type in validation and rendering.
|
||||
*
|
||||
* @return string The fully qualified class name (e.g., 'TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton')
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* Renders the component as an HTML string.
|
||||
*
|
||||
* This method should only be called after validating the component with isValid().
|
||||
* The returned HTML is ready to be output to the browser.
|
||||
*
|
||||
* @return string The rendered HTML markup
|
||||
*/
|
||||
public function render(): string;
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to modify the preview URL used for QR codes
|
||||
* in the backend. This allows extensions to provide alternative URLs, for example
|
||||
* workspace-aware preview URLs that work without backend authentication.
|
||||
*/
|
||||
final class ModifyPreviewUrlForQrCodeEvent
|
||||
{
|
||||
private ?string $previewUrl = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly int $pageId,
|
||||
private readonly int $languageId,
|
||||
private readonly ?string $fallbackUrl,
|
||||
) {}
|
||||
|
||||
public function getPageId(): int
|
||||
{
|
||||
return $this->pageId;
|
||||
}
|
||||
|
||||
public function getLanguageId(): int
|
||||
{
|
||||
return $this->languageId;
|
||||
}
|
||||
|
||||
public function getFallbackUrl(): ?string
|
||||
{
|
||||
return $this->fallbackUrl;
|
||||
}
|
||||
|
||||
public function getPreviewUrl(): ?string
|
||||
{
|
||||
return $this->previewUrl ?? $this->fallbackUrl;
|
||||
}
|
||||
|
||||
public function setPreviewUrl(?string $previewUrl): void
|
||||
{
|
||||
$this->previewUrl = $previewUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?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\Menu;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Represents a navigation menu in the backend module document header, typically rendered
|
||||
* as a dropdown selector that allows users to switch between different views or modes
|
||||
* within a module.
|
||||
*
|
||||
* Menus consist of multiple MenuItems and are registered with the MenuRegistry in the
|
||||
* DocHeaderComponent. The menu is automatically rendered in the module's document header.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
|
||||
* $menu = $this->componentFactory->createMenu();
|
||||
* $menu->setIdentifier('myModuleMenu')
|
||||
* ->setLabel('Select View');
|
||||
*
|
||||
* $menuItem1 = $this->componentFactory->createMenuItem()
|
||||
* ->setTitle('List View')
|
||||
* ->setHref($listViewUrl)
|
||||
* ->setActive(true);
|
||||
* $menu->addMenuItem($menuItem1);
|
||||
*
|
||||
* $menuItem2 = $this->componentFactory->createMenuItem()
|
||||
* ->setTitle('Grid View')
|
||||
* ->setHref($gridViewUrl);
|
||||
* $menu->addMenuItem($menuItem2);
|
||||
*
|
||||
* $menuRegistry->addMenu($menu);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class Menu
|
||||
{
|
||||
protected string $identifier = '';
|
||||
|
||||
/**
|
||||
* Label of the Menu (displayed as the dropdown label)
|
||||
*/
|
||||
protected string $label = '';
|
||||
|
||||
protected array $menuItems = [];
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function getDataIdentifier(): string
|
||||
{
|
||||
$dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($this->identifier);
|
||||
return str_replace('_', '-', $dataMenuIdentifier);
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getMenuItems(): array
|
||||
{
|
||||
return $this->menuItems;
|
||||
}
|
||||
|
||||
public function setIdentifier(string $identifier): static
|
||||
{
|
||||
$this->identifier = $identifier;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $label LabelText for the menu (accepts LLL syntax)
|
||||
*/
|
||||
public function setLabel(string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new menuItem
|
||||
*
|
||||
* @param MenuItem $menuItem The menuItem to add to the menu
|
||||
*
|
||||
* @throws \InvalidArgumentException In case a menuItem is not valid
|
||||
*/
|
||||
public function addMenuItem(MenuItem $menuItem): static
|
||||
{
|
||||
if (!$menuItem->isValid()) {
|
||||
throw new \InvalidArgumentException('MenuItem "' . $menuItem->getTitle() . '" is not valid', 1442236317);
|
||||
}
|
||||
// @todo implement sorting of menu items
|
||||
// @todo maybe even things like spacers/sections?
|
||||
$this->menuItems[] = clone $menuItem;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return trim($this->getIdentifier()) !== '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?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\Menu;
|
||||
|
||||
use TYPO3\CMS\Backend\Template\Components\AbstractControl;
|
||||
|
||||
/**
|
||||
* Represents a single item within a Menu in the backend module document header.
|
||||
* Each MenuItem has a title, URL (href), and can be marked as active to indicate
|
||||
* the current selection.
|
||||
*
|
||||
* MenuItems inherit from AbstractControl, providing access to common properties like
|
||||
* title, CSS classes, and data attributes.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* $menu = $this->componentFactory->createMenu();
|
||||
* $menuItem = $this->componentFactory->createMenuItem()
|
||||
* ->setTitle('List View')
|
||||
* ->setHref('/my-module?view=list')
|
||||
* ->setActive(true) // Marks this item as currently selected
|
||||
* ->setClasses('my-custom-class')
|
||||
* ->setDataAttributes(['action' => 'switch-view']);
|
||||
* $menu->addMenuItem($menuItem);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class MenuItem extends AbstractControl
|
||||
{
|
||||
protected string $href = '';
|
||||
|
||||
protected bool $active = false;
|
||||
|
||||
public function setHref(string $href): static
|
||||
{
|
||||
$this->href = $href;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHref(): string
|
||||
{
|
||||
return $this->href;
|
||||
}
|
||||
|
||||
public function setActive(bool $active): static
|
||||
{
|
||||
$this->active = $active;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->getHref() !== '' && $this->getTitle() !== '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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 TYPO3\CMS\Backend\Template\Components\Menu\Menu;
|
||||
|
||||
/**
|
||||
* Central registry for managing menus in the backend module document header.
|
||||
* The MenuRegistry is part of the DocHeaderComponent and holds all menus that should
|
||||
* be displayed in the module's header area.
|
||||
*
|
||||
* Menus are typically used to provide navigation between different views or modes within
|
||||
* a module (e.g., switching between list/grid view, or different content types).
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* public function myAction(): ResponseInterface
|
||||
* {
|
||||
* // Get the menu registry from the module template
|
||||
* $menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
|
||||
*
|
||||
* // Create and configure a menu
|
||||
* $menu = $this->componentFactory->createMenu();
|
||||
* $menu->setIdentifier('viewSelector')->setLabel('View');
|
||||
*
|
||||
* // Add menu items
|
||||
* $listItem = $this->componentFactory->createMenuItem()
|
||||
* ->setTitle('List')
|
||||
* ->setHref('/module?view=list')
|
||||
* ->setActive($currentView === 'list');
|
||||
* $menu->addMenuItem($listItem);
|
||||
*
|
||||
* // Register the menu
|
||||
* $menuRegistry->addMenu($menu);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class MenuRegistry
|
||||
{
|
||||
/**
|
||||
* Internal array that stores all registered menus
|
||||
*
|
||||
* @var array<string, Menu>
|
||||
*/
|
||||
protected array $menus = [];
|
||||
|
||||
/**
|
||||
* Adds a menu to the registry
|
||||
*
|
||||
* @throws \InvalidArgumentException In case a menu is not valid
|
||||
*/
|
||||
public function addMenu(Menu $menu): static
|
||||
{
|
||||
if (!$menu->isValid()) {
|
||||
throw new \InvalidArgumentException('Menu "' . $menu->getIdentifier() . '" is not valid', 1442236362);
|
||||
}
|
||||
$this->menus[$menu->getIdentifier()] = clone $menu;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all menus in an abstract array
|
||||
*
|
||||
* @return Menu[]
|
||||
*/
|
||||
public function getMenus(): array
|
||||
{
|
||||
foreach ($this->menus as $key => $menu) {
|
||||
if (empty($menu->getMenuItems())) {
|
||||
unset($this->menus[$key]);
|
||||
}
|
||||
}
|
||||
return $this->menus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* PSR-14 event that allows listeners to modify the buttons in the backend module
|
||||
* document header button bar. This event is dispatched after all buttons have been
|
||||
* added to the button bar, but before they are rendered.
|
||||
*
|
||||
* Use cases:
|
||||
* - Add custom buttons to existing modules
|
||||
* - Remove or hide buttons based on conditions
|
||||
* - Modify button properties (labels, icons, etc.)
|
||||
* - Reorder buttons
|
||||
*
|
||||
* Example event listener:
|
||||
*
|
||||
* ```
|
||||
* use TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent;
|
||||
* use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
* use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
* use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
*
|
||||
* final class MyButtonBarListener
|
||||
* {
|
||||
* public function __construct(
|
||||
* protected readonly ComponentFactory $componentFactory,
|
||||
* ) {}
|
||||
*
|
||||
* #[AsEventListener]
|
||||
* public function __invoke(ModifyButtonBarEvent $event): void
|
||||
* {
|
||||
* $buttons = $event->getButtons();
|
||||
* $buttonBar = $event->getButtonBar();
|
||||
*
|
||||
* $myButton = $this->componentFactory->createLinkButton()
|
||||
* ->setHref('/my-action')
|
||||
* ->setTitle('My Action')
|
||||
* ->setIcon($iconFactory->getIcon('actions-heart'));
|
||||
*
|
||||
* $buttons[ButtonBar::BUTTON_POSITION_RIGHT][1][] = clone $myButton;
|
||||
*
|
||||
* $event->setButtons($buttons);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @phpstan-import-type Buttons from ButtonBar
|
||||
*/
|
||||
final class ModifyButtonBarEvent
|
||||
{
|
||||
/**
|
||||
* @param Buttons $buttons
|
||||
*/
|
||||
public function __construct(private array $buttons, private readonly ButtonBar $buttonBar, private readonly ServerRequestInterface $request) {}
|
||||
|
||||
/**
|
||||
* @return Buttons
|
||||
*/
|
||||
public function getButtons(): array
|
||||
{
|
||||
return $this->buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Buttons $buttons
|
||||
*/
|
||||
public function setButtons(array $buttons): void
|
||||
{
|
||||
$this->buttons = $buttons;
|
||||
}
|
||||
|
||||
public function getButtonBar(): ButtonBar
|
||||
{
|
||||
return $this->buttonBar;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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\MultiRecordSelection;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Defines a bulk action that can be performed on multiple selected records in the backend.
|
||||
* These actions appear when users select multiple records in list views or other record
|
||||
* listings, allowing operations like mass delete, mass edit, etc.
|
||||
*
|
||||
* Actions are readonly DTOs that encapsulate the configuration,
|
||||
* icon, and label for a multi-record operation.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* $action = new Action(
|
||||
* name: 'delete',
|
||||
* configuration: ['action' => 'deleteRecords'],
|
||||
* iconIdentifier: 'actions-delete',
|
||||
* labelKey: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete'
|
||||
* );
|
||||
*
|
||||
* // The action provides formatted output for rendering
|
||||
* $actionName = $action->getName(); // 'delete'
|
||||
* $jsonConfig = $action->getConfiguration(); // JSON-encoded for HTML attributes
|
||||
* $label = $action->getLabel(); // Translated label
|
||||
* $icon = $action->getIcon(); // Rendered icon HTML
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class Action
|
||||
{
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected array $configuration,
|
||||
protected string $iconIdentifier,
|
||||
protected string $labelKey,
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getConfiguration(): string
|
||||
{
|
||||
return GeneralUtility::jsonEncodeForHtmlAttribute($this->configuration);
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->getLanguageService()->sL($this->labelKey);
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return GeneralUtility::makeInstance(IconFactory::class)->getIcon($this->iconIdentifier, IconSize::SMALL)->render();
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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\Enum;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
enum ModuleLayout: string
|
||||
{
|
||||
case WIDE = 'wide';
|
||||
case NORMAL = 'normal';
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?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;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Module\ModuleInterface;
|
||||
use TYPO3\CMS\Backend\Module\ModuleProvider;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;
|
||||
use TYPO3\CMS\Backend\Template\Enum\ModuleLayout;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ResponsableViewInterface;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
|
||||
/**
|
||||
* A class taking care of the "outer" HTML of a module, especially
|
||||
* the doc header and other related parts.
|
||||
*/
|
||||
final class ModuleTemplate implements ViewInterface, ResponsableViewInterface
|
||||
{
|
||||
use PageRendererBackendSetupTrait;
|
||||
|
||||
private bool $uiBlock = false;
|
||||
|
||||
private string $moduleId = '';
|
||||
private string $moduleName = '';
|
||||
private string $moduleClass = '';
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private ModuleLayout $moduleLayout = ModuleLayout::WIDE;
|
||||
private string $title = '';
|
||||
private string $bodyTag = '<body>';
|
||||
private string $formTag = '';
|
||||
|
||||
private FlashMessageQueue $flashMessageQueue;
|
||||
private DocHeaderComponent $docHeaderComponent;
|
||||
|
||||
/**
|
||||
* Init PageRenderer and properties.
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly PageRenderer $pageRenderer,
|
||||
protected readonly IconFactory $iconFactory,
|
||||
protected readonly UriBuilder $uriBuilder,
|
||||
protected readonly ModuleProvider $moduleProvider,
|
||||
protected readonly FlashMessageService $flashMessageService,
|
||||
protected readonly ExtensionConfiguration $extensionConfiguration,
|
||||
protected readonly ViewInterface $view,
|
||||
protected readonly ComponentFactory $componentFactory,
|
||||
protected readonly ServerRequestInterface $request,
|
||||
) {
|
||||
$module = $request->getAttribute('module');
|
||||
if ($module instanceof ModuleInterface) {
|
||||
// third level, needs the second level in order to keep highlighting in the module menu
|
||||
if ($module->getParentModule()?->getParentModule()) {
|
||||
$this->setModuleName($module->getParentModule()->getIdentifier());
|
||||
} else {
|
||||
$this->setModuleName($module->getIdentifier());
|
||||
}
|
||||
$this->setModuleName($module->getIdentifier());
|
||||
} else {
|
||||
$this->setModuleName($request->getAttribute('route')?->getOption('_identifier') ?? '');
|
||||
}
|
||||
$this->flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$this->docHeaderComponent = GeneralUtility::makeInstance(DocHeaderComponent::class);
|
||||
$this->setUpBasicPageRendererForBackend($pageRenderer, $extensionConfiguration, $request, $this->getLanguageService());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a variable to the view data collection.
|
||||
*/
|
||||
public function assign(string $key, mixed $value): self
|
||||
{
|
||||
$this->view->assign($key, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple variables to the view data collection.
|
||||
*/
|
||||
public function assignMultiple(array $values): self
|
||||
{
|
||||
$this->view->assignMultiple($values);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the module.
|
||||
*/
|
||||
public function render(string $templateFileName = ''): string
|
||||
{
|
||||
$this->prepareRender($templateFileName);
|
||||
return $this->pageRenderer->render($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the module and create an HTML 200 response from it. This is a
|
||||
* lazy shortcut so controllers don't need to take care of this in the backend.
|
||||
*/
|
||||
public function renderResponse(string $templateFileName = ''): ResponseInterface
|
||||
{
|
||||
$this->prepareRender($templateFileName);
|
||||
return $this->pageRenderer->renderResponse($this->request);
|
||||
}
|
||||
|
||||
private function prepareRender(string $templateFileName): void
|
||||
{
|
||||
if ($templateFileName === '') {
|
||||
$extbaseRequestMessage = '';
|
||||
/** @var ExtbaseRequestParameters|null $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = $this->request->getAttribute('extbase');
|
||||
if ($extbaseRequestParameters) {
|
||||
// This extbase specific code is a helper for a more detailed exception
|
||||
// message, and a tribute to extbase backend extensions being upgraded.
|
||||
// Introduced with v13, it could potentially vanish at some point again.
|
||||
$templateFileName = $extbaseRequestParameters->getControllerName() . '/'
|
||||
. ucfirst($extbaseRequestParameters->getControllerActionName());
|
||||
$extbaseRequestMessage = ' Expected template filename is "' . $templateFileName . '".';
|
||||
}
|
||||
throw new \InvalidArgumentException('A template filename must be provided.' . $extbaseRequestMessage, 1732184506);
|
||||
}
|
||||
|
||||
$this->assignMultiple([
|
||||
'docHeader' => $this->docHeaderComponent->docHeaderContent($this->request),
|
||||
'moduleId' => $this->moduleId,
|
||||
'moduleName' => $this->moduleName,
|
||||
'moduleClass' => $this->moduleClass,
|
||||
'moduleLayout' => $this->moduleLayout->value,
|
||||
'uiBlock' => $this->uiBlock,
|
||||
'flashMessageQueueIdentifier' => $this->flashMessageQueue->getIdentifier(),
|
||||
'formTag' => $this->formTag,
|
||||
]);
|
||||
$this->pageRenderer->getJavaScriptRenderer()->includeAllImports();
|
||||
$this->pageRenderer->loadJavaScriptModule('bootstrap');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/dropdown.js');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-help.js');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/global-event-handler.js');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/action-dispatcher.js');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/element/immediate-action-element.js');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/hotkeys.js');
|
||||
$this->pageRenderer->addBodyContent($this->bodyTag . $this->view->render($templateFileName));
|
||||
$this->pageRenderer->setTitle($this->title);
|
||||
$updateSignalDetails = BackendUtility::getUpdateSignalDetails();
|
||||
if (!empty($updateSignalDetails['html'])) {
|
||||
$this->pageRenderer->addHeaderData(implode("\n", $updateSignalDetails['html']));
|
||||
}
|
||||
$this->dispatchNotificationMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setLayout(ModuleLayout $moduleLayout): self
|
||||
{
|
||||
$this->moduleLayout = $moduleLayout;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to something like '<body id="foo">' when a special body tag is needed.
|
||||
*/
|
||||
public function setBodyTag(string $bodyTag): self
|
||||
{
|
||||
$this->bodyTag = $bodyTag;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Title string of the module: "My module · Edit view"
|
||||
*/
|
||||
public function setTitle(string $title, string $context = ''): self
|
||||
{
|
||||
$titleComponents = [$title];
|
||||
if ($context !== '') {
|
||||
$titleComponents[] = $context;
|
||||
}
|
||||
$this->title = implode(' · ', $titleComponents);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DocHeader. Can be used in controllers to add custom
|
||||
* buttons / menus / ... to the doc header.
|
||||
*/
|
||||
public function getDocHeaderComponent(): DocHeaderComponent
|
||||
{
|
||||
return $this->docHeaderComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* A "<form>" tag encapsulating the entire module, including doc-header.
|
||||
*/
|
||||
public function setForm(string $formTag = ''): self
|
||||
{
|
||||
$this->formTag = $formTag;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional 'data-module-id="{moduleId}"' on first <div> in body.
|
||||
* Can be helpful in JavaScript.
|
||||
*/
|
||||
public function setModuleId(string $moduleId): self
|
||||
{
|
||||
$this->moduleId = $moduleId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional 'data-module-name="{moduleName}"' on first <div> in body.
|
||||
* Can be helpful in JavaScript.
|
||||
*/
|
||||
public function setModuleName(string $moduleName): self
|
||||
{
|
||||
$this->moduleName = $moduleName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional 'class="module {moduleClass}"' on first <div> in body.
|
||||
* Can be helpful styling modules.
|
||||
*/
|
||||
public function setModuleClass(string $moduleClass): self
|
||||
{
|
||||
$this->moduleClass = $moduleClass;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message object and adds it to the FlashMessageQueue.
|
||||
* These messages are automatically rendered when the view is rendered.
|
||||
*/
|
||||
public function addFlashMessage(string $messageBody, string $messageTitle = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK, bool $storeInSession = true): self
|
||||
{
|
||||
$flashMessage = new FlashMessage($messageBody, $messageTitle, $severity, $storeInSession);
|
||||
$this->flashMessageQueue->enqueue($flashMessage);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleTemplate by default uses queue 'core.template.flashMessages'. Modules
|
||||
* may want to maintain an own queue. Use this method to render flash messages
|
||||
* of a non-default queue at the default position in module HTML output. Call
|
||||
* this method *before* adding single messages with addFlashMessage().
|
||||
*/
|
||||
public function setFlashMessageQueue(FlashMessageQueue $flashMessageQueue): self
|
||||
{
|
||||
$this->flashMessageQueue = $flashMessageQueue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI block is a spinner shown during browser rendering phase of the module,
|
||||
* automatically removed when rendering finished. This is done by default,
|
||||
* but the UI block can be turned off when needed for whatever reason.
|
||||
*/
|
||||
public function setUiBlock(bool $uiBlock): self
|
||||
{
|
||||
$this->uiBlock = $uiBlock;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a module actions dropdown in the docheader button bar.
|
||||
*
|
||||
* Creates a dropdown button on the LEFT side (group 0) containing navigation to
|
||||
* submodules or module actions. The button label shows the currently active module/action.
|
||||
*/
|
||||
public function makeDocHeaderModuleMenu(array $additionalQueryParams = []): self
|
||||
{
|
||||
$currentModule = $this->request->getAttribute('module');
|
||||
if (!($currentModule instanceof ModuleInterface)) {
|
||||
// Early return in case the current request does not provide a module
|
||||
return $this;
|
||||
}
|
||||
if ($currentModule->getParentModule()?->hasParentModule()) {
|
||||
$menuModule = $this->moduleProvider->getModuleForMenu($currentModule->getParentIdentifier(), $this->getBackendUser());
|
||||
} else {
|
||||
// This is a fallback in case a second level module is called here
|
||||
$menuModule = $this->moduleProvider->getModuleForMenu($currentModule->getIdentifier(), $this->getBackendUser());
|
||||
}
|
||||
|
||||
if ($menuModule === null || !$menuModule->hasSubModules()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$itemCount = 0;
|
||||
$dropdownButton = $this->componentFactory->createDropDownButton()
|
||||
->setLabel($this->getLanguageService()->sL('backend.messages:moduleMenu.dropdown.label'))
|
||||
->setShowActiveLabelText(true)
|
||||
->setShowLabelText(true);
|
||||
|
||||
// Add "Overview" link if exists
|
||||
if ($menuModule->hasSubmoduleOverview()) {
|
||||
$isActive = $menuModule->getIdentifier() === $currentModule->getIdentifier();
|
||||
$overviewLabel = $this->getLanguageService()->sL('backend.messages:moduleMenu.dropdown.overview');
|
||||
$dropdownItem = $this->componentFactory->createDropDownRadio()
|
||||
->setHref((string)$this->uriBuilder->buildUriFromRoute($menuModule->getIdentifier(), $additionalQueryParams))
|
||||
->setLabel($overviewLabel)
|
||||
->setActive($isActive);
|
||||
|
||||
$dropdownButton->addItem($dropdownItem);
|
||||
$itemCount++;
|
||||
}
|
||||
|
||||
// Add all submodules
|
||||
foreach ($menuModule->getSubModules() as $module) {
|
||||
$isActive = $module->getIdentifier() === $currentModule->getIdentifier();
|
||||
$moduleTitle = $this->getLanguageService()->sL($module->getTitle());
|
||||
$dropdownItem = $this->componentFactory->createDropDownRadio()
|
||||
->setHref((string)$this->uriBuilder->buildUriFromRoute($module->getIdentifier(), $additionalQueryParams))
|
||||
->setLabel($moduleTitle)
|
||||
->setActive($isActive);
|
||||
|
||||
$dropdownButton->addItem($dropdownItem);
|
||||
$itemCount++;
|
||||
}
|
||||
|
||||
// Only add dropdown if there's more than one item
|
||||
if ($itemCount > 1) {
|
||||
// Add to button bar at LEFT, group 0 (first position)
|
||||
$this->getDocHeaderComponent()->getButtonBar()->addButton($dropdownButton, ButtonBar::BUTTON_POSITION_LEFT, 0);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand method to add a new button to the button bar
|
||||
*/
|
||||
public function addButtonToButtonBar(
|
||||
ButtonInterface $button,
|
||||
string $buttonPosition = ButtonBar::BUTTON_POSITION_LEFT,
|
||||
int $buttonGroup = 1
|
||||
): self {
|
||||
$this->getDocHeaderComponent()->getButtonBar()->addButton($button, $buttonPosition, $buttonGroup);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches all messages in a special FlashMessageQueue to the PageRenderer to be rendered as inline notifications
|
||||
*/
|
||||
private function dispatchNotificationMessages(): void
|
||||
{
|
||||
$notificationQueue = $this->flashMessageService->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE);
|
||||
foreach ($notificationQueue->getAllMessagesAndFlush() as $message) {
|
||||
$notificationInstruction = JavaScriptModuleInstruction::create('@typo3/backend/notification.js');
|
||||
$notificationInstruction->invoke('showMessage', $message->getTitle(), $message->getMessage(), $message->getSeverity());
|
||||
$this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($notificationInstruction);
|
||||
}
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Module\ModuleProvider;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
|
||||
/**
|
||||
* A factory class creating backend related ModuleTemplate view objects.
|
||||
*/
|
||||
#[Autoconfigure(public: true, shared: false)]
|
||||
final readonly class ModuleTemplateFactory
|
||||
{
|
||||
public function __construct(
|
||||
private PageRenderer $pageRenderer,
|
||||
private IconFactory $iconFactory,
|
||||
private UriBuilder $uriBuilder,
|
||||
private ModuleProvider $moduleProvider,
|
||||
private FlashMessageService $flashMessageService,
|
||||
private ExtensionConfiguration $extensionConfiguration,
|
||||
private BackendViewFactory $viewFactory,
|
||||
private ComponentFactory $componentFactory,
|
||||
) {}
|
||||
|
||||
public function create(ServerRequestInterface $request): ModuleTemplate
|
||||
{
|
||||
return new ModuleTemplate(
|
||||
$this->pageRenderer,
|
||||
$this->iconFactory,
|
||||
$this->uriBuilder,
|
||||
$this->moduleProvider,
|
||||
$this->flashMessageService,
|
||||
$this->extensionConfiguration,
|
||||
$this->viewFactory->create($request),
|
||||
$this->componentFactory,
|
||||
$request,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?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;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolvePublicResourceException;
|
||||
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolveSystemResourceException;
|
||||
use TYPO3\CMS\Core\SystemResource\Exception\InvalidSystemResourceIdentifierException;
|
||||
use TYPO3\CMS\Core\SystemResource\Identifier\PackageResourceIdentifier;
|
||||
use TYPO3\CMS\Core\SystemResource\Identifier\SystemResourceIdentifierFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* This is an internal helper trait to DRY basic PageRenderer backend
|
||||
* setup code in the backend. It configures the PageRenderer for general
|
||||
* backend use (charset, favicon, ...).
|
||||
* Most prominent use is ModuleTemplate - The View used to render backend
|
||||
* modules that have doc headers. It's also used in controllers that render
|
||||
* backend things that have no doc header: For instance the login, the main
|
||||
* frame and link handlers - the iframes within modals.
|
||||
* The PageRenderer in general is more on the "maybe we can get rid of it soon"
|
||||
* side. This trait exists to simplify a possible refactoring. In general,
|
||||
* controllers should strive to do as little PageRenderer calls as possible and
|
||||
* move existing calls to templates using f:be.pageRenderer ViewHelper. This
|
||||
* will simplify substituting PageRenderer with a slim dedicated backend solution.
|
||||
*
|
||||
* @internal helper. Do not use in extensions.
|
||||
*/
|
||||
trait PageRendererBackendSetupTrait
|
||||
{
|
||||
/**
|
||||
* Sets mandatory parameters for the PageRenderer.
|
||||
*/
|
||||
protected function setUpBasicPageRendererForBackend(
|
||||
PageRenderer $pageRenderer,
|
||||
ExtensionConfiguration $extensionConfiguration,
|
||||
ServerRequestInterface $request,
|
||||
LanguageService $languageService,
|
||||
): void {
|
||||
$pageRenderer->setLanguage($languageService->getLocale(), $request);
|
||||
$pageRenderer->setMetaTag('name', 'viewport', 'width=device-width, initial-scale=1');
|
||||
$pageRenderer->setFavIcon($this->getBackendFavicon($extensionConfiguration, $request));
|
||||
$nonce = $request->getAttribute('nonce');
|
||||
if ($nonce !== null) {
|
||||
$pageRenderer->setNonce($nonce);
|
||||
$pageRenderer->setApplyNonceHint(true);
|
||||
}
|
||||
$this->loadStylesheets($pageRenderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all registered stylesheets from $GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets']
|
||||
*/
|
||||
protected function loadStylesheets(PageRenderer $pageRenderer): void
|
||||
{
|
||||
// @todo this needs to be replaced with usage of the yet to be created
|
||||
// System Resource API that can handle folders
|
||||
// This will then remove the need to use internal SystemResourceIdentifierFactory here
|
||||
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
|
||||
$identifierFactory = new SystemResourceIdentifierFactory($packageManager);
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets'] ?? [] as $potentialResourceIdentifier) {
|
||||
try {
|
||||
$resourceIdentifier = $identifierFactory->create($potentialResourceIdentifier);
|
||||
} catch (InvalidSystemResourceIdentifierException) {
|
||||
continue;
|
||||
}
|
||||
if (!$resourceIdentifier instanceof PackageResourceIdentifier) {
|
||||
continue;
|
||||
}
|
||||
$package = $resourceIdentifier->getPackage();
|
||||
$relativePath = $resourceIdentifier->getRelativePath();
|
||||
$absolutePath = $package->getPackagePath() . $relativePath;
|
||||
if (is_dir($absolutePath)) {
|
||||
// Path like 'PKG:vendor/my-extension:Resources/Public/Css/Backend'
|
||||
foreach (GeneralUtility::getFilesInDir($absolutePath, 'css') as $cssFile) {
|
||||
$pageRenderer->addCssFile((string)$resourceIdentifier->withRelativePath(rtrim($relativePath, '/') . '/' . $cssFile));
|
||||
}
|
||||
} elseif (file_exists($absolutePath)) {
|
||||
// A single file 'PKG:my_extension:Resources/Public/Css/Backend/main.css' or just a single file
|
||||
$pageRenderer->addCssFile((string)$resourceIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configured favicon for backend with fallback.
|
||||
*/
|
||||
protected function getBackendFavicon(ExtensionConfiguration $extensionConfiguration, ServerRequestInterface $request): string
|
||||
{
|
||||
$backendFavicon = $extensionConfiguration->get('backend', 'backendFavicon');
|
||||
if (!empty($backendFavicon)) {
|
||||
return $this->getUriForFileName($request, $backendFavicon);
|
||||
}
|
||||
return $this->getUriForFileName($request, 'EXT:backend/Resources/Public/Icons/favicon.ico');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uri for a system resource
|
||||
*
|
||||
* @throws CanNotResolvePublicResourceException
|
||||
* @throws CanNotResolveSystemResourceException
|
||||
*/
|
||||
protected function getUriForFileName(ServerRequestInterface $request, string $resourceIdentifier): string
|
||||
{
|
||||
return (string)PathUtility::getSystemResourceUri($resourceIdentifier, $request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user