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,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();
}
}