Files
cms-backend/Classes/Form/Element/SelectMultipleSideBySideElement.php
T

483 lines
25 KiB
PHP

<?php
/*
* 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\Form\Element;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render a widget with two boxes side by side.
*
* This is rendered for config type=select, renderType=selectMultipleSideBySide set
*/
class SelectMultipleSideBySideElement extends AbstractFormElement
{
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'editPopup' => [
'renderType' => 'editPopup',
'disabled' => true,
],
'addRecord' => [
'renderType' => 'addRecord',
'disabled' => true,
],
'listModule' => [
'renderType' => 'listModule',
'disabled' => true,
'after' => [ 'addRecord' ],
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
) {}
/**
* Merge field control configuration with default controls and render them.
*
* @return array Result array
*/
protected function renderFieldControl(): array
{
$alternativeResult = [
'additionalInlineLanguageLabelFiles' => [],
'stylesheetFiles' => [],
'javaScriptModules' => [],
'inlineData' => [],
'html' => '',
];
$options = $this->data;
$fieldControl = $this->defaultFieldControl;
$fieldControlFromTca = $options['parameterArray']['fieldConf']['config']['fieldControl'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
$options['renderType'] = 'fieldControl';
if (isset($fieldControl['editPopup'])) {
$editPopupControl = $fieldControl['editPopup'];
unset($fieldControl['editPopup']);
$alternativeOptions = $options;
$alternativeOptions['renderData']['fieldControl'] = ['editPopup' => $editPopupControl];
$alternativeResult = $this->nodeFactory->create($alternativeOptions)->render();
}
$options['renderData']['fieldControl'] = $fieldControl;
return [$this->nodeFactory->create($options)->render(), $alternativeResult];
}
/**
* Render side by side element.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
if ($config['readOnly'] ?? false) {
// Early return for the relatively simple read only case
return $this->renderReadOnly();
}
$filterTextfield = [];
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$elementName = $parameterArray['itemFormElName'];
$possibleItems = $config['items'];
$selectedItems = $parameterArray['itemFormElValue'] ?: [];
$maxItems = $config['maxitems'];
$size = (int)($config['size'] ?? 2);
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
if ($autoSizeMax > 0) {
$size = MathUtility::forceIntegerInRange($size, 1);
$size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax);
}
$itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
$listOfSelectedValues = [];
$selectedItemsHtml = [];
foreach ($selectedItems as $itemValue) {
foreach ($possibleItems as $possibleItem) {
if ($possibleItem['value'] == $itemValue) {
$title = $possibleItem['label'];
$listOfSelectedValues[] = $itemValue;
$selectedItemsHtml[] = '<option value="' . htmlspecialchars((string)$itemValue) . '" title="' . htmlspecialchars((string)$title) . '">' . htmlspecialchars($this->appendValueToLabelInDebugMode($title, $itemValue)) . '</option>';
break;
}
}
}
$selectableItemCounter = 0;
$selectableItemGroupCounter = 0;
$selectableItemGroups = [];
$selectableItemsHtml = [];
// Initialize groups
foreach ($possibleItems as $possibleItem) {
$disableAttributes = [];
if (!$itemCanBeSelectedMoreThanOnce && in_array((string)$possibleItem['value'], $selectedItems, true)) {
$disableAttributes = [
'disabled' => 'disabled',
'class' => 'hidden',
];
}
if ($possibleItem['value'] === '--div--') {
if ($selectableItemCounter !== 0) {
$selectableItemGroupCounter++;
}
$selectableItemGroups[$selectableItemGroupCounter]['header']['title'] = $possibleItem['label'];
} else {
$selectableItemGroups[$selectableItemGroupCounter]['items'][] = [
'label' => $this->appendValueToLabelInDebugMode($possibleItem['label'], $possibleItem['value']),
'attributes' => array_merge(['title' => $possibleItem['label'], 'value' => $possibleItem['value']], $disableAttributes),
];
// In case the item is not disabled, enable the group (if any)
if ($disableAttributes === [] && isset($selectableItemGroups[$selectableItemGroupCounter]['header'])) {
$selectableItemGroups[$selectableItemGroupCounter]['header']['disabled'] = false;
}
$selectableItemCounter++;
}
}
// Process groups
foreach ($selectableItemGroups as $selectableItemGroup) {
if (!is_array($selectableItemGroup['items'] ?? false)) {
continue;
}
$optionGroup = isset($selectableItemGroup['header']);
if ($optionGroup) {
$selectableItemsHtml[] = '<optgroup label="' . htmlspecialchars($selectableItemGroup['header']['title']) . '"' . (($selectableItemGroup['header']['disabled'] ?? true) ? 'class="hidden" disabled="disabled"' : '') . '>';
}
foreach ($selectableItemGroup['items'] as $item) {
$selectableItemsHtml[] = '
<option ' . GeneralUtility::implodeAttributes($item['attributes'], true, true) . '>
' . htmlspecialchars($item['label']) . '
</option>';
}
if ($optionGroup) {
$selectableItemsHtml[] = '</optgroup>';
}
}
// Html stuff for filter and select filter on top of right side of multi select boxes
$filterTextfieldId = StringUtility::getUniqueId('tceforms-multiselect-filter-');
$filterTextfield[] = '<input type="search" id="' . $filterTextfieldId . '" autocomplete="off" class="t3js-formengine-multiselect-filter-textfield form-control" value="">';
$filterDropDownOptions = [];
if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
foreach ($config['multiSelectFilterItems'] as $optionElement) {
$value = $languageService->sL($optionElement[0]);
$label = $value;
if (isset($optionElement[1]) && trim($optionElement[1]) !== '') {
$label = $languageService->sL($optionElement[1]);
}
$filterDropDownOptions[] = '<option value="' . htmlspecialchars($value) . '">' . htmlspecialchars($label) . '</option>';
}
}
$filterHtml = [];
$filterHtml[] = '<div class="form-wizards-item-filter">';
if (!empty($filterDropDownOptions)) {
$filterHtml[] = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">';
$filterHtml[] = '<div class="form-multigroup-item">';
$filterHtml[] = '<select class="form-select t3js-formengine-multiselect-filter-dropdown">';
$filterHtml[] = implode(LF, $filterDropDownOptions);
$filterHtml[] = '</select>';
$filterHtml[] = '</div>';
$filterHtml[] = '<div class="form-multigroup-item">';
$filterHtml[] = implode(LF, $filterTextfield);
$filterHtml[] = '</div>';
$filterHtml[] = '</div>';
} else {
$filterHtml[] = implode(LF, $filterTextfield);
}
$filterHtml[] = '</div>';
$multipleAttribute = '';
if ($maxItems !== 1 && $size !== 1) {
$multipleAttribute = ' multiple="multiple"';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
[$fieldControlResult, $alternativeControlResult] = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$alternativeFieldControlHtml = $alternativeControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $alternativeControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$selectedOptionsFieldId = StringUtility::getUniqueId('tceforms-multiselect-');
$availableOptionsFieldId = StringUtility::getUniqueId('tceforms-multiselect-');
$html = [];
$html[] = $this->renderLabel($selectedOptionsFieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . (int)$itemCanBeSelectedMoreThanOnce . '" />';
$html[] = '<div class="form-multigroup-wrap t3js-formengine-field-group">';
$html[] = '<div class="form-multigroup-item">';
$html[] = '<label>';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<select';
$html[] = ' id="' . $selectedOptionsFieldId . '"';
$html[] = ' size="' . $size . '"';
$html[] = ' class="form-select"';
$html[] = $multipleAttribute;
$html[] = ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
$html[] = '>';
$html[] = implode(LF, $selectedItemsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--move">';
$html[] = '<div class="btn-group-vertical">';
if ($maxItems > 1 && $size >= 2) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-top"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-top', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-up"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-up', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '</span>';
$html[] = '</button>';
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-down"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-down', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1 && $size >= 2) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-bottom"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-bottom', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '</span>';
$html[] = '</button>';
}
$html[] = $alternativeFieldControlHtml;
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-removeoption"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-selection-delete', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '</span>';
$html[] = '</button>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<div class="form-multigroup-item">';
$html[] = '<label for="' . $filterTextfieldId . '">';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.items'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap">';
$html[] = implode(LF, $filterHtml);
$html[] = '<div class="form-wizards-item-element">';
$selectElementAttrs = array_merge(
[
'size' => $size,
'id' => $availableOptionsFieldId,
'class' => 'form-select t3js-formengine-select-itemstoselect',
'data-relatedfieldname' => $elementName,
'data-exclusivevalues' => $config['exclusiveKeys'] ?? '',
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectElementAttrs, true) . '>';
$html[] = implode(LF, $selectableItemsHtml);
$html[] = '</select>';
$html[] = '</div>';
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group-vertical">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
$html[] = '</div>';
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/select-multiple-side-by-side-element.js'
)->instance($selectedOptionsFieldId, $availableOptionsFieldId);
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* Create HTML of a read only multi select. Right side is not
* rendered, but just the left side with the selected items.
*
* @return array
*/
protected function renderReadOnly()
{
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$fieldName = $parameterArray['itemFormElName'];
$possibleItems = $config['items'];
$selectedItems = $parameterArray['itemFormElValue'] ?: [];
if (!is_array($selectedItems)) {
$selectedItems = GeneralUtility::trimExplode(',', $selectedItems, true);
}
$size = (int)($config['size'] ?? 2);
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
if ($autoSizeMax > 0) {
$size = MathUtility::forceIntegerInRange($size, 1);
$size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax);
}
$multiple = '';
if ($size !== 1) {
$multiple = ' multiple="multiple"';
}
$listOfSelectedValues = [];
$optionsHtml = [];
foreach ($selectedItems as $itemValue) {
foreach ($possibleItems as $possibleItem) {
if ($possibleItem['value'] == $itemValue) {
$title = $possibleItem['label'];
$listOfSelectedValues[] = $itemValue;
$optionsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($title) . '</option>';
break;
}
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$selectId = StringUtility::getUniqueId('tceforms-multiselect-');
$html = [];
$html[] = $this->renderLabel($selectId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<label>';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<select';
$html[] = ' id="' . $selectId . '"';
$html[] = ' size="' . $size . '"';
$html[] = ' class="form-select"';
$html[] = $multiple;
$html[] = ' data-formengine-input-name="' . htmlspecialchars($fieldName) . '"';
$html[] = ' disabled="disabled">';
$html[] = '/>';
$html[] = implode(LF, $optionsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
protected function getBackendUserAuthentication(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}