1<?php
2namespace TYPO3\CMS\Backend\Form\Element;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
18use TYPO3\CMS\Core\Imaging\Icon;
19use TYPO3\CMS\Core\Localization\LanguageService;
20use TYPO3\CMS\Core\Utility\GeneralUtility;
21use TYPO3\CMS\Core\Utility\MathUtility;
22use TYPO3\CMS\Core\Utility\StringUtility;
23
24/**
25 * Render a widget with two boxes side by side.
26 *
27 * This is rendered for config type=select, renderType=selectMultipleSideBySide set
28 */
29class SelectMultipleSideBySideElement extends AbstractFormElement
30{
31    /**
32     * Default field information enabled for this element.
33     *
34     * @var array
35     */
36    protected $defaultFieldInformation = [
37        'tcaDescription' => [
38            'renderType' => 'tcaDescription',
39        ],
40    ];
41
42    /**
43     * Default field controls for this element.
44     *
45     * @var array
46     */
47    protected $defaultFieldControl = [
48        'editPopup' => [
49            'renderType' => 'editPopup',
50            'disabled' => true,
51        ],
52        'addRecord' => [
53            'renderType' => 'addRecord',
54            'disabled' => true,
55            'after' => [ 'editPopup' ],
56        ],
57        'listModule' => [
58            'renderType' => 'listModule',
59            'disabled' => true,
60            'after' => [ 'addRecord' ],
61        ],
62    ];
63
64    /**
65     * Default field wizards enabled for this element.
66     *
67     * @var array
68     */
69    protected $defaultFieldWizard = [
70        'localizationStateSelector' => [
71            'renderType' => 'localizationStateSelector',
72        ],
73        'otherLanguageContent' => [
74            'renderType' => 'otherLanguageContent',
75            'after' => [
76                'localizationStateSelector'
77            ],
78        ],
79        'defaultLanguageDifferences' => [
80            'renderType' => 'defaultLanguageDifferences',
81            'after' => [
82                'otherLanguageContent',
83            ],
84        ],
85    ];
86
87    /**
88     * Render side by side element.
89     *
90     * @return array As defined in initializeResultArray() of AbstractNode
91     */
92    public function render()
93    {
94        $languageService = $this->getLanguageService();
95        $resultArray = $this->initializeResultArray();
96
97        $parameterArray = $this->data['parameterArray'];
98        $config = $parameterArray['fieldConf']['config'];
99        $elementName = $parameterArray['itemFormElName'];
100
101        if ($config['readOnly']) {
102            // Early return for the relatively simple read only case
103            return $this->renderReadOnly();
104        }
105
106        $possibleItems = $config['items'];
107        $selectedItems = $parameterArray['itemFormElValue'] ?: [];
108        $selectedItemsCount = count($selectedItems);
109
110        $maxItems = $config['maxitems'];
111        $autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
112        $size = 2;
113        if (isset($config['size'])) {
114            $size = (int)$config['size'];
115        }
116        if ($autoSizeMax >= 1) {
117            $size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
118        }
119        $itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
120
121        $listOfSelectedValues = [];
122        $selectedItemsHtml = [];
123        foreach ($selectedItems as $itemValue) {
124            foreach ($possibleItems as $possibleItem) {
125                if ($possibleItem[1] == $itemValue) {
126                    $title = $possibleItem[0];
127                    $listOfSelectedValues[] = $itemValue;
128                    $selectedItemsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($this->appendValueToLabelInDebugMode($title, $itemValue)) . '</option>';
129                    break;
130                }
131            }
132        }
133
134        $selectableItemsHtml = [];
135        foreach ($possibleItems as $possibleItem) {
136            $disabledAttr = '';
137            $classAttr = '';
138            if (!$itemCanBeSelectedMoreThanOnce && in_array((string)$possibleItem[1], $selectedItems, true)) {
139                $disabledAttr = ' disabled="disabled"';
140                $classAttr = ' class="hidden"';
141            }
142            $selectableItemsHtml[] =
143                '<option value="'
144                    . htmlspecialchars($possibleItem[1])
145                    . '" title="' . htmlspecialchars($possibleItem[0]) . '"'
146                    . $classAttr . $disabledAttr
147                . '>'
148                    . htmlspecialchars($this->appendValueToLabelInDebugMode($possibleItem[0], $possibleItem[1])) .
149                '</option>';
150        }
151
152        // Html stuff for filter and select filter on top of right side of multi select boxes
153        $filterTextfield = [];
154        if ($config['enableMultiSelectFilterTextfield']) {
155            $filterTextfield[] = '<span class="input-group input-group-sm">';
156            $filterTextfield[] =    '<span class="input-group-addon">';
157            $filterTextfield[] =        '<span class="fa fa-filter"></span>';
158            $filterTextfield[] =    '</span>';
159            $filterTextfield[] =    '<input class="t3js-formengine-multiselect-filter-textfield form-control" value="">';
160            $filterTextfield[] = '</span>';
161        }
162        $filterDropDownOptions = [];
163        if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
164            foreach ($config['multiSelectFilterItems'] as $optionElement) {
165                $value = $languageService->sL($optionElement[0]);
166                $label = $value;
167                if (isset($optionElement[1]) && trim($optionElement[1]) !== '') {
168                    $label = $languageService->sL($optionElement[1]);
169                }
170                $filterDropDownOptions[] = '<option value="' . htmlspecialchars($value) . '">' . htmlspecialchars($label) . '</option>';
171            }
172        }
173        $filterHtml = [];
174        if (!empty($filterTextfield) || !empty($filterDropDownOptions)) {
175            $filterHtml[] = '<div class="form-multigroup-item-wizard">';
176            if (!empty($filterTextfield) && !empty($filterDropDownOptions)) {
177                $filterHtml[] = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">';
178                $filterHtml[] =     '<div class="form-multigroup-item form-multigroup-element">';
179                $filterHtml[] =         '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">';
180                $filterHtml[] =             implode(LF, $filterDropDownOptions);
181                $filterHtml[] =         '</select>';
182                $filterHtml[] =     '</div>';
183                $filterHtml[] =     '<div class="form-multigroup-item form-multigroup-element">';
184                $filterHtml[] =         implode(LF, $filterTextfield);
185                $filterHtml[] =     '</div>';
186                $filterHtml[] = '</div>';
187            } elseif (!empty($filterTextfield)) {
188                $filterHtml[] = implode(LF, $filterTextfield);
189            } else {
190                $filterHtml[] = '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">';
191                $filterHtml[] =     implode(LF, $filterDropDownOptions);
192                $filterHtml[] = '</select>';
193            }
194            $filterHtml[] = '</div>';
195        }
196
197        $classes = [];
198        $classes[] = 'form-control';
199        $classes[] = 'tceforms-multiselect';
200        if ($maxItems === 1) {
201            $classes[] = 'form-select-no-siblings';
202        }
203        $multipleAttribute = '';
204        if ($maxItems !== 1 && $size !== 1) {
205            $multipleAttribute = ' multiple="multiple"';
206        }
207
208        $fieldInformationResult = $this->renderFieldInformation();
209        $fieldInformationHtml = $fieldInformationResult['html'];
210        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
211
212        $fieldControlResult = $this->renderFieldControl();
213        $fieldControlHtml = $fieldControlResult['html'];
214        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
215
216        $fieldWizardResult = $this->renderFieldWizard();
217        $fieldWizardHtml = $fieldWizardResult['html'];
218        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
219
220        $html = [];
221        $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
222        $html[] =   $fieldInformationHtml;
223        $html[] =   '<div class="form-wizards-wrap">';
224        $html[] =       '<div class="form-wizards-element">';
225        $html[] =           '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . (int)$itemCanBeSelectedMoreThanOnce . '" />';
226        $html[] =           '<div class="form-multigroup-wrap t3js-formengine-field-group">';
227        $html[] =               '<div class="form-multigroup-item form-multigroup-element">';
228        $html[] =                   '<label>';
229        $html[] =                       htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
230        $html[] =                   '</label>';
231        $html[] =                   '<div class="form-wizards-wrap form-wizards-aside">';
232        $html[] =                       '<div class="form-wizards-element">';
233        $html[] =                           '<select';
234        $html[] =                               ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
235        $html[] =                               ' size="' . $size . '"';
236        $html[] =                               ' class="' . implode(' ', $classes) . '"';
237        $html[] =                               $multipleAttribute;
238        $html[] =                               ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
239        $html[] =                           '>';
240        $html[] =                               implode(LF, $selectedItemsHtml);
241        $html[] =                           '</select>';
242        $html[] =                       '</div>';
243        $html[] =                       '<div class="form-wizards-items-aside">';
244        $html[] =                           '<div class="btn-group-vertical">';
245        if ($maxItems > 1 && $size >= 5) {
246            $html[] =                           '<a href="#"';
247            $html[] =                               ' class="btn btn-default t3js-btn-moveoption-top"';
248            $html[] =                               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
249            $html[] =                               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
250            $html[] =                           '>';
251            $html[] =                               $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render();
252            $html[] =                           '</a>';
253        }
254        if ($maxItems > 1) {
255            $html[] =                           '<a href="#"';
256            $html[] =                               ' class="btn btn-default t3js-btn-moveoption-up"';
257            $html[] =                               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
258            $html[] =                               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
259            $html[] =                           '>';
260            $html[] =                               $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render();
261            $html[] =                           '</a>';
262            $html[] =                           '<a href="#"';
263            $html[] =                               ' class="btn btn-default t3js-btn-moveoption-down"';
264            $html[] =                               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
265            $html[] =                               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
266            $html[] =                           '>';
267            $html[] =                               $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render();
268            $html[] =                           '</a>';
269        }
270        if ($maxItems > 1 && $size >= 5) {
271            $html[] =                           '<a href="#"';
272            $html[] =                               ' class="btn btn-default t3js-btn-moveoption-bottom"';
273            $html[] =                               ' data-fieldname="' . htmlspecialchars($elementName) . '"';
274            $html[] =                               ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
275            $html[] =                           '>';
276            $html[] =                               $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render();
277            $html[] =                           '</a>';
278        }
279        $html[] =                               '<a href="#"';
280        $html[] =                                   ' class="btn btn-default t3js-btn-removeoption"';
281        $html[] =                                   ' data-fieldname="' . htmlspecialchars($elementName) . '"';
282        $html[] =                                   ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
283        $html[] =                               '>';
284        $html[] =                                   $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render();
285        $html[] =                               '</a>';
286        $html[] =                           '</div>';
287        $html[] =                       '</div>';
288        $html[] =                   '</div>';
289        $html[] =               '</div>';
290        $html[] =               '<div class="form-multigroup-item form-multigroup-element">';
291        $html[] =                   '<label>';
292        $html[] =                       htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.items'));
293        $html[] =                   '</label>';
294        $html[] =                   '<div class="form-wizards-wrap form-wizards-aside">';
295        $html[] =                       '<div class="form-wizards-element">';
296        $html[] =                           implode(LF, $filterHtml);
297        $html[] =                           '<select';
298        $html[] =                               ' data-relatedfieldname="' . htmlspecialchars($elementName) . '"';
299        $html[] =                               ' data-exclusivevalues="' . htmlspecialchars($config['exclusiveKeys']) . '"';
300        $html[] =                               ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
301        $html[] =                               ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
302        $html[] =                               ' class="form-control t3js-formengine-select-itemstoselect"';
303        $html[] =                               ' size="' . $size . '"';
304        $html[] =                               ' onchange="' . htmlspecialchars(implode('', $parameterArray['fieldChangeFunc'])) . '"';
305        $html[] =                               ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
306        $html[] =                           '>';
307        $html[] =                               implode(LF, $selectableItemsHtml);
308        $html[] =                           '</select>';
309        $html[] =                       '</div>';
310        if (!empty($fieldControlHtml)) {
311            $html[] =                       '<div class="form-wizards-items-aside">';
312            $html[] =                           '<div class="btn-group-vertical">';
313            $html[] =                               $fieldControlHtml;
314            $html[] =                           '</div>';
315            $html[] =                       '</div>';
316        }
317        $html[] =                   '</div>';
318        $html[] =               '</div>';
319        $html[] =           '</div>';
320        $html[] =           '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
321        $html[] =       '</div>';
322        if (!empty($fieldWizardHtml)) {
323            $html[] = '<div class="form-wizards-items-bottom">';
324            $html[] = $fieldWizardHtml;
325            $html[] = '</div>';
326        }
327        $html[] =   '</div>';
328        $html[] = '</div>';
329
330        $resultArray['html'] = implode(LF, $html);
331        return $resultArray;
332    }
333
334    /**
335     * Create HTML of a read only multi select. Right side is not
336     * rendered, but just the left side with the selected items.
337     *
338     * @return array
339     */
340    protected function renderReadOnly()
341    {
342        $languageService = $this->getLanguageService();
343        $resultArray = $this->initializeResultArray();
344
345        $parameterArray = $this->data['parameterArray'];
346        $config = $parameterArray['fieldConf']['config'];
347        $fieldName = $parameterArray['itemFormElName'];
348
349        $possibleItems = $config['items'];
350        $selectedItems = $parameterArray['itemFormElValue'] ?: [];
351        if (!is_array($selectedItems)) {
352            $selectedItems = GeneralUtility::trimExplode(',', $selectedItems, true);
353        }
354        $selectedItemsCount = count($selectedItems);
355
356        $autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
357        $size = 2;
358        if (isset($config['size'])) {
359            $size = (int)$config['size'];
360        }
361        if ($autoSizeMax >= 1) {
362            $size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
363        }
364        $multiple = '';
365        if ($size !== 1) {
366            $multiple = ' multiple="multiple"';
367        }
368
369        $listOfSelectedValues = [];
370        $optionsHtml = [];
371        foreach ($selectedItems as $itemValue) {
372            foreach ($possibleItems as $possibleItem) {
373                if ($possibleItem[1] == $itemValue) {
374                    $title = $possibleItem[0];
375                    $listOfSelectedValues[] = $itemValue;
376                    $optionsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($title) . '</option>';
377                    break;
378                }
379            }
380        }
381
382        $fieldInformationResult = $this->renderFieldInformation();
383        $fieldInformationHtml = $fieldInformationResult['html'];
384        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
385
386        $html = [];
387        $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
388        $html[] =   $fieldInformationHtml;
389        $html[] =   '<div class="form-wizards-wrap">';
390        $html[] =       '<div class="form-wizards-element">';
391        $html[] =           '<label>';
392        $html[] =               htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
393        $html[] =           '</label>';
394        $html[] =           '<div class="form-wizards-wrap form-wizards-aside">';
395        $html[] =               '<div class="form-wizards-element">';
396        $html[] =                   '<select';
397        $html[] =                       ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
398        $html[] =                       ' size="' . $size . '"';
399        $html[] =                       ' class="form-control tceforms-multiselect"';
400        $html[] =                       $multiple;
401        $html[] =                       ' data-formengine-input-name="' . htmlspecialchars($fieldName) . '"';
402        $html[] =                       ' disabled="disabled">';
403        $html[] =                   '/>';
404        $html[] =                       implode(LF, $optionsHtml);
405        $html[] =                   '</select>';
406        $html[] =               '</div>';
407        $html[] =           '</div>';
408        $html[] =           '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
409        $html[] =       '</div>';
410        $html[] =   '</div>';
411        $html[] = '</div>';
412
413        $resultArray['html'] = implode(LF, $html);
414        return $resultArray;
415    }
416
417    /**
418     * @return LanguageService
419     */
420    protected function getLanguageService()
421    {
422        return $GLOBALS['LANG'];
423    }
424
425    /**
426     * @return BackendUserAuthentication
427     */
428    protected function getBackendUserAuthentication()
429    {
430        return $GLOBALS['BE_USER'];
431    }
432}
433