1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Backend\Form\Element;
17
18use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
19use TYPO3\CMS\Core\Imaging\Icon;
20use TYPO3\CMS\Core\Localization\LanguageService;
21use TYPO3\CMS\Core\Utility\GeneralUtility;
22use TYPO3\CMS\Core\Utility\MathUtility;
23use TYPO3\CMS\Core\Utility\StringUtility;
24
25/**
26 * Generation of elements of the type "group"
27 */
28class GroupElement extends AbstractFormElement
29{
30    /**
31     * Default field information enabled for this element.
32     *
33     * @var array
34     */
35    protected $defaultFieldInformation = [
36        'tcaDescription' => [
37            'renderType' => 'tcaDescription',
38        ],
39    ];
40
41    /**
42     * Default field controls for this element.
43     *
44     * @var array
45     */
46    protected $defaultFieldControl = [
47        'elementBrowser' => [
48            'renderType' => 'elementBrowser',
49        ],
50        'insertClipboard' => [
51            'renderType' => 'insertClipboard',
52            'after' => [ 'elementBrowser' ],
53        ],
54        'editPopup' => [
55            'renderType' => 'editPopup',
56            'disabled' => true,
57            'after' => [ 'insertClipboard' ],
58        ],
59        'addRecord' => [
60            'renderType' => 'addRecord',
61            'disabled' => true,
62            'after' => [ 'editPopup' ],
63        ],
64        'listModule' => [
65            'renderType' => 'listModule',
66            'disabled' => true,
67            'after' => [ 'addRecord' ],
68        ],
69    ];
70
71    /**
72     * Default field wizards for this element
73     *
74     * @var array
75     */
76    protected $defaultFieldWizard = [
77        'tableList' => [
78            'renderType' => 'tableList',
79        ],
80        'recordsOverview' => [
81            'renderType' => 'recordsOverview',
82            'after' => [ 'tableList' ],
83        ],
84        'localizationStateSelector' => [
85            'renderType' => 'localizationStateSelector',
86            'after' => [ 'recordsOverview' ],
87        ],
88        'otherLanguageContent' => [
89            'renderType' => 'otherLanguageContent',
90            'after' => [ 'localizationStateSelector' ],
91        ],
92        'defaultLanguageDifferences' => [
93            'renderType' => 'defaultLanguageDifferences',
94            'after' => [ 'otherLanguageContent' ],
95        ],
96    ];
97
98    /**
99     * This will render a selector box into which elements from either
100     * the file system or database can be inserted. Relations.
101     *
102     * @return array As defined in initializeResultArray() of AbstractNode
103     * @throws \RuntimeException
104     */
105    public function render()
106    {
107        $languageService = $this->getLanguageService();
108        $backendUser = $this->getBackendUserAuthentication();
109        $resultArray = $this->initializeResultArray();
110
111        $table = $this->data['tableName'];
112        $fieldName = $this->data['fieldName'];
113        $row = $this->data['databaseRow'];
114        $parameterArray = $this->data['parameterArray'];
115        $config = $parameterArray['fieldConf']['config'];
116        $elementName = $parameterArray['itemFormElName'];
117
118        $selectedItems = $parameterArray['itemFormElValue'];
119        $selectedItemsCount = count($selectedItems);
120
121        $maxItems = $config['maxitems'];
122        $autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
123        $size = 5;
124        if (isset($config['size'])) {
125            $size = (int)$config['size'];
126        }
127        if ($autoSizeMax >= 1) {
128            $size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
129        }
130
131        $internalType = (string)$config['internal_type'];
132        $maxTitleLength = $backendUser->uc['titleLen'];
133
134        $listOfSelectedValues = [];
135        $selectorOptionsHtml = [];
136        if ($internalType === 'folder') {
137            foreach ($selectedItems as $selectedItem) {
138                $folder = $selectedItem['folder'];
139                $listOfSelectedValues[] = $folder;
140                $selectorOptionsHtml[] =
141                    '<option value="' . htmlspecialchars($folder) . '" title="' . htmlspecialchars($folder) . '">'
142                        . htmlspecialchars($folder)
143                    . '</option>';
144            }
145        } elseif ($internalType === 'db') {
146            foreach ($selectedItems as $selectedItem) {
147                $tableWithUid = $selectedItem['table'] . '_' . $selectedItem['uid'];
148                $listOfSelectedValues[] = $tableWithUid;
149                $title = $selectedItem['title'];
150                if (empty($title)) {
151                    $title = '[' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']';
152                }
153                $shortenedTitle = GeneralUtility::fixed_lgd_cs($title, $maxTitleLength);
154                $selectorOptionsHtml[] =
155                    '<option value="' . htmlspecialchars($tableWithUid) . '" title="' . htmlspecialchars($title) . '">'
156                        . htmlspecialchars($this->appendValueToLabelInDebugMode($shortenedTitle, $tableWithUid))
157                    . '</option>';
158            }
159        } else {
160            throw new \RuntimeException(
161                'internal_type missing on type="group" field',
162                1485007097
163            );
164        }
165
166        $fieldInformationResult = $this->renderFieldInformation();
167        $fieldInformationHtml = $fieldInformationResult['html'];
168        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
169
170        if (isset($config['readOnly']) && $config['readOnly']) {
171            // Return early if element is read only
172            $html = [];
173            $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
174            $html[] =   $fieldInformationHtml;
175            $html[] =   '<div class="form-wizards-wrap">';
176            $html[] =       '<div class="form-wizards-element">';
177            $html[] =           '<select';
178            $html[] =               ' size="' . $size . '"';
179            $html[] =               ' disabled="disabled"';
180            $html[] =               ' class="form-control tceforms-multiselect"';
181            $html[] =               ($maxItems !== 1 && $size !== 1) ? ' multiple="multiple"' : '';
182            $html[] =           '>';
183            $html[] =               implode(LF, $selectorOptionsHtml);
184            $html[] =           '</select>';
185            $html[] =       '</div>';
186            $html[] =       '<div class="form-wizards-items-aside">';
187            $html[] =       '</div>';
188            $html[] =   '</div>';
189            $html[] = '</div>';
190            $resultArray['html'] = implode(LF, $html);
191            return $resultArray;
192        }
193
194        // Need some information if in flex form scope for the suggest element
195        $dataStructureIdentifier = '';
196        $flexFormSheetName = '';
197        $flexFormFieldName = '';
198        $flexFormContainerName = '';
199        $flexFormContainerFieldName = '';
200        if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
201            $flexFormConfig = $this->data['processedTca']['columns'][$fieldName];
202            $dataStructureIdentifier = $flexFormConfig['config']['dataStructureIdentifier'];
203            if (!isset($flexFormConfig['config']['dataStructureIdentifier'])) {
204                throw new \RuntimeException(
205                    'A data structure identifier must be set in [\'config\'] part of a flex form.'
206                    . ' This is usually added by TcaFlexPrepare data processor',
207                    1485206970
208                );
209            }
210            if (isset($this->data['flexFormSheetName'])) {
211                $flexFormSheetName = $this->data['flexFormSheetName'];
212            }
213            if (isset($this->data['flexFormFieldName'])) {
214                $flexFormFieldName = $this->data['flexFormFieldName'];
215            }
216            if (isset($this->data['flexFormContainerName'])) {
217                $flexFormContainerName = $this->data['flexFormContainerName'];
218            }
219            if (isset($this->data['flexFormContainerFieldName'])) {
220                $flexFormContainerFieldName = $this->data['flexFormContainerFieldName'];
221            }
222        }
223        // Get minimum characters for suggest from TCA and override by TsConfig
224        $suggestMinimumCharacters = 0;
225        if (isset($config['suggestOptions']['default']['minimumCharacters'])) {
226            $suggestMinimumCharacters = (int)$config['suggestOptions']['default']['minimumCharacters'];
227        }
228        if (isset($parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'])) {
229            $suggestMinimumCharacters = (int)$parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'];
230        }
231        $suggestMinimumCharacters = $suggestMinimumCharacters > 0 ? $suggestMinimumCharacters : 2;
232
233        $itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
234
235        $showMoveIcons = true;
236        if (isset($config['hideMoveIcons']) && $config['hideMoveIcons']) {
237            $showMoveIcons = false;
238        }
239        $showDeleteControl = true;
240        if (isset($config['hideDeleteIcon']) && $config['hideDeleteIcon']) {
241            $showDeleteControl = false;
242        }
243
244        $fieldId = StringUtility::getUniqueId('tceforms-multiselect-');
245
246        $selectorAttributes = [
247            'id' => $fieldId,
248            'data-formengine-input-name' => htmlspecialchars($elementName),
249            'data-maxitems' => (string)$maxItems,
250            'size' => (string)$size,
251        ];
252        $selectorClasses = [
253            'form-control',
254            'tceforms-multiselect',
255        ];
256        if ($maxItems === 1) {
257            $selectorClasses[] = 'form-select-no-siblings';
258        }
259        $selectorAttributes['class'] = implode(' ', $selectorClasses);
260        if ($maxItems !== 1 && $size !== 1) {
261            $selectorAttributes['multiple'] = 'multiple';
262        }
263
264        $fieldControlResult = $this->renderFieldControl();
265        $fieldControlHtml = $fieldControlResult['html'];
266        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
267
268        $fieldWizardResult = $this->renderFieldWizard();
269        $fieldWizardHtml = $fieldWizardResult['html'];
270        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
271
272        $html = [];
273        $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
274        $html[] =   $fieldInformationHtml;
275        $html[] =   '<div class="form-wizards-wrap">';
276        if ($internalType === 'db' && (!isset($config['hideSuggest']) || (bool)$config['hideSuggest'] !== true)) {
277            $html[] =   '<div class="form-wizards-items-top">';
278            $html[] =       '<div class="autocomplete t3-form-suggest-container">';
279            $html[] =           '<div class="input-group">';
280            $html[] =               '<span class="input-group-addon">';
281            $html[] =                   $this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render();
282            $html[] =               '</span>';
283            $html[] =               '<input type="search" class="t3-form-suggest form-control"';
284            $html[] =                   ' placeholder="' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.findRecord') . '"';
285            $html[] =                   ' data-fieldname="' . htmlspecialchars($fieldName) . '"';
286            $html[] =                   ' data-tablename="' . htmlspecialchars($table) . '"';
287            $html[] =                   ' data-field="' . htmlspecialchars($elementName) . '"';
288            $html[] =                   ' data-uid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
289            $html[] =                   ' data-pid="' . htmlspecialchars($this->data['parentPageRow']['uid'] ?? 0) . '"';
290            $html[] =                   ' data-fieldtype="' . htmlspecialchars($config['type']) . '"';
291            $html[] =                   ' data-minchars="' . htmlspecialchars((string)$suggestMinimumCharacters) . '"';
292            $html[] =                   ' data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"';
293            $html[] =                   ' data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
294            $html[] =                   ' data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
295            $html[] =                   ' data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
296            $html[] =                   ' data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"';
297            $html[] =               '/>';
298            $html[] =           '</div>';
299            $html[] =       '</div>';
300            $html[] =   '</div>';
301        }
302        $html[] =       '<div class="form-wizards-element">';
303        $html[] =           '<input type="hidden" class="t3js-group-hidden-field" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . $itemCanBeSelectedMoreThanOnce . '" />';
304        $html[] =           '<select ' . GeneralUtility::implodeAttributes($selectorAttributes, true) . '>';
305        $html[] =               implode(LF, $selectorOptionsHtml);
306        $html[] =           '</select>';
307        $html[] =       '</div>';
308        $html[] =       '<div class="form-wizards-items-aside">';
309        $html[] =           '<div class="btn-group-vertical">';
310        if ($maxItems > 1 && $size >=5 && $showMoveIcons) {
311            $html[] =           '<a href="#"';
312            $html[] =               ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-top"';
313            $html[] =               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
314            $html[] =               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
315            $html[] =           '>';
316            $html[] =               $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render();
317            $html[] =           '</a>';
318        }
319        if ($maxItems > 1 && $size > 1 && $showMoveIcons) {
320            $html[] =           '<a href="#"';
321            $html[] =               ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-up"';
322            $html[] =               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
323            $html[] =               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
324            $html[] =           '>';
325            $html[] =               $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render();
326            $html[] =           '</a>';
327            $html[] =           '<a href="#"';
328            $html[] =               ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-down"';
329            $html[] =               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
330            $html[] =               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
331            $html[] =           '>';
332            $html[] =               $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render();
333            $html[] =           '</a>';
334        }
335        if ($maxItems > 1 && $size >= 5 && $showMoveIcons) {
336            $html[] =           '<a href="#"';
337            $html[] =               ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-bottom"';
338            $html[] =               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
339            $html[] =               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
340            $html[] =           '>';
341            $html[] =               $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render();
342            $html[] =           '</a>';
343        }
344        if ($showDeleteControl) {
345            $html[] =           '<a href="#"';
346            $html[] =               ' class="btn btn-default t3js-btn-option t3js-btn-removeoption t3js-revert-unique"';
347            $html[] =               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
348            $html[] =               ' data-uid="' . htmlspecialchars($row['uid']) . '"';
349            $html[] =               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
350            $html[] =           '>';
351            $html[] =               $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render();
352            $html[] =           '</a>';
353        }
354        $html[] =           '</div>';
355        $html[] =       '</div>';
356        $html[] =       '<div class="form-wizards-items-aside">';
357        $html[] =           '<div class="btn-group-vertical">';
358        $html[] =               $fieldControlHtml;
359        $html[] =           '</div>';
360        $html[] =       '</div>';
361        if (!empty($fieldWizardHtml)) {
362            $html[] = '<div class="form-wizards-items-bottom">';
363            $html[] = $fieldWizardHtml;
364            $html[] = '</div>';
365        }
366        $html[] =   '</div>';
367        $html[] =   '<input type="hidden"';
368        $html[] =       ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
369        $html[] =       ' name="' . htmlspecialchars($elementName) . '"';
370        $html[] =       ' value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '"';
371        $html[] =       ' onchange="' . htmlspecialchars(implode('', $parameterArray['fieldChangeFunc'])) . '"';
372        $html[] =   ' />';
373        $html[] = '</div>';
374
375        $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/GroupElement' => '
376            function(GroupElement) {
377                new GroupElement(' . GeneralUtility::quoteJSvalue($fieldId) . ');
378            }'
379        ];
380
381        $resultArray['html'] = implode(LF, $html);
382        return $resultArray;
383    }
384
385    /**
386     * @return BackendUserAuthentication
387     */
388    protected function getBackendUserAuthentication()
389    {
390        return $GLOBALS['BE_USER'];
391    }
392
393    /**
394     * @return LanguageService
395     */
396    protected function getLanguageService()
397    {
398        return $GLOBALS['LANG'];
399    }
400}
401