TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -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;
}
}