TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user