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