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