From 8a455d39c0f1ef1d6b96ad4c7714ebf11317c8df Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Wed, 24 Nov 2021 19:04:03 +0100
Subject: [PATCH] [BUGFIX] Make selectCheckBox work with readOnly

Using "readOnly" for a TCA field with renderType
"selectCheckBox" previously led to the fact, that
nothing was rendered at all. This can be seen with
`tx_styleguide_elements_select.select_checkbox_4`.

This is now fixed, while the whole element got a
light code cleanup. This will increase performance
a bit, since unneeded JavaScript is no longer loaded.

Additionally, this also fixes the "expandAll" option,
which was broken due to the bootstrap 5 upgrade.

Resolves: #96058
Resolves: #96068
Releases: master, 11.5
Change-Id: I762d328c9f4c83558dd8fd98683f8ccfe6c4b3f0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72279
Tested-by: core-ci <typo3@b13.com>
Tested-by: Jochen <rothjochen@gmail.com>
Tested-by: Nikita Hovratov <nikita.h@live.de>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Jochen <rothjochen@gmail.com>
Reviewed-by: Nikita Hovratov <nikita.h@live.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
---
 .../Form/Element/SelectCheckBoxElement.php    | 321 +++++++++---------
 1 file changed, 158 insertions(+), 163 deletions(-)

diff --git a/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php b/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php
index f3017b2263d8..e7962ecab036 100644
--- a/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php
+++ b/typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php
@@ -75,210 +75,205 @@ class SelectCheckBoxElement extends AbstractFormElement
     {
         $resultArray = $this->initializeResultArray();
 
-        $html = [];
         // Field configuration from TCA:
         $parameterArray = $this->data['parameterArray'];
         $config = $parameterArray['fieldConf']['config'];
-        $disabled = !empty($config['readOnly']);
+        $readOnly = (bool)($config['readOnly'] ?? false);
 
-        $selItems = $config['items'];
-        if (!empty($selItems)) {
-            // Get values in an array (and make unique, which is fine because there can be no duplicates anyway)
-            // In case e.g. "l10n_display" is set to "defaultAsReadonly" only one value (as string) could be handed in
-            if (is_array($parameterArray['itemFormElValue'])) {
-                $itemArray = $parameterArray['itemFormElValue'];
-            } else {
-                $itemArray = [(string)$parameterArray['itemFormElValue']];
-            }
-            $itemArray = array_flip($itemArray);
-
-            // Traverse the Array of selector box items:
-            $groups = [];
-            $currentGroup = 0;
-            $c = 0;
-            $onFieldChangeAttrs = [];
-            if (!$disabled) {
-                $onFieldChangeAttrs = $this->getOnFieldChangeAttrs('click', $parameterArray['fieldChangeFunc'] ?? []);
-                // Used to accumulate the JS needed to restore the original selection.
-                foreach ($selItems as $p) {
-                    // Non-selectable element:
-                    if ($p[1] === '--div--') {
-                        $selIcon = '';
-                        if (isset($p[2]) && $p[2] !== 'empty-empty') {
-                            $selIcon = FormEngineUtility::getIconHtml($p[2]);
-                        }
-                        $currentGroup++;
-                        $groups[$currentGroup]['header'] = [
-                            'icon' => $selIcon,
-                            'title' => $p[0],
-                        ];
-                    } else {
-                        // Check if some help text is available
-                        // Help text is expected to be an associative array
-                        // with two key, "title" and "description"
-                        // For the sake of backwards compatibility, we test if the help text
-                        // is a string and use it as a description (this could happen if items
-                        // are modified with an itemProcFunc)
-                        $hasHelp = false;
-                        $help = '';
-                        $helpArray = [];
-                        if (!empty($p[4])) {
-                            $hasHelp = true;
-                            if (is_array($p[4])) {
-                                $helpArray = $p[4];
-                            } else {
-                                $helpArray['description'] = $p[4];
-                            }
-                        }
-                        if ($hasHelp) {
-                            $help = BackendUtility::wrapInHelp('', '', '', $helpArray);
-                        }
+        $selectItems = $config['items'] ?? [];
+        if (empty($selectItems)) {
+            // Early return in case the field does not contain any items
+            return $resultArray;
+        }
 
-                        // Selected or not by default:
-                        $checked = 0;
-                        if (isset($itemArray[$p[1]])) {
-                            $checked = 1;
-                            unset($itemArray[$p[1]]);
-                        }
+        // Get values in an array (and make unique, which is fine because there can be no duplicates anyway)
+        // In case e.g. "l10n_display" is set to "defaultAsReadonly" only one value (as string) could be handed in
+        if (is_array($parameterArray['itemFormElValue'])) {
+            $itemArray = $parameterArray['itemFormElValue'];
+        } else {
+            $itemArray = [(string)$parameterArray['itemFormElValue']];
+        }
+        $itemArray = array_flip($itemArray);
 
-                        // Build item array
-                        $groups[$currentGroup]['items'][] = [
-                            'id' => StringUtility::getUniqueId('select_checkbox_row_'),
-                            'name' => $parameterArray['itemFormElName'] . '[' . $c . ']',
-                            'value' => $p[1],
-                            'checked' => $checked,
-                            'disabled' => false,
-                            'class' => '',
-                            'icon' => FormEngineUtility::getIconHtml(!empty($p[2]) ? $p[2] : 'empty-empty'),
-                            'title' => $p[0],
-                            'help' => $help,
-                        ];
-                        $c++;
+        // Initialize variables and traverse the items
+        $groups = [];
+        $currentGroup = 0;
+        $counter = 0;
+        foreach ($selectItems as $item) {
+            // Non-selectable element:
+            if ($item[1] === '--div--') {
+                $selIcon = '';
+                if (isset($item[2]) && $item[2] !== 'empty-empty') {
+                    $selIcon = FormEngineUtility::getIconHtml($item[2]);
+                }
+                $currentGroup++;
+                $groups[$currentGroup]['header'] = [
+                    'icon' => $selIcon,
+                    'title' => $item[0],
+                ];
+            } else {
+                // Check if some help text is available
+                // Help text is expected to be an associative array
+                // with two key, "title" and "description"
+                // For the sake of backwards compatibility, we test if the help text
+                // is a string and use it as a description (this could happen if items
+                // are modified with an itemProcFunc)
+                $help = '';
+                if (!empty($item[4])) {
+                    if (is_array($item[4])) {
+                        $helpArray = $item[4];
+                    } else {
+                        $helpArray['description'] = $item[4];
                     }
+                    $help = BackendUtility::wrapInHelp('', '', '', $helpArray);
                 }
+
+                // Check if current item is selected. If found, unset the key in the $itemArray.
+                $checked = isset($itemArray[$item[1]]);
+                if ($checked) {
+                    unset($itemArray[$item[1]]);
+                }
+
+                // Build item array
+                $groups[$currentGroup]['items'][] = [
+                    'id' => StringUtility::getUniqueId('select_checkbox_row_'),
+                    'name' => $parameterArray['itemFormElName'] . '[' . $counter . ']',
+                    'value' => $item[1],
+                    'checked' => $checked,
+                    'icon' => FormEngineUtility::getIconHtml(!empty($item[2]) ? $item[2] : 'empty-empty'),
+                    'title' => $item[0],
+                    'help' => $help,
+                ];
+                $counter++;
             }
+        }
 
-            $fieldInformationResult = $this->renderFieldInformation();
-            $fieldInformationHtml = $fieldInformationResult['html'];
-            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
 
-            $fieldWizardResult = $this->renderFieldWizard();
-            $fieldWizardHtml = $fieldWizardResult['html'];
-            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
 
-            $html[] = '<div class="formengine-field-item t3js-formengine-field-item" data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '">';
-            $html[] = $fieldInformationHtml;
-            $html[] =   '<div class="form-wizards-wrap">';
-            $html[] =       '<div class="form-wizards-element">';
+        $html[] = '<div class="formengine-field-item t3js-formengine-field-item" data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '">';
+        $html[] = $fieldInformationHtml;
+        $html[] =   '<div class="form-wizards-wrap">';
+        $html[] =       '<div class="form-wizards-element">';
 
+        if (!$readOnly) {
             // Add an empty hidden field which will send a blank value if all items are unselected.
             $html[] = '<input type="hidden" class="select-checkbox" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
+        }
 
-            // Building the checkboxes
-            foreach ($groups as $groupKey => $group) {
-                $group += [
-                    'items' => [],
-                    'header' => false,
-                ];
-                $groupId = htmlspecialchars($parameterArray['itemFormElID']) . '-group-' . $groupKey;
-                $groupIdCollapsible = $groupId . '-collapse';
-                $html[] = '<div id="' . $groupId . '" class="panel panel-default">';
-                if (is_array($group['header'] ?? false)) {
-                    $html[] = '<div class="panel-heading">';
-                    $html[] = '<a data-bs-toggle="collapse" href="#' . $groupIdCollapsible . '" aria-expanded="false" aria-controls="' . $groupIdCollapsible . '">';
-                    $html[] = $group['header']['icon'];
-                    $html[] = htmlspecialchars($group['header']['title']);
-                    $html[] = '</a>';
-                    $html[] = '</div>';
-                }
-                if (!empty($group['items']) && is_array($group['items'])) {
-                    $tableRows = [];
+        // Building the checkboxes
+        foreach ($groups as $groupKey => $group) {
+            $groupId = htmlspecialchars($parameterArray['itemFormElID']) . '-group-' . $groupKey;
+            $groupIdCollapsible = $groupId . '-collapse';
+            $hasGroupHeader = is_array($group['header'] ?? false);
 
-                    // Render rows
-                    foreach ($group['items'] as $item) {
-                        $inputElementAttrs = array_merge(
-                            [
-                                'type' => 'checkbox',
-                                'class' => 't3js-checkbox',
-                                'id' => $item['id'],
-                                'name' => $item['name'],
-                                'value' => $item['value'],
-                            ],
-                            $onFieldChangeAttrs
-                        );
-                        if ($item['checked']) {
-                            $inputElementAttrs['checked'] = 'checked';
-                        }
-                        if ($item['disabled']) {
-                            $inputElementAttrs['disabled'] = 'disabled';
-                        }
+            $html[] = '<div id="' . $groupId . '" class="panel panel-default">';
+            if ($hasGroupHeader) {
+                $html[] = '<div class="panel-heading">';
+                $html[] =    '<a data-bs-toggle="collapse" href="#' . $groupIdCollapsible . '" aria-expanded="false" aria-controls="' . $groupIdCollapsible . '">';
+                $html[] =        $group['header']['icon'];
+                $html[] =        htmlspecialchars($group['header']['title']);
+                $html[] =    '</a>';
+                $html[] = '</div>';
+            }
+            if (!empty($group['items']) && is_array($group['items'])) {
+                $tableRows = [];
 
-                        $tableRows[] = '<tr class="' . $item['class'] . '">';
-                        $tableRows[] =    '<td class="col-checkbox">';
-                        $tableRows[] =        '<input ' . GeneralUtility::implodeAttributes($inputElementAttrs, true) . '>';
-                        $tableRows[] =    '</td>';
-                        $tableRows[] =    '<td class="col-title">';
-                        $tableRows[] =        '<label class="label-block nowrap-disabled" for="' . $item['id'] . '">';
-                        $tableRows[] =            '<span class="inline-icon">' . $item['icon'] . '</span>';
-                        $tableRows[] =            htmlspecialchars($this->appendValueToLabelInDebugMode($item['title'], $item['value']), ENT_COMPAT, 'UTF-8', false);
-                        $tableRows[] =        '</label>';
-                        $tableRows[] =    '</td>';
-                        $tableRows[] =    '<td class="text-end">' . $item['help'] . '</td>';
-                        $tableRows[] = '</tr>';
-                    }
+                // Render rows
+                foreach ($group['items'] as $item) {
+                    $inputElementAttrs = [
+                        'type' => 'checkbox',
+                        'class' => 't3js-checkbox',
+                        'id' => $item['id'],
+                        'name' => $item['name'],
+                        'value' => $item['value'],
+                    ];
 
-                    // Build reset group button
-                    $resetGroupBtn = '';
-                    if (!empty($group['items'])) {
-                        $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection'));
-                        $resetGroupBtn = '<button type="button" '
-                            . 'class="btn btn-default btn-sm t3js-revert-selection" '
-                            . 'title="' . $title . '"'
-                            . '>'
-                            . $this->iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL)->render() . ' '
-                            . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection') . '</button>';
+                    if ($item['checked']) {
+                        $inputElementAttrs['checked'] = 'checked';
                     }
 
-                    if (is_array($group['header'] ?? false)) {
-                        $expandAll = (bool)($config['appearance']['expandAll'] ?? false) ? 'in' : '';
-                        $html[] = '<div id="' . $groupIdCollapsible . '" class="panel-collapse collapse ' . $expandAll . '" role="tabpanel">';
+                    if ($readOnly) {
+                        // Disable item if the element is readonly
+                        $inputElementAttrs['disabled'] = 'disabled';
+                    } else {
+                        // Add fieldChange attributes if element is not readOnly
+                        $inputElementAttrs = array_merge(
+                            $inputElementAttrs,
+                            $this->getOnFieldChangeAttrs('click', $parameterArray['fieldChangeFunc'] ?? [])
+                        );
                     }
+
+                    $tableRows[] = '<tr>';
+                    $tableRows[] =    '<td class="col-checkbox">';
+                    $tableRows[] =        '<input ' . GeneralUtility::implodeAttributes($inputElementAttrs, true) . '>';
+                    $tableRows[] =    '</td>';
+                    $tableRows[] =    '<td class="col-title">';
+                    $tableRows[] =        '<label class="label-block nowrap-disabled" for="' . $item['id'] . '">';
+                    $tableRows[] =            '<span class="inline-icon">' . $item['icon'] . '</span>';
+                    $tableRows[] =            htmlspecialchars($this->appendValueToLabelInDebugMode($item['title'], $item['value']), ENT_COMPAT, 'UTF-8', false);
+                    $tableRows[] =        '</label>';
+                    $tableRows[] =    '</td>';
+                    $tableRows[] =    '<td class="text-end">' . $item['help'] . '</td>';
+                    $tableRows[] = '</tr>';
+                }
+
+                if ($hasGroupHeader) {
+                    $expandAll = ($config['appearance']['expandAll'] ?? false) ? 'show' : '';
+                    $html[] = '<div id="' . $groupIdCollapsible . '" class="panel-collapse collapse ' . $expandAll . '" role="tabpanel">';
+                }
+
+                $html[] =    '<div class="table-responsive">';
+                $html[] =        '<table class="table table-transparent table-hover">';
+                if (!$readOnly) {
                     $checkboxId = StringUtility::getUniqueId($groupId);
                     $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall'));
-                    $html[] =    '<div class="table-responsive">';
-                    $html[] =        '<table class="table table-transparent table-hover">';
+
+                    // Add table header with actions, in case the element is not readOnly
                     $html[] =            '<thead>';
                     $html[] =                '<tr>';
                     $html[] =                    '<th class="col-checkbox">';
                     $html[] =                       '<input type="checkbox" id="' . $checkboxId . '" class="t3js-toggle-checkboxes" data-bs-trigger="hover" data-bs-placement="right" title="' . $title . '" data-bs-toggle="tooltip" />';
                     $html[] =                    '</th>';
                     $html[] =                    '<th class="col-title"><label for="' . $checkboxId . '">' . $title . '</label></th>';
-                    $html[] =                    '<th class="text-end">' . $resetGroupBtn . '</th>';
+                    $html[] =                    '<th class="text-end">';
+                    $html[] =                       '<button type="button" class="btn btn-default btn-sm t3js-revert-selection">';
+                    $html[] =                           $this->iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL)->render() . ' ';
+                    $html[] =                           htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection'));
+                    $html[] =                       '</buttn>';
+                    $html[] =                    '</th>';
                     $html[] =                '</tr>';
                     $html[] =            '</thead>';
-                    $html[] =            '<tbody>' . implode(LF, $tableRows) . '</tbody>';
-                    $html[] =        '</table>';
-                    $html[] =    '</div>';
-                    if (is_array($group['header'])) {
-                        $html[] = '</div>';
-                    }
 
+                    // Add RequireJS module. This is only needed, in case the element
+                    // is not readOnly, since otherwise no checkbox changes take place.
                     $resultArray['requireJsModules'][] = JavaScriptModuleInstruction::forRequireJS(
                         'TYPO3/CMS/Backend/FormEngine/Element/SelectCheckBoxElement'
                     )->instance($checkboxId);
                 }
-                $html[] = '</div>';
+                $html[] =            '<tbody>' . implode(LF, $tableRows) . '</tbody>';
+                $html[] =        '</table>';
+                $html[] =    '</div>';
+                if ($hasGroupHeader) {
+                    $html[] = '</div>';
+                }
             }
+            $html[] = '</div>';
+        }
 
-            $html[] =       '</div>';
-            if (!$disabled && !empty($fieldWizardHtml)) {
-                $html[] =   '<div class="form-wizards-items-bottom">';
-                $html[] =       $fieldWizardHtml;
-                $html[] =   '</div>';
-            }
+        $html[] =       '</div>';
+        if (!$readOnly && !empty($fieldWizardHtml)) {
+            $html[] =   '<div class="form-wizards-items-bottom">';
+            $html[] =       $fieldWizardHtml;
             $html[] =   '</div>';
-            $html[] = '</div>';
         }
+        $html[] =   '</div>';
+        $html[] = '</div>';
 
         $resultArray['html'] = implode(LF, $html);
         $resultArray['requireJsModules'][] = JavaScriptModuleInstruction::forRequireJS('TYPO3/CMS/Backend/Tooltip');
-- 
GitLab