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